From 55fd11a5a429258659ca4d7ce7e1f8c3929923fa Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Thu, 24 Apr 2025 17:56:35 +0300 Subject: [PATCH 0001/1006] Update CHANGELOG --- CHANGELOG.md | 44 ++++++++++++++------------------------------ 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a014975c88d0..c45c582dde07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,36 +2,6 @@ ## 8.0.2 -### 🐛 Bug Fixes - -- Fix Indexer.Helper.http_get_request function ([#12317](https://github.com/blockscout/blockscout/pull/12317)) -- Rename left props in API v2 with new naming convention ([#12314](https://github.com/blockscout/blockscout/issues/12314)) -- Temporary disable PendingTransactionOperation ([#12312](https://github.com/blockscout/blockscout/issues/12312)) -- Add `bash` to `builder-deps` build stage ([#12316](https://github.com/blockscout/blockscout/issues/12316)) -- Build on macos ([#12308](https://github.com/blockscout/blockscout/issues/12308)) - -### ⚙️ Miscellaneous Tasks - -- Add Scroll Euclid upgrade support ([#12294](https://github.com/blockscout/blockscout/issues/12294)) - -## 8.0.1 - -### 🐛 Bug Fixes - -- Fix MissingBlockRange.fill_ranges_between/3 for empty range ([#12319](https://github.com/blockscout/blockscout/pull/12319)) -- Fix CSV export "to" range to include the whole day in all cases ([#12286](https://github.com/blockscout/blockscout/pull/12286)) -- Return compatibility with previous version of health endpoint([#12280](https://github.com/blockscout/blockscout/pull/12280)) -- Unbind import from compile-time chain_type ([#12277](https://github.com/blockscout/blockscout/pull/12277)) -- Read `CHAIN_TYPE` and `MUD_INDEXER_ENABLED` envs in runtime config ([#12270](https://github.com/blockscout/blockscout/issues/12270)) - -### ⚙️ Miscellaneous Tasks - -- Decrease PBO to PTO migration batch size ([#12279](https://github.com/blockscout/blockscout/pull/12279)) -- Decrease PendingOperationsHelper blocks_batch_size ([#12276](https://github.com/blockscout/blockscout/pull/12276)) -- Update docker compose to use ghcr.io images ([#12177](https://github.com/blockscout/blockscout/issues/12177)) - -## 8.0.0 - ### 🚀 Features - Rate limiter for on-demand fetchers ([#12218](https://github.com/blockscout/blockscout/issues/12218)) @@ -57,6 +27,16 @@ ### 🐛 Bug Fixes +- Fix Indexer.Helper.http_get_request function ([#12317](https://github.com/blockscout/blockscout/pull/12317)) +- Rename left props in API v2 with new naming convention ([#12314](https://github.com/blockscout/blockscout/issues/12314)) +- Temporary disable PendingTransactionOperation ([#12312](https://github.com/blockscout/blockscout/issues/12312)) +- Add `bash` to `builder-deps` build stage ([#12316](https://github.com/blockscout/blockscout/issues/12316)) +- Build on macos ([#12308](https://github.com/blockscout/blockscout/issues/12308)) +- Fix MissingBlockRange.fill_ranges_between/3 for empty range ([#12319](https://github.com/blockscout/blockscout/pull/12319)) +- Fix CSV export "to" range to include the whole day in all cases ([#12286](https://github.com/blockscout/blockscout/pull/12286)) +- Return compatibility with previous version of health endpoint([#12280](https://github.com/blockscout/blockscout/pull/12280)) +- Unbind import from compile-time chain_type ([#12277](https://github.com/blockscout/blockscout/pull/12277)) +- Read `CHAIN_TYPE` and `MUD_INDEXER_ENABLED` envs in runtime config ([#12270](https://github.com/blockscout/blockscout/issues/12270)) - Limit max import concurrency ([#12261](https://github.com/blockscout/blockscout/pull/12261)) - CSV export: download items for the given day if from / to period are equal ([#12260](https://github.com/blockscout/blockscout/pull/12260)) - Upgrade missing balanceOf token condition ([#12254](https://github.com/blockscout/blockscout/pull/12254)) @@ -105,6 +85,10 @@ ### ⚙️ Miscellaneous Tasks +- Add Scroll Euclid upgrade support ([#12294](https://github.com/blockscout/blockscout/issues/12294)) +- Decrease PBO to PTO migration batch size ([#12279](https://github.com/blockscout/blockscout/pull/12279)) +- Decrease PendingOperationsHelper blocks_batch_size ([#12276](https://github.com/blockscout/blockscout/pull/12276)) +- Update docker compose to use ghcr.io images ([#12177](https://github.com/blockscout/blockscout/issues/12177)) - Add typed_ecto_schema to release ([#12255](https://github.com/blockscout/blockscout/pull/12255)) - Suppress logging for expected 404 errors in account abstraction ([#12242](https://github.com/blockscout/blockscout/pull/12242)) - Upgrade on demand balances fetchers ([#12104](https://github.com/blockscout/blockscout/pull/12104)) From 2eac8b7dddb19c013a7eea323c3224e58d7df3a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 15:06:29 +0300 Subject: [PATCH 0002/1006] chore(deps): bump @babel/runtime in /apps/block_scout_web/assets (#12318) Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.16.0 to 7.27.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.0/packages/babel-runtime) --- updated-dependencies: - dependency-name: "@babel/runtime" dependency-version: 7.27.0 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index c484b41945c8..9e6fc2b6611c 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -1899,11 +1899,12 @@ } }, "node_modules/@babel/runtime": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.0.tgz", - "integrity": "sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", "dependencies": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" }, "engines": { "node": ">=6.9.0" @@ -13677,9 +13678,10 @@ } }, "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" }, "node_modules/regenerator-transform": { "version": "0.15.2", @@ -17556,11 +17558,11 @@ } }, "@babel/runtime": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.0.tgz", - "integrity": "sha512-Nht8L0O8YCktmsDV6FqFue7vQLRx3Hb0B37lS5y0jDRqRxlBG4wIJHnf9/bgSE2UyipKFA01YtS+npRdTWBUyw==", + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "requires": { - "regenerator-runtime": "^0.13.4" + "regenerator-runtime": "^0.14.0" } }, "@babel/template": { @@ -26320,9 +26322,9 @@ } }, "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regenerator-transform": { "version": "0.15.2", From 3a72896f89046cdccae11a0bbe6ec3a44b279d0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:18:08 +0300 Subject: [PATCH 0003/1006] chore(deps): bump image from 0.59.2 to 0.59.3 (#12339) Bumps [image](https://github.com/kipcole9/image) from 0.59.2 to 0.59.3. - [Release notes](https://github.com/kipcole9/image/releases) - [Changelog](https://github.com/elixir-image/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/kipcole9/image/compare/v0.59.2...v0.59.3) --- updated-dependencies: - dependency-name: image dependency-version: 0.59.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 20ac3e689023..f5207b9792f0 100644 --- a/mix.lock +++ b/mix.lock @@ -84,7 +84,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.59.2", "b4b8deb31193ff2eb4f2c508e9f3b6f62c8238d34c3807d60a3f2dd5b2d04c28", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.23", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "c8d29db920ff611bcb424e52ffc81e08ff737af0d1c1b30f67b51d05e2257013"}, + "image": {:hex, :image, "0.59.3", "fcc8f75e61eef3dda091018f920c1c8d931e6b9c37b6787d48536c6ab9aef3a7", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "b86cf0a17c8ea31d12cf6eb907ca3be27377cd41c0d0fe4be6f3da78e71743d5"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -168,7 +168,7 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, - "vix": {:hex, :vix, "0.33.0", "cd98084529fd8fe3d2336f157db6de03b297fb096508d820068117d58eadb6f1", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "9acde72b27bdfeadeb51f790f7a6cc0d06cf555718c05cf57e43c5cf93d8471b"}, + "vix": {:hex, :vix, "0.33.1", "72717f9b7a2ccd314c0b320a91d8cae67f100c0df378251acea19151fbf6e4b6", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "d9143e03fcd7f2f0e4db9a86d4b64aca0e39e99c303059d5841450cc4481b29c"}, "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, From 0a71dbce333fa8db6b0df3521b74f7194dd2e90b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 29 Apr 2025 12:24:38 +0300 Subject: [PATCH 0004/1006] chore(deps): bump ex_cldr_numbers from 2.35.0 to 2.35.1 (#12338) Bumps [ex_cldr_numbers](https://github.com/elixir-cldr/cldr_numbers) from 2.35.0 to 2.35.1. - [Release notes](https://github.com/elixir-cldr/cldr_numbers/releases) - [Changelog](https://github.com/elixir-cldr/cldr_numbers/blob/main/CHANGELOG.md) - [Commits](https://github.com/elixir-cldr/cldr_numbers/compare/v2.35.0...v2.35.1) --- updated-dependencies: - dependency-name: ex_cldr_numbers dependency-version: 2.35.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index f5207b9792f0..b755826a5231 100644 --- a/mix.lock +++ b/mix.lock @@ -19,7 +19,7 @@ "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, - "cldr_utils": {:hex, :cldr_utils, "2.28.2", "f500667164a9043369071e4f9dcef31f88b8589b2e2c07a1eb9f9fa53cb1dce9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c506eb1a170ba7cdca59b304ba02a56795ed119856662f6b1a420af80ec42551"}, + "cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -55,7 +55,7 @@ "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.0", "66d3e6af936abcaad6a1ab89ecd6ccef107e3198152315a78b76b398782be367", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "0146dbe6c9665fb725ae2345c8c6d033e1d9526a8ef848e187cc9042886729bf"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, From 700de52acd05056801de9e36044cbb029002327a Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 30 Apr 2025 12:10:52 +0400 Subject: [PATCH 0005/1006] fix: Handle pending operations for empty blocks as well (#12349) --- apps/indexer/lib/indexer/block/fetcher.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index b90f5120e743..8820faa47bb2 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -500,10 +500,10 @@ defmodule Indexer.Block.Fetcher do def async_import_created_contract_codes(_, _), do: :ok - def async_import_internal_transactions(%{blocks: blocks, transactions: transactions}, realtime?) do + def async_import_internal_transactions(%{blocks: blocks} = imported, realtime?) do blocks |> Enum.map(fn %Block{number: block_number} -> block_number end) - |> InternalTransaction.async_fetch(transactions, realtime?, 10_000) + |> InternalTransaction.async_fetch(Map.get(imported, :transactions, []), realtime?, 10_000) end def async_import_internal_transactions(_, _), do: :ok From 4fef19d21e9b2bd7d3f9d6ebf6622f9f0e200246 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 30 Apr 2025 12:47:37 +0300 Subject: [PATCH 0006/1006] refactor: Single definition of smart-contract internal creation transaction query (#12335) * refactor: Single definition of smart-contract internal creation transaction query * Remove unused where_block_number_is_not_null function * Move definition of creation_internal_transaction_query/1 to Address module * Remove block_index order in contract_creation_internal_transaction_preload_query/0 * Remove unused internal_transaction_count/0 function * Update apps/explorer/lib/explorer/chain/address.ex Co-authored-by: Fedor Ivanov --------- Co-authored-by: Fedor Ivanov --- apps/explorer/lib/explorer/chain.ex | 35 +---- apps/explorer/lib/explorer/chain/address.ex | 26 +++- .../lib/explorer/chain/bridged_token.ex | 9 +- .../explorer/chain/internal_transaction.ex | 124 +--------------- .../lib/explorer/chain/smart_contract.ex | 8 +- .../chain/internal_transaction_test.exs | 134 ------------------ 6 files changed, 31 insertions(+), 305 deletions(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 1425debebffc..4c3b2851206d 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1441,24 +1441,6 @@ defmodule Explorer.Chain do Repo.all(query) end - @doc """ - The number of `t:Explorer.Chain.InternalTransaction.t/0`. - - iex> transaction = :transaction |> insert() |> with_block() - iex> insert(:internal_transaction, index: 0, transaction: transaction, block_hash: transaction.block_hash, block_index: 0) - iex> Explorer.Chain.internal_transaction_count() - 1 - - If there are none, the count is `0`. - - iex> Explorer.Chain.internal_transaction_count() - 0 - - """ - def internal_transaction_count do - Repo.aggregate(InternalTransaction.where_nonpending_block(), :count, :transaction_hash) - end - @doc """ Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`. @@ -2791,20 +2773,9 @@ defmodule Explorer.Chain do %{init: Data.to_string(input), created_contract_code: Data.to_string(created_contract_code)} end else - creation_int_transaction_query = - from( - itx in InternalTransaction, - join: t in assoc(itx, :transaction), - where: itx.created_contract_address_hash == ^address_hash, - where: t.status == ^1, - select: %{init: itx.init, created_contract_code: itx.created_contract_code}, - order_by: [desc: itx.block_number], - limit: ^1 - ) - - res = creation_int_transaction_query |> Repo.one() - - case res do + case address_hash + |> Address.creation_internal_transaction_query() + |> Repo.one() do %{init: init, created_contract_code: created_contract_code} -> init_str = Data.to_string(init) created_contract_code_str = Data.to_string(created_contract_code) diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 2bf29aee14a2..fc85d335bb75 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -805,8 +805,7 @@ defmodule Explorer.Chain.Address do it in InternalTransaction, order_by: [ asc_nulls_first: it.error, - desc: it.block_number, - desc: it.block_index + desc: it.block_number ], limit: 1 ) @@ -1008,4 +1007,27 @@ defmodule Explorer.Chain.Address do address_result end end + + @doc """ + Constructs a query to retrieve the most recent internal transaction that created + a smart contract at the specified `address_hash`. + + The query joins the `InternalTransaction` with its associated `Transaction`, + filters for internal transactions where the `created_contract_address_hash` matches + the given `address_hash`, and ensures that the transaction status is successful (`status == 1`). + + The results are ordered by `block_number` in descending order, and the query is limited + to return only the most recent matching internal transaction. + """ + @spec creation_internal_transaction_query(binary() | Hash.t()) :: Ecto.Query.t() + def creation_internal_transaction_query(address_hash) do + from( + it in InternalTransaction, + inner_join: t in assoc(it, :transaction), + where: it.created_contract_address_hash == ^address_hash, + where: t.status == ^1, + order_by: [desc: it.block_number], + limit: 1 + ) + end end diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex index 2f648e3396b2..8bb0a46f9efc 100644 --- a/apps/explorer/lib/explorer/chain/bridged_token.ex +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -20,6 +20,7 @@ defmodule Explorer.Chain.BridgedToken do alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} alias Explorer.Chain.{ + Address, BridgedToken, Hash, InternalTransaction, @@ -218,16 +219,10 @@ defmodule Explorer.Chain.BridgedToken do def fetch_omni_bridged_tokens_metadata(token_addresses) do Enum.each(token_addresses, fn token_address_hash -> created_from_internal_transaction_success_query = - from( - it in InternalTransaction, - inner_join: t in assoc(it, :transaction), - where: it.created_contract_address_hash == ^token_address_hash, - where: t.status == ^1 - ) + Address.creation_internal_transaction_query(token_address_hash) created_from_internal_transaction_success = created_from_internal_transaction_success_query - |> limit(1) |> Repo.one() created_from_transaction_query = diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 72ffcfc2ad88..1ce033eead5f 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} alias Explorer.Chain.DenormalizationHelper - alias Explorer.Chain.InternalTransaction.{Action, CallType, Result, Type} + alias Explorer.Chain.InternalTransaction.{CallType, Type} import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] @@ -574,10 +574,6 @@ defmodule Explorer.Chain.InternalTransaction do ) end - def where_block_number_is_not_null(query) do - where(query, [t], not is_nil(t.block_number)) - end - @doc """ Filters out internal_transactions of blocks that are flagged as needing fetching of internal_transactions @@ -590,123 +586,6 @@ defmodule Explorer.Chain.InternalTransaction do ) end - def internal_transactions_to_raw(internal_transactions) when is_list(internal_transactions) do - internal_transactions - |> Enum.map(&internal_transaction_to_raw/1) - |> add_subtraces() - end - - defp internal_transaction_to_raw(%{type: :call} = transaction) do - %{ - call_type: call_type, - to_address_hash: to_address_hash, - from_address_hash: from_address_hash, - input: input, - gas: gas, - value: value, - trace_address: trace_address - } = transaction - - action = %{ - "callType" => call_type, - "to" => to_address_hash, - "from" => from_address_hash, - "input" => input, - "gas" => gas, - "value" => value - } - - %{ - "type" => "call", - "action" => Action.to_raw(action), - "traceAddress" => trace_address - } - |> put_raw_call_error_or_result(transaction) - end - - defp internal_transaction_to_raw(%{type: type} = transaction) when type in [:create, :create2] do - %{ - from_address_hash: from_address_hash, - gas: gas, - init: init, - trace_address: trace_address, - value: value - } = transaction - - action = %{"from" => from_address_hash, "gas" => gas, "init" => init, "value" => value} - - %{ - "type" => Atom.to_string(type), - "action" => Action.to_raw(action), - "traceAddress" => trace_address - } - |> put_raw_create_error_or_result(transaction) - end - - defp internal_transaction_to_raw(%{type: :selfdestruct} = transaction) do - %{ - to_address_hash: to_address_hash, - from_address_hash: from_address_hash, - trace_address: trace_address, - value: value - } = transaction - - action = %{ - "address" => from_address_hash, - "balance" => value, - "refundAddress" => to_address_hash - } - - %{ - "type" => "suicide", - "action" => Action.to_raw(action), - "traceAddress" => trace_address - } - end - - defp add_subtraces(traces) do - Enum.map(traces, fn trace -> - Map.put(trace, "subtraces", count_subtraces(trace, traces)) - end) - end - - defp count_subtraces(%{"traceAddress" => trace_address}, traces) do - Enum.count(traces, fn %{"traceAddress" => trace_address_candidate} -> - direct_descendant?(trace_address, trace_address_candidate) - end) - end - - defp direct_descendant?([], [_]), do: true - - defp direct_descendant?([elem | remaining_left], [elem | remaining_right]), - do: direct_descendant?(remaining_left, remaining_right) - - defp direct_descendant?(_, _), do: false - - defp put_raw_call_error_or_result(raw, %{error: error}) when not is_nil(error) do - Map.put(raw, "error", error) - end - - defp put_raw_call_error_or_result(raw, %{gas_used: gas_used, output: output}) do - Map.put(raw, "result", Result.to_raw(%{"gasUsed" => gas_used, "output" => output})) - end - - defp put_raw_create_error_or_result(raw, %{error: error}) when not is_nil(error) do - Map.put(raw, "error", error) - end - - defp put_raw_create_error_or_result(raw, %{ - created_contract_code: code, - created_contract_address_hash: created_contract_address_hash, - gas_used: gas_used - }) do - Map.put( - raw, - "result", - Result.to_raw(%{"gasUsed" => gas_used, "code" => code, "address" => created_contract_address_hash}) - ) - end - @doc """ `t:Explorer.Chain.InternalTransaction/0`s in `t:Explorer.Chain.Transaction.t/0` with `hash`. @@ -720,7 +599,6 @@ defmodule Explorer.Chain.InternalTransaction do the `index` that is passed. """ - @spec all_transaction_to_internal_transactions(Hash.Full.t(), [ Chain.paging_options() | Chain.necessity_by_association_option() | Chain.api?() ]) :: [ diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 02f918a5e967..ffd1ee65a1af 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -685,13 +685,7 @@ defmodule Explorer.Chain.SmartContract do %{init: Data.to_string(input), transaction: transaction} end else - creation_int_transaction_query = - from( - itx in InternalTransaction, - join: t in assoc(itx, :transaction), - where: itx.created_contract_address_hash == ^address_hash, - where: t.status == ^1 - ) + creation_int_transaction_query = Address.creation_internal_transaction_query(address_hash) internal_transaction = creation_int_transaction_query |> Repo.one() diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index e2b5a3bde073..9c8396c2cb1e 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -683,138 +683,4 @@ defmodule Explorer.Chain.InternalTransactionTest do struct!(InternalTransaction, Keyword.merge(defaults, opts)) end - - describe "internal_transactions_to_raw" do - test "it adds subtrace count" do - transactions = [ - call_type(trace_address: []), - call_type(trace_address: [0]), - call_type(trace_address: [1]), - call_type(trace_address: [2]), - call_type(trace_address: [0, 0]), - call_type(trace_address: [0, 1]), - call_type(trace_address: [1, 0]), - call_type(trace_address: [0, 0, 0]), - call_type(trace_address: [0, 0, 1]), - call_type(trace_address: [0, 0, 2]), - call_type(trace_address: [0, 1, 0]), - call_type(trace_address: [0, 1, 1]) - ] - - subtraces = - transactions - |> InternalTransaction.internal_transactions_to_raw() - |> Enum.map(&Map.get(&1, "subtraces")) - - assert subtraces == [3, 2, 1, 0, 3, 2, 0, 0, 0, 0, 0, 0] - end - - test "it correctly formats a call" do - from = Factory.address_hash() - to = Factory.address_hash() - gas = 50_000 - gas_used = 25_000 - input = Factory.transaction_input() - value = 50 - output = Factory.transaction_input() - - call_transaction = - call_type( - from_address_hash: from, - to_address_hash: to, - gas: Decimal.new(gas), - gas_used: Decimal.new(gas_used), - input: input, - value: %Wei{value: value}, - output: output - ) - - [call] = InternalTransaction.internal_transactions_to_raw([call_transaction]) - - assert call == %{ - "action" => %{ - "callType" => "call", - "from" => to_string(from), - "gas" => integer_to_quantity(gas), - "input" => to_string(input), - "to" => to_string(to), - "value" => integer_to_quantity(value) - }, - "result" => %{ - "gasUsed" => integer_to_quantity(gas_used), - "output" => to_string(output) - }, - "subtraces" => 0, - "traceAddress" => [], - "type" => "call" - } - end - - test "it correctly formats a create" do - {:ok, contract_code} = Data.cast(Factory.contract_code_info().bytecode) - contract_address = Factory.address_hash() - from = Factory.address_hash() - gas = 50_000 - gas_used = 25_000 - init = Factory.transaction_input() - value = 50 - - create_transaction = - create_type( - from_address_hash: from, - created_contract_code: contract_code, - created_contract_address_hash: contract_address, - gas: Decimal.new(gas), - gas_used: Decimal.new(gas_used), - init: init, - value: %Wei{value: value} - ) - - [create] = InternalTransaction.internal_transactions_to_raw([create_transaction]) - - assert create == %{ - "action" => %{ - "from" => to_string(from), - "gas" => integer_to_quantity(gas), - "init" => to_string(init), - "value" => integer_to_quantity(value) - }, - "result" => %{ - "address" => to_string(contract_address), - "code" => to_string(contract_code), - "gasUsed" => integer_to_quantity(gas_used) - }, - "subtraces" => 0, - "traceAddress" => [], - "type" => "create" - } - end - - test "it correctly formats a selfdestruct" do - from_address = Factory.address_hash() - to_address = Factory.address_hash() - - value = 50 - - selfdestruct_transaction = - selfdestruct_type( - to_address_hash: to_address, - from_address_hash: from_address, - value: %Wei{value: value} - ) - - [selfdestruct] = InternalTransaction.internal_transactions_to_raw([selfdestruct_transaction]) - - assert selfdestruct == %{ - "action" => %{ - "address" => to_string(from_address), - "balance" => integer_to_quantity(value), - "refundAddress" => to_string(to_address) - }, - "subtraces" => 0, - "traceAddress" => [], - "type" => "suicide" - } - end - end end From e071644931f07ffbfd66349208602befd3651229 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 30 Apr 2025 15:14:05 +0300 Subject: [PATCH 0007/1006] refactor: Move `address_to_internal_transactions/2` to `Explorer.Chain.InternalTransaction` module (#12346) * refactor: Single definition of smart-contract internal creation transaction query * Move definition of creation_internal_transaction_query/1 to Address module * Refactor: Move address_to_internal_transactions/2 to InternalTransaction module --- ...address_internal_transaction_controller.ex | 4 +- .../controllers/api/v2/address_controller.ex | 4 +- apps/explorer/lib/explorer/chain.ex | 205 -------- .../address/internal_transactions.ex | 5 +- .../explorer/chain/internal_transaction.ex | 212 +++++++- .../chain/internal_transaction_test.exs | 454 ++++++++++++++++++ apps/explorer/test/explorer/chain_test.exs | 454 ------------------ 7 files changed, 669 insertions(+), 669 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex index f4c8844e2581..dd029269f00b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView} alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.{Address, InternalTransaction, Wei} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Phoenix.View @@ -34,7 +34,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) - internal_transactions_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) + internal_transactions_plus_one = InternalTransaction.address_to_internal_transactions(address_hash, full_options) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) next_page_path = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 51560ae4b4e8..0a548140394c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -30,7 +30,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Hash, Transaction} + alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction} alias Explorer.Chain.Address.Counters alias Explorer.Chain.Token.Instance @@ -417,7 +417,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> Keyword.merge(current_filter(params)) |> Keyword.merge(@api_true) - results_plus_one = Chain.address_to_internal_transactions(address_hash, full_options) + results_plus_one = InternalTransaction.address_to_internal_transactions(address_hash, full_options) {internal_transactions, next_page} = split_list_by_page(results_plus_one) next_page_params = diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 4c3b2851206d..96e107afef92 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -20,7 +20,6 @@ defmodule Explorer.Chain do select: 2, select: 3, subquery: 1, - union: 2, where: 2, where: 3 ] @@ -166,89 +165,6 @@ defmodule Explorer.Chain do @type ip :: {:ip, String.t()} @type show_scam_tokens? :: {:show_scam_tokens?, true | false} - @doc """ - `t:Explorer.Chain.InternalTransaction/0`s from the address with the given `hash`. - - This function excludes any internal transactions in the results where the - internal transaction has no siblings within the parent transaction. - - ## Options - - * `:direction` - if specified, will filter internal transactions by address type. If `:to` is specified, only - internal transactions where the "to" address matches will be returned. Likewise, if `:from` is specified, only - internal transactions where the "from" address matches will be returned. If `:direction` is omitted, internal - transactions either to or from the address will be returned. - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, - then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the page `entries`. - * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and - `:key` (a tuple of the lowest/oldest `{block_number, transaction_index, index}`) and. Results will be the internal - transactions older than the `block_number`, `transaction index`, and `index` that are passed. - - """ - @spec address_to_internal_transactions(Hash.Address.t(), [paging_options | necessity_by_association_option]) :: [ - InternalTransaction.t() - ] - def address_to_internal_transactions(hash, options \\ []) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - direction = Keyword.get(options, :direction) - - from_block = from_block(options) - to_block = to_block(options) - - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - case paging_options do - %PagingOptions{key: {0, 0, 0}} -> - [] - - _ -> - if direction == nil || direction == "" do - query_to_address_hash_wrapped = - InternalTransaction - |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(hash, :to_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> common_where_limit_order(paging_options) - |> wrapped_union_subquery() - - query_from_address_hash_wrapped = - InternalTransaction - |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(hash, :from_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> common_where_limit_order(paging_options) - |> wrapped_union_subquery() - - query_created_contract_address_hash_wrapped = - InternalTransaction - |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(hash, :created_contract_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> common_where_limit_order(paging_options) - |> wrapped_union_subquery() - - query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) - |> wrapped_union_subquery() - |> common_where_limit_order(paging_options) - |> preload(:block) - |> join_associations(necessity_by_association) - |> select_repo(options).all() - else - InternalTransaction - |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(hash, direction) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> common_where_limit_order(paging_options) - |> preload(:block) - |> join_associations(necessity_by_association) - |> select_repo(options).all() - end - end - end - def wrapped_union_subquery(query) do from( q in subquery(query), @@ -256,19 +172,6 @@ defmodule Explorer.Chain do ) end - defp common_where_limit_order(query, paging_options) do - query - |> InternalTransaction.where_is_different_from_parent_transaction() - |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) - |> limit(^paging_options.page_size) - |> order_by( - [it], - desc: it.block_number, - desc: it.transaction_index, - desc: it.index - ) - end - def address_hashes_to_mined_transactions_without_rewards(address_hashes, options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) @@ -3000,114 +2903,6 @@ defmodule Explorer.Chain do where(query, [coin_balance], coin_balance.block_number < ^block_number) end - def page_internal_transaction(_, _, _ \\ %{index_internal_transaction_desc_order: false}) - - def page_internal_transaction(query, %PagingOptions{key: nil}, _), do: query - - def page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}, %{ - index_internal_transaction_desc_order: desc_order - }) do - hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc_order) - end - - def page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_internal_transaction_desc_order: desc_order}) do - if desc_order do - query - else - where(query, [internal_transaction], internal_transaction.index > 0) - end - end - - def page_internal_transaction(query, %PagingOptions{key: {index}}, %{ - index_internal_transaction_desc_order: desc_order - }) do - if desc_order do - where(query, [internal_transaction], internal_transaction.index < ^index) - else - where(query, [internal_transaction], internal_transaction.index > ^index) - end - end - - defp hardcoded_where_for_page_internal_transaction(query, 0, 0, index, false), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number == 0 and - internal_transaction.transaction_index == 0 and internal_transaction.index > ^index - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, index, false), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index == 0 and internal_transaction.index > ^index) - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, false), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index < ^transaction_index) or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index == ^transaction_index and internal_transaction.index > ^index) - ) - - defp hardcoded_where_for_page_internal_transaction(query, 0, 0, index, true), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number == 0 and - internal_transaction.transaction_index == 0 and internal_transaction.index < ^index - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, 0, true), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, index, true), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index == 0 and internal_transaction.index < ^index) - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, 0, true), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index < ^transaction_index) - ) - - defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, true), - do: - where( - query, - [internal_transaction], - internal_transaction.block_number < ^block_number or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index < ^transaction_index) or - (internal_transaction.block_number == ^block_number and - internal_transaction.transaction_index == ^transaction_index and internal_transaction.index < ^index) - ) - defp page_logs(query, %PagingOptions{key: nil}), do: query defp page_logs(query, %PagingOptions{key: {index}}) do diff --git a/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex b/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex index 74b09cad11b6..5003d24b6cd9 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex @@ -3,8 +3,7 @@ defmodule Explorer.Chain.CsvExport.Address.InternalTransactions do Exports internal transactions to a csv file. """ - alias Explorer.Chain - alias Explorer.Chain.{Address, Hash, Transaction, Wei} + alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction, Wei} alias Explorer.Chain.CsvExport.Helper @spec export(Hash.Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() @@ -40,7 +39,7 @@ defmodule Explorer.Chain.CsvExport.Address.InternalTransactions do else: &1 )).() - Chain.address_to_internal_transactions(address_hash, options) + InternalTransaction.address_to_internal_transactions(address_hash, options) end defp to_csv_format(internal_transactions) do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 1ce033eead5f..74918e94c874 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -5,6 +5,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} + alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{CallType, Type} @@ -612,7 +613,7 @@ defmodule Explorer.Chain.InternalTransaction do |> for_parent_transaction(hash) |> Chain.join_associations(necessity_by_association) |> where_nonpending_block() - |> Chain.page_internal_transaction(paging_options) + |> page_internal_transaction(paging_options) |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.index) |> Chain.select_repo(options).all() @@ -634,7 +635,7 @@ defmodule Explorer.Chain.InternalTransaction do |> where_transaction_has_multiple_internal_transactions() |> where_is_different_from_parent_transaction() |> where_nonpending_block() - |> Chain.page_internal_transaction(paging_options) + |> page_internal_transaction(paging_options) |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.index) |> preload(:block) @@ -666,6 +667,211 @@ defmodule Explorer.Chain.InternalTransaction do |> Chain.select_repo(options).all() end + @doc """ + `t:Explorer.Chain.InternalTransaction/0`s from the address with the given `hash`. + + This function excludes any internal transactions in the results where the + internal transaction has no siblings within the parent transaction. + + ## Options + + * `:direction` - if specified, will filter internal transactions by address type. If `:to` is specified, only + internal transactions where the "to" address matches will be returned. Likewise, if `:from` is specified, only + internal transactions where the "from" address matches will be returned. If `:direction` is omitted, internal + transactions either to or from the address will be returned. + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, + then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the page `entries`. + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and + `:key` (a tuple of the lowest/oldest `{block_number, transaction_index, index}`) and. Results will be the internal + transactions older than the `block_number`, `transaction index`, and `index` that are passed. + + """ + @spec address_to_internal_transactions(Hash.Address.t(), [paging_options | Chain.necessity_by_association_option()]) :: + [ + __MODULE__.t() + ] + def address_to_internal_transactions(hash, options \\ []) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + direction = Keyword.get(options, :direction) + + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + case paging_options do + %PagingOptions{key: {0, 0, 0}} -> + [] + + _ -> + if direction == nil || direction == "" do + query_to_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :to_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_from_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :from_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_created_contract_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :created_contract_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_to_address_hash_wrapped + |> union(^query_from_address_hash_wrapped) + |> union(^query_created_contract_address_hash_wrapped) + |> Chain.wrapped_union_subquery() + |> common_where_limit_order(paging_options) + |> preload(:block) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + else + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, direction) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> common_where_limit_order(paging_options) + |> preload(:block) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + end + end + + defp common_where_limit_order(query, paging_options) do + query + |> where_is_different_from_parent_transaction() + |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) + |> limit(^paging_options.page_size) + |> order_by( + [it], + desc: it.block_number, + desc: it.transaction_index, + desc: it.index + ) + end + + defp page_internal_transaction(_, _, _ \\ %{index_internal_transaction_desc_order: false}) + + defp page_internal_transaction(query, %PagingOptions{key: nil}, _), do: query + + defp page_internal_transaction(query, %PagingOptions{key: {block_number, transaction_index, index}}, %{ + index_internal_transaction_desc_order: desc_order + }) do + hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, desc_order) + end + + defp page_internal_transaction(query, %PagingOptions{key: {0}}, %{index_internal_transaction_desc_order: desc_order}) do + if desc_order do + query + else + where(query, [internal_transaction], internal_transaction.index > 0) + end + end + + defp page_internal_transaction(query, %PagingOptions{key: {index}}, %{ + index_internal_transaction_desc_order: desc_order + }) do + if desc_order do + where(query, [internal_transaction], internal_transaction.index < ^index) + else + where(query, [internal_transaction], internal_transaction.index > ^index) + end + end + + defp hardcoded_where_for_page_internal_transaction(query, 0, 0, index, false), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number == 0 and + internal_transaction.transaction_index == 0 and internal_transaction.index > ^index + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, index, false), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index == 0 and internal_transaction.index > ^index) + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, false), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index < ^transaction_index) or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index == ^transaction_index and internal_transaction.index > ^index) + ) + + defp hardcoded_where_for_page_internal_transaction(query, 0, 0, index, true), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number == 0 and + internal_transaction.transaction_index == 0 and internal_transaction.index < ^index + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, 0, true), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, 0, index, true), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index == 0 and internal_transaction.index < ^index) + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, 0, true), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index < ^transaction_index) + ) + + defp hardcoded_where_for_page_internal_transaction(query, block_number, transaction_index, index, true), + do: + where( + query, + [internal_transaction], + internal_transaction.block_number < ^block_number or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index < ^transaction_index) or + (internal_transaction.block_number == ^block_number and + internal_transaction.transaction_index == ^transaction_index and internal_transaction.index < ^index) + ) + defp for_parent_transaction(query, %Hash{byte_count: unquote(Hash.Full.byte_count())} = hash) do from( child in query, @@ -741,7 +947,7 @@ defmodule Explorer.Chain.InternalTransaction do __MODULE__ |> where_nonpending_block() - |> Chain.page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) + |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) |> where_internal_transactions_by_transaction_hash(Keyword.get(options, :transaction_hash)) |> order_by([internal_transaction], desc: internal_transaction.block_number, diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index 9c8396c2cb1e..f82532e9f1de 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -636,6 +636,460 @@ defmodule Explorer.Chain.InternalTransactionTest do end end + describe "address_to_internal_transactions/1" do + test "with single transaction containing two internal transactions" do + address = insert(:address) + + block = insert(:block, number: 2000) + + transaction = + :transaction + |> insert() + |> with_block(block) + + %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = + insert(:internal_transaction, + index: 1, + transaction: transaction, + to_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 1, + transaction_index: transaction.index + ) + + %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = + insert(:internal_transaction, + index: 2, + transaction: transaction, + to_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 2, + transaction_index: transaction.index + ) + + result = + address.hash + |> InternalTransaction.address_to_internal_transactions() + |> Enum.map(&{&1.transaction_hash, &1.index}) + + assert Enum.member?(result, {first_transaction_hash, first_index}) + assert Enum.member?(result, {second_transaction_hash, second_index}) + end + + test "loads associations in necessity_by_association" do + %Address{hash: address_hash} = address = insert(:address) + block = insert(:block, number: 2000) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + insert(:internal_transaction, + transaction: transaction, + to_address: address, + index: 1, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 1, + transaction_index: transaction.index + ) + + assert [ + %InternalTransaction{ + from_address: %Ecto.Association.NotLoaded{}, + to_address: %Ecto.Association.NotLoaded{}, + transaction: %Ecto.Association.NotLoaded{} + } + | _ + ] = InternalTransaction.address_to_internal_transactions(address_hash) + + assert [ + %InternalTransaction{ + from_address: %Address{}, + to_address: %Address{}, + transaction: %Transaction{} + } + | _ + ] = + InternalTransaction.address_to_internal_transactions( + address_hash, + necessity_by_association: %{ + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :transaction => :optional + } + ) + end + + test "returns results in reverse chronological order by block number, transaction index, internal transaction index" do + address = insert(:address) + + block = insert(:block, number: 7000) + + pending_transaction = + :transaction + |> insert() + |> with_block(block) + + %InternalTransaction{transaction_hash: first_pending_transaction_hash, index: first_pending_index} = + insert( + :internal_transaction, + transaction: pending_transaction, + to_address: address, + index: 1, + block_number: pending_transaction.block_number, + block_hash: pending_transaction.block_hash, + block_index: 1, + transaction_index: pending_transaction.index + ) + + %InternalTransaction{transaction_hash: second_pending_transaction_hash, index: second_pending_index} = + insert( + :internal_transaction, + transaction: pending_transaction, + to_address: address, + index: 2, + block_number: pending_transaction.block_number, + block_hash: pending_transaction.block_hash, + block_index: 2, + transaction_index: pending_transaction.index + ) + + a_block = insert(:block, number: 2000) + + first_a_transaction = + :transaction + |> insert() + |> with_block(a_block) + + %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = + insert( + :internal_transaction, + transaction: first_a_transaction, + to_address: address, + index: 1, + block_number: first_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 1, + transaction_index: first_a_transaction.index + ) + + %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = + insert( + :internal_transaction, + transaction: first_a_transaction, + to_address: address, + index: 2, + block_number: first_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 2, + transaction_index: first_a_transaction.index + ) + + second_a_transaction = + :transaction + |> insert() + |> with_block(a_block) + + %InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} = + insert( + :internal_transaction, + transaction: second_a_transaction, + to_address: address, + index: 1, + block_number: second_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 4, + transaction_index: second_a_transaction.index + ) + + %InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} = + insert( + :internal_transaction, + transaction: second_a_transaction, + to_address: address, + index: 2, + block_number: second_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 5, + transaction_index: second_a_transaction.index + ) + + b_block = insert(:block, number: 6000) + + first_b_transaction = + :transaction + |> insert() + |> with_block(b_block) + + %InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} = + insert( + :internal_transaction, + transaction: first_b_transaction, + to_address: address, + index: 1, + block_number: first_b_transaction.block_number, + block_hash: b_block.hash, + block_index: 1, + transaction_index: first_b_transaction.index + ) + + %InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} = + insert( + :internal_transaction, + transaction: first_b_transaction, + to_address: address, + index: 2, + block_number: first_b_transaction.block_number, + block_hash: b_block.hash, + block_index: 2, + transaction_index: first_b_transaction.index + ) + + result = + address.hash + |> InternalTransaction.address_to_internal_transactions() + |> Enum.map(&{&1.transaction_hash, &1.index}) + + assert [ + {second_pending_transaction_hash, second_pending_index}, + {first_pending_transaction_hash, first_pending_index}, + {sixth_transaction_hash, sixth_index}, + {fifth_transaction_hash, fifth_index}, + {fourth_transaction_hash, fourth_index}, + {third_transaction_hash, third_index}, + {second_transaction_hash, second_index}, + {first_transaction_hash, first_index} + ] == result + end + + test "pages by {block_number, transaction_index, index}" do + address = insert(:address) + + pending_transaction = insert(:transaction) + + old_block = insert(:block, consensus: false) + + insert( + :internal_transaction, + transaction: pending_transaction, + to_address: address, + block_hash: old_block.hash, + block_index: 1, + index: 1 + ) + + insert( + :internal_transaction, + transaction: pending_transaction, + to_address: address, + block_hash: old_block.hash, + block_index: 2, + index: 2 + ) + + a_block = insert(:block, number: 2000) + + first_a_transaction = + :transaction + |> insert() + |> with_block(a_block) + + %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = + insert( + :internal_transaction, + transaction: first_a_transaction, + to_address: address, + index: 1, + block_number: first_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 1, + transaction_index: first_a_transaction.index + ) + + %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = + insert( + :internal_transaction, + transaction: first_a_transaction, + to_address: address, + index: 2, + block_number: first_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 2, + transaction_index: first_a_transaction.index + ) + + second_a_transaction = + :transaction + |> insert() + |> with_block(a_block) + + %InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} = + insert( + :internal_transaction, + transaction: second_a_transaction, + to_address: address, + index: 1, + block_number: second_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 4, + transaction_index: second_a_transaction.index + ) + + %InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} = + insert( + :internal_transaction, + transaction: second_a_transaction, + to_address: address, + index: 2, + block_number: second_a_transaction.block_number, + block_hash: a_block.hash, + block_index: 5, + transaction_index: second_a_transaction.index + ) + + b_block = insert(:block, number: 6000) + + first_b_transaction = + :transaction + |> insert() + |> with_block(b_block) + + %InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} = + insert( + :internal_transaction, + transaction: first_b_transaction, + to_address: address, + index: 1, + block_number: first_b_transaction.block_number, + block_hash: b_block.hash, + block_index: 1, + transaction_index: first_b_transaction.index + ) + + %InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} = + insert( + :internal_transaction, + transaction: first_b_transaction, + to_address: address, + index: 2, + block_number: first_b_transaction.block_number, + block_hash: b_block.hash, + block_index: 2, + transaction_index: first_b_transaction.index + ) + + # When paged, internal transactions need an associated block number, so `second_pending` and `first_pending` are + # excluded. + assert [ + {sixth_transaction_hash, sixth_index}, + {fifth_transaction_hash, fifth_index}, + {fourth_transaction_hash, fourth_index}, + {third_transaction_hash, third_index}, + {second_transaction_hash, second_index}, + {first_transaction_hash, first_index} + ] == + address.hash + |> InternalTransaction.address_to_internal_transactions( + paging_options: %PagingOptions{key: {6001, 3, 2}, page_size: 8} + ) + |> Enum.map(&{&1.transaction_hash, &1.index}) + + # block number ==, transaction index ==, internal transaction index < + assert [ + {fourth_transaction_hash, fourth_index}, + {third_transaction_hash, third_index}, + {second_transaction_hash, second_index}, + {first_transaction_hash, first_index} + ] == + address.hash + |> InternalTransaction.address_to_internal_transactions( + paging_options: %PagingOptions{key: {6000, 0, 1}, page_size: 8} + ) + |> Enum.map(&{&1.transaction_hash, &1.index}) + + # block number ==, transaction index < + assert [ + {fourth_transaction_hash, fourth_index}, + {third_transaction_hash, third_index}, + {second_transaction_hash, second_index}, + {first_transaction_hash, first_index} + ] == + address.hash + |> InternalTransaction.address_to_internal_transactions( + paging_options: %PagingOptions{key: {6000, -1, -1}, page_size: 8} + ) + |> Enum.map(&{&1.transaction_hash, &1.index}) + + # block number < + assert [] == + address.hash + |> InternalTransaction.address_to_internal_transactions( + paging_options: %PagingOptions{key: {2000, -1, -1}, page_size: 8} + ) + |> Enum.map(&{&1.transaction_hash, &1.index}) + end + + test "excludes internal transactions of type `call` when they are alone in the parent transaction" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + insert(:internal_transaction, + index: 0, + to_address: address, + transaction: transaction, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + assert Enum.empty?(InternalTransaction.address_to_internal_transactions(address_hash)) + end + + test "includes internal transactions of type `create` even when they are alone in the parent transaction" do + %Address{hash: address_hash} = address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + expected = + insert( + :internal_transaction_create, + index: 0, + from_address: address, + transaction: transaction, + block_hash: transaction.block_hash, + block_index: 0, + block_number: transaction.block_number, + transaction_index: transaction.index + ) + + actual = Enum.at(InternalTransaction.address_to_internal_transactions(address_hash), 0) + + assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index} + end + end + defp call_type(opts) do defaults = [ type: :call, diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 036a4f272eab..7732bd54bdcb 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1549,460 +1549,6 @@ defmodule Explorer.ChainTest do end end - describe "address_to_internal_transactions/1" do - test "with single transaction containing two internal transactions" do - address = insert(:address) - - block = insert(:block, number: 2000) - - transaction = - :transaction - |> insert() - |> with_block(block) - - %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = - insert(:internal_transaction, - index: 1, - transaction: transaction, - to_address: address, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 1, - transaction_index: transaction.index - ) - - %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = - insert(:internal_transaction, - index: 2, - transaction: transaction, - to_address: address, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 2, - transaction_index: transaction.index - ) - - result = - address.hash - |> Chain.address_to_internal_transactions() - |> Enum.map(&{&1.transaction_hash, &1.index}) - - assert Enum.member?(result, {first_transaction_hash, first_index}) - assert Enum.member?(result, {second_transaction_hash, second_index}) - end - - test "loads associations in necessity_by_association" do - %Address{hash: address_hash} = address = insert(:address) - block = insert(:block, number: 2000) - - transaction = - :transaction - |> insert() - |> with_block(block) - - insert(:internal_transaction, - transaction: transaction, - to_address: address, - index: 0, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - insert(:internal_transaction, - transaction: transaction, - to_address: address, - index: 1, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 1, - transaction_index: transaction.index - ) - - assert [ - %InternalTransaction{ - from_address: %Ecto.Association.NotLoaded{}, - to_address: %Ecto.Association.NotLoaded{}, - transaction: %Ecto.Association.NotLoaded{} - } - | _ - ] = Chain.address_to_internal_transactions(address_hash) - - assert [ - %InternalTransaction{ - from_address: %Address{}, - to_address: %Address{}, - transaction: %Transaction{} - } - | _ - ] = - Chain.address_to_internal_transactions( - address_hash, - necessity_by_association: %{ - [from_address: :names] => :optional, - [to_address: :names] => :optional, - :transaction => :optional - } - ) - end - - test "returns results in reverse chronological order by block number, transaction index, internal transaction index" do - address = insert(:address) - - block = insert(:block, number: 7000) - - pending_transaction = - :transaction - |> insert() - |> with_block(block) - - %InternalTransaction{transaction_hash: first_pending_transaction_hash, index: first_pending_index} = - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - index: 1, - block_number: pending_transaction.block_number, - block_hash: pending_transaction.block_hash, - block_index: 1, - transaction_index: pending_transaction.index - ) - - %InternalTransaction{transaction_hash: second_pending_transaction_hash, index: second_pending_index} = - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - index: 2, - block_number: pending_transaction.block_number, - block_hash: pending_transaction.block_hash, - block_index: 2, - transaction_index: pending_transaction.index - ) - - a_block = insert(:block, number: 2000) - - first_a_transaction = - :transaction - |> insert() - |> with_block(a_block) - - %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = - insert( - :internal_transaction, - transaction: first_a_transaction, - to_address: address, - index: 1, - block_number: first_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 1, - transaction_index: first_a_transaction.index - ) - - %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = - insert( - :internal_transaction, - transaction: first_a_transaction, - to_address: address, - index: 2, - block_number: first_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 2, - transaction_index: first_a_transaction.index - ) - - second_a_transaction = - :transaction - |> insert() - |> with_block(a_block) - - %InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} = - insert( - :internal_transaction, - transaction: second_a_transaction, - to_address: address, - index: 1, - block_number: second_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 4, - transaction_index: second_a_transaction.index - ) - - %InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} = - insert( - :internal_transaction, - transaction: second_a_transaction, - to_address: address, - index: 2, - block_number: second_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 5, - transaction_index: second_a_transaction.index - ) - - b_block = insert(:block, number: 6000) - - first_b_transaction = - :transaction - |> insert() - |> with_block(b_block) - - %InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} = - insert( - :internal_transaction, - transaction: first_b_transaction, - to_address: address, - index: 1, - block_number: first_b_transaction.block_number, - block_hash: b_block.hash, - block_index: 1, - transaction_index: first_b_transaction.index - ) - - %InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} = - insert( - :internal_transaction, - transaction: first_b_transaction, - to_address: address, - index: 2, - block_number: first_b_transaction.block_number, - block_hash: b_block.hash, - block_index: 2, - transaction_index: first_b_transaction.index - ) - - result = - address.hash - |> Chain.address_to_internal_transactions() - |> Enum.map(&{&1.transaction_hash, &1.index}) - - assert [ - {second_pending_transaction_hash, second_pending_index}, - {first_pending_transaction_hash, first_pending_index}, - {sixth_transaction_hash, sixth_index}, - {fifth_transaction_hash, fifth_index}, - {fourth_transaction_hash, fourth_index}, - {third_transaction_hash, third_index}, - {second_transaction_hash, second_index}, - {first_transaction_hash, first_index} - ] == result - end - - test "pages by {block_number, transaction_index, index}" do - address = insert(:address) - - pending_transaction = insert(:transaction) - - old_block = insert(:block, consensus: false) - - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - block_hash: old_block.hash, - block_index: 1, - index: 1 - ) - - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - block_hash: old_block.hash, - block_index: 2, - index: 2 - ) - - a_block = insert(:block, number: 2000) - - first_a_transaction = - :transaction - |> insert() - |> with_block(a_block) - - %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = - insert( - :internal_transaction, - transaction: first_a_transaction, - to_address: address, - index: 1, - block_number: first_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 1, - transaction_index: first_a_transaction.index - ) - - %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = - insert( - :internal_transaction, - transaction: first_a_transaction, - to_address: address, - index: 2, - block_number: first_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 2, - transaction_index: first_a_transaction.index - ) - - second_a_transaction = - :transaction - |> insert() - |> with_block(a_block) - - %InternalTransaction{transaction_hash: third_transaction_hash, index: third_index} = - insert( - :internal_transaction, - transaction: second_a_transaction, - to_address: address, - index: 1, - block_number: second_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 4, - transaction_index: second_a_transaction.index - ) - - %InternalTransaction{transaction_hash: fourth_transaction_hash, index: fourth_index} = - insert( - :internal_transaction, - transaction: second_a_transaction, - to_address: address, - index: 2, - block_number: second_a_transaction.block_number, - block_hash: a_block.hash, - block_index: 5, - transaction_index: second_a_transaction.index - ) - - b_block = insert(:block, number: 6000) - - first_b_transaction = - :transaction - |> insert() - |> with_block(b_block) - - %InternalTransaction{transaction_hash: fifth_transaction_hash, index: fifth_index} = - insert( - :internal_transaction, - transaction: first_b_transaction, - to_address: address, - index: 1, - block_number: first_b_transaction.block_number, - block_hash: b_block.hash, - block_index: 1, - transaction_index: first_b_transaction.index - ) - - %InternalTransaction{transaction_hash: sixth_transaction_hash, index: sixth_index} = - insert( - :internal_transaction, - transaction: first_b_transaction, - to_address: address, - index: 2, - block_number: first_b_transaction.block_number, - block_hash: b_block.hash, - block_index: 2, - transaction_index: first_b_transaction.index - ) - - # When paged, internal transactions need an associated block number, so `second_pending` and `first_pending` are - # excluded. - assert [ - {sixth_transaction_hash, sixth_index}, - {fifth_transaction_hash, fifth_index}, - {fourth_transaction_hash, fourth_index}, - {third_transaction_hash, third_index}, - {second_transaction_hash, second_index}, - {first_transaction_hash, first_index} - ] == - address.hash - |> Chain.address_to_internal_transactions( - paging_options: %PagingOptions{key: {6001, 3, 2}, page_size: 8} - ) - |> Enum.map(&{&1.transaction_hash, &1.index}) - - # block number ==, transaction index ==, internal transaction index < - assert [ - {fourth_transaction_hash, fourth_index}, - {third_transaction_hash, third_index}, - {second_transaction_hash, second_index}, - {first_transaction_hash, first_index} - ] == - address.hash - |> Chain.address_to_internal_transactions( - paging_options: %PagingOptions{key: {6000, 0, 1}, page_size: 8} - ) - |> Enum.map(&{&1.transaction_hash, &1.index}) - - # block number ==, transaction index < - assert [ - {fourth_transaction_hash, fourth_index}, - {third_transaction_hash, third_index}, - {second_transaction_hash, second_index}, - {first_transaction_hash, first_index} - ] == - address.hash - |> Chain.address_to_internal_transactions( - paging_options: %PagingOptions{key: {6000, -1, -1}, page_size: 8} - ) - |> Enum.map(&{&1.transaction_hash, &1.index}) - - # block number < - assert [] == - address.hash - |> Chain.address_to_internal_transactions( - paging_options: %PagingOptions{key: {2000, -1, -1}, page_size: 8} - ) - |> Enum.map(&{&1.transaction_hash, &1.index}) - end - - test "excludes internal transactions of type `call` when they are alone in the parent transaction" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(to_address: address) - |> with_block() - - insert(:internal_transaction, - index: 0, - to_address: address, - transaction: transaction, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - assert Enum.empty?(Chain.address_to_internal_transactions(address_hash)) - end - - test "includes internal transactions of type `create` even when they are alone in the parent transaction" do - %Address{hash: address_hash} = address = insert(:address) - - transaction = - :transaction - |> insert(to_address: address) - |> with_block() - - expected = - insert( - :internal_transaction_create, - index: 0, - from_address: address, - transaction: transaction, - block_hash: transaction.block_hash, - block_index: 0, - block_number: transaction.block_number, - transaction_index: transaction.index - ) - - actual = Enum.at(Chain.address_to_internal_transactions(address_hash), 0) - - assert {actual.transaction_hash, actual.index} == {expected.transaction_hash, expected.index} - end - end - describe "pending_transactions/0" do test "without transactions" do assert [] = Chain.recent_pending_transactions() From 1adf8e8709d6c24c6e11723ff5c74607e5407988 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Wed, 30 Apr 2025 22:36:20 +0400 Subject: [PATCH 0008/1006] fix: on demand bytecode fetcher for eip7702 addresses (#12330) * fix: on demand bytecode fetcher for eip7702 addresses * fix: update eip7702 proxy_implementations * chore: use on_exit in tests * chore: refactor to Chain.import * chore: apply suggestions --- .../explorer/chain/import/runner/addresses.ex | 29 ++++-- .../explorer/chain/smart_contract/proxy.ex | 2 +- .../proxy/models/implementation.ex | 25 ++++- .../fetcher/on_demand/contract_code.ex | 41 +++++++-- .../fetcher/on_demand/contract_creator.ex | 1 + .../fetcher/on_demand/contract_code_test.exs | 91 ++++++++++++++++++- 6 files changed, 166 insertions(+), 23 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/addresses.ex b/apps/explorer/lib/explorer/chain/import/runner/addresses.ex index 0c01af5bc917..7f416a810066 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/addresses.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/addresses.ex @@ -9,7 +9,7 @@ defmodule Explorer.Chain.Import.Runner.Addresses do alias Ecto.{Multi, Repo} alias Explorer.Chain.Filecoin.PendingAddressOperation, as: FilecoinPendingAddressOperation alias Explorer.Chain.Import.Runner - alias Explorer.Chain.{Address, Hash, Import, Transaction} + alias Explorer.Chain.{Address, Import, Transaction} alias Explorer.Prometheus.Instrumenter require Ecto.Query @@ -73,7 +73,7 @@ defmodule Explorer.Chain.Import.Runner.Addresses do multi |> Multi.run(:filter_addresses, fn repo, _ -> Instrumenter.block_import_stage_runner( - fn -> filter_addresses(repo, ordered_changes_list) end, + fn -> filter_addresses(repo, ordered_changes_list, options[option_key()][:fields_to_update]) end, :addresses, :addresses, :filter_addresses @@ -120,10 +120,13 @@ defmodule Explorer.Chain.Import.Runner.Addresses do @impl Import.Runner def timeout, do: @timeout + @impl Import.Runner + def runner_specific_options, do: [:fields_to_update] + ## Private Functions - @spec filter_addresses(Repo.t(), [map()]) :: {:ok, {[map()], map()}} - defp filter_addresses(repo, changes_list) do + @spec filter_addresses(Repo.t(), [map()], [atom()] | nil) :: {:ok, {[map()], map()}} + defp filter_addresses(repo, changes_list, fields_to_update) do hashes = Enum.map(changes_list, & &1.hash) existing_addresses_query = @@ -142,7 +145,7 @@ defmodule Explorer.Chain.Import.Runner.Addresses do |> Enum.reduce([], fn address, acc -> existing_address = existing_addresses_map[address.hash] - if should_update?(address, existing_address) do + if should_update?(address, existing_address, fields_to_update) do [address | acc] else acc @@ -153,9 +156,10 @@ defmodule Explorer.Chain.Import.Runner.Addresses do {:ok, {filtered_addresses, existing_addresses_map}} end - defp should_update?(new_address, existing_address) do - is_nil(existing_address) or - (not is_nil(new_address[:contract_code]) and new_address[:contract_code] != existing_address.contract_code) or + defp should_update?(_new_address, nil, _fields_to_replace), do: true + + defp should_update?(new_address, existing_address, nil) do + (not is_nil(new_address[:contract_code]) and new_address[:contract_code] != existing_address.contract_code) or (not is_nil(new_address[:fetched_coin_balance_block_number]) and (is_nil(existing_address.fetched_coin_balance_block_number) or new_address[:fetched_coin_balance_block_number] >= existing_address.fetched_coin_balance_block_number)) or @@ -163,7 +167,12 @@ defmodule Explorer.Chain.Import.Runner.Addresses do (is_nil(existing_address.nonce) or new_address[:nonce] > existing_address.nonce)) end - @spec insert(Repo.t(), [%{hash: Hash.Address.t()}], %{ + defp should_update?(new_address, existing_address, fields_to_replace) do + fields_to_replace + |> Enum.any?(fn field -> Map.get(existing_address, field) != Map.get(new_address, field) end) + end + + @spec insert(Repo.t(), [map()], %{ optional(:on_conflict) => Import.Runner.on_conflict(), required(:timeout) => timeout, required(:timestamps) => Import.timestamps() @@ -232,7 +241,7 @@ defmodule Explorer.Chain.Import.Runner.Addresses do # where any of `set`s would make a change # This is so that tuples are only generated when a change would occur where: - fragment("COALESCE(?, EXCLUDED.contract_code) IS DISTINCT FROM ?", address.contract_code, address.contract_code) or + fragment("COALESCE(EXCLUDED.contract_code, ?) IS DISTINCT FROM ?", address.contract_code, address.contract_code) or fragment( "EXCLUDED.fetched_coin_balance_block_number IS NOT NULL AND (? IS NULL OR EXCLUDED.fetched_coin_balance_block_number >= ?)", address.fetched_coin_balance_block_number, diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex index e31711703d46..e86cf6a98323 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -201,8 +201,8 @@ defmodule Explorer.Chain.SmartContract.Proxy do functions = [ :get_implementation_address_hash_string_eip1167, - :get_implementation_address_hash_string_clones_with_immutable_arguments, :get_implementation_address_hash_string_eip7702, + :get_implementation_address_hash_string_clones_with_immutable_arguments, :get_implementation_address_hash_string_eip1967, :get_implementation_address_hash_string_eip1822, :get_implementation_address_hash_string_eip2535, diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index 13fb70519dfd..7a03d5601004 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -11,7 +11,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do import Ecto.Query, only: [ from: 2, - select: 3 + select: 3, + where: 3 ] alias Explorer.{Chain, Repo} @@ -423,6 +424,28 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do |> Repo.update() end + @doc """ + Deletes all proxy implementations associated with the given proxy address hash. + + ## Parameters + + - `address_hash` (binary): The hash of the proxy address whose implementations + should be deleted. + + ## Returns + + - `{count, nil}`: A tuple where `count` is the number of records deleted. + + This function uses a query to find all proxy implementations matching the + provided `address_hash` and deletes them from the database. + """ + @spec delete_implementations(Hash.Address.t()) :: {non_neg_integer(), nil} + def delete_implementations(address_hash) do + __MODULE__ + |> where([proxy_implementations], proxy_implementations.proxy_address_hash == ^address_hash) + |> Repo.delete_all() + end + # Cut off implementations per proxy up to @max_implementations_number_per_proxy number # before insert into the DB to prevent DoS via the verification endpoint of Diamond smart contracts. @spec sanitize_implementation_address_hash_strings([EthereumJSONRPC.hash()]) :: [EthereumJSONRPC.hash()] diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex index 3f7bfcd72c80..6e43593ad604 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex @@ -11,9 +11,11 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do import EthereumJSONRPC, only: [fetch_codes: 2] alias Explorer.Chain - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Data} alias Explorer.Chain.Cache.Counters.Helper alias Explorer.Chain.Events.Publisher + alias Explorer.Chain.SmartContract.Proxy.EIP7702 + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Utility.{AddressContractCodeFetchAttempt, RateLimiter} alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @@ -21,7 +23,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do @spec trigger_fetch(String.t() | nil, Address.t()) :: :ok def trigger_fetch(caller \\ nil, address) do - if is_nil(address.contract_code) do + if is_nil(address.contract_code) or Address.eoa_with_code?(address) do case RateLimiter.check_rate(caller, :on_demand) do :allow -> GenServer.cast(__MODULE__, {:fetch, address}) :deny -> :ok @@ -106,17 +108,42 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do )}, contract_code_object = List.first(fetched_codes), false <- is_nil(contract_code_object), - true <- contract_code_object.code !== "0x" do - case Chain.import(%{addresses: %{params: [%{hash: address.hash, contract_code: contract_code_object.code}]}}) do + {:ok, fetched_code} <- + (contract_code_object.code == "0x" && {:ok, nil}) || Data.cast(contract_code_object.code), + true <- fetched_code != address.contract_code do + case Chain.import(%{ + addresses: %{ + params: [%{hash: address.hash, contract_code: fetched_code}], + on_conflict: {:replace, [:contract_code, :updated_at]}, + fields_to_update: [:contract_code] + } + }) do {:ok, _} -> + # Update EIP7702 proxy addresses to avoid inconsistencies between addresses and proxy_implementations tables. + # Other proxy types are not handled here, since their bytecode doesn't change the way EIP7702 bytecode does. + cond do + Address.smart_contract?(address) and !Address.eoa_with_code?(address) -> + :ok + + is_nil(fetched_code) -> + Implementation.delete_implementations(address.hash) + + # TODO: it's better to use a generic code like this, but it does unnecessary minimal proxy checks and DB lookups + # Address.eoa_with_code?(new_address) -> + # Proxy.fetch_implementation_address_hash(address.hash, [], []) + + delegate = EIP7702.get_delegate_address(fetched_code.bytes) -> + Implementation.save_implementation_data([delegate |> to_string()], address.hash, :eip7702, []) + + true -> + :ok + end + Publisher.broadcast(%{fetched_bytecode: [address.hash, contract_code_object.code]}, :on_demand) ContractCreatorOnDemand.trigger_fetch(address) AddressContractCodeFetchAttempt.delete(address.hash) - - error -> - Logger.error(fn -> "Error while setting address #{address.hash} deployed bytecode: #{inspect(error)}" end) end else {:fetched_code, {:error, _}} -> diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex index c75a1770c5dc..82ce3004d755 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex @@ -43,6 +43,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do with false <- is_nil(address.contract_code), true <- is_nil(creator_hash), + false <- Address.eoa_with_code?(address), {:address_lookup, [{_, contract_creation_block_number}]} <- {:address_lookup, :ets.lookup(@table_name, address_cache_name(address.hash))}, {:pending_blocks_lookup, [{@pending_blocks_cache_key, blocks}]} <- diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs index 010aa14faf9a..0741e5d3b8a3 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs @@ -4,11 +4,13 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do import Mox - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Data, Hash} + alias Explorer.Chain.SmartContract.Proxy.EIP7702 + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Chain.Events.Subscriber alias Explorer.Utility.AddressContractCodeFetchAttempt alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand - + alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @moduletag :capture_log setup :set_mox_global @@ -20,6 +22,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) start_supervised!({ContractCodeOnDemand, [mocked_json_rpc_named_arguments, [name: ContractCodeOnDemand]]}) + start_supervised!({ContractCreatorOnDemand, name: ContractCreatorOnDemand}) %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} end @@ -112,6 +115,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do end test "updates contract_code after 2nd attempt" do + threshold_configuration = Application.get_env(:indexer, ContractCodeOnDemand)[:threshold] threshold = parse_time_env_var("CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD", "500ms") Application.put_env(:indexer, ContractCodeOnDemand, threshold: threshold) @@ -187,8 +191,87 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do assert_receive({:chain_event, :fetched_bytecode, :on_demand, [^address_hash, ^contract_code]}) - default_threshold = parse_time_env_var("CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD", "5s") - Application.put_env(:indexer, ContractCodeOnDemand, threshold: default_threshold) + on_exit(fn -> + Application.put_env(:indexer, ContractCodeOnDemand, threshold: threshold_configuration) + end) + end + + test "updates code for eip7702 address" do + threshold_configuration = Application.get_env(:indexer, ContractCodeOnDemand)[:threshold] + threshold = parse_time_env_var("CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD", "1ms") + Application.put_env(:indexer, ContractCodeOnDemand, threshold: threshold) + + address = insert(:address) + address_hash = address.hash + string_address_hash = to_string(address.hash) + + test_cases = [ + {"0x", 1}, + {"0x", 2}, + {"0xef0100aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 0}, + {"0xef0100aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 1}, + {"0xef0100bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 0}, + {"0xef0100bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 1}, + {"0xef0100bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 2}, + {"0x", 0}, + {"0x", 1} + ] + + test_cases + |> Enum.map(fn {code, attempts_number} -> + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: code}]} + end) + + code = code |> Data.cast() |> elem(1) + + address = assert(Repo.get(Address, address_hash)) + assert ContractCodeOnDemand.trigger_fetch(address) == :ok + + :timer.sleep(100) + + address = assert(Repo.get(Address, address_hash)) + + if Data.empty?(code) do + assert is_nil(address.contract_code) + else + assert address.contract_code == code + end + + attempts = Repo.get(AddressContractCodeFetchAttempt, address_hash) + + if attempts_number == 0 do + assert is_nil(attempts) + else + assert attempts.retries_number == attempts_number + end + + proxy_implementations = Implementation.get_proxy_implementations(address_hash) + + if Data.empty?(code) do + assert is_nil(proxy_implementations) + else + implementations = [EIP7702.get_delegate_address(code.bytes) |> Hash.Address.cast() |> elem(1)] + + assert proxy_implementations.proxy_address_hash == address_hash + assert proxy_implementations.proxy_type == :eip7702 + assert proxy_implementations.address_hashes == implementations + assert proxy_implementations.names == [nil] + end + end) + + on_exit(fn -> + Application.put_env(:indexer, ContractCodeOnDemand, threshold: threshold_configuration) + end) end defp parse_time_env_var(env_var, default_value) do From 11e23d17ba320480f462aa7d46d2bce230ab7006 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 21:32:01 +0300 Subject: [PATCH 0009/1006] chore(deps): bump core-js in /apps/block_scout_web/assets (#12356) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.41.0 to 3.42.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.42.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-version: 3.42.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 9e6fc2b6611c..47f454b03bdf 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -17,7 +17,7 @@ "chart.js": "^4.4.8", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.41.0", + "core-js": "^3.42.0", "crypto-browserify": "^3.12.1", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", @@ -5651,9 +5651,9 @@ } }, "node_modules/core-js": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", - "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==", + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -20364,9 +20364,9 @@ } }, "core-js": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.41.0.tgz", - "integrity": "sha512-SJ4/EHwS36QMJd6h/Rg+GyR4A5xE0FSI3eZ+iBVpfqf1x0eTSg1smWLHrA+2jQThZSh97fmSgFSU8B61nxosxA==" + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", + "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==" }, "core-js-compat": { "version": "3.41.0", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index e3782e8ab7cc..9c4cb087b117 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -29,7 +29,7 @@ "chart.js": "^4.4.8", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.41.0", + "core-js": "^3.42.0", "crypto-browserify": "^3.12.1", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", From d0d1efce80da5581054572bc0fe8ab083ff1efab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 21:55:32 +0300 Subject: [PATCH 0010/1006] chore(deps): bump @amplitude/analytics-browser (#12362) Bumps [@amplitude/analytics-browser](https://github.com/amplitude/Amplitude-TypeScript) from 2.12.2 to 2.16.1. - [Release notes](https://github.com/amplitude/Amplitude-TypeScript/releases) - [Commits](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@2.12.2...@amplitude/analytics-browser@2.16.1) --- updated-dependencies: - dependency-name: "@amplitude/analytics-browser" dependency-version: 2.16.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 133 +++++++----------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 53 insertions(+), 82 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 47f454b03bdf..5b85dfb7468a 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -7,7 +7,7 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "@amplitude/analytics-browser": "^2.12.2", + "@amplitude/analytics-browser": "^2.16.1", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", @@ -122,15 +122,15 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.12.2.tgz", - "integrity": "sha512-Z41NKTbda144yo3/lLGENlVQjrIV/4J8TZyoPlysHCAXQdGh78cv1j29RKy/zqc8nu6ieY1qoH/aDRdPC1UvEw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.16.1.tgz", + "integrity": "sha512-ssd5MPff6tm8BmzHq2j1HJ0/5KmupFX9ztlrVq0Kj5T3vJhDNgEOIyBZkdjsJAyEPyQITZ790wLZNVm6uJBKfg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.1.2", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.12", + "@amplitude/plugin-autocapture-browser": "^1.1.10", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.20", "tslib": "^2.4.1" } }, @@ -140,13 +140,13 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/@amplitude/analytics-client-common": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.11.tgz", - "integrity": "sha512-1Q1obcN14R7lpK6+UfqFdhLIhmwWrxLgHoJhEV/FwDPq1YvwhENtzVGmNuQtUUWOPIuavltmZ6w1zdL18YIahQ==", + "version": "2.3.17", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.17.tgz", + "integrity": "sha512-NCOEQzRn4bIMFLICtfg72KHF9KZyCSHYmALlOMh9wXmgBndPF4e2vrhXwNu65rIfP21Vb9Zm/MbPh8RKJ+bJ5w==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "@amplitude/analytics-types": "^2.9.1", "tslib": "^2.4.1" } @@ -157,19 +157,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/@amplitude/analytics-connector": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.1.tgz", - "integrity": "sha512-QAGeOfBQc3tamwcECu6YqAPD4mFI1TLBoWi+n0iViYWUZma2FeDLPMihwIquxI8CVvqpn4gswFZsIPRit3q9tQ==", - "dependencies": { - "@amplitude/experiment-core": "^0.10.0" - } + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz", + "integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==", + "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.6.1.tgz", - "integrity": "sha512-PaTs1T02j8/zGNsLuHsTrMh1kFjxtNxVaaUgGuqqM1dl2Jf5sPFQdwu8O/mqrgmV4vPSStvf7odSUpPNN1tFnQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.10.1.tgz", + "integrity": "sha512-QWCGNVDYIsQKoEMJF3o79QlWFytYxowUYtfEQshTQcV9DXo7+sbuntkPbX0kEgZJRAiIwO1NA1bMMQZiSTeuXg==", "license": "MIT", "dependencies": { + "@amplitude/analytics-connector": "^1.6.4", "tslib": "^2.4.1" } }, @@ -200,21 +199,13 @@ "integrity": "sha512-Hw0pgUQCDV6AgvZD8ymBF3mzyBnPrIvXiPxfTNTg/bAkg1h+GVNdJIL6VAXd53kYuO7Lx/vjdhZS54i+RYtgMA==", "license": "MIT" }, - "node_modules/@amplitude/experiment-core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@amplitude/experiment-core/-/experiment-core-0.10.0.tgz", - "integrity": "sha512-FBfM6a4aHp+7OYLYiHO3kni3vCThInT6o4ucuNIB+EIvQ141gQDSjMPOET3ik82fMuJPQITex9sdpZ6cO30ALw==", - "dependencies": { - "js-base64": "^3.7.5" - } - }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.2.tgz", - "integrity": "sha512-/P5hX3FtcMgYlNn38bVbqElETN+BSwLvfopwXRoBTIjfaBS340QytbvfXlo18qfeq8mHU6bi4l8fVPZdessBZQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.10.tgz", + "integrity": "sha512-Wgnl9BFmM0fG+ZbE92UwEeFgJ27YzqYOeMZJno9zja8kdaLrHoaakZrmr2d8tie4DcHVZtMjQ8j/NAMmVEJycQ==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "rxjs": "^7.8.1", "tslib": "^2.4.1" } @@ -226,12 +217,12 @@ "license": "0BSD" }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.12.tgz", - "integrity": "sha512-vsfe4UqjrPvbJ9pS3X0x3bN8FL9Phlt/2BsC6SaHsZCqiScvJ+Hn+r5D36/wKbZp1XNrmc2YOjrOuWwzzdUsNA==", + "version": "2.3.20", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.20.tgz", + "integrity": "sha512-gZIgaTY21sm3x5NXxVv8Pe8xc244+rMHFCWttu2HF0Hkd90qlCFBSqJmBqmw+RfpfjsB6+2JJs0LKO5zTRQopA==", "license": "MIT", "dependencies": { - "@amplitude/analytics-client-common": "^2.3.11", + "@amplitude/analytics-client-common": "^2.3.17", "@amplitude/analytics-types": "^2.9.1", "tslib": "^2.4.1" } @@ -11092,11 +11083,6 @@ "resolved": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", "integrity": "sha512-GXhSjfOPyDemM005YCEHvzrEALhKDIswtxSHSR2e4K/suHVJKJxxRCGz3skPjNxjJjQa9AVSGGlYjv1M3VLIPg==" }, - "node_modules/js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" - }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", @@ -16313,14 +16299,14 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "@amplitude/analytics-browser": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.12.2.tgz", - "integrity": "sha512-Z41NKTbda144yo3/lLGENlVQjrIV/4J8TZyoPlysHCAXQdGh78cv1j29RKy/zqc8nu6ieY1qoH/aDRdPC1UvEw==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.16.1.tgz", + "integrity": "sha512-ssd5MPff6tm8BmzHq2j1HJ0/5KmupFX9ztlrVq0Kj5T3vJhDNgEOIyBZkdjsJAyEPyQITZ790wLZNVm6uJBKfg==", "requires": { - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.1.2", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.12", + "@amplitude/plugin-autocapture-browser": "^1.1.10", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.20", "tslib": "^2.4.1" }, "dependencies": { @@ -16332,12 +16318,12 @@ } }, "@amplitude/analytics-client-common": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.11.tgz", - "integrity": "sha512-1Q1obcN14R7lpK6+UfqFdhLIhmwWrxLgHoJhEV/FwDPq1YvwhENtzVGmNuQtUUWOPIuavltmZ6w1zdL18YIahQ==", + "version": "2.3.17", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.17.tgz", + "integrity": "sha512-NCOEQzRn4bIMFLICtfg72KHF9KZyCSHYmALlOMh9wXmgBndPF4e2vrhXwNu65rIfP21Vb9Zm/MbPh8RKJ+bJ5w==", "requires": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "@amplitude/analytics-types": "^2.9.1", "tslib": "^2.4.1" }, @@ -16350,18 +16336,16 @@ } }, "@amplitude/analytics-connector": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.1.tgz", - "integrity": "sha512-QAGeOfBQc3tamwcECu6YqAPD4mFI1TLBoWi+n0iViYWUZma2FeDLPMihwIquxI8CVvqpn4gswFZsIPRit3q9tQ==", - "requires": { - "@amplitude/experiment-core": "^0.10.0" - } + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-connector/-/analytics-connector-1.6.4.tgz", + "integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==" }, "@amplitude/analytics-core": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.6.1.tgz", - "integrity": "sha512-PaTs1T02j8/zGNsLuHsTrMh1kFjxtNxVaaUgGuqqM1dl2Jf5sPFQdwu8O/mqrgmV4vPSStvf7odSUpPNN1tFnQ==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.10.1.tgz", + "integrity": "sha512-QWCGNVDYIsQKoEMJF3o79QlWFytYxowUYtfEQshTQcV9DXo7+sbuntkPbX0kEgZJRAiIwO1NA1bMMQZiSTeuXg==", "requires": { + "@amplitude/analytics-connector": "^1.6.4", "tslib": "^2.4.1" }, "dependencies": { @@ -16395,20 +16379,12 @@ "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.1.tgz", "integrity": "sha512-Hw0pgUQCDV6AgvZD8ymBF3mzyBnPrIvXiPxfTNTg/bAkg1h+GVNdJIL6VAXd53kYuO7Lx/vjdhZS54i+RYtgMA==" }, - "@amplitude/experiment-core": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@amplitude/experiment-core/-/experiment-core-0.10.0.tgz", - "integrity": "sha512-FBfM6a4aHp+7OYLYiHO3kni3vCThInT6o4ucuNIB+EIvQ141gQDSjMPOET3ik82fMuJPQITex9sdpZ6cO30ALw==", - "requires": { - "js-base64": "^3.7.5" - } - }, "@amplitude/plugin-autocapture-browser": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.2.tgz", - "integrity": "sha512-/P5hX3FtcMgYlNn38bVbqElETN+BSwLvfopwXRoBTIjfaBS340QytbvfXlo18qfeq8mHU6bi4l8fVPZdessBZQ==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.10.tgz", + "integrity": "sha512-Wgnl9BFmM0fG+ZbE92UwEeFgJ27YzqYOeMZJno9zja8kdaLrHoaakZrmr2d8tie4DcHVZtMjQ8j/NAMmVEJycQ==", "requires": { - "@amplitude/analytics-core": "^2.6.1", + "@amplitude/analytics-core": "^2.10.1", "rxjs": "^7.8.1", "tslib": "^2.4.1" }, @@ -16421,11 +16397,11 @@ } }, "@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.12.tgz", - "integrity": "sha512-vsfe4UqjrPvbJ9pS3X0x3bN8FL9Phlt/2BsC6SaHsZCqiScvJ+Hn+r5D36/wKbZp1XNrmc2YOjrOuWwzzdUsNA==", + "version": "2.3.20", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.20.tgz", + "integrity": "sha512-gZIgaTY21sm3x5NXxVv8Pe8xc244+rMHFCWttu2HF0Hkd90qlCFBSqJmBqmw+RfpfjsB6+2JJs0LKO5zTRQopA==", "requires": { - "@amplitude/analytics-client-common": "^2.3.11", + "@amplitude/analytics-client-common": "^2.3.17", "@amplitude/analytics-types": "^2.9.1", "tslib": "^2.4.1" }, @@ -24395,11 +24371,6 @@ "resolved": "https://registry.npmjs.org/jquery-mousewheel/-/jquery-mousewheel-3.1.13.tgz", "integrity": "sha512-GXhSjfOPyDemM005YCEHvzrEALhKDIswtxSHSR2e4K/suHVJKJxxRCGz3skPjNxjJjQa9AVSGGlYjv1M3VLIPg==" }, - "js-base64": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", - "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==" - }, "js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 9c4cb087b117..c9fed8c30813 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -19,7 +19,7 @@ "eslint": "eslint js/**" }, "dependencies": { - "@amplitude/analytics-browser": "^2.12.2", + "@amplitude/analytics-browser": "^2.16.1", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", From 73af5fd2c077c5f411fd2a3b1fb3cea60ed0072c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 21:55:59 +0300 Subject: [PATCH 0011/1006] chore(deps-dev): bump @babel/preset-env in /apps/block_scout_web/assets (#12359) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.26.9 to 7.27.1. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.1/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-version: 7.27.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 1872 +++++++++-------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 959 insertions(+), 915 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 5b85dfb7468a..68665da8d603 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -72,7 +72,7 @@ }, "devDependencies": { "@babel/core": "^7.26.10", - "@babel/preset-env": "^7.26.9", + "@babel/preset-env": "^7.27.1", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", @@ -258,22 +258,23 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", + "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -315,13 +316,13 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -331,24 +332,25 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", + "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -371,17 +373,18 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -392,13 +395,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -425,38 +429,41 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -466,35 +473,37 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -504,14 +513,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -521,51 +531,56 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -585,12 +600,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", + "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -600,13 +615,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -616,12 +632,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -631,12 +648,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -646,14 +664,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -663,13 +682,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -727,12 +747,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -742,12 +763,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -914,12 +936,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -929,15 +952,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -947,14 +970,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -964,13 +988,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -980,12 +1004,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -995,13 +1020,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1011,13 +1037,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1027,16 +1054,17 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -1057,13 +1085,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1073,12 +1102,13 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1088,13 +1118,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1104,12 +1135,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1119,13 +1151,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1135,12 +1168,13 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1150,13 +1184,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1166,12 +1200,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1181,14 +1216,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", - "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1198,14 +1233,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1215,12 +1251,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1230,12 +1267,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1245,12 +1283,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1260,12 +1299,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1275,13 +1315,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1291,14 +1332,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1308,15 +1349,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1326,13 +1368,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1342,13 +1385,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1358,12 +1402,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1373,13 +1418,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1389,12 +1434,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1404,14 +1450,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", + "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1421,13 +1468,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1437,12 +1485,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1452,13 +1501,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1468,12 +1518,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1483,13 +1534,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1499,14 +1551,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1516,12 +1569,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1531,13 +1585,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1547,13 +1601,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1563,12 +1618,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1609,12 +1665,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1624,13 +1681,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1640,12 +1698,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1655,13 +1714,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1671,13 +1730,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1687,12 +1746,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1702,13 +1762,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1718,13 +1779,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1734,13 +1796,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1750,75 +1813,75 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", - "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", + "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.26.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.1", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", @@ -1902,30 +1965,30 @@ } }, "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", + "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1943,13 +2006,13 @@ } }, "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -13669,15 +13732,6 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", @@ -16434,19 +16488,19 @@ } }, "@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "requires": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" } }, "@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==" + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", + "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==" }, "@babel/core": { "version": "7.26.10", @@ -16478,32 +16532,32 @@ } }, "@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "requires": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "requires": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" } }, "@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", + "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", "requires": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -16525,28 +16579,28 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" } }, @@ -16564,104 +16618,104 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", "dev": true, "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", + "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", "requires": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "requires": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" } }, "@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==" + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==" }, "@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/helper-replace-supers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.9.tgz", - "integrity": "sha512-IiDqTOTBQy0sWyeXyGSC5TBJpGFXBkRynjBeXsvbhQFKj2viwJC76Epz35YLU1fpe/Am6Vppb7W7zM4fPQzLsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "requires": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==" }, "@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==" }, "@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==" + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==" }, "@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", "dev": true, "requires": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/helpers": { @@ -16674,60 +16728,60 @@ } }, "@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", + "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "requires": { - "@babel/types": "^7.27.0" + "@babel/types": "^7.27.1" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" } }, "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/plugin-proposal-private-property-in-object": { @@ -16765,21 +16819,21 @@ } }, "@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-syntax-import-meta": { @@ -16892,85 +16946,85 @@ } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", + "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "dependencies": { @@ -16983,322 +17037,321 @@ } }, "@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" } }, "@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", + "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-for-of": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", - "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, "@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" } }, "@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", + "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1" } }, "@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" } }, "@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, "@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", + "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-runtime": { @@ -17326,159 +17379,159 @@ } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" } }, "@babel/preset-env": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", - "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", + "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.26.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.1", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", @@ -17542,25 +17595,25 @@ } }, "@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", + "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "requires": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "requires": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", + "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -17573,12 +17626,12 @@ } }, "@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "requires": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" } }, "@bcoe/v8-coverage": { @@ -26297,15 +26350,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, "regexp.prototype.flags": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index c9fed8c30813..c25041859e16 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -84,7 +84,7 @@ }, "devDependencies": { "@babel/core": "^7.26.10", - "@babel/preset-env": "^7.26.9", + "@babel/preset-env": "^7.27.1", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", From c59bdb69d68d13d5bc1316055ee9efc563d60c47 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 22:10:20 +0300 Subject: [PATCH 0012/1006] chore(deps-dev): bump @babel/core in /apps/block_scout_web/assets (#12360) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.26.10 to 7.27.1. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.1/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.27.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 70 +++++++++---------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 68665da8d603..5d06d4075fa7 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -71,7 +71,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.26.10", + "@babel/core": "^7.27.1", "@babel/preset-env": "^7.27.1", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", @@ -281,21 +281,21 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -587,13 +587,13 @@ } }, "node_modules/@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -16503,20 +16503,20 @@ "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==" }, "@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "requires": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -16719,12 +16719,12 @@ } }, "@babel/helpers": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.10.tgz", - "integrity": "sha512-UPYc3SauzZ3JGgj87GgZ89JVdC5dj0AoetR5Bw6wj4niittNyFh6+eOGonYvJ1ao6B8lEa3Q3klS7ADZ53bc5g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "requires": { - "@babel/template": "^7.26.9", - "@babel/types": "^7.26.10" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" } }, "@babel/parser": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index c25041859e16..5b59b6b70535 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -83,7 +83,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.26.10", + "@babel/core": "^7.27.1", "@babel/preset-env": "^7.27.1", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", From 631a552cafd1e9cb58133f9a005f6f659ae2869c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 11:51:06 +0300 Subject: [PATCH 0013/1006] chore(deps): bump luxon in /apps/block_scout_web/assets (#12358) Bumps [luxon](https://github.com/moment/luxon) from 3.5.0 to 3.6.1. - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/3.5.0...3.6.1) --- updated-dependencies: - dependency-name: luxon dependency-version: 3.6.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 15 ++++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 5d06d4075fa7..9aca6b937640 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -44,7 +44,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.5.0", + "luxon": "^3.6.1", "malihu-custom-scrollbar-plugin": "3.1.5", "mixpanel-browser": "^2.63.0", "moment": "^2.30.1", @@ -11750,9 +11750,10 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "node_modules/luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -24943,9 +24944,9 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "luxon": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", - "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", + "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==" }, "make-dir": { "version": "4.0.0", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 5b59b6b70535..64c196c58e95 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -56,7 +56,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.5.0", + "luxon": "^3.6.1", "malihu-custom-scrollbar-plugin": "3.1.5", "mixpanel-browser": "^2.63.0", "moment": "^2.30.1", From 075b5fa03209b0915974f61ef8094c855662355f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 11:51:34 +0300 Subject: [PATCH 0014/1006] chore(deps-dev): bump sass in /apps/block_scout_web/assets (#12355) Bumps [sass](https://github.com/sass/dart-sass) from 1.86.1 to 1.87.0. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.86.1...1.87.0) --- updated-dependencies: - dependency-name: sass dependency-version: 1.87.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 9aca6b937640..2fbaa396ec88 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -90,7 +90,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.1", "postcss-loader": "^8.1.1", - "sass": "^1.86.1", + "sass": "^1.87.0", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.98.0", @@ -14059,9 +14059,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.86.1.tgz", - "integrity": "sha512-Yaok4XELL1L9Im/ZUClKu//D2OP1rOljKj0Gf34a+GzLbMveOzL7CfqYo+JUa5Xt1nhTCW+OcKp/FtR7/iqj1w==", + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -26615,9 +26615,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.86.1.tgz", - "integrity": "sha512-Yaok4XELL1L9Im/ZUClKu//D2OP1rOljKj0Gf34a+GzLbMveOzL7CfqYo+JUa5Xt1nhTCW+OcKp/FtR7/iqj1w==", + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "dev": true, "requires": { "@parcel/watcher": "^2.4.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 64c196c58e95..187123468ad5 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -102,7 +102,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.1", "postcss-loader": "^8.1.1", - "sass": "^1.86.1", + "sass": "^1.87.0", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.98.0", From 9883bc590abbf5b5df90ffb35843612a7839e82b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 11:52:19 +0300 Subject: [PATCH 0015/1006] chore(deps): bump sweetalert2 in /apps/block_scout_web/assets (#12357) Bumps [sweetalert2](https://github.com/sweetalert2/sweetalert2) from 11.17.2 to 11.19.1. - [Release notes](https://github.com/sweetalert2/sweetalert2/releases) - [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md) - [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.17.2...v11.19.1) --- updated-dependencies: - dependency-name: sweetalert2 dependency-version: 11.19.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 2fbaa396ec88..ae1654cebff3 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -61,7 +61,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.17.2", + "sweetalert2": "^11.19.1", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", @@ -14765,9 +14765,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.17.2", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.17.2.tgz", - "integrity": "sha512-HKxDr1IyV3Lxr3W6sb61qm/p2epFIEdr5EKwteRFHnIg6f8nHFl2kX++DBVz16Mac+fFiU3hMpjq1L6yE2Ge5w==", + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.19.1.tgz", + "integrity": "sha512-+8yws3Sc1srAZbrgdhmEIZny1I1UOYhJOIOdtOlv4TYaP5kkwQ9Zm8/BT23Qg+KdByCNOazltxEJAHzXVu8mhA==", "license": "MIT", "funding": { "type": "individual", @@ -27105,9 +27105,9 @@ } }, "sweetalert2": { - "version": "11.17.2", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.17.2.tgz", - "integrity": "sha512-HKxDr1IyV3Lxr3W6sb61qm/p2epFIEdr5EKwteRFHnIg6f8nHFl2kX++DBVz16Mac+fFiU3hMpjq1L6yE2Ge5w==" + "version": "11.19.1", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.19.1.tgz", + "integrity": "sha512-+8yws3Sc1srAZbrgdhmEIZny1I1UOYhJOIOdtOlv4TYaP5kkwQ9Zm8/BT23Qg+KdByCNOazltxEJAHzXVu8mhA==" }, "symbol-tree": { "version": "3.2.4", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 187123468ad5..d42ef487085f 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -73,7 +73,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.17.2", + "sweetalert2": "^11.19.1", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", From d9ba6e3050408bec4c525339dbf45039bd5e2a38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 11:53:56 +0300 Subject: [PATCH 0016/1006] chore(deps): bump bignumber.js in /apps/block_scout_web/assets (#12354) Bumps [bignumber.js](https://github.com/MikeMcl/bignumber.js) from 9.1.2 to 9.3.0. - [Release notes](https://github.com/MikeMcl/bignumber.js/releases) - [Changelog](https://github.com/MikeMcl/bignumber.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/MikeMcl/bignumber.js/compare/v9.1.2...v9.3.0) --- updated-dependencies: - dependency-name: bignumber.js dependency-version: 9.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 15 ++++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index ae1654cebff3..f14b8e6dcf80 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -12,7 +12,7 @@ "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", - "bignumber.js": "^9.1.2", + "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", "chart.js": "^4.4.8", "chartjs-adapter-luxon": "^1.3.1", @@ -5021,9 +5021,10 @@ } }, "node_modules/bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "license": "MIT", "engines": { "node": "*" } @@ -19868,9 +19869,9 @@ "dev": true }, "bignumber.js": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", - "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", + "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==" }, "blakejs": { "version": "1.1.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index d42ef487085f..84abac089eaf 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -24,7 +24,7 @@ "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", - "bignumber.js": "^9.1.2", + "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", "chart.js": "^4.4.8", "chartjs-adapter-luxon": "^1.3.1", From dc434dcc0b4d071906bed318a188fe7f06c44283 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 13:43:04 +0300 Subject: [PATCH 0017/1006] chore(deps): bump chart.js in /apps/block_scout_web/assets (#12363) Bumps [chart.js](https://github.com/chartjs/Chart.js) from 4.4.8 to 4.4.9. - [Release notes](https://github.com/chartjs/Chart.js/releases) - [Commits](https://github.com/chartjs/Chart.js/compare/v4.4.8...v4.4.9) --- updated-dependencies: - dependency-name: chart.js dependency-version: 4.4.9 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index f14b8e6dcf80..0b66cf16d56a 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -14,7 +14,7 @@ "assert": "^2.1.0", "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", - "chart.js": "^4.4.8", + "chart.js": "^4.4.9", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", "core-js": "^3.42.0", @@ -5463,9 +5463,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", - "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -20203,9 +20203,9 @@ "dev": true }, "chart.js": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", - "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", + "version": "4.4.9", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", + "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", "requires": { "@kurkle/color": "^0.3.0" } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 84abac089eaf..c40e55ceeb53 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -26,7 +26,7 @@ "assert": "^2.1.0", "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", - "chart.js": "^4.4.8", + "chart.js": "^4.4.9", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", "core-js": "^3.42.0", From 724496374e91583c4dbe7ce44e4223aec54443f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 13:43:24 +0300 Subject: [PATCH 0018/1006] chore(deps-dev): bump postcss in /apps/block_scout_web/assets (#12361) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.1 to 8.5.3. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.1...8.5.3) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 0b66cf16d56a..9c605d3f44a8 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -88,7 +88,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.1", + "postcss": "^8.5.3", "postcss-loader": "^8.1.1", "sass": "^1.87.0", "sass-loader": "^14.2.1", @@ -12671,9 +12671,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -25642,9 +25642,9 @@ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" }, "postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "requires": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index c40e55ceeb53..35f4c18e3973 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -100,7 +100,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.1", + "postcss": "^8.5.3", "postcss-loader": "^8.1.1", "sass": "^1.87.0", "sass-loader": "^14.2.1", From 13c9494913c21feafa532e09c0b7f2a2568b43ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 2 May 2025 13:43:57 +0300 Subject: [PATCH 0019/1006] chore(deps): bump base-x in /apps/block_scout_web/assets (#12351) Bumps [base-x](https://github.com/cryptocoinjs/base-x) from 3.0.9 to 3.0.11. - [Commits](https://github.com/cryptocoinjs/base-x/compare/v3.0.9...v3.0.11) --- updated-dependencies: - dependency-name: base-x dependency-version: 3.0.11 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 9c605d3f44a8..6aa75c9d7dc8 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -4968,9 +4968,10 @@ "dev": true }, "node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } @@ -19837,9 +19838,9 @@ "dev": true }, "base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", + "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", "requires": { "safe-buffer": "^5.0.1" } From 00d1613af7a5e69f48ccf67555cbdbd7c8398302 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 2 May 2025 15:21:09 +0300 Subject: [PATCH 0020/1006] Update CONTRIBUTING.md --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b16fd67570a4..b73c86239bec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,7 @@ When contributing to the codebase, please adhere to the following naming convent - Instead of "tx" or "txn", use "transaction". - Instead of "txs", use "transactions". - Instead of "tx_hash" or "txn_hash", use "transaction_hash". + - Instead of "address", use "address_hash". - Instead of "block_num", use "block_number". - Ensure that variable names are descriptive and convey the purpose or content clearly. - Consistent naming helps in maintaining readability and understanding of the code, especially for new contributors. From b2679cda53f363c6654fb2dd59159cc0804b7b8c Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Fri, 2 May 2025 06:42:22 -0600 Subject: [PATCH 0021/1006] fix: support updated zkSync calldata format in batch proof tracking (#12234) --- .../zksync/constants/contracts.ex | 139 ++++++++++++++++++ .../fetcher/zksync/status_tracking/proven.ex | 120 +-------------- .../lib/indexer/fetcher/zksync/utils/rpc.ex | 64 ++++++++ 3 files changed, 205 insertions(+), 118 deletions(-) create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zksync/constants/contracts.ex diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zksync/constants/contracts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zksync/constants/contracts.ex new file mode 100644 index 000000000000..f18a87b5c769 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zksync/constants/contracts.ex @@ -0,0 +1,139 @@ +defmodule EthereumJSONRPC.ZkSync.Constants.Contracts do + @moduledoc """ + Provides constants and ABI definitions for zkSync-specific smart contracts. + """ + + # /// @notice Rollup batch stored data + # /// @param batchNumber Rollup batch number + # /// @param batchHash Hash of L2 batch + # /// @param indexRepeatedStorageChanges The serial number of the shortcut index that's used as a unique identifier for storage keys that were used twice or more + # /// @param numberOfLayer1Txs Number of priority operations to be processed + # /// @param priorityOperationsHash Hash of all priority operations from this batch + # /// @param l2LogsTreeRoot Root hash of tree that contains L2 -> L1 messages from this batch + # /// @param timestamp Rollup batch timestamp, have the same format as Ethereum batch constant + # /// @param commitment Verified input for the ZKsync circuit + # struct StoredBatchInfo { + # uint64 batchNumber; + # bytes32 batchHash; + # uint64 indexRepeatedStorageChanges; + # uint256 numberOfLayer1Txs; + # bytes32 priorityOperationsHash; + # bytes32 l2LogsTreeRoot; + # uint256 timestamp; + # bytes32 commitment; + # } + @stored_batch_info_tuple [ + # batchNumber + uint: 64, + # batchHash + bytes: 32, + # indexRepeatedStorageChanges + uint: 64, + # numberOfLayer1Txs + uint: 256, + # priorityOperationsHash + bytes: 32, + # l2LogsTreeRoot + bytes: 32, + # timestamp + uint: 256, + # commitment + bytes: 32 + ] + + # /// @notice Recursive proof input data (individual commitments are constructed onchain) + # struct ProofInput { + # uint256[] recursiveAggregationInput; + # uint256[] serializedProof; + # } + @proof_input_tuple [ + # recursiveAggregationInput + array: {:uint, 256}, + # serializedProof + array: {:uint, 256} + ] + + @selector_prove_batches "7f61885c" + @selector_prove_batches_shared_bridge_c37533bb "c37533bb" + @selector_prove_batches_shared_bridge_e12a6137 "e12a6137" + + @doc """ + Returns selector of the `proveBatches` function + """ + def prove_batches_selector, do: @selector_prove_batches + + @doc """ + Returns selector of the `proveBatchesSharedBridge` function + """ + def prove_batches_shared_bridge_c37533bb_selector, do: @selector_prove_batches_shared_bridge_c37533bb + + @doc """ + Returns selector of the `proveBatchesSharedBridge` function with selector e12a6137 + """ + def prove_batches_shared_bridge_e12a6137_selector, do: @selector_prove_batches_shared_bridge_e12a6137 + + @doc """ + Returns selector with ABI (object of `ABI.FunctionSelector`) of the function: + + proveBatches( + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) + """ + def prove_batches_selector_with_abi, + do: %ABI.FunctionSelector{ + function: "proveBatches", + types: [ + {:tuple, @stored_batch_info_tuple}, + {:array, {:tuple, @stored_batch_info_tuple}}, + {:tuple, @proof_input_tuple} + ] + } + + @doc """ + Returns selector with ABI (object of `ABI.FunctionSelector`) of the function: + + proveBatchesSharedBridge( + uint256 _chainId, + StoredBatchInfo calldata _prevBatch, + StoredBatchInfo[] calldata _committedBatches, + ProofInput calldata _proof + ) + """ + def prove_batches_shared_bridge_c37533bb_selector_with_abi, + do: %ABI.FunctionSelector{ + function: "proveBatchesSharedBridge", + types: [ + {:uint, 256}, + {:tuple, @stored_batch_info_tuple}, + {:array, {:tuple, @stored_batch_info_tuple}}, + {:tuple, @proof_input_tuple} + ] + } + + @doc """ + Returns selector with ABI (object of `ABI.FunctionSelector`) of the function: + + proveBatchesSharedBridge( + uint256 _chainId, + uint256 _processBatchFrom, + uint256 _processBatchTo, + bytes calldata _proofData + ) + """ + def prove_batches_shared_bridge_e12a6137_selector_with_abi, + do: %ABI.FunctionSelector{ + function: "proveBatchesSharedBridge", + types: [ + # _chainId + {:uint, 256}, + # _processBatchFrom + {:uint, 256}, + # _processBatchTo + {:uint, 256}, + # _proofData + :bytes + ] + } +end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex index 0fb3e21a8c17..19497c1e3b0d 100644 --- a/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex +++ b/apps/indexer/lib/indexer/fetcher/zksync/status_tracking/proven.ex @@ -3,7 +3,6 @@ defmodule Indexer.Fetcher.ZkSync.StatusTracking.Proven do Functionality to discover proven batches """ - alias ABI.{FunctionSelector, TypeDecoder} alias Indexer.Fetcher.ZkSync.Utils.{Db, Rpc} import Indexer.Fetcher.ZkSync.StatusTracking.CommonUtils, @@ -12,7 +11,7 @@ defmodule Indexer.Fetcher.ZkSync.StatusTracking.Proven do associate_and_import_or_prepare_for_recovery: 4 ] - import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_error: 1, log_info: 1] + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_info: 1] @doc """ Checks if the oldest unproven batch in the database has the associated L1 proving transaction @@ -60,7 +59,7 @@ defmodule Indexer.Fetcher.ZkSync.StatusTracking.Proven do :look_for_batches -> log_info("The batch #{expected_batch_number} looks like proven") prove_transaction = Rpc.fetch_transaction_by_hash(transaction_hash, json_l1_rpc_named_arguments) - batches_numbers_from_rpc = get_proven_batches_from_calldata(prove_transaction["input"]) + batches_numbers_from_rpc = Rpc.get_proven_batches_from_calldata(prove_transaction["input"]) associate_and_import_or_prepare_for_recovery( batches_numbers_from_rpc, @@ -71,119 +70,4 @@ defmodule Indexer.Fetcher.ZkSync.StatusTracking.Proven do end end end - - defp get_proven_batches_from_calldata(calldata) do - # /// @param batchNumber Rollup batch number - # /// @param batchHash Hash of L2 batch - # /// @param indexRepeatedStorageChanges The serial number of the shortcut index that's used as a unique identifier for storage keys that were used twice or more - # /// @param numberOfLayer1Txs Number of priority operations to be processed - # /// @param priorityOperationsHash Hash of all priority operations from this batch - # /// @param l2LogsTreeRoot Root hash of tree that contains L2 -> L1 messages from this batch - # /// @param timestamp Rollup batch timestamp, have the same format as Ethereum batch constant - # /// @param commitment Verified input for the zkSync circuit - # struct StoredBatchInfo { - # uint64 batchNumber; - # bytes32 batchHash; - # uint64 indexRepeatedStorageChanges; - # uint256 numberOfLayer1Txs; - # bytes32 priorityOperationsHash; - # bytes32 l2LogsTreeRoot; - # uint256 timestamp; - # bytes32 commitment; - # } - # /// @notice Recursive proof input data (individual commitments are constructed onchain) - # struct ProofInput { - # uint256[] recursiveAggregationInput; - # uint256[] serializedProof; - # } - proven_batches = - case calldata do - "0x7f61885c" <> encoded_params -> - # proveBatches(StoredBatchInfo calldata _prevBatch, StoredBatchInfo[] calldata _committedBatches, ProofInput calldata _proof) - # IO.inspect(FunctionSelector.decode("proveBatches((uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32),(uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32)[],(uint256[],uint256[]))")) - [_prev_batch, proven_batches, _proof] = - TypeDecoder.decode( - Base.decode16!(encoded_params, case: :lower), - %FunctionSelector{ - function: "proveBatches", - types: [ - tuple: [ - uint: 64, - bytes: 32, - uint: 64, - uint: 256, - bytes: 32, - bytes: 32, - uint: 256, - bytes: 32 - ], - array: - {:tuple, - [ - uint: 64, - bytes: 32, - uint: 64, - uint: 256, - bytes: 32, - bytes: 32, - uint: 256, - bytes: 32 - ]}, - tuple: [array: {:uint, 256}, array: {:uint, 256}] - ] - } - ) - - proven_batches - - "0xc37533bb" <> encoded_params -> - # proveBatchesSharedBridge(uint256 _chainId, StoredBatchInfo calldata _prevBatch, StoredBatchInfo[] calldata _committedBatches, ProofInput calldata _proof) - # IO.inspect(FunctionSelector.decode("proveBatchesSharedBridge(uint256,(uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32),(uint64,bytes32,uint64,uint256,bytes32,bytes32,uint256,bytes32)[],(uint256[],uint256[]))")) - [_chainid, _prev_batch, proven_batches, _proof] = - TypeDecoder.decode( - Base.decode16!(encoded_params, case: :lower), - %FunctionSelector{ - function: "proveBatchesSharedBridge", - types: [ - {:uint, 256}, - tuple: [ - uint: 64, - bytes: 32, - uint: 64, - uint: 256, - bytes: 32, - bytes: 32, - uint: 256, - bytes: 32 - ], - array: - {:tuple, - [ - uint: 64, - bytes: 32, - uint: 64, - uint: 256, - bytes: 32, - bytes: 32, - uint: 256, - bytes: 32 - ]}, - tuple: [array: {:uint, 256}, array: {:uint, 256}] - ] - } - ) - - proven_batches - - _ -> - log_error("Unknown calldata format: #{calldata}") - - [] - end - - log_info("Discovered #{length(proven_batches)} proven batches in the prove transaction") - - proven_batches - |> Enum.map(fn batch_info -> elem(batch_info, 0) end) - end end diff --git a/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex index 98df6235e38b..f199a2248004 100644 --- a/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex +++ b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex @@ -5,9 +5,13 @@ defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do import EthereumJSONRPC, only: [json_rpc: 2, quantity_to_integer: 1] + alias ABI.{FunctionSelector, TypeDecoder} + alias EthereumJSONRPC.ZkSync.Constants.Contracts, as: ZkSyncContracts alias Explorer.Helper, as: ExplorerHelper alias Indexer.Helper, as: IndexerHelper + import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_error: 1, log_info: 1] + @zero_hash "0000000000000000000000000000000000000000000000000000000000000000" @zero_hash_binary <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> @@ -376,4 +380,64 @@ defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do responses end + + @doc """ + Extracts batch numbers from the calldata of a proof transaction. + + ## Parameters + - `calldata`: The calldata from the parent chain transaction + + ## Returns + - A list of batch numbers that were proven by the transaction + """ + @spec get_proven_batches_from_calldata(binary()) :: [non_neg_integer()] + def get_proven_batches_from_calldata(calldata) do + proven_batches = + case calldata do + "0x7f61885c" <> encoded_params -> + [_prev_batch, proven_batches, _proof] = + decode_params(encoded_params, ZkSyncContracts.prove_batches_selector_with_abi()) + + extract_batch_numbers(proven_batches) + + # Pre-v26 proveBatchesSharedBridge + "0xc37533bb" <> encoded_params -> + [_chainid, _prev_batch, proven_batches, _proof] = + decode_params(encoded_params, ZkSyncContracts.prove_batches_shared_bridge_c37533bb_selector_with_abi()) + + extract_batch_numbers(proven_batches) + + # v26+ proveBatchesSharedBridge + "0xe12a6137" <> encoded_params -> + [_chainid, process_from, process_to, _proof_data] = + decode_params(encoded_params, ZkSyncContracts.prove_batches_shared_bridge_e12a6137_selector_with_abi()) + + Enum.to_list(process_from..process_to) + + _ -> + log_error("Unknown calldata format: #{calldata}") + + [] + end + + log_info("Discovered #{length(proven_batches)} proven batches in the prove transaction") + + proven_batches + end + + # Decodes encoded parameters using the provided function selector. + # credo:disable-for-next-line Credo.Check.Warning.SpecWithStruct + @spec decode_params(binary(), %FunctionSelector{}) :: list() + defp decode_params(encoded_params, function_selector) do + encoded_params + |> Base.decode16!(case: :lower) + |> TypeDecoder.decode(function_selector) + end + + # Extracts batch numbers from a list of StoredBatchInfo tuples. + @spec extract_batch_numbers([any()]) :: [non_neg_integer()] + defp extract_batch_numbers(proven_batches) do + proven_batches + |> Enum.map(fn batch_info -> elem(batch_info, 0) end) + end end From f0d84e2d3022fe5cf65178739e01f9873e8386ce Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 5 May 2025 13:44:01 +0300 Subject: [PATCH 0022/1006] refactor: Define pending block operations by set of block hashes query (#12375) --- apps/explorer/lib/explorer/chain.ex | 5 +---- .../import/runner/internal_transactions.ex | 19 ++++++----------- .../chain/pending_operations_helper.ex | 21 ++++++++++++++++++- .../indexer/fetcher/empty_blocks_sanitizer.ex | 6 +++--- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 96e107afef92..f4c3a254da63 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1621,10 +1621,7 @@ defmodule Explorer.Chain do query = case PendingOperationsHelper.pending_operations_type() do "blocks" -> - from( - pbo in PendingBlockOperation, - where: pbo.block_hash in ^block_hashes - ) + PendingOperationsHelper.block_hash_in_query(block_hashes) "transactions" -> from( diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index 771afc5ac26f..d562cae32547 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -15,7 +15,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do Hash, Import, InternalTransaction, - PendingBlockOperation, PendingOperationsHelper, PendingTransactionOperation, Transaction @@ -324,14 +323,11 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do case PendingOperationsHelper.pending_operations_type() do "blocks" -> query = - from( - pending_ops in PendingBlockOperation, - where: pending_ops.block_hash in ^block_hashes, - select: pending_ops.block_hash, - # Enforce PendingBlockOperation ShareLocks order (see docs: sharelocks.md) - order_by: [asc: pending_ops.block_hash], - lock: "FOR UPDATE" - ) + block_hashes + |> PendingOperationsHelper.block_hash_in_query() + |> select([pbo], pbo.block_hash) + |> order_by([pbo], asc: pbo.block_hash) + |> lock("FOR UPDATE") {:ok, {:block_hashes, repo.all(query)}} @@ -829,10 +825,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do |> MapSet.difference(MapSet.new(invalid_block_hashes)) |> MapSet.to_list() - from( - pending_ops in PendingBlockOperation, - where: pending_ops.block_hash in ^valid_block_hashes - ) + PendingOperationsHelper.block_hash_in_query(valid_block_hashes) {:transaction_hashes, transaction_hashes} -> from( diff --git a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex index 6d7d1b415a86..35d817ffbd0b 100644 --- a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex +++ b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.PendingOperationsHelper do import Ecto.Query - alias Explorer.Chain.{PendingBlockOperation, PendingTransactionOperation, Transaction} + alias Explorer.Chain.{Hash, PendingBlockOperation, PendingTransactionOperation, Transaction} alias Explorer.Repo @transactions_batch_size 1000 @@ -118,4 +118,23 @@ defmodule Explorer.Chain.PendingOperationsHelper do Enum.map(params, &Map.merge(&1, %{inserted_at: now, updated_at: now})) end + + @doc """ + Generates a query to find pending block operations that match any of the given block hashes. + + ## Parameters + + - `block_hashes`: A list of block hashes to filter the pending block operations. + + ## Returns + + - An Ecto query that can be executed to retrieve the matching pending block operations. + """ + @spec block_hash_in_query([Hash.Full.t()]) :: Ecto.Query.t() + def block_hash_in_query(block_hashes) do + from( + pending_ops in PendingBlockOperation, + where: pending_ops.block_hash in ^block_hashes + ) + end end diff --git a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex index e14606a95955..b53ebfe80722 100644 --- a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex +++ b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex @@ -15,7 +15,7 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do alias EthereumJSONRPC.Block.ByNumber alias EthereumJSONRPC.Blocks alias Explorer.Repo - alias Explorer.Chain.{Block, Hash, PendingBlockOperation, PendingOperationsHelper, Transaction} + alias Explorer.Chain.{Block, Hash, PendingOperationsHelper, Transaction} alias Explorer.Chain.Cache.BlockNumber @update_timeout 60_000 @@ -196,8 +196,8 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do case PendingOperationsHelper.pending_operations_type() do "blocks" -> - PendingBlockOperation - |> where([po], po.block_hash in ^block_hashes) + block_hashes + |> PendingOperationsHelper.block_hash_in_query() |> Repo.delete_all() "transactions" -> From dea9280acfa8e5a7ff9e0bdeecf99a0c0cc993ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 15:19:46 +0300 Subject: [PATCH 0023/1006] chore(deps): bump bcrypt_elixir from 3.2.1 to 3.3.1 (#12379) Bumps [bcrypt_elixir](https://github.com/riverrun/bcrypt_elixir) from 3.2.1 to 3.3.1. - [Changelog](https://github.com/riverrun/bcrypt_elixir/blob/master/CHANGELOG.md) - [Commits](https://github.com/riverrun/bcrypt_elixir/compare/v3.2.1...v3.3.1) --- updated-dependencies: - dependency-name: bcrypt_elixir dependency-version: 3.3.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index b755826a5231..8f4ddd012263 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,7 @@ "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "bamboo": {:hex, :bamboo, "2.4.0", "d26bd9f22a18cfa4448dac9b305debfd533b73064aeaea10d251400e543b472b", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "85e15dfce007faa04ef3baa46fe0737aba955cba5843bad17514519f9cb86592"}, - "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.1", "e361261a0401d82dadc1ab7b969f91d250bf7577283e933fe8c5b72f8f5b3c46", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "81170177d5c2e280d12141a0b9d9e299bf731535e2d959982bdcd4cfe3c82865"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.1", "9f2e7e00f661a6acfae1431f1bc590e28698aaecc962c2a7b33150dfe9289c3d", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9f539e9d3828fad4ffc8152dadc0d27c6d78cb2853a9a1d6518cfe8a5adb7f50"}, "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, "blake2": {:hex, :blake2, "1.0.4", "8263c69a191142922bc2510f1ffc0de0ae96e8c3bd5e2ad3fac7e87aed94c8b1", [:mix], [], "hexpm", "e9f4120d163ba14d86304195e50745fa18483e6ad2be94c864ae449bbdd6a189"}, From 627993042d6d89f3261f3c06a4202d93dde6e60b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 18:42:02 +0300 Subject: [PATCH 0024/1006] chore(deps): bump phoenix_ecto from 4.6.3 to 4.6.4 (#12378) Bumps [phoenix_ecto](https://github.com/phoenixframework/phoenix_ecto) from 4.6.3 to 4.6.4. - [Changelog](https://github.com/phoenixframework/phoenix_ecto/blob/main/CHANGELOG.md) - [Commits](https://github.com/phoenixframework/phoenix_ecto/commits) --- updated-dependencies: - dependency-name: phoenix_ecto dependency-version: 4.6.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 8f4ddd012263..9bb02088ac1d 100644 --- a/mix.lock +++ b/mix.lock @@ -122,7 +122,7 @@ "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.5.14", "2d5db884be496eefa5157505ec0134e66187cb416c072272420c5509d67bf808", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "207f1aa5520320cbb7940d7ff2dde2342162cf513875848f88249ea0ba02fef7"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, From da81db9a6c6b76fadc2ecff52f3b629f391c7f4b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 7 May 2025 16:24:24 +0300 Subject: [PATCH 0025/1006] refactor: Eliminate join with internal_transactions table to get list logs in API v1 (#12352) --- apps/explorer/lib/explorer/etherscan/logs.ex | 113 ++++--------------- 1 file changed, 22 insertions(+), 91 deletions(-) diff --git a/apps/explorer/lib/explorer/etherscan/logs.ex b/apps/explorer/lib/explorer/etherscan/logs.ex index 14afe2c16709..cef67eeebc6e 100644 --- a/apps/explorer/lib/explorer/etherscan/logs.ex +++ b/apps/explorer/lib/explorer/etherscan/logs.ex @@ -5,10 +5,10 @@ defmodule Explorer.Etherscan.Logs do """ - import Ecto.Query, only: [dynamic: 2, from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, union: 2] + import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3] alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, DenormalizationHelper, InternalTransaction, Log, Transaction} + alias Explorer.Chain.{DenormalizationHelper, Log, Transaction} @base_filter %{ from_block: nil, @@ -75,16 +75,16 @@ defmodule Explorer.Etherscan.Logs do paging_options = if is_nil(paging_options), do: @default_paging_options, else: paging_options prepared_filter = Map.merge(@base_filter, filter) - if DenormalizationHelper.transactions_denormalization_finished?() do - logs_query = - Log - |> where_topic_match(prepared_filter) - |> where([log], log.address_hash == ^address_hash) - |> where([log], log.block_number >= ^prepared_filter.from_block) - |> where([log], log.block_number <= ^prepared_filter.to_block) - |> limit(1000) - |> page_logs(paging_options) + logs_query = + Log + |> where_topic_match(prepared_filter) + |> where([log], log.address_hash == ^address_hash) + |> where([log], log.block_number >= ^prepared_filter.from_block) + |> where([log], log.block_number <= ^prepared_filter.to_block) + |> limit(1000) + |> page_logs(paging_options) + if DenormalizationHelper.transactions_denormalization_finished?() do all_transaction_logs_query = from(log in subquery(logs_query), join: transaction in Transaction, @@ -107,75 +107,27 @@ defmodule Explorer.Etherscan.Logs do |> order_by([log], asc: log.block_number, asc: log.index) |> Repo.replica().all() else - logs_query = where_topic_match(Log, prepared_filter) - - query_to_address_hash_wrapped = - logs_query - |> internal_transaction_query(:to_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - query_from_address_hash_wrapped = - logs_query - |> internal_transaction_query(:from_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - query_created_contract_address_hash_wrapped = - logs_query - |> internal_transaction_query(:created_contract_address_hash, prepared_filter, address_hash) - |> Chain.wrapped_union_subquery() - - internal_transaction_log_query = - query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) - - all_transaction_logs_query_base = - from(transaction in Transaction, - join: log in ^logs_query, - on: log.transaction_hash == transaction.hash, - where: transaction.block_number >= ^prepared_filter.from_block, - where: transaction.block_number <= ^prepared_filter.to_block, + all_transaction_logs_query = + from(log in subquery(logs_query), + join: transaction in Transaction, + on: log.transaction_hash == transaction.hash and log.block_hash == transaction.block_hash, + inner_join: block in assoc(transaction, :block), + where: block.consensus == true, select: map(log, ^@log_fields), select_merge: %{ gas_price: transaction.gas_price, gas_used: transaction.gas_used, transaction_index: transaction.index, - block_number: transaction.block_number - }, - union: ^internal_transaction_log_query - ) - - dynamic = - dynamic( - [transaction], - ^Transaction.where_transactions_to_from(address_hash) or - transaction.created_contract_address_hash == ^address_hash - ) - - all_transaction_logs_query = - all_transaction_logs_query_base - |> where([transaction], ^dynamic) - - query_with_blocks = - from(log_transaction_data in subquery(all_transaction_logs_query), - join: block in Block, - on: block.number == log_transaction_data.block_number, - where: log_transaction_data.address_hash == ^address_hash, - where: block.consensus == true, - order_by: block.number, - limit: 1000, - select_merge: %{ - transaction_index: log_transaction_data.transaction_index, - block_hash: block.hash, - block_number: block.number, + block_hash: transaction.block_hash, + block_number: transaction.block_number, block_timestamp: block.timestamp, block_consensus: block.consensus } ) - query_with_blocks - |> order_by([log], asc: log.index) - |> page_logs(paging_options) + all_transaction_logs_query + |> Chain.wrapped_union_subquery() + |> order_by([log], asc: log.block_number, asc: log.index) |> Repo.replica().all() end end @@ -358,25 +310,4 @@ defmodule Explorer.Etherscan.Logs do where: {data.block_number, data.index} > {^block_number, ^log_index} ) end - - defp internal_transaction_query(logs_query, direction, prepared_filter, address_hash) do - query = - from(internal_transaction in InternalTransaction.where_nonpending_block(), - join: transaction in assoc(internal_transaction, :transaction), - join: log in ^logs_query, - on: log.transaction_hash == internal_transaction.transaction_hash, - where: internal_transaction.block_number >= ^prepared_filter.from_block, - where: internal_transaction.block_number <= ^prepared_filter.to_block, - select: - merge(map(log, ^@log_fields), %{ - gas_price: transaction.gas_price, - gas_used: transaction.gas_used, - transaction_index: transaction.index, - block_number: internal_transaction.block_number - }) - ) - - query - |> InternalTransaction.where_address_fields_match(address_hash, direction) - end end From bd2a8442b48b67abf0061d826f3da66698fc0512 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 7 May 2025 22:11:08 +0300 Subject: [PATCH 0026/1006] fix: Fix FunctionClauseError on internal transactions indexing (#12246) --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index 6eb0d061e6cc..d191b38bfaa2 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -278,7 +278,7 @@ defmodule EthereumJSONRPC.Geth do responses |> Enum.map(fn %{result: %{"structLogs" => nil}} -> - [] + {:ok, []} %{id: id, result: %{"structLogs" => _} = result} -> debug_trace_transaction_response_to_internal_transactions_params( From b4000c9dc1b09305becbee00613412a73e9aee47 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 7 May 2025 22:12:33 +0300 Subject: [PATCH 0027/1006] chore: Send DB read queries to replica in on-demand fetchers (#12383) --- apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex | 4 ++-- .../lib/indexer/fetcher/on_demand/neon_solana_transactions.ex | 2 +- .../lib/indexer/fetcher/on_demand/token_total_supply.ex | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex index c9e868ca2b4b..ac7ba153aab0 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex @@ -184,7 +184,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do {:stale, latest_block_number} else - case Repo.one(query_balances) do + case Repo.replica().one(query_balances) do nil -> # There is no recent coin balance to fetch, so we check to see how old the # balance is on the address. If it is too old, we check again, just to be safe. @@ -206,7 +206,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do end defp do_trigger_balance_daily_fetch_query(address, latest_block_number, query) do - if Repo.one(query) == nil do + if Repo.replica().one(query) == nil do BufferedTask.buffer( __MODULE__, [{:fetch_and_import_daily_balances, latest_block_number, to_string(address.hash)}], diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/neon_solana_transactions.ex b/apps/indexer/lib/indexer/fetcher/on_demand/neon_solana_transactions.ex index c2bd311a5e47..bbbb870a1d45 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/neon_solana_transactions.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/neon_solana_transactions.ex @@ -29,7 +29,7 @@ defmodule Indexer.Fetcher.OnDemand.NeonSolanaTransactions do end defp cache(transaction_hash) do - Repo.all( + Repo.replica().all( from( solanaTransaction in LinkedSolanaTransactions, where: solanaTransaction.neon_transaction_hash == ^transaction_hash.bytes, diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex index d4281c473a92..7fbdbba50a3b 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex @@ -47,7 +47,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenTotalSupply do ## Implementation defp do_fetch(address_hash) when not is_nil(address_hash) do - token = Repo.get_by(Token, contract_address_hash: address_hash) + token = Repo.replica().get_by(Token, contract_address_hash: address_hash) if (token && !token.skip_metadata && is_nil(token.total_supply_updated_at_block)) or BlockNumber.get_max() - token.total_supply_updated_at_block > @ttl_in_blocks do From 6c2d8dfb2dcfdfa3d146764c5c94b11de715e0a1 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Fri, 9 May 2025 14:46:22 +0400 Subject: [PATCH 0028/1006] chore: Support Celestia Alt-DA in OP batches indexer and Super Roots in OP withdrawals indexer (#12332) * Support Celestia Alt-DA blobs * Add specs and docs * Minor fixes * Refactoring * Refactoring --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../views/api/v2/optimism_view.ex | 4 +- .../import/runner/optimism/dispute_games.ex | 6 +- .../runner/optimism/withdrawal_events.ex | 6 +- .../explorer/chain/optimism/dispute_game.ex | 6 +- .../lib/explorer/chain/optimism/withdrawal.ex | 60 +++++++--- .../chain/optimism/withdrawal_event.ex | 27 ++++- ...0401_game_address_field_in_withdrawals.exs | 12 ++ apps/indexer/lib/indexer/fetcher/optimism.ex | 16 ++- .../indexer/fetcher/optimism/dispute_game.ex | 16 +-- .../fetcher/optimism/transaction_batch.ex | 78 ++++++++++--- .../fetcher/optimism/withdrawal_event.ex | 103 ++++++++++++------ 11 files changed, 246 insertions(+), 88 deletions(-) create mode 100644 apps/explorer/priv/optimism/migrations/20250428080401_game_address_field_in_withdrawals.exs diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index c412214f623d..17bbf66540b0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -127,8 +127,8 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "index" => g.index, "game_type" => g.game_type, # todo: It should be removed in favour `contract_address_hash` property with the next release after 8.0.0 - "contract_address" => g.address, - "contract_address_hash" => g.address, + "contract_address" => g.address_hash, + "contract_address_hash" => g.address_hash, "l2_block_number" => l2_block_number, "created_at" => g.created_at, "status" => status, diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/dispute_games.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/dispute_games.ex index 9b84c6af6607..304ecd0c2aa4 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/optimism/dispute_games.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/dispute_games.ex @@ -86,7 +86,7 @@ defmodule Explorer.Chain.Import.Runner.Optimism.DisputeGames do set: [ # don't update `index` as it is a primary key and used for the conflict target game_type: fragment("EXCLUDED.game_type"), - address: fragment("EXCLUDED.address"), + address_hash: fragment("EXCLUDED.address_hash"), extra_data: fragment("EXCLUDED.extra_data"), created_at: fragment("EXCLUDED.created_at"), resolved_at: fragment("EXCLUDED.resolved_at"), @@ -97,9 +97,9 @@ defmodule Explorer.Chain.Import.Runner.Optimism.DisputeGames do ], where: fragment( - "(EXCLUDED.game_type, EXCLUDED.address, EXCLUDED.extra_data, EXCLUDED.created_at, EXCLUDED.resolved_at, EXCLUDED.status) IS DISTINCT FROM (?, ?, ?, ?, ?, ?)", + "(EXCLUDED.game_type, EXCLUDED.address_hash, EXCLUDED.extra_data, EXCLUDED.created_at, EXCLUDED.resolved_at, EXCLUDED.status) IS DISTINCT FROM (?, ?, ?, ?, ?, ?)", game.game_type, - game.address, + game.address_hash, game.extra_data, game.created_at, game.resolved_at, diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex index 1816db039549..f2fe0077873d 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/withdrawal_events.ex @@ -90,16 +90,18 @@ defmodule Explorer.Chain.Import.Runner.Optimism.WithdrawalEvents do l1_timestamp: fragment("EXCLUDED.l1_timestamp"), l1_block_number: fragment("EXCLUDED.l1_block_number"), game_index: fragment("EXCLUDED.game_index"), + game_address_hash: fragment("EXCLUDED.game_address_hash"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", we.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", we.updated_at) ] ], where: fragment( - "(EXCLUDED.l1_timestamp, EXCLUDED.l1_block_number, EXCLUDED.game_index) IS DISTINCT FROM (?, ?, ?)", + "(EXCLUDED.l1_timestamp, EXCLUDED.l1_block_number, EXCLUDED.game_index, EXCLUDED.game_address_hash) IS DISTINCT FROM (?, ?, ?, ?)", we.l1_timestamp, we.l1_block_number, - we.game_index + we.game_index, + we.game_address_hash ) ) end diff --git a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex index 4ad8318b9f9b..80461e605d87 100644 --- a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex +++ b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex @@ -9,13 +9,13 @@ defmodule Explorer.Chain.Optimism.DisputeGame do alias Explorer.Chain.{Data, Hash} alias Explorer.{PagingOptions, Repo} - @required_attrs ~w(index game_type address created_at)a + @required_attrs ~w(index game_type address_hash created_at)a @optional_attrs ~w(extra_data resolved_at status)a @typedoc """ * `index` - A unique index of the dispute game. * `game_type` - A number encoding a type of the dispute game. - * `address` - The dispute game contract address. + * `address_hash` - The dispute game contract address. * `extra_data` - An extra data of the dispute game (contains L2 block number). Equals to `nil` when the game is written to database but the rest data is not known yet. * `created_at` - UTC timestamp of when the dispute game was created. @@ -28,7 +28,7 @@ defmodule Explorer.Chain.Optimism.DisputeGame do typed_schema "op_dispute_games" do field(:index, :integer, primary_key: true) field(:game_type, :integer) - field(:address, Hash.Address) + field(:address_hash, Hash.Address) field(:extra_data, Data) field(:created_at, :utc_datetime_usec) field(:resolved_at, :utc_datetime_usec) diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex index cb1f1bec9650..3b42fb47215a 100644 --- a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex @@ -25,6 +25,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do @proof_maturity_delay_seconds "optimism_proof_maturity_delay_seconds" @required_attrs ~w(msg_nonce hash l2_transaction_hash l2_block_number)a + @game_fields ~w(created_at, resolved_at, status)a @typedoc """ * `msg_nonce` - A nonce of the withdrawal message. @@ -274,22 +275,44 @@ defmodule Explorer.Chain.Optimism.Withdrawal do withdrawal_l2_block_number <= last_root_l2_block_number end - defp game_by_index(game_index) do - if not is_nil(game_index) do - Repo.replica().one( - from( - g in DisputeGame, - select: %{created_at: g.created_at, resolved_at: g.resolved_at, status: g.status}, - where: g.index == ^game_index - ) + # Fetches dispute game info from DB by game's contract address hash or game's unique index. + # + # ## Parameters + # - `game_address_hash`: Game's contract address hash. Must be `nil` if unknown. + # - `game_index`: Game unique index. Must be `nil` if unknown. + # + # ## Returns + # - A map with necessary info about the dispute game. + # - `nil` if the dispute game not found in DB or both input parameters are `nil`. + @spec game_by_address_hash_or_index(Hash.t() | nil, non_neg_integer() | nil) :: + %{:created_at => DateTime.t(), :resolved_at => DateTime.t(), :status => non_neg_integer()} | nil + defp game_by_address_hash_or_index(nil, nil), do: nil + + defp game_by_address_hash_or_index(game_address_hash, nil) do + Repo.replica().one( + from( + g in DisputeGame, + select: map(g, ^@game_fields), + where: g.address_hash == ^game_address_hash, + limit: 1 ) - end + ) + end + + defp game_by_address_hash_or_index(_game_address_hash, game_index) when not is_nil(game_index) do + Repo.replica().one( + from( + g in DisputeGame, + select: map(g, ^@game_fields), + where: g.index == ^game_index + ) + ) end # Determines the current withdrawal status by the list of the bound WithdrawalProven events. # # ## Parameters - # - `proven_events`: A list of WithdrawalProven events. Each item is `{l1_timestamp, game_index}` tuple. + # - `proven_events`: A list of WithdrawalProven events. Each item is `{l1_timestamp, game_address_hash, game_index}` tuple. # - `respected_games`: A list of games returned by the `respected_games()` function. # # ## Returns @@ -300,15 +323,15 @@ defmodule Explorer.Chain.Optimism.Withdrawal do defp handle_proven_status(proven_events, respected_games) do statuses = proven_events - |> Enum.reduce_while([], fn {l1_timestamp, game_index}, acc -> - game = game_by_index(game_index) + |> Enum.reduce_while([], fn {l1_timestamp, game_address_hash, game_index}, acc -> + game = game_by_address_hash_or_index(game_address_hash, game_index) # credo:disable-for-lines:16 Credo.Check.Refactor.PipeChainStart cond do - is_nil(game_index) and not Enum.empty?(respected_games) -> + is_nil(game) and not Enum.empty?(respected_games) -> # here we cannot exactly determine the status `Waiting a game to resolve` or # `Ready for relay` or `In challenge period` - # as we don't know the game index. In this case we display the `Proven` status + # as we don't know the game index and address. In this case we display the `Proven` status {@withdrawal_status_proven, nil} is_nil(game) or DateTime.compare(l1_timestamp, game.created_at) == :lt -> @@ -349,14 +372,15 @@ defmodule Explorer.Chain.Optimism.Withdrawal do # - `withdrawal_hash`: The withdrawal hash for which the function should return the events. # # ## Returns - # - A list of `{l1_timestamp, game_index}` tuples where `l1_timestamp` is the L1 block timestamp - # when the event appeared, `game_index` is the bound dispute game index (can be `nil`). - @spec proven_events_by_hash(Hash.t()) :: [{DateTime.t(), non_neg_integer()}] + # - A list of `{l1_timestamp, game_address_hash, game_index}` tuples where `l1_timestamp` is the L1 block timestamp + # when the event appeared, `game_address_hash` is the bound dispute game contract address hash (can be `nil`), + # `game_index` is the bound dispute game index (can be `nil`). + @spec proven_events_by_hash(Hash.t()) :: [{DateTime.t(), Hash.t() | nil, non_neg_integer() | nil}] defp proven_events_by_hash(withdrawal_hash) do Repo.replica().all( from( we in WithdrawalEvent, - select: {we.l1_timestamp, we.game_index}, + select: {we.l1_timestamp, we.game_address_hash, we.game_index}, where: we.withdrawal_hash == ^withdrawal_hash and we.l1_event_type == :WithdrawalProven, order_by: [asc: we.l1_block_number] ), diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex index 0ca44f41e58f..b33010ed18ba 100644 --- a/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal_event.ex @@ -6,9 +6,10 @@ defmodule Explorer.Chain.Optimism.WithdrawalEvent do alias Explorer.Chain.Hash @required_attrs ~w(withdrawal_hash l1_event_type l1_timestamp l1_transaction_hash l1_block_number)a - @optional_attrs ~w(game_index)a + @optional_attrs ~w(game_index game_address_hash)a @typedoc """ + Descriptor of the withdrawal event: * `withdrawal_hash` - A withdrawal hash. * `l1_event_type` - A type of withdrawal event: `WithdrawalProven` or `WithdrawalFinalized`. * `l1_timestamp` - A timestamp of when the withdrawal event appeared. @@ -16,6 +17,29 @@ defmodule Explorer.Chain.Optimism.WithdrawalEvent do * `l1_block_number` - An L1 block number of the L1 transaction. * `game_index` - An index of a dispute game (if available in L1 transaction input) when the withdrawal is proven. Equals to `nil` if not available. + * `game_address_hash` - Contract address of a dispute game (if available in L1 transaction input) when + the withdrawal is proven. Equals to `nil` if not available. + """ + @type to_import :: %{ + withdrawal_hash: Hash.t(), + l1_event_type: String.t(), + l1_timestamp: DateTime.t(), + l1_transaction_hash: Hash.t(), + l1_block_number: non_neg_integer(), + game_index: non_neg_integer(), + game_address_hash: Hash.t() + } + + @typedoc """ + * `withdrawal_hash` - A withdrawal hash. + * `l1_event_type` - A type of withdrawal event: `WithdrawalProven` or `WithdrawalFinalized`. + * `l1_timestamp` - A timestamp of when the withdrawal event appeared. + * `l1_transaction_hash` - An hash of L1 transaction that contains the event. + * `l1_block_number` - An L1 block number of the L1 transaction. + * `game_index` - An index of a dispute game (if available in L1 transaction input) when + the withdrawal is proven. Equals to `nil` if not available. + * `game_address_hash` - Contract address of a dispute game (if available in L1 transaction input) when + the withdrawal is proven. Equals to `nil` if not available. """ @primary_key false typed_schema "op_withdrawal_events" do @@ -25,6 +49,7 @@ defmodule Explorer.Chain.Optimism.WithdrawalEvent do field(:l1_transaction_hash, Hash.Full, primary_key: true) field(:l1_block_number, :integer) field(:game_index, :integer) + field(:game_address_hash, Hash.Address) timestamps() end diff --git a/apps/explorer/priv/optimism/migrations/20250428080401_game_address_field_in_withdrawals.exs b/apps/explorer/priv/optimism/migrations/20250428080401_game_address_field_in_withdrawals.exs new file mode 100644 index 000000000000..265c64d5f168 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20250428080401_game_address_field_in_withdrawals.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Optimism.Migrations.GameAddressFieldInWithdrawals do + use Ecto.Migration + + def change do + alter table(:op_withdrawal_events) do + add(:game_address_hash, :bytea, null: true) + end + + rename(table(:op_dispute_games), :address, to: :address_hash) + create(index(:op_dispute_games, :address_hash)) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism.ex b/apps/indexer/lib/indexer/fetcher/optimism.ex index 9657fd2070bc..16d64816a0de 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism.ex @@ -308,6 +308,7 @@ defmodule Indexer.Fetcher.Optimism do Updates the last handled block hash by a fetcher. The new block hash is written to the `last_fetched_counters` table. This function accepts the block number for which the block hash must be determined. + If RPC node returns `nil` as the successful result, this function doesn't do anything. ## Parameters - `block_number`: The number of the block. @@ -319,11 +320,16 @@ defmodule Indexer.Fetcher.Optimism do """ @spec set_last_block_hash_by_number(non_neg_integer(), binary(), EthereumJSONRPC.json_rpc_named_arguments()) :: any() def set_last_block_hash_by_number(block_number, counter_type, json_rpc_named_arguments) do - [block_number] - |> get_blocks_by_numbers(json_rpc_named_arguments, Helper.infinite_retries_number()) - |> List.first() - |> Map.get("hash") - |> set_last_block_hash(counter_type) + block = + [block_number] + |> get_blocks_by_numbers(json_rpc_named_arguments, Helper.infinite_retries_number()) + |> List.first() + + if not is_nil(block) do + block + |> Map.get("hash") + |> set_last_block_hash(counter_type) + end end @doc """ diff --git a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex index f70f82e416fd..10f730a3efd0 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex @@ -214,7 +214,7 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do query = from( game in DisputeGame, - select: %{index: game.index, address: game.address}, + select: %{index: game.index, address_hash: game.address_hash}, where: is_nil(game.resolved_at), order_by: [desc: game.index], limit: 1000 @@ -385,12 +385,12 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do defp decode_games(responses) do responses |> Enum.map(fn response -> - [game_type, created_at, address] = Helper.decode_data(response.result, [{:uint, 32}, {:uint, 64}, :address]) + [game_type, created_at, address_hash] = Helper.decode_data(response.result, [{:uint, 32}, {:uint, 64}, :address]) %{ index: response.id, game_type: game_type, - address: address, + address_hash: address_hash, created_at: Timex.from_unix(created_at) } end) @@ -400,14 +400,14 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do requests = games |> Enum.map(fn game -> - address = - if is_binary(game.address) do - ExplorerHelper.add_0x_prefix(game.address) + address_hash = + if is_binary(game.address_hash) do + ExplorerHelper.add_0x_prefix(game.address_hash) else - game.address + game.address_hash end - Contract.eth_call_request(method_id, address, game.index, nil, nil) + Contract.eth_call_request(method_id, address_hash, game.index, nil, nil) end) error_message = &"Cannot call #{method_name} public getter of FaultDisputeGame. Error: #{inspect(&1)}" diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index db6fd855273a..171625cae67a 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -649,24 +649,44 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do end end - defp celestia_blob_to_input("0x" <> transaction_input, transaction_hash, blobs_api_url) do + # Gets Celestia blob data & metadata by L1 transaction input (encoding blob's height and commitment). + # The data is read from the remote `da-indexer` service. + # + # ## Parameters + # - `transaction_input`: The contents of transaction `input` field. + # - `offset`: Offset (in bytes) within the `transaction_input` where blob's height is encoded. + # - `transaction_hash`: The L1 transaction hash for logging purposes. + # - `blobs_api_url`: The URL to `da-indexer` API. + # + # ## Returns + # - A list with a map containing blob's data and metadata. + # - An empty list in case of an error (`da-indexer` didn't respond, URL is not defined, or transaction input is incorrect). + @spec celestia_blob_to_input(binary(), non_neg_integer(), String.t(), String.t()) :: [ + %{ + :bytes => binary(), + :celestia_blob_metadata => %{ + :height => non_neg_integer(), + :namespace => String.t(), + :commitment => String.t() + } + } + ] + defp celestia_blob_to_input("0x" <> transaction_input, offset, transaction_hash, blobs_api_url) do transaction_input |> Base.decode16!(case: :mixed) - |> celestia_blob_to_input(transaction_hash, blobs_api_url) + |> celestia_blob_to_input(offset, transaction_hash, blobs_api_url) end - defp celestia_blob_to_input(transaction_input, _transaction_hash, blobs_api_url) - when byte_size(transaction_input) == 1 + 8 + 32 and blobs_api_url != "" do - # the first byte encodes Celestia sign 0xCE - - # the next 8 bytes encode little-endian Celestia blob height + defp celestia_blob_to_input(transaction_input, offset, _transaction_hash, blobs_api_url) + when byte_size(transaction_input) == offset + 8 + 32 and blobs_api_url != "" do + # 8 bytes after alt-da signature encode little-endian Celestia blob height height = transaction_input - |> binary_part(1, 8) + |> binary_part(offset, 8) |> :binary.decode_unsigned(:little) # the next 32 bytes contain the commitment - commitment = binary_part(transaction_input, 1 + 8, 32) + commitment = binary_part(transaction_input, offset + 8, 32) commitment_string = Base.encode16(commitment, case: :lower) url = blobs_api_url <> "?height=#{height}&commitment=" <> commitment_string @@ -699,13 +719,13 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do end end - defp celestia_blob_to_input(_transaction_input, transaction_hash, blobs_api_url) when blobs_api_url != "" do + defp celestia_blob_to_input(_transaction_input, _offset, transaction_hash, blobs_api_url) when blobs_api_url != "" do Logger.error("L1 transaction with Celestia commitment has incorrect input length. Tx hash: #{transaction_hash}") [] end - defp celestia_blob_to_input(_transaction_input, _transaction_hash, "") do + defp celestia_blob_to_input(_transaction_input, _offset, _transaction_hash, "") do Logger.error( "Cannot read Celestia blobs from the server as the API URL is not defined. Please, check INDEXER_OPTIMISM_L1_BATCH_CELESTIA_BLOBS_API_URL env variable." ) @@ -739,9 +759,13 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do chain_id_l1 ) - first_byte(transaction.input) == 0xCE -> + commitment_alt_da_signature(transaction.input) == 0x01010C -> # this is Celestia DA transaction, so we get the data from Celestia blob - celestia_blob_to_input(transaction.input, transaction.hash, celestia_blobs_api_url) + celestia_blob_to_input(transaction.input, 3, transaction.hash, celestia_blobs_api_url) + + first_byte(transaction.input) == 0xCE -> + # backward compatibility with OP Celestia Raspberry + celestia_blob_to_input(transaction.input, 1, transaction.hash, celestia_blobs_api_url) true -> # this is calldata transaction, so the data is in the transaction input @@ -1422,14 +1446,38 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do end end + # Retrieves Alt-DA signature from L1 transaction input as described in + # https://github.com/ethereum-optimism/specs/blob/main/specs/experimental/alt-da.md#example-commitments + # + # ## Parameters + # - `transaction_input`: The contents of transaction `input` field. + # + # ## Returns + # - An integer encoding the signature. + # - `nil` if the input doesn't contain Alt-DA signature. + @spec commitment_alt_da_signature(binary()) :: non_neg_integer() | nil + defp commitment_alt_da_signature("0x" <> transaction_input) do + transaction_input + |> Base.decode16!(case: :mixed) + |> commitment_alt_da_signature() + end + + defp commitment_alt_da_signature(<<0, _rest::binary>>), do: 0x00 + defp commitment_alt_da_signature(<<1, 0, _rest::binary>>), do: 0x0100 + + defp commitment_alt_da_signature(<<1, 1, da_layer_byte::size(8), _rest::binary>>), + do: :binary.decode_unsigned(<<0x01, 0x01, da_layer_byte>>) + + defp commitment_alt_da_signature(_), do: nil + defp first_byte("0x" <> transaction_input) do transaction_input |> Base.decode16!(case: :mixed) |> first_byte() end - defp first_byte(<>) do - version_byte + defp first_byte(<>) do + first_byte end defp first_byte(_transaction_input) do diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex index 4f26648d6daa..9280ef6ebfb3 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex @@ -191,6 +191,15 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do end) end + # Prepares withdrawal events from `eth_getLogs` response to be imported to DB. + # + # ## Parameters + # - `events`: The list of L1 withdrawal events from `eth_getLogs` response. + # - `json_rpc_named_arguments`: JSON-RPC configuration containing transport options for L1. + # + # ## Returns + # - A list of `WithdrawalEvent` maps. + @spec prepare_events([map()], EthereumJSONRPC.json_rpc_named_arguments()) :: [WithdrawalEvent.to_import()] defp prepare_events(events, json_rpc_named_arguments) do blocks = events @@ -220,16 +229,16 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do |> Enum.map(fn event -> transaction_hash = event["transactionHash"] - {l1_event_type, game_index} = + {l1_event_type, game_index, game_address_hash} = if Enum.member?([@withdrawal_proven_event, @withdrawal_proven_event_blast], Enum.at(event["topics"], 0)) do - game_index = + {game_index, game_address_hash} = input_by_hash |> Map.get(transaction_hash) - |> input_to_game_index() + |> input_to_game_index_or_address_hash() - {"WithdrawalProven", game_index} + {:WithdrawalProven, game_index, game_address_hash} else - {"WithdrawalFinalized", nil} + {:WithdrawalFinalized, nil, nil} end l1_block_number = quantity_to_integer(event["blockNumber"]) @@ -240,20 +249,10 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do l1_timestamp: Map.get(timestamps, l1_block_number), l1_transaction_hash: transaction_hash, l1_block_number: l1_block_number, - game_index: game_index + game_index: game_index, + game_address_hash: game_address_hash } end) - |> Enum.reduce(%{}, fn e, acc -> - key = {e.withdrawal_hash, e.l1_event_type} - prev_game_index = Map.get(acc, key, %{game_index: 0}).game_index - - if prev_game_index < e.game_index or is_nil(prev_game_index) do - Map.put(acc, key, e) - else - acc - end - end) - |> Map.values() end @doc """ @@ -323,26 +322,68 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do end end - defp input_to_game_index(input) do + # Parses input of the prove L1 transaction and retrieves dispute game index or contract address hash + # (depending on whether Super Roots are active) from that. + # + # ## Parameters + # - `input`: The L1 transaction input in form of `0x` string. + # + # ## Returns + # - `{game_index, game_address_hash}` tuple where one of the elements is not `nil`, but another one is `nil` (and vice versa). + # Both elements can be `nil` if the input cannot be parsed (or has unsupported format). + @spec input_to_game_index_or_address_hash(String.t()) :: {non_neg_integer() | nil, String.t() | nil} + defp input_to_game_index_or_address_hash(input) do method_signature = String.slice(input, 0..9) - if method_signature == "0x4870496f" do - # the signature of `proveWithdrawalTransaction(tuple _transaction, uint256 _disputeGameIndex, tuple _outputRootProof, bytes[] _withdrawalProof)` method + case method_signature do + "0x4870496f" -> + # the signature of `proveWithdrawalTransaction(tuple _transaction, uint256 _disputeGameIndex, tuple _outputRootProof, bytes[] _withdrawalProof)` method + {game_index, ""} = + method_signature + |> slice_game_index_or_address_hash(input) + |> Integer.parse(16) - # to get (slice) `_disputeGameIndex` from the transaction input, we need to know its offset in the input string (represented as 0x...): - # offset = 10 symbols of signature (incl. `0x` prefix) + 64 symbols (representing 32 bytes) of the `_transaction` tuple offset, totally is 74 - game_index_offset = String.length(method_signature) + 32 * 2 - game_index_length = 32 * 2 + {game_index, nil} - game_index_range_start = game_index_offset - game_index_range_end = game_index_range_start + game_index_length - 1 + "0x8c90dd65" -> + # the signature of `proveWithdrawalTransaction(tuple _transaction, address _disputeGameProxy, uint256 _outputRootIndex, tuple _superRootProof, tuple _outputRootProof, bytes[] _withdrawalProof)` method + game_address_hash = + method_signature + |> slice_game_index_or_address_hash(input) + |> String.trim_leading("000000000000000000000000") + |> String.pad_leading(42, "0x") - {game_index, ""} = - input - |> String.slice(game_index_range_start..game_index_range_end) - |> Integer.parse(16) + {nil, game_address_hash} - game_index + _ -> + {nil, nil} end end + + # Gets (slices) the dispute game index or its address hash from the transaction input represented as `0x` string. + # + # The input is calldata for either + # `proveWithdrawalTransaction(tuple _transaction, uint256 _disputeGameIndex, tuple _outputRootProof, bytes[] _withdrawalProof)` + # or + # `proveWithdrawalTransaction(tuple _transaction, address _disputeGameProxy, uint256 _outputRootIndex, tuple _superRootProof, tuple _outputRootProof, bytes[] _withdrawalProof)` + # method. + # + # ## Parameters + # - `method_signature`: The method signature string (including `0x` prefix). + # - `input`: The input string (including `0x` prefix). + # + # ## Returns + # - The slice of the input containing dispute game index or address hash. + @spec slice_game_index_or_address_hash(String.t(), String.t()) :: String.t() + defp slice_game_index_or_address_hash(method_signature, input) do + # to get (slice) the index or address from the transaction input, we need to know its offset in the input string (represented as 0x...): + # offset = signature_length (10 symbols including `0x`) + 64 symbols (representing 32 bytes) of the `_transaction` tuple offset, totally is 74 + offset = String.length(method_signature) + 32 * 2 + length = 32 * 2 + + range_start = offset + range_end = range_start + length - 1 + + String.slice(input, range_start..range_end) + end end From c250f5ed9129fa0101fed9730dbfafbcb7f6f30f Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 9 May 2025 14:50:55 +0400 Subject: [PATCH 0029/1006] fix: Start PubSub before Endpoint (#12274) * fix: Start PubSub before Endpoint * Add children order explanation --- .../lib/block_scout_web/application.ex | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 6e8b5b4b541c..a3bf07f2d4bb 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -14,9 +14,15 @@ defmodule BlockScoutWeb.Application do if Application.get_env(:nft_media_handler, :standalone_media_worker?) do Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. base_children = [Supervisor.child_spec(Endpoint, [])] - api_children = setup_and_define_children() - all_children = base_children ++ api_children + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children Supervisor.start_link(all_children, opts) end @@ -32,7 +38,7 @@ defmodule BlockScoutWeb.Application do if @disable_api? do defp setup_and_define_children do BlockScoutWeb.Prometheus.Exporter.setup() - [] + {[], []} end else defp setup_and_define_children do @@ -60,18 +66,21 @@ defmodule BlockScoutWeb.Application do ) # Define workers and child supervisors to be supervised - [ - # Start the endpoint when the application starts - {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, - {Absinthe.Subscription, Endpoint}, - {MainPageRealtimeEventHandler, name: MainPageRealtimeEventHandler}, - {RealtimeEventHandler, name: RealtimeEventHandler}, - {SmartContractRealtimeEventHandler, name: SmartContractRealtimeEventHandler}, - {BlocksIndexedCounter, name: BlocksIndexedCounter}, - {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, - {EventHandlersMetrics, []}, - {ChainMetrics, []} - ] + { + [ + {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, + {MainPageRealtimeEventHandler, name: MainPageRealtimeEventHandler}, + {RealtimeEventHandler, name: RealtimeEventHandler}, + {SmartContractRealtimeEventHandler, name: SmartContractRealtimeEventHandler}, + {BlocksIndexedCounter, name: BlocksIndexedCounter}, + {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, + {EventHandlersMetrics, []}, + {ChainMetrics, []} + ], + [ + {Absinthe.Subscription, Endpoint} + ] + } end end end From 2f03cfafc9786f41cacd9687c63cc72e6f9518f4 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 9 May 2025 14:53:17 +0400 Subject: [PATCH 0030/1006] fix: Fix retrieving max block number in MissingRangesCollector (#12333) --- .../block/catchup/missing_ranges_collector.ex | 23 ++++----- .../bound_interval_supervisor_test.exs | 4 ++ .../catchup/missing_ranges_collector_test.exs | 48 +++++++++++++++++++ 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex index 26f526855e53..20a7a7d7b8b9 100644 --- a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex +++ b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex @@ -63,7 +63,6 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.{Chain, Helper, Repo} - alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Utility.{MissingBlockRange, MissingRangesManipulator} @@ -393,23 +392,17 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do @spec last_block() :: non_neg_integer() defp last_block do last_block = Application.get_env(:indexer, :last_block) - if last_block, do: last_block + 1, else: fetch_max_block_number() + if last_block, do: last_block + 1, else: fetch_max_block_number_from_node() end - # Retrieves the highest block number from cache or blockchain. - @spec fetch_max_block_number() :: non_neg_integer() - defp fetch_max_block_number do - case BlockNumber.get_max() do - 0 -> - json_rpc_named_arguments = Application.get_env(:indexer, :json_rpc_named_arguments) + # Retrieves the highest block number from blockchain. + @spec fetch_max_block_number_from_node() :: non_neg_integer() + defp fetch_max_block_number_from_node do + json_rpc_named_arguments = Application.get_env(:indexer, :json_rpc_named_arguments) - case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do - {:ok, number} -> number - _ -> 0 - end - - number -> - number + case EthereumJSONRPC.fetch_block_number_by_tag("latest", json_rpc_named_arguments) do + {:ok, number} -> number + _ -> 0 end end diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index fc81b000867e..8fd2eb4fd445 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -434,6 +434,10 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do insert(:block, number: 0) insert(:block, number: 1) + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + MissingRangesCollector.start_link([]) start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) diff --git a/apps/indexer/test/indexer/block/catchup/missing_ranges_collector_test.exs b/apps/indexer/test/indexer/block/catchup/missing_ranges_collector_test.exs index dea7a91791ec..41f1ac05fa0c 100644 --- a/apps/indexer/test/indexer/block/catchup/missing_ranges_collector_test.exs +++ b/apps/indexer/test/indexer/block/catchup/missing_ranges_collector_test.exs @@ -1,9 +1,13 @@ defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do use Explorer.DataCase, async: false + import Mox + alias Explorer.Utility.MissingBlockRange alias Indexer.Block.Catchup.MissingRangesCollector + setup :set_mox_global + describe "default_init" do setup do initial_env = Application.get_all_env(:indexer) @@ -11,6 +15,11 @@ defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do end test "empty envs" do + stub(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, method: "eth_getBlockByNumber", params: ["latest", false]}], + _options -> + block_response(id, 1_000_000) + end) + insert(:block, number: 1_000_000) insert(:block, number: 500_123) MissingRangesCollector.start_link([]) @@ -23,6 +32,11 @@ defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do assert [999_799..999_700//-1] = batch = MissingBlockRange.get_latest_batch(100) MissingBlockRange.clear_batch(batch) + stub(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, method: "eth_getBlockByNumber", params: ["latest", false]}], + _options -> + block_response(id, 1_000_200) + end) + insert(:block, number: 1_000_200) Process.sleep(1000) @@ -41,6 +55,11 @@ defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do end test "infinite range" do + stub(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, method: "eth_getBlockByNumber", params: ["latest", false]}], + _options -> + block_response(id, 200) + end) + Application.put_env(:indexer, :block_ranges, "1..5,3..5,2qw1..12,10..11a,,asd..qwe,10..latest") insert(:block, number: 200) @@ -96,4 +115,33 @@ defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do assert MissingRangesCollector.parse_block_ranges("10..20,5..15,18..25,35..40,30..50,150..200") == {:finite_ranges, [5..25, 30..50, 150..200]} end + + defp block_response(id, block_number) do + {:ok, + [ + %{ + id: id, + result: %{ + "difficulty" => "0x0", + "gasLimit" => "0x0", + "gasUsed" => "0x0", + "hash" => "0x29c850324e357f3c0c836d79860c5af55f7b651e5d7ee253c1af1b14908af49c", + "extraData" => "0x0", + "logsBloom" => "0x0", + "miner" => "0x0", + "number" => block_number, + "parentHash" => "0x0", + "receiptsRoot" => "0x0", + "size" => "0x0", + "sha3Uncles" => "0x0", + "stateRoot" => "0x0", + "timestamp" => "0x0", + "totalDifficulty" => "0x0", + "transactions" => [], + "transactionsRoot" => "0x0", + "uncles" => [] + } + } + ]} + end end From f74d5bfcc9d85916b7f651a3572d4304b8e7b20f Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 9 May 2025 15:24:40 +0400 Subject: [PATCH 0031/1006] chore: Sanitize ERC-1155 token balances without token ids (#12305) * chore: Sanitize ERC-1155 token balances without token ids * Process review --- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 1 + ...c_1155_token_balances_without_token_ids.ex | 50 +++++++++++++++++++ 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/lib/explorer/migrator/sanitize_erc_1155_token_balances_without_token_ids.ex diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index e9382e5a5195..026f1680b924 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -131,7 +131,8 @@ for migrator <- [ Explorer.Migrator.SanitizeVerifiedAddresses, Explorer.Migrator.SmartContractLanguage, Explorer.Migrator.SanitizeEmptyContractCodeAddresses, - Explorer.Migrator.BackfillMetadataURL + Explorer.Migrator.BackfillMetadataURL, + Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds ] do config :explorer, migrator, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 99cf467ef614..44ac3f5a7e70 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -67,6 +67,7 @@ for migrator <- [ Explorer.Migrator.SmartContractLanguage, Explorer.Migrator.SanitizeEmptyContractCodeAddresses, Explorer.Migrator.BackfillMetadataURL, + Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index a291bdf6bdc4..083a78a9f640 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -154,6 +154,7 @@ defmodule Explorer.Application do configure(Explorer.Migrator.RestoreOmittedWETHTransfers), configure(Explorer.Migrator.FilecoinPendingAddressOperations), configure(Explorer.Migrator.SmartContractLanguage), + configure(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds), Explorer.Migrator.BackfillMultichainSearchDB |> configure_mode_dependent_process(:indexer) |> configure_multichain_search_microservice(), diff --git a/apps/explorer/lib/explorer/migrator/sanitize_erc_1155_token_balances_without_token_ids.ex b/apps/explorer/lib/explorer/migrator/sanitize_erc_1155_token_balances_without_token_ids.ex new file mode 100644 index 000000000000..1c561e1dc4ec --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/sanitize_erc_1155_token_balances_without_token_ids.ex @@ -0,0 +1,50 @@ +defmodule Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds do + @moduledoc """ + Deletes token balances of ERC-1155 tokens with empty token_id. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "sanitize_erc_1155_token_balances_without_token_ids" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> select([tb], tb.id) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + from( + tb in TokenBalance, + join: t in assoc(tb, :token), + where: tb.token_type == ^"ERC-1155" and t.type == ^"ERC-1155" and is_nil(tb.token_id) + ) + end + + @impl FillingMigration + def update_batch(token_balance_ids) do + query = from(tb in TokenBalance, where: tb.id in ^token_balance_ids) + + Repo.delete_all(query, timeout: :infinity) + end + + @impl FillingMigration + def update_cache, do: :ok +end From 28ca29c8ad24359b10dfd3eb86f42917cedddae3 Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Fri, 9 May 2025 16:23:11 +0300 Subject: [PATCH 0032/1006] Update comment --- apps/block_scout_web/lib/block_scout_web/routers/api_router.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 236724dec0fa..256f7cc5274a 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -497,7 +497,8 @@ defmodule BlockScoutWeb.Routers.ApiRouter do # leave the same endpoint in v1 in order to keep backward compatibility get("/search", SearchController, :search) - # todo: remove these old CSV export related endpoints in 7.2.0 or higher since they are moved to /api/v2/** path + # todo: remove these old CSV export related endpoints in 7.2.0 or higher since they are moved to /api/v2/** path. + # Related frontend task https://github.com/blockscout/frontend/issues/2718. get("/transactions-csv", V2.CsvExportController, :transactions_csv) get("/token-transfers-csv", V2.CsvExportController, :token_transfers_csv) get("/internal-transactions-csv", V2.CsvExportController, :internal_transactions_csv) From 317416b0da3c4ec77b726d9e4268eacdab273c41 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 9 May 2025 17:40:41 +0400 Subject: [PATCH 0033/1006] fix: Force index usage on select current token balances (#12390) * fix: Force index usage on select current token balances * Update apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex Co-authored-by: Victor Baranov * Remove changelog record --------- Co-authored-by: Victor Baranov --- .../runner/address/current_token_balances.ex | 66 +++++++++++++------ .../address/current_token_balances_test.exs | 34 ++++++++++ 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index 7780bc301776..ac9a9c6a01de 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -239,35 +239,59 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do defp select_existing_current_token_balances(_repo, [], _with_token_id?), do: [] - defp select_existing_current_token_balances(repo, params, with_token_id?) do - params - |> existing_ctb_query(with_token_id?) - |> repo.all() - end - - defp existing_ctb_query(params, false) do + defp select_existing_current_token_balances(repo, params, false) do ids = params |> Enum.map(&{&1.address_hash.bytes, &1.token_contract_address_hash.bytes}) |> Enum.uniq() - from( - ctb in CurrentTokenBalance, - where: is_nil(ctb.token_id), - where: ^QueryHelper.tuple_in([:address_hash, :token_contract_address_hash], ids) - ) + existing_ctb_query = + from( + ctb in CurrentTokenBalance, + where: is_nil(ctb.token_id), + where: ^QueryHelper.tuple_in([:address_hash, :token_contract_address_hash], ids) + ) + + repo.all(existing_ctb_query) end - defp existing_ctb_query(params, true) do - ids = - params - |> Enum.map(&{&1.address_hash.bytes, &1.token_contract_address_hash.bytes, &1.token_id}) - |> Enum.uniq() + defp select_existing_current_token_balances(repo, params, true) do + ids = Enum.map(params, &[&1.address_hash.bytes, &1.token_contract_address_hash.bytes, &1.token_id]) + + placeholders = + ids + |> Enum.with_index(1) + |> Enum.map_join(",", fn {_, i} -> + # The value 3 corresponds to the number of parameters in each group within the WHERE clause. + # If this number changes, make sure to update it accordingly. For example, placeholders for + # an array of ids [[1, 2, 3], [4, 5, 6]] would be formatted as: ($1, $2, $3),($4, $5, $6)". + "($#{3 * i - 2}, $#{3 * i - 1}, $#{3 * i})" + end) - from( - ctb in CurrentTokenBalance, - where: ^QueryHelper.tuple_in([:address_hash, :token_contract_address_hash, :token_id], ids) - ) + # Using raw SQL here is needed to be able to add the `COALESCE` statement + # which is needed to force `fetched_current_token_balances` full index usage + existing_ctb_query = + """ + SELECT address_hash, token_contract_address_hash, token_id, block_number, value, value_fetched_at + FROM address_current_token_balances + WHERE (address_hash, token_contract_address_hash, COALESCE(token_id, -1)) IN (#{placeholders}) + """ + + query_params = List.flatten(ids) + + existing_ctb_query + |> repo.query!(query_params) + |> Map.get(:rows, []) + |> Enum.map(fn [address_hash, token_contract_address_hash, token_id, block_number, value, value_fetched_at] -> + %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + block_number: block_number, + value: value, + value_fetched_at: value_fetched_at + } + end) end # ctb does not exist diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs index 6789d37b5a8e..5795c32a27f5 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs @@ -273,6 +273,40 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do assert current_token_balance.value == Decimal.new(200) end + test "updates NFT when the new block number is greater", %{ + address: address, + token: token, + options: options + } do + insert( + :address_current_token_balance, + address: address, + block_number: 1, + token_contract_address_hash: token.contract_address_hash, + token_id: 123, + token_type: "ERC-1155", + value: 100 + ) + + run_changes( + %{ + address_hash: address.hash, + block_number: 2, + token_contract_address_hash: token.contract_address_hash, + token_id: 123, + token_type: "ERC-1155", + value: Decimal.new(200), + value_fetched_at: DateTime.utc_now() + }, + options + ) + + current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address.hash) + + assert current_token_balance.block_number == 2 + assert current_token_balance.value == Decimal.new(200) + end + test "ignores when the new block number is lesser", %{ address: %Address{hash: address_hash} = address, token: %Token{contract_address_hash: token_contract_address_hash}, From 09587df4c67dc7c0bc4235b20ac6094cbdb29a60 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 9 May 2025 19:13:47 +0400 Subject: [PATCH 0034/1006] chore: Update blocks consensus in case of import failure (#12243) * chore: Update blocks consensus in case of import failure * Process review --- apps/explorer/lib/explorer/chain/import.ex | 9 ++-- .../explorer/chain/import/runner/blocks.ex | 50 +++++++++++++++---- .../chain/import/runner/blocks_test.exs | 2 +- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 32291e0cb754..d8c20ed4756b 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -348,12 +348,12 @@ defmodule Explorer.Chain.Import do {:ok, result} error -> - set_refetch_needed_for_partially_imported_blocks(options) + handle_partially_imported_blocks(options) error end rescue exception -> - set_refetch_needed_for_partially_imported_blocks(options) + handle_partially_imported_blocks(options) reraise exception, __STACKTRACE__ end @@ -406,14 +406,15 @@ defmodule Explorer.Chain.Import do end) end - defp set_refetch_needed_for_partially_imported_blocks(%{blocks: %{params: blocks_params}}) do + defp handle_partially_imported_blocks(%{blocks: %{params: blocks_params}}) do block_numbers = Enum.map(blocks_params, & &1.number) Block.set_refetch_needed(block_numbers) + Import.Runner.Blocks.process_blocks_consensus(blocks_params) Logger.warning("Set refetch_needed for partially imported block because of error: #{inspect(block_numbers)}") end - defp set_refetch_needed_for_partially_imported_blocks(_options), do: :ok + defp handle_partially_imported_blocks(_options), do: :ok @spec timestamps() :: timestamps def timestamps do diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 4972461ff1e2..fe110686d523 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -34,6 +34,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances alias Explorer.Chain.Import.Runner.{Addresses, TokenInstances, Tokens} alias Explorer.Prometheus.Instrumenter + alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator @behaviour Runner @@ -68,11 +69,9 @@ defmodule Explorer.Chain.Import.Runner.Blocks do hashes = Enum.map(changes_list, & &1.hash) - consensus_block_numbers = consensus_block_numbers(changes_list) - # Enforce ShareLocks tables order (see docs: sharelocks.md) run_func = fn repo -> - {:ok, nonconsensus_items} = lose_consensus(repo, hashes, consensus_block_numbers, changes_list, insert_options) + {:ok, nonconsensus_items} = process_blocks_consensus(changes_list, repo, insert_options) {:ok, RangesHelper.filter_by_height_range(nonconsensus_items, fn {number, _hash} -> @@ -399,10 +398,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do |> Enum.map(& &1.number) end - def lose_consensus(repo, hashes, consensus_block_numbers, changes_list, %{ - timeout: timeout, - timestamps: %{updated_at: updated_at} - }) do + defp lose_consensus(repo, changes_list, %{ + timeout: timeout, + timestamps: %{updated_at: updated_at} + }) do + hashes = Enum.map(changes_list, & &1.hash) + consensus_block_numbers = consensus_block_numbers(changes_list) + acquire_query = from( block in where_invalid_neighbor(changes_list), @@ -415,7 +417,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do lock: "FOR NO KEY UPDATE" ) - {_, removed_consensus_block_hashes} = + {_, removed_consensus_blocks} = repo.update_all( from( block in Block, @@ -453,15 +455,41 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) - removed_consensus_block_hashes + removed_consensus_blocks |> Enum.map(fn {number, _hash} -> number end) |> Enum.reject(&Enum.member?(consensus_block_numbers, &1)) |> MissingRangesManipulator.add_ranges_by_block_numbers() - {:ok, removed_consensus_block_hashes} + {:ok, removed_consensus_blocks} rescue postgrex_error in Postgrex.Error -> - {:error, %{exception: postgrex_error, consensus_block_numbers: consensus_block_numbers}} + {:error, %{exception: postgrex_error}} + end + + @doc """ + Processes consensus for blocks that failed to import completely. + + This function handles the consistency updates needed when a block import fails, + ensuring that the chain's consensus state remains valid. + + ## Parameters + - `blocks_changes`: List of block changes to process + + ## Returns + - `{:ok, removed_consensus_blocks}`: List of tuples {number, hash} for blocks that lost consensus + - `{:error, reason}`: The error encountered during processing + """ + @spec process_blocks_consensus([map()], module(), map() | nil) :: + {:ok, [{non_neg_integer(), binary()}]} | {:error, map()} + def process_blocks_consensus(blocks_changes, repo \\ ExplorerRepo, insert_options \\ nil) do + opts = + insert_options || + %{ + timeout: @timeout, + timestamps: %{updated_at: DateTime.utc_now()} + } + + lose_consensus(repo, blocks_changes, opts) end defp new_pending_block_operations(repo, inserted_blocks, %{timeout: timeout, timestamps: timestamps}) do diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 6d1edce350f0..a2386f8fe09c 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -692,7 +692,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do timestamps: %{updated_at: DateTime.utc_now()} } - assert {:ok, [{0, _}, {1, _}]} = Blocks.lose_consensus(Repo, [], [1], [new_block1_changes], opts) + assert {:ok, [{0, _}, {1, _}]} = Blocks.process_blocks_consensus([new_block1_changes], Repo, opts) end end From f406c18d232cbdfde3a3b3268a857133a0d83cfc Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Mon, 12 May 2025 12:13:57 +0400 Subject: [PATCH 0035/1006] Remove commas (#12404) --- apps/explorer/lib/explorer/chain/optimism/withdrawal.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex index 3b42fb47215a..649482062c80 100644 --- a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex @@ -25,7 +25,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do @proof_maturity_delay_seconds "optimism_proof_maturity_delay_seconds" @required_attrs ~w(msg_nonce hash l2_transaction_hash l2_block_number)a - @game_fields ~w(created_at, resolved_at, status)a + @game_fields ~w(created_at resolved_at status)a @typedoc """ * `msg_nonce` - A nonce of the withdrawal message. From 8b27c1ff3d83e0549ef0a00fc1e3980a23dee6a6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 12 May 2025 12:55:27 +0300 Subject: [PATCH 0036/1006] feat: Add `internal_transactions_count` prop in api/v2/blocks/:block endpoint (#12405) --- .../controllers/api/v2/block_controller.ex | 3 ++- .../block_scout_web/views/api/v2/block_view.ex | 15 +++++++++++---- apps/explorer/lib/explorer/chain/block.ex | 3 +++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 05ce918b1292..0cd1c39b4b75 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -127,7 +127,8 @@ defmodule BlockScoutWeb.API.V2.BlockController do :nephews => :optional, :rewards => :optional, :transactions => :optional, - :withdrawals => :optional + :withdrawals => :optional, + :internal_transactions => :optional } |> Map.merge(@chain_type_block_necessity_by_association), api?: true diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 157fdc0957f3..7c2510dc2f1e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -40,6 +40,7 @@ defmodule BlockScoutWeb.API.V2.BlockView do "transactions_count" => count_transactions(block), # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 "transaction_count" => count_transactions(block), + "internal_transactions_count" => count_internal_transactions(block), "miner" => Helper.address_with_info(nil, block.miner, block.miner_hash, false), "size" => block.size, "hash" => block.hash, @@ -96,11 +97,17 @@ defmodule BlockScoutWeb.API.V2.BlockView do def burnt_fees_percentage(_, _), do: nil - def count_transactions(%Block{transactions: transactions}) when is_list(transactions), do: Enum.count(transactions) - def count_transactions(_), do: nil + defp count_transactions(%Block{transactions: transactions}) when is_list(transactions), do: Enum.count(transactions) + defp count_transactions(_), do: nil - def count_withdrawals(%Block{withdrawals: withdrawals}) when is_list(withdrawals), do: Enum.count(withdrawals) - def count_withdrawals(_), do: nil + defp count_internal_transactions(%Block{internal_transactions: internal_transactions}) + when is_list(internal_transactions), + do: Enum.count(internal_transactions) + + defp count_internal_transactions(_), do: nil + + defp count_withdrawals(%Block{withdrawals: withdrawals}) when is_list(withdrawals), do: Enum.count(withdrawals) + defp count_withdrawals(_), do: nil case @chain_type do :rsk -> diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 92e8620e792b..685f3bee5fcd 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -11,6 +11,7 @@ defmodule Explorer.Chain.Block.Schema do Address, Block, Hash, + InternalTransaction, PendingBlockOperation, Transaction, Wei, @@ -165,6 +166,8 @@ defmodule Explorer.Chain.Block.Schema do has_many(:transactions, Transaction, references: :hash) has_many(:transaction_forks, Transaction.Fork, foreign_key: :uncle_hash, references: :hash) + has_many(:internal_transactions, InternalTransaction, foreign_key: :block_hash, references: :hash) + has_many(:rewards, Reward, foreign_key: :block_hash, references: :hash) has_many(:withdrawals, Withdrawal, foreign_key: :block_hash, references: :hash) From fca1ebd5e5f6c269b3f56db514710c4c352aa03e Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 12 May 2025 14:42:51 +0300 Subject: [PATCH 0037/1006] fix: Fix :checkout_timeout error (#12406) * fix: Fix :checkout_timeout error * Add env to .env * Fix .env file * Set hackney: [pool: false] for multichain search --- apps/explorer/lib/explorer/application.ex | 7 ++++++- .../explorer/microservice_interfaces/multichain_search.ex | 2 +- config/runtime.exs | 3 ++- docker-compose/envs/common-blockscout.env | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 083a78a9f640..38c2a22cd959 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -89,7 +89,12 @@ defmodule Explorer.Application do con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), {Redix, redix_opts()}, {Explorer.Utility.MissingRangesManipulator, []}, - {Explorer.Utility.ReplicaAccessibilityManager, []} + {Explorer.Utility.ReplicaAccessibilityManager, []}, + :hackney_pool.child_spec(:default, + recv_timeout: 60_000, + timeout: 60_000, + max_connections: Application.get_env(:explorer, :hackney_default_pool_size) + ) ] children = base_children ++ configurable_children() diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 41e99b9ac4a2..45d7a08102bb 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -59,7 +59,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout, hackney: [pool: false]) do {:ok, %Response{body: response_body, status_code: 200}} -> response_body |> Jason.decode() diff --git a/config/runtime.exs b/config/runtime.exs index 9accfedb908f..02e0741956cb 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -269,7 +269,8 @@ config :explorer, base_fee_max_change_denominator: ConfigHelper.parse_integer_env_var("EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR", 8), csv_export_limit: ConfigHelper.parse_integer_env_var("CSV_EXPORT_LIMIT", 10_000), shrink_internal_transactions_enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), - replica_max_lag: ConfigHelper.parse_time_env_var("REPLICA_MAX_LAG", "5m") + replica_max_lag: ConfigHelper.parse_time_env_var("REPLICA_MAX_LAG", "5m"), + hackney_default_pool_size: ConfigHelper.parse_integer_env_var("HACKNEY_DEFAULT_POOL_SIZE", 1000) config :explorer, Explorer.Chain.Health.Monitor, check_interval: ConfigHelper.parse_time_env_var("HEALTH_MONITOR_CHECK_INTERVAL", "1m"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 886d5f31e211..74bf6eea2dc8 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -563,3 +563,4 @@ RELEASE_COOKIE=secret_cookie # NETWORK_PATH=/ # RE_CAPTCHA_CLIENT_KEY= # RE_CAPTCHA_V3_CLIENT_KEY= +# HACKNEY_DEFAULT_POOL_SIZE=1000 \ No newline at end of file From 76b6645dde9bb4caf85dbac7ff38519917462f91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 15:27:27 +0300 Subject: [PATCH 0038/1006] chore(deps-dev): bump sobelow from 0.13.0 to 0.14.0 (#12409) Bumps [sobelow](https://github.com/sobelow/sobelow) from 0.13.0 to 0.14.0. - [Changelog](https://github.com/sobelow/sobelow/blob/main/CHANGELOG.md) - [Commits](https://github.com/sobelow/sobelow/commits) --- updated-dependencies: - dependency-name: sobelow dependency-version: 0.14.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 9bb02088ac1d..a43de6d1cad2 100644 --- a/mix.lock +++ b/mix.lock @@ -150,7 +150,7 @@ "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, "siwe": {:git, "https://github.com/royal-markets/siwe-ex.git", "51c9c08240eb7eea3c35693011f8d260cd9bb3be", [ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, - "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, + "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, "spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"}, "spandex_datadog": {:hex, :spandex_datadog, "1.4.0", "0594b9655b0af00ab9137122616bc0208b68ceec01e9916ab13d6fbb33dcce35", [:mix], [{:msgpax, "~> 2.2.1 or ~> 2.3", [hex: :msgpax, repo: "hexpm", optional: false]}, {:spandex, "~> 3.2", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "360f8e1b4db238c1749c4872b1697b096429927fa42b8858d0bb782067380123"}, "spandex_ecto": {:hex, :spandex_ecto, "0.7.0", "259ad2feb7c834e774ec623f99c0fbacca8d60a73be212f92b75e37f853c81be", [:mix], [{:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}], "hexpm", "c64784be79d95538013b7c60828830411c5c7aff1f4e8d66dfe564b3c83b500e"}, From 1b52874f8ff6c7659c1ea71b8f79b0d278438334 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 15:30:15 +0300 Subject: [PATCH 0039/1006] chore(deps-dev): bump ex_doc from 0.37.3 to 0.38.1 (#12408) Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.37.3 to 0.38.1. - [Release notes](https://github.com/elixir-lang/ex_doc/releases) - [Changelog](https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md) - [Commits](https://github.com/elixir-lang/ex_doc/compare/v0.37.3...v0.38.1) --- updated-dependencies: - dependency-name: ex_doc dependency-version: 0.38.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index c616c5de97e7..f00d5fed26d4 100644 --- a/mix.exs +++ b/mix.exs @@ -99,7 +99,7 @@ defmodule BlockScout.Mixfile do {:tesla, "~> 1.14.1"}, {:mint, "~> 1.7.1"}, # Documentation - {:ex_doc, "~> 0.37.2", only: :dev, runtime: false}, + {:ex_doc, "~> 0.38.1", only: :dev, runtime: false}, {:number, "~> 1.0.3"} ] end diff --git a/mix.lock b/mix.lock index a43de6d1cad2..dd3917d231bf 100644 --- a/mix.lock +++ b/mix.lock @@ -57,7 +57,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, - "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"}, + "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.10.2", "7c4b8c1481fdeb1741e2ce66223976edfb9bccebc8014f6aec35d4efe964fb71", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "37f43be60f8407659d4d0155a7e45e7f406dab1f827051d3d35858a709baf6a6"}, "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, From 1a7814955f5279971f8897cb441cf6f8d661f57b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 May 2025 17:25:38 +0300 Subject: [PATCH 0040/1006] chore(deps): bump ex_json_schema from 0.10.2 to 0.11.0 (#12407) Bumps [ex_json_schema](https://github.com/jonasschmidt/ex_json_schema) from 0.10.2 to 0.11.0. - [Commits](https://github.com/jonasschmidt/ex_json_schema/commits) --- updated-dependencies: - dependency-name: ex_json_schema dependency-version: 0.11.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index c43b97ea1c67..9dc909757654 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -154,7 +154,7 @@ defmodule BlockScoutWeb.Mixfile do {:timex, "~> 3.7.1"}, {:wallaby, "~> 0.30", only: :test, runtime: false}, # `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility - {:ex_json_schema, "~> 0.10.1"}, + {:ex_json_schema, "~> 0.11.0"}, {:ueberauth, "~> 0.7"}, {:ueberauth_auth0, "~> 2.0"}, {:utils, in_umbrella: true}, diff --git a/mix.lock b/mix.lock index dd3917d231bf..0438251e2c3f 100644 --- a/mix.lock +++ b/mix.lock @@ -59,7 +59,7 @@ "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.10.2", "7c4b8c1481fdeb1741e2ce66223976edfb9bccebc8014f6aec35d4efe964fb71", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "37f43be60f8407659d4d0155a7e45e7f406dab1f827051d3d35858a709baf6a6"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.11.0", "4909d7ae1f0546b6f3c8f73b71732115c86d2198cc7f4e09fda908e7ca86aa9e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "23ea702a36d46906d3fd723a8aeac0b80e5d76c6165fdcb37d83b2df94062f28"}, "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, From f449009bb3d94fe5fd753c50142b1030ac3ce073 Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Tue, 13 May 2025 21:03:00 +0400 Subject: [PATCH 0041/1006] fix: add prague Solidity EVM version (#12115) --- config/runtime.exs | 2 +- docker-compose/envs/common-blockscout.env | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 02e0741956cb..54195ecf92e5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -253,7 +253,7 @@ config :explorer, coin_name: System.get_env("COIN_NAME") || "ETH", allowed_solidity_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS") || - "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,default", + "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,default", allowed_vyper_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS") || "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 74bf6eea2dc8..733dd9c0690d 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -128,7 +128,7 @@ RELEASE_LINK= # TOKEN_INSTANCE_METADATA_REFETCH_ON_DEMAND_FETCHER_THRESHOLD= # ADDRESSES_TABS_COUNTERS_TTL=10m # TOKEN_METADATA_UPDATE_INTERVAL=172800 -# CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,default +# CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,default # CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default # CONTRACT_VERIFICATION_MAX_LIBRARIES=10 # CONTRACT_AUDIT_REPORTS_AIRTABLE_URL= From a1255762a4c0268177ddda0821bf83ffb0481610 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Tue, 13 May 2025 11:07:31 -0600 Subject: [PATCH 0042/1006] fix: frozen confirmations discovery on Arbitrum Nova (#12385) * Sorting of rollup block numbers from SendRootUpdated * format issues addressed * Potentional re-confirmation --- .../arbitrum/reader/indexer/settlement.ex | 38 ++++++-- .../fetcher/arbitrum/utils/db/settlement.ex | 37 +++++++- .../arbitrum/workers/confirmations/events.ex | 79 +++++++++++++++- .../workers/confirmations/rollup_blocks.ex | 92 ++++++++----------- 4 files changed, 176 insertions(+), 70 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/settlement.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/settlement.ex index 205dff778ad3..ba9ee0762eda 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/settlement.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/settlement.ex @@ -247,11 +247,34 @@ defmodule Explorer.Chain.Arbitrum.Reader.Indexer.Settlement do end @doc """ - Retrieves all unconfirmed rollup blocks within the specified range from `first_block` to `last_block`, - inclusive, where `first_block` is less than or equal to `last_block`. + Retrieves all rollup blocks within the specified range from `first_block` to `last_block`, inclusive. - Since the function relies on the block data generated by the block fetcher, the returned list - may contain fewer blocks than actually exist if some of the blocks have not been indexed by the fetcher yet. + The results are ordered by block_number in descending order. + + ## Parameters + - `first_block`: The rollup block number starting the lookup range. + - `last_block`: The rollup block number ending the lookup range. + + ## Returns + - A list of blocks, ordered by block_number in descending order. Returns `[]` + if no blocks are found within the range. + """ + @spec rollup_blocks_by_range(FullBlock.block_number(), FullBlock.block_number()) :: [BatchBlock.t()] + def rollup_blocks_by_range(first_block, last_block) + when is_integer(first_block) and first_block >= 0 and + is_integer(last_block) and first_block <= last_block do + query = + from( + rb in BatchBlock, + where: rb.block_number >= ^first_block and rb.block_number <= ^last_block, + order_by: [desc: rb.block_number] + ) + + Repo.all(query) + end + + @doc """ + Retrieves all unconfirmed rollup blocks within the specified range from `first_block` to `last_block`, inclusive. The results are ordered by block_number in descending order to take advantage of the index (confirmation_id, block_number DESC) where confirmation_id IS NULL. @@ -261,10 +284,9 @@ defmodule Explorer.Chain.Arbitrum.Reader.Indexer.Settlement do - `last_block`:The rollup block number ending the lookup range. ## Returns - - A list of maps containing the batch number, rollup block number and hash for each - unconfirmed block within the range, ordered by block_number in descending order. - Returns `[]` if no unconfirmed blocks are found within the range, or if the block - fetcher has not indexed them. + - A list of unconfirmed blocks within the range, ordered by block_number in + descending order. Returns `[]` if no unconfirmed blocks are found within + the range. """ @spec unconfirmed_rollup_blocks(FullBlock.block_number(), FullBlock.block_number()) :: [BatchBlock.t()] def unconfirmed_rollup_blocks(first_block, last_block) diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex index 076008a7cc5f..811bbca3de03 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex @@ -210,11 +210,14 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Db.Settlement do end @doc """ - Retrieves rollup blocks within a specified block range that have not yet been confirmed. + Retrieves rollup blocks within the specified range from `first_block` to `last_block`, inclusive, + that either: + - Have not been confirmed yet, or + - May need re-confirmation due to potentially incorrect confirmation assignments ## Parameters - - `first_block`: The starting block number of the range to search for unconfirmed rollup blocks. - - `last_block`: The ending block number of the range. + - `first_block`: The rollup block number starting the lookup range. + - `last_block`: The rollup block number ending the lookup range. ## Returns - A list of maps, each representing an unconfirmed rollup block within the specified range, @@ -227,8 +230,32 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Db.Settlement do def unconfirmed_rollup_blocks(first_block, last_block) when is_integer(first_block) and first_block >= 0 and is_integer(last_block) and first_block <= last_block do - # credo:disable-for-lines:2 Credo.Check.Refactor.PipeChainStart - Reader.unconfirmed_rollup_blocks(first_block, last_block) + # Get truly unconfirmed blocks first + unconfirmed_blocks = Reader.unconfirmed_rollup_blocks(first_block, last_block) + + # If there are unconfirmed blocks, check if we need to add more blocks for re-confirmation + blocks_to_transform = + case unconfirmed_blocks do + [] -> + [] + + blocks -> + # Since blocks are in descending order, the first one is the highest unconfirmed + highest_unconfirmed = hd(blocks) + + # If the highest unconfirmed block is not the last_block, it means last_block is already confirmed + # but potentially with wrong transaction. Get all blocks from highest_unconfirmed + 1 to last_block + if highest_unconfirmed.block_number < last_block do + # Get blocks eligible for re-confirmation and combine with truly unconfirmed blocks + reconfirmation_blocks = Reader.rollup_blocks_by_range(highest_unconfirmed.block_number + 1, last_block) + reconfirmation_blocks ++ blocks + else + blocks + end + end + + # Transform blocks to the expected format and maintain ascending order + blocks_to_transform |> Enum.reverse() |> Enum.map(&rollup_block_to_map/1) end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex index 3ea3764db147..cf47ca219b3b 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex @@ -11,7 +11,9 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.Events do alias EthereumJSONRPC.Arbitrum.Constants.Events, as: ArbitrumEvents alias Indexer.Helper, as: IndexerHelper - import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_debug: 1] + alias Indexer.Fetcher.Arbitrum.Utils.Db.Settlement, as: DbSettlement + + import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_debug: 1, log_warning: 1] require Logger @@ -91,4 +93,79 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.Events do l2_block_hash end + + @doc """ + Fetches and sorts rollup block numbers from `SendRootUpdated` events in the specified L1 block range. + + Retrieves logs from the Outbox contract and extracts the top confirmed rollup block numbers. + The block numbers are sorted in descending order to ensure proper handling of overlapping + confirmations by finding the highest already-confirmed block below the current confirmation. + Uses caching to minimize RPC calls. + + ## Parameters + - `log_start`: Starting L1 block number for log retrieval + - `log_end`: Ending L1 block number for log retrieval + - `l1_outbox_config`: Configuration for the Arbitrum outbox contract + - `cache`: Cache for logs to minimize RPC calls + + ## Returns + A tuple containing: + - `{:ok, sorted_block_numbers, new_cache, logs_length}` where: + * `sorted_block_numbers` is a list of rollup block numbers in descending order + * `new_cache` is the updated logs cache + * `logs_length` is the number of logs processed + - `{:error, nil, new_cache, logs_length}` if any block hash cannot be resolved + """ + @spec fetch_and_sort_confirmations_logs( + non_neg_integer(), + non_neg_integer(), + %{ + :outbox_address => binary(), + :json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + optional(any()) => any() + }, + cached_logs() + ) :: + {:ok, [non_neg_integer()], cached_logs(), non_neg_integer()} | {:error, nil, cached_logs(), non_neg_integer()} + def fetch_and_sort_confirmations_logs(log_start, log_end, l1_outbox_config, cache) do + {logs, new_cache} = + get_logs_for_confirmations( + log_start, + log_end, + l1_outbox_config.outbox_address, + l1_outbox_config.json_rpc_named_arguments, + cache + ) + + logs_length = length(logs) + + # Process each log to extract block numbers + blocks = + Enum.reduce_while(logs, {:ok, []}, fn log, {:ok, acc} -> + log_debug("Examining the transaction #{log["transactionHash"]}") + + rollup_block_hash = send_root_updated_event_parse(log) + rollup_block_num = DbSettlement.rollup_block_hash_to_num(rollup_block_hash) + + case rollup_block_num do + nil -> + log_warning("The rollup block ##{rollup_block_hash} not found") + {:halt, :error} + + value -> + log_debug("Found rollup block ##{rollup_block_num}") + {:cont, {:ok, [value | acc]}} + end + end) + + case blocks do + {:ok, list} -> + # Sort block numbers in descending order to find highest confirmed block first + sorted = Enum.sort(list, :desc) + {:ok, sorted, new_cache, logs_length} + + :error -> + {:error, nil, new_cache, logs_length} + end + end end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex index 4b8f5360115e..36a87e9840cd 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex @@ -509,29 +509,20 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.RollupBlocks do |> Enum.to_list() end - # Checks if any blocks within a specific range are identified as the top of confirmed blocks by scanning `SendRootUpdated` events. - # - # This function fetches logs for `SendRootUpdated` events within the specified - # L1 block range to determine if any rollup blocks within the given rollup block - # range are mentioned in the events, indicating the top of confirmed blocks up - # to that log. It uses caching to minimize `eth_getLogs` calls. + # Scans `SendRootUpdated` events in the given L1 block range to find the highest + # rollup block within the specified range that has been confirmed. Uses caching + # to minimize `eth_getLogs` calls. # # ## Parameters - # - A tuple `{rollup_start_block, rollup_end_block}` specifying the rollup block - # range to check for confirmations - # - A tuple `{log_start, log_end}` specifying the L1 block range to fetch logs. - # - `l1_outbox_config`: Configuration for the Arbitrum Outbox contract. - # - `cache`: A cache of previously fetched logs to reduce `eth_getLogs` calls. + # - `{rollup_start_block, rollup_end_block}`: Range of rollup blocks to check + # - `{log_start, log_end}`: Range of L1 blocks to scan for events + # - `l1_outbox_config`: Arbitrum Outbox contract configuration + # - `cache`: Cache of previously fetched logs # # ## Returns - # - A tuple `{:ok, latest_block_confirmed, new_cache, logs_length}`: - # - `latest_block_confirmed` is the highest rollup block number confirmed within - # the specified range. - # - A tuple `{:ok, nil, new_cache, logs_length}` if no rollup blocks within the - # specified range are confirmed. - # - A tuple `{:error, nil, new_cache, logs_length}` if during parsing logs a rollup - # block with given hash is not being found in the database. - # For all three cases the `new_cache` contains the updated logs cache. + # - `{:ok, block_num, new_cache, logs_length}`: Found confirmed block in range + # - `{:ok, nil, new_cache, logs_length}`: No confirmed blocks in range + # - `{:error, nil, new_cache, logs_length}`: Block hash resolution failed @spec do_check_if_batch_confirmed( {non_neg_integer(), non_neg_integer()}, {non_neg_integer(), non_neg_integer()}, @@ -545,48 +536,37 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.RollupBlocks do {:ok, nil | non_neg_integer(), EventsUtils.cached_logs(), non_neg_integer()} | {:error, nil, EventsUtils.cached_logs(), non_neg_integer()} defp do_check_if_batch_confirmed( - {rollup_start_block, rollup_end_block}, + batch_block_range, {log_start, log_end}, l1_outbox_config, cache ) do - # The logs in the given L1 blocks range - {logs, new_cache} = - EventsUtils.get_logs_for_confirmations( - log_start, - log_end, - l1_outbox_config.outbox_address, - l1_outbox_config.json_rpc_named_arguments, - cache - ) - - # For every discovered event check if the rollup block in the confirmation - # is within the specified range which usually means that the event - # is the confirmation of the batch described by the range. - {status, latest_block_confirmed} = - logs - |> Enum.reduce_while({:ok, nil}, fn event, _acc -> - log_debug("Examining the transaction #{event["transactionHash"]}") - - rollup_block_hash = EventsUtils.send_root_updated_event_parse(event) - rollup_block_num = DbSettlement.rollup_block_hash_to_num(rollup_block_hash) - - case rollup_block_num do - nil -> - log_warning("The rollup block ##{rollup_block_hash} not found") - {:halt, {:error, nil}} - - value when value >= rollup_start_block and value <= rollup_end_block -> - log_debug("The rollup block ##{rollup_block_num} within the range") - {:halt, {:ok, rollup_block_num}} - - _ -> - log_debug("The rollup block ##{rollup_block_num} outside of the range") - {:cont, {:ok, nil}} - end - end) + case EventsUtils.fetch_and_sort_confirmations_logs(log_start, log_end, l1_outbox_config, cache) do + {:error, nil, new_cache, logs_length} -> + {:error, nil, new_cache, logs_length} + + # For every discovered event check if the rollup block in the confirmation + # is within the specified range which usually means that the event + # is the confirmation of the batch described by the range. + {:ok, sorted_block_numbers, new_cache, logs_length} -> + latest_block_confirmed = find_first_block_in_range(sorted_block_numbers, batch_block_range) + {:ok, latest_block_confirmed, new_cache, logs_length} + end + end - {status, latest_block_confirmed, new_cache, length(logs)} + # Finds the first block number from the sorted list that falls within the specified range. + @spec find_first_block_in_range([non_neg_integer()], {non_neg_integer(), non_neg_integer()}) :: + non_neg_integer() | nil + defp find_first_block_in_range(sorted_block_numbers, {start_block, end_block}) do + Enum.find_value(sorted_block_numbers, nil, fn block_num -> + if block_num >= start_block and block_num <= end_block do + log_debug("The rollup block ##{block_num} within the range") + block_num + else + log_debug("The rollup block ##{block_num} outside of the range") + nil + end + end) end # Simplifies the process of updating counters for the `eth_getLogs` requests From 1864753a4c29d629cc92faae0ce9a5c643475910 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 14 May 2025 15:59:39 +0400 Subject: [PATCH 0043/1006] chore: Internal transactions unique index (#12394) * chore: Internal transactions unique index * Add reindex IT migration envs to common-blockscout.env --- apps/explorer/config/config.exs | 6 +- apps/explorer/config/runtime/test.exs | 4 +- apps/explorer/lib/explorer/application.ex | 5 + ...sh_transaction_index_index_unique_index.ex | 61 ++++++++++++ .../heavy_db_index_operation/helper.ex | 10 +- ...eindex_duplicated_internal_transactions.ex | 94 +++++++++++++++++++ ..._duplicated_internal_transactions_test.exs | 78 +++++++++++++++ config/runtime.exs | 5 + docker-compose/envs/common-blockscout.env | 3 + 9 files changed, 258 insertions(+), 8 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex create mode 100644 apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex create mode 100644 apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 026f1680b924..4ee164b1cb0d 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -132,7 +132,8 @@ for migrator <- [ Explorer.Migrator.SmartContractLanguage, Explorer.Migrator.SanitizeEmptyContractCodeAddresses, Explorer.Migrator.BackfillMetadataURL, - Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds + Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, + Explorer.Migrator.ReindexDuplicatedInternalTransactions ] do config :explorer, migrator, enabled: true end @@ -164,7 +165,8 @@ for index_operation <- [ Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsToAddressHashWithPendingIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex ] do config :explorer, index_operation, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 44ac3f5a7e70..a2775855cfec 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -68,6 +68,7 @@ for migrator <- [ Explorer.Migrator.SanitizeEmptyContractCodeAddresses, Explorer.Migrator.BackfillMetadataURL, Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, + Explorer.Migrator.ReindexDuplicatedInternalTransactions, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, @@ -95,7 +96,8 @@ for migrator <- [ Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsToAddressHashWithPendingIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex ] do config :explorer, migrator, enabled: false end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 38c2a22cd959..8bbe921094d3 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -187,6 +187,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.SanitizeVerifiedAddresses, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeEmptyContractCodeAddresses, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus, :indexer), + configure_mode_dependent_process(Explorer.Migrator.ReindexDuplicatedInternalTransactions, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer @@ -293,6 +294,10 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex new file mode 100644 index 000000000000..ad5b3fff3719 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex @@ -0,0 +1,61 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex do + @moduledoc """ + Create unique B-tree index `internal_transactions_block_hash_transaction_index_index_index` on `internal_transactions` table for (`block_hash`, `transaction_index`, `index`) columns. + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + require Logger + + alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus, ReindexDuplicatedInternalTransactions} + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :internal_transactions + @index_name "internal_transactions_block_hash_transaction_index_index_index" + @operation_type :create + @table_columns ["block_hash", "transaction_index", "index"] + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations, + do: [ + ReindexDuplicatedInternalTransactions.migration_name() + ] + + @impl HeavyDbIndexOperation + def db_index_operation do + HeavyDbIndexOperationHelper.create_db_index(@index_name, @table_name, @table_columns, true) + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + operation = HeavyDbIndexOperationHelper.create_index_query_string(@index_name, @table_name, @table_columns, true) + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_creation_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache, do: :ok +end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex index d8920bfab76a..d9d167e2717a 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex @@ -106,9 +106,9 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.Helper do Creates DB index with the given name and table name atom, if it doesn't exist. """ @spec create_db_index(String.t(), atom(), list()) :: :ok | :error - def create_db_index(raw_index_name, table_name_atom, table_columns) do + def create_db_index(raw_index_name, table_name_atom, table_columns, unique? \\ false) do index_name = sanitize_index_name(raw_index_name) - query = create_index_query_string(index_name, table_name_atom, table_columns) + query = create_index_query_string(index_name, table_name_atom, table_columns, unique?) run_create_db_index_query(query) end @@ -153,9 +153,9 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.Helper do "CREATE INDEX CONCURRENTLY IF NOT EXISTS \"my_index\" on my_table (column1, column2);" """ - @spec create_index_query_string(String.t(), atom(), list()) :: String.t() - def create_index_query_string(index_name, table_name_atom, table_columns) do - "CREATE INDEX #{add_concurrently_flag?()} IF NOT EXISTS \"#{index_name}\" on #{to_string(table_name_atom)} (#{Enum.join(table_columns, ", ")});" + @spec create_index_query_string(String.t(), atom(), list(), boolean()) :: String.t() + def create_index_query_string(index_name, table_name_atom, table_columns, unique? \\ false) do + "CREATE #{(unique? && "UNIQUE") || ""} INDEX #{add_concurrently_flag?()} IF NOT EXISTS \"#{index_name}\" on #{to_string(table_name_atom)} (#{Enum.join(table_columns, ", ")});" end @doc """ diff --git a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex new file mode 100644 index 000000000000..d0b656219711 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex @@ -0,0 +1,94 @@ +defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do + @moduledoc """ + Searches for all blocks that contains internal transactions with duplicated block_hash, transaction_index, index, + deletes all internal transactions for such blocks and adds them to pending operations. + """ + + use Explorer.Migrator.FillingMigration + + require Logger + + import Ecto.Query + + alias Explorer.Repo + + alias Explorer.Chain.{ + Block, + InternalTransaction, + PendingBlockOperation + } + + alias Explorer.Migrator.FillingMigration + alias Indexer.Fetcher.InternalTransaction, as: InternalTransactionFetcher + + @migration_name "reindex_duplicated_internal_transactions" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> distinct(true) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + from( + it in InternalTransaction, + group_by: [it.block_hash, it.transaction_index, it.index], + having: count("*") > 1, + select: it.block_hash + ) + end + + @impl FillingMigration + def update_batch(block_hashes) do + now = DateTime.utc_now() + + result = + Repo.transaction(fn -> + InternalTransaction + |> where([it], it.block_hash in ^block_hashes) + |> Repo.delete_all() + + pbo_params = + Block + |> where([b], b.hash in ^block_hashes) + |> where([b], b.consensus == true) + |> select([b], %{block_hash: b.hash, block_number: b.number}) + |> Repo.all() + |> Enum.map(&Map.merge(&1, %{inserted_at: now, updated_at: now})) + + {_total, inserted} = + Repo.insert_all(PendingBlockOperation, pbo_params, on_conflict: :nothing, returning: [:block_number]) + + inserted + end) + + case result do + {:ok, inserted_pbo} -> + unless is_nil(Process.whereis(InternalTransactionFetcher)) do + inserted_pbo + |> Enum.map(& &1.block_number) + |> InternalTransactionFetcher.async_fetch([], false) + end + + :ok + + {:error, error} -> + Logger.error("Migration #{@migration_name} failed: #{inspect(error)}") + {:error, error} + end + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs new file mode 100644 index 000000000000..438f0d410e1f --- /dev/null +++ b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs @@ -0,0 +1,78 @@ +defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.{InternalTransaction, PendingBlockOperation} + alias Explorer.Migrator.{MigrationStatus, ReindexDuplicatedInternalTransactions} + alias Explorer.Repo + + test "Reindex duplicated internal transactions" do + transaction_from_invalid_block = + :transaction + |> insert() + |> with_block() + + Enum.each(1..10, fn index -> + insert( + :internal_transaction, + transaction: transaction_from_invalid_block, + index: index, + block_number: transaction_from_invalid_block.block_number, + transaction_index: transaction_from_invalid_block.index, + block_hash: transaction_from_invalid_block.block_hash, + block_index: index + ) + end) + + Enum.each(11..13, fn index -> + insert( + :internal_transaction, + transaction: transaction_from_invalid_block, + index: index - 10, + block_number: transaction_from_invalid_block.block_number, + transaction_index: transaction_from_invalid_block.index, + block_hash: transaction_from_invalid_block.block_hash, + block_index: index + ) + end) + + transaction_from_valid_block = + :transaction + |> insert() + |> with_block() + + Enum.each(1..10, fn index -> + insert( + :internal_transaction, + transaction: transaction_from_valid_block, + index: index, + block_number: transaction_from_valid_block.block_number, + transaction_index: transaction_from_valid_block.index, + block_hash: transaction_from_valid_block.block_hash, + block_index: index + ) + end) + + assert MigrationStatus.get_status("reindex_duplicated_internal_transactions") == nil + + ReindexDuplicatedInternalTransactions.start_link([]) + + wait_for_results(fn -> + Repo.one!(from(pbo in PendingBlockOperation, limit: 1)) + end) + + assert MigrationStatus.get_status("reindex_duplicated_internal_transactions") == "completed" + + internal_transactions = Repo.all(InternalTransaction) + + assert Enum.count(internal_transactions) == 10 + + Enum.each(internal_transactions, fn it -> + assert it.block_hash == transaction_from_valid_block.block_hash + end) + + pbo = Repo.one(PendingBlockOperation) + + assert pbo.block_hash == transaction_from_invalid_block.block_hash + assert pbo.block_number == transaction_from_invalid_block.block_number + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 54195ecf92e5..601c617aeff6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -710,6 +710,11 @@ config :explorer, Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleS concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_INTERNAL_TRANSACTIONS_STATUS_CONCURRENCY", 1), timeout: ConfigHelper.parse_time_env_var("MIGRATION_REINDEX_INTERNAL_TRANSACTIONS_STATUS_TIMEOUT", "0s") +config :explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_CONCURRENCY", 1), + timeout: ConfigHelper.parse_time_env_var("MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_TIMEOUT", "0s") + config :explorer, Explorer.Migrator.RestoreOmittedWETHTransfers, concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_CONCURRENCY", 5), batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_BATCH_SIZE", 50), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 733dd9c0690d..2d7b6ab569f1 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -461,6 +461,9 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_REINDEX_INTERNAL_TRANSACTIONS_STATUS_BATCH_SIZE= # MIGRATION_REINDEX_INTERNAL_TRANSACTIONS_STATUS_CONCURRENCY= # MIGRATION_REINDEX_INTERNAL_TRANSACTIONS_STATUS_TIMEOUT= +# MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_BATCH_SIZE= +# MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_CONCURRENCY= +# MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_TIMEOUT= # MIGRATION_SHRINK_INTERNAL_TRANSACTIONS_BATCH_SIZE= # MIGRATION_SHRINK_INTERNAL_TRANSACTIONS_CONCURRENCY= # MIGRATION_ARBITRUM_DA_RECORDS_NORMALIZATION_BATCH_SIZE= From fbc72139a9bd45deb4cc0b7fa114d8d6aac9936f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 15 May 2025 10:35:22 +0300 Subject: [PATCH 0044/1006] chore: Improve logic behind emerging of custom fields in the response of `eth_getTransactionByHash` ETH RPC API endpoint (#12416) * chore: Improve logic behind emerging of custom fields in the response of eth_getTransactionByHash ETH RPC API endpoint * Process nil type * Improve maybe_add_access_list/2 function clause --- apps/explorer/lib/explorer/eth_rpc.ex | 29 ++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/apps/explorer/lib/explorer/eth_rpc.ex b/apps/explorer/lib/explorer/eth_rpc.ex index a8ec94ee6913..01eefd3a1260 100644 --- a/apps/explorer/lib/explorer/eth_rpc.ex +++ b/apps/explorer/lib/explorer/eth_rpc.ex @@ -868,8 +868,6 @@ defmodule Explorer.EthRPC do "from" => transaction.from_address_hash, "gas" => encode_quantity(transaction.gas), "gasPrice" => transaction.gas_price |> Wei.to(:wei) |> encode_quantity(), - "maxPriorityFeePerGas" => transaction.max_priority_fee_per_gas |> Wei.to(:wei) |> encode_quantity(), - "maxFeePerGas" => transaction.max_fee_per_gas |> Wei.to(:wei) |> encode_quantity(), "hash" => transaction.hash, "input" => transaction.input, "nonce" => encode_quantity(transaction.nonce), @@ -879,10 +877,11 @@ defmodule Explorer.EthRPC do "type" => encode_quantity(transaction.type), "chainId" => chain_id(), "v" => encode_quantity(transaction.v), - "yParity" => encode_quantity(transaction.v), "r" => encode_quantity(transaction.r), "s" => encode_quantity(transaction.s) } + |> maybe_add_eip_1559_fields(transaction) + |> maybe_add_y_parity(transaction) |> maybe_add_signed_authorizations(transaction) |> maybe_add_chain_type_extra_transaction_info_properties(transaction) |> maybe_add_access_list(transaction) @@ -934,13 +933,33 @@ defmodule Explorer.EthRPC do "to" => transaction.to_address_hash, "transactionHash" => transaction.hash, "transactionIndex" => encode_quantity(transaction.index), - "type" => encode_quantity(transaction.type) + "type" => encode_quantity(transaction.type) || "0x0" } |> maybe_add_chain_type_extra_receipt_properties(transaction) {:ok, props} end + defp maybe_add_eip_1559_fields(props, %Transaction{ + max_fee_per_gas: max_fee_per_gas, + max_priority_fee_per_gas: max_priority_fee_per_gas + }) + when not is_nil(max_fee_per_gas) and not is_nil(max_priority_fee_per_gas) do + props + |> Map.put("maxFeePerGas", max_fee_per_gas |> Wei.to(:wei) |> encode_quantity()) + |> Map.put("maxPriorityFeePerGas", max_priority_fee_per_gas |> Wei.to(:wei) |> encode_quantity()) + end + + defp maybe_add_eip_1559_fields(props, _), do: props + + # yParity shouldn't be added for legacy (type 0) and is_nil(type) transactions + defp maybe_add_y_parity(props, %Transaction{type: type, v: v}) when not is_nil(type) and type > 0 do + props + |> Map.put("yParity", encode_quantity(v)) + end + + defp maybe_add_y_parity(props, %Transaction{type: _type}), do: props + defp maybe_add_signed_authorizations(props, %Transaction{type: 4, signed_authorizations: signed_authorizations}) do prepared_signed_authorizations = signed_authorizations @@ -966,7 +985,7 @@ defmodule Explorer.EthRPC do defp maybe_add_signed_authorizations(props, _transaction), do: props - defp maybe_add_access_list(props, %Transaction{type: type}) when type > 0 do + defp maybe_add_access_list(props, %Transaction{type: type}) when not is_nil(type) and type > 0 do props |> Map.put("accessList", []) end From 550cd26dbf6838fcc7fe304ea39a6d81ead67719 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 15 May 2025 14:26:14 +0300 Subject: [PATCH 0045/1006] feat: Add TAC operation search (#12367) Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> --- .dialyzer_ignore.exs | 4 +- .../views/api/v2/search_view.ex | 8 + .../api/v2/search_controller_test.exs | 553 ++++++++++++++++++ apps/explorer/lib/explorer/chain/search.ex | 47 +- .../tac_operation_lifecycle.ex | 72 +++ config/runtime.exs | 4 + docker-compose/envs/common-blockscout.env | 2 + 7 files changed, 677 insertions(+), 13 deletions(-) create mode 100644 apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 9fe80cb6f9a9..eba0066a0dc9 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -11,6 +11,6 @@ {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 14}, ~r/lib\/phoenix\/router.ex/, {"lib/explorer/chain/search.ex", :pattern_match, 80}, - {"lib/explorer/chain/search.ex", :pattern_match, 227}, - {"lib/explorer/chain/search.ex", :pattern_match, 322} + {"lib/explorer/chain/search.ex", :pattern_match, 232}, + {"lib/explorer/chain/search.ex", :pattern_match, 327} ] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index 65a6fc7a6a80..57328447da3b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -153,6 +153,14 @@ defmodule BlockScoutWeb.API.V2.SearchView do } end + def prepare_search_result(%{type: "tac_operation"} = search_result) do + %{ + "type" => search_result.type, + "tac_operation" => search_result.tac_operation, + "priority" => search_result.priority + } + end + defp hash(%Hash{} = hash), do: hash defp hash(bytes), diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 63e9d8fd96f6..97da1d39b0d8 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -1117,6 +1117,303 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert ens["address"] == to_string(ens_address) assert ens["ens_info"]["name"] == name end + + test "finds a TAC operation", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search?q=#{operation_id}") + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(request, 200) + end + + test "handles 404 from TAC microservice", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + + tac_response = """ + { + "code": 5, + "message": "cannot find operation id" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 404, tac_response) end + ) + + request = get(conn, "/api/v2/search?q=#{operation_id}") + + assert %{ + "items" => [], + "next_page_params" => nil + } == json_response(request, 200) + end + + test "finds a TAC operation with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + transaction = insert(:transaction) |> with_block() + + operation_id = "#{transaction.hash}" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) + + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } in tl(response["items"]) + + assert %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } in response["items"] + end + + test "finds a TAC operation with block", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + transaction = insert(:transaction) |> with_block() + + operation_id = "#{transaction.block_hash}" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) + + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } in tl(response["items"]) + + assert %{ + "block_hash" => "#{transaction.block_hash}", + "block_number" => transaction.block_number, + "block_type" => "block", + "priority" => 3, + "type" => "block", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/block/#{transaction.block_hash}" + } in response["items"] + end end describe "/search/check-redirect" do @@ -1410,6 +1707,262 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end) end + test "finds a TAC operation", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } + ] == json_response(request, 200) + end + + test "finds a TAC operation with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + transaction = insert(:transaction) |> with_block() + + operation_id = "#{transaction.hash}" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) + + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } in tl(response) + + assert %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } in response + end + + test "finds a TAC operation with block", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + transaction = insert(:transaction) |> with_block() + + operation_id = "#{transaction.block_hash}" + + tac_response = """ + { + "operation_id": "#{operation_id}", + "sender": null, + "status_history": [ + { + "is_exist": true, + "is_success": true, + "note": null, + "timestamp": "1746444308", + "transactions": [ + { + "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type": "TON" + } + ], + "type": "COLLECTED_IN_TAC" + } + ], + "timestamp": "1746444308", + "type": "PENDING" + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations/#{operation_id}", + fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + ) + + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) + + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "status_history" => [ + %{ + "is_exist" => true, + "is_success" => true, + "note" => nil, + "timestamp" => "1746444308", + "transactions" => [ + %{ + "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", + "type" => "TON" + } + ], + "type" => "COLLECTED_IN_TAC" + } + ], + "timestamp" => "1746444308", + "type" => "PENDING" + }, + "type" => "tac_operation" + } in tl(response) + + assert %{ + "block_hash" => "#{transaction.block_hash}", + "block_number" => transaction.block_number, + "block_type" => "block", + "priority" => 3, + "type" => "block", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/block/#{transaction.block_hash}" + } in response + end + test "returns empty list and don't crash", %{conn: conn} do request = get(conn, "/api/v2/search/quick?q=qwertyuioiuytrewertyuioiuytrertyuio") assert [] = json_response(request, 200) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index cfdacb6e997e..3674b8da9685 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -29,7 +29,7 @@ defmodule Explorer.Chain.Search do UserOperation } - alias Explorer.MicroserviceInterfaces.Metadata + alias Explorer.MicroserviceInterfaces.{Metadata, TACOperationLifecycle} use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] @@ -83,6 +83,8 @@ defmodule Explorer.Chain.Search do |> select_repo(options).all(), nil} {:full_hash, full_hash} -> + tac_operation_task = Task.async(fn -> search_tac_operation(query_string) end) + transaction_block_query = full_hash |> search_transaction_query() @@ -108,8 +110,11 @@ defmodule Explorer.Chain.Search do transaction_block_op_query end - {result_query - |> select_repo(options).all(), nil} + results = select_repo(options).all(result_query) + + tac_operation_results = await_task(tac_operation_task) + + {results ++ tac_operation_results, nil} {:number, block_number} -> {block_number @@ -142,7 +147,7 @@ defmodule Explorer.Chain.Search do |> order_and_page_text_search_result(paging_options) |> select_repo(options).all() - ens_result = (ens_task && await_ens_task(ens_task)) || [] + ens_result = (ens_task && await_task(ens_task)) || [] (ens_result ++ items) |> trim_list_and_prepare_next_page_params( @@ -327,6 +332,8 @@ defmodule Explorer.Chain.Search do ] {:full_hash, full_hash} -> + tac_operation_task = Task.async(fn -> search_tac_operation(query_string) end) + transaction_block_query = full_hash |> search_transaction_query() @@ -352,10 +359,10 @@ defmodule Explorer.Chain.Search do transaction_block_op_query end - [ - result_query - |> select_repo(options).all() - ] + results = select_repo(options).all(result_query) + tac_operation_results = await_task(tac_operation_task) + + [results ++ tac_operation_results] {:number, block_number} -> [ @@ -374,7 +381,7 @@ defmodule Explorer.Chain.Search do search_by_string_balanced(prepared_term, paging_options, options, query_string) end - ens_result = await_ens_task(ens_task) + ens_result = await_task(ens_task) non_empty_lists = [ @@ -430,8 +437,8 @@ defmodule Explorer.Chain.Search do [tokens_results, contracts_results, labels_results, metadata_tags_addresses] end - defp await_ens_task(ens_task) do - case Task.yield(ens_task, 5000) || Task.shutdown(ens_task) do + defp await_task(task) do + case Task.yield(task, 5000) || Task.shutdown(task) do {:ok, result} -> result @@ -903,6 +910,24 @@ defmodule Explorer.Chain.Search do end end + defp search_tac_operation(search_query) do + case TACOperationLifecycle.get_operation_by_id(search_query) do + {:ok, operation} -> + [ + search_fields() + |> Map.merge(%{ + type: "tac_operation", + tac_operation: operation, + address_hash: nil, + timestamp: nil + }) + ] + + _ -> + [] + end + end + @doc """ Try to resolve ENS domain via BENS """ diff --git a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex new file mode 100644 index 000000000000..464466fde94f --- /dev/null +++ b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex @@ -0,0 +1,72 @@ +defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do + @moduledoc """ + Interface to interact with Tac Operation Lifecycle Service (https://github.com/blockscout/blockscout-rs/tree/main/tac-operation-lifecycle) + """ + + alias Explorer.Utility.Microservice + alias HTTPoison.Response + require Logger + + @request_error_msg "Error while sending request to Tac Operation Lifecycle Service" + + @doc """ + Retrieves operation details from the TAC Operation Lifecycle Service by operation ID. + + Fetches complete operation information including type, timestamp, sender, and + status history from the TAC Operation Lifecycle Service. The function first + checks if the microservice is enabled before making the request. + + ## Parameters + - `operation_id`: Unique identifier for the operation to retrieve + + ## Returns + - `{:ok, map()}`: Operation details containing operation_id, type, timestamp, + sender, and status_history if the request succeeds + - `{:error, :disabled}`: If the TAC Operation Lifecycle microservice is disabled + - `{:error, :not_found}`: If the operation with the given ID does not exist + - `{:error, String.t()}`: Error message if the request fails + """ + @spec get_operation_by_id(String.t()) :: + {:ok, map()} | {:error, :disabled | :not_found | Jason.DecodeError.t() | String.t()} + def get_operation_by_id(operation_id) do + with :ok <- Microservice.check_enabled(__MODULE__) do + query_params = %{} + + operation_id + |> operation_by_id_url() + |> http_get_request(query_params) + end + end + + defp http_get_request(url, query_params) do + case HTTPoison.get(url, [], params: query_params) do + {:ok, %Response{body: body, status_code: 200}} -> + Jason.decode(body) + + {:ok, %Response{body: _body, status_code: 404}} -> + {:error, :not_found} + + {_, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "#{@request_error_msg}: #{url}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + defp operation_by_id_url(operation_id) do + "#{base_url()}/tac/operations/#{operation_id}" + end + + defp base_url do + "#{Microservice.base_url(__MODULE__)}/api/v1" + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 601c617aeff6..a0c4744fd248 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -625,6 +625,10 @@ config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL") +config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), + service_url: System.get_env("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") + config :explorer, :air_table_public_tags, table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 2d7b6ab569f1..410c0f915c1b 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -407,6 +407,8 @@ MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ # MICROSERVICE_METADATA_PROXY_REQUESTS_TIMEOUT= # MICROSERVICE_STYLUS_VERIFIER_URL= # MICROSERVICE_MULTICHAIN_SEARCH_URL= +# MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED= +# MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL= DECODE_NOT_A_CONTRACT_CALLS=true # DATABASE_READ_ONLY_API_URL= # ACCOUNT_DATABASE_URL= From 7662bc50e5d3438b131c721a9c3151e0e17c53c8 Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Thu, 15 May 2025 18:13:38 +0300 Subject: [PATCH 0046/1006] Fix for type output in ETH RPC API transaction by hash endpoint --- apps/explorer/lib/explorer/eth_rpc.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/eth_rpc.ex b/apps/explorer/lib/explorer/eth_rpc.ex index 01eefd3a1260..f73603b676b8 100644 --- a/apps/explorer/lib/explorer/eth_rpc.ex +++ b/apps/explorer/lib/explorer/eth_rpc.ex @@ -874,7 +874,7 @@ defmodule Explorer.EthRPC do "to" => transaction.to_address_hash, "transactionIndex" => encode_quantity(transaction.index), "value" => transaction.value |> Wei.to(:wei) |> encode_quantity(), - "type" => encode_quantity(transaction.type), + "type" => encode_quantity(transaction.type) || "0x0", "chainId" => chain_id(), "v" => encode_quantity(transaction.v), "r" => encode_quantity(transaction.r), From 8e36c75a1127a54df210111cc6a8e66975c0c59d Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 16 May 2025 12:15:01 +0300 Subject: [PATCH 0047/1006] fix: Refactor TokenInstanceMetadataRefetch on demand fetcher (#12419) * fix: Refactor TokenInstanceMetadataRefetch on demand fetcher * mix format * Process review comment * Apply suggestions from code review Co-authored-by: Victor Baranov * Process review comments --------- Co-authored-by: Victor Baranov --- .../lib/block_scout_web/notifier.ex | 11 ++ .../block_scout_web/realtime_event_handler.ex | 1 + .../api/v2/token_controller_test.exs | 80 +++++++++++++- .../lib/explorer/chain/events/publisher.ex | 2 +- .../lib/explorer/chain/events/subscriber.ex | 2 +- .../lib/explorer/chain/token/instance.ex | 16 ++- .../token_instance_metadata_refetch.ex | 100 +++++++++--------- .../indexer/fetcher/token_instance/helper.ex | 50 ++++++++- .../token_instance_metadata_refetch_test.exs | 6 ++ 9 files changed, 211 insertions(+), 57 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 04e91a226980..fdd541d56dfc 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -364,6 +364,17 @@ defmodule BlockScoutWeb.Notifier do ) end + def handle_event( + {:chain_event, :not_fetched_token_instance_metadata, :on_demand, + [token_contract_address_hash_string, token_id, reason]} + ) do + Endpoint.broadcast( + "token_instances:#{token_contract_address_hash_string}", + "not_fetched_token_instance_metadata", + %{token_id: token_id, reason: reason} + ) + end + def handle_event({:chain_event, :changed_bytecode, :on_demand, [address_hash]}) do # TODO: delete duplicated event when old UI becomes deprecated Endpoint.broadcast("addresses_old:#{to_string(address_hash)}", "changed_bytecode", %{}) diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index f075509c6867..9cfece78f54b 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -47,6 +47,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do Subscriber.to(:changed_bytecode, :on_demand) Subscriber.to(:fetched_bytecode, :on_demand) Subscriber.to(:fetched_token_instance_metadata, :on_demand) + Subscriber.to(:not_fetched_token_instance_metadata, :on_demand) Subscriber.to(:zkevm_confirmed_batches, :realtime) # Does not come from the indexer Subscriber.to(:exchange_rate) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index 1757af144822..b5d23b4d79cc 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1537,6 +1537,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) + old_explorer_http_adapter = Application.get_env(:explorer, :http_adapter) v2_secret_key = "v2_secret_key" v3_secret_key = "v3_secret_key" @@ -1561,10 +1562,12 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} Subscriber.to(:fetched_token_instance_metadata, :on_demand) + Subscriber.to(:not_fetched_token_instance_metadata, :on_demand) on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) + Application.put_env(:explorer, :http_adapter, old_explorer_http_adapter) end) {:ok, %{v2_secret_key: v2_secret_key, v3_secret_key: v3_secret_key}} @@ -1641,8 +1644,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert(token_instance_from_db) assert token_instance_from_db.metadata == metadata - - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "don't fetch token instance metadata for non-existent token instance", %{ @@ -1751,8 +1752,81 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert(token_instance_from_db) assert token_instance_from_db.metadata == metadata + end + + test "emit not_fetched_token_instance_metadata event when fetching token instance metadata fails", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do + expected_body = "secret=#{v2_secret_key}&response=123" + + Explorer.Mox.HTTPoison + |> expect(:post, fn _url, ^expected_body, _headers, _options -> + {:ok, + %HTTPoison.Response{ + status_code: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end) + + token = insert(:token, type: "ERC-721") + token_id = 1 + + insert(:token_instance, + token_id: token_id, + token_contract_address_hash: token.contract_address_hash, + metadata: nil + ) + + url = "http://metadata.endpoint.com" + token_contract_address_hash_string = to_string(token.contract_address_hash) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Application.put_env(:explorer, :http_adapter, HTTPoison) + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 500, body: "Error"}} + end) + + topic = "token_instances:#{token_contract_address_hash_string}" + + {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join(topic) + + request = + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ + "recaptcha_response" => "123" + }) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :not_fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, "error"]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, reason: "error"}, + event: "not_fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert is_nil(token_instance_from_db.metadata) end test "fetch token instance metadata using scoped bypass api key", %{conn: conn} do diff --git a/apps/explorer/lib/explorer/chain/events/publisher.ex b/apps/explorer/lib/explorer/chain/events/publisher.ex index 97e052e0da6e..9338c0bd0ebf 100644 --- a/apps/explorer/lib/explorer/chain/events/publisher.ex +++ b/apps/explorer/lib/explorer/chain/events/publisher.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Publisher do @common_allowed_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result - token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata + token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata not_fetched_token_instance_metadata smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 7f4133cd5428..95759a8713ad 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Events.Subscriber do @common_allowed_broadcast_events ~w(addresses address_coin_balances address_token_balances address_current_token_balances blocks block_rewards internal_transactions last_block_number token_transfers transactions contract_verification_result - token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata + token_total_supply changed_bytecode fetched_bytecode fetched_token_instance_metadata not_fetched_token_instance_metadata smart_contract_was_verified zkevm_confirmed_batches eth_bytecode_db_lookup_started smart_contract_was_not_verified)a diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 3a947106371e..ff94261f908a 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -658,7 +658,8 @@ defmodule Explorer.Chain.Token.Instance do Sets metadata for the given Explorer.Chain.Token.Instance """ @spec set_metadata(t(), map()) :: {non_neg_integer(), nil} - def set_metadata(token_instance, metadata) when is_map(metadata) do + def set_metadata(token_instance, %{metadata: metadata, skip_metadata_url: skip_metadata_url} = result) + when is_map(metadata) do now = DateTime.utc_now() Repo.update_all( @@ -666,7 +667,18 @@ defmodule Explorer.Chain.Token.Instance do where: instance.token_contract_address_hash == ^token_instance.token_contract_address_hash, where: instance.token_id == ^token_instance.token_id ), - [set: [metadata: metadata, error: nil, updated_at: now, thumbnails: nil, media_type: nil, cdn_upload_error: nil]], + [ + set: [ + metadata: metadata, + error: nil, + updated_at: now, + thumbnails: nil, + media_type: nil, + cdn_upload_error: nil, + skip_metadata_url: skip_metadata_url, + metadata_url: result[:metadata_url] + ] + ], timeout: @timeout ) end diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex index 882bac64bd8c..7a1bb17299d0 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex @@ -12,9 +12,8 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do alias Explorer.Chain.Cache.Counters.Helper, as: CountersHelper alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Token.Instance, as: TokenInstance - alias Explorer.SmartContract.Reader - alias Explorer.Token.MetadataRetriever alias Explorer.Utility.{RateLimiter, TokenInstanceMetadataRefetchAttempt} + alias Indexer.Fetcher.TokenInstance.Helper alias Indexer.NFTMediaHandler.Queue @max_delay :timer.hours(168) @@ -27,7 +26,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do end end - defp fetch_metadata(token_instance, state) do + defp fetch_metadata(token_instance) do with {:retries_number, {retries_number, updated_at}} <- {:retries_number, TokenInstanceMetadataRefetchAttempt.get_retries_number( @@ -39,63 +38,68 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do {:retry, CountersHelper.current_time() - updated_at_ms > threshold(retries_number)} do - fetch_and_broadcast_metadata(token_instance, state) + fetch_and_broadcast_metadata(token_instance) else {:retries_number, nil} -> - fetch_and_broadcast_metadata(token_instance, state) + fetch_and_broadcast_metadata(token_instance) {:retry, false} -> + Publisher.broadcast( + %{ + not_fetched_token_instance_metadata: [ + to_string(token_instance.token_contract_address_hash), + NFT.prepare_token_id(token_instance.token_id), + "retry_cooldown" + ] + }, + :on_demand + ) + :ok end end - defp fetch_and_broadcast_metadata(token_instance, _state) do - token_id = NFT.prepare_token_id(token_instance.token_id) - contract_address_hash_string = to_string(token_instance.token_contract_address_hash) - - request = - NFT.prepare_request( - token_instance.token.type, - contract_address_hash_string, - token_id, - false - ) - - result = - case Reader.query_contracts([request], NFT.erc_721_1155_abi(), [], false) do - [ok: [uri]] -> - {:ok, [uri]} - - _ -> - nil - end - - with {:empty_result, false} <- {:empty_result, is_nil(result)}, - {:fetched_metadata, {:ok, %{metadata: metadata}}} <- - {:fetched_metadata, - result - |> MetadataRetriever.fetch_json(token_id, nil, false) - |> MetadataRetriever.parse_fetch_json_response()} do - TokenInstance.set_metadata(token_instance, metadata) - - Publisher.broadcast( - %{fetched_token_instance_metadata: [to_string(token_instance.token_contract_address_hash), token_id, metadata]}, - :on_demand - ) - - Queue.process_new_instances([%TokenInstance{token_instance | metadata: metadata}]) - else - {:empty_result, true} -> - :ok + defp fetch_and_broadcast_metadata( + %{token_id: token_id, token_contract_address_hash: token_contract_address_hash} = token_instance + ) do + case Helper.batch_prepare_instances_insert_params([ + %{contract_address_hash: token_contract_address_hash, token_id: token_id} + ]) do + [%{error: nil, metadata: metadata} = result] -> + TokenInstance.set_metadata(token_instance, result) + + Publisher.broadcast( + %{ + fetched_token_instance_metadata: [ + to_string(token_contract_address_hash), + NFT.prepare_token_id(token_id), + metadata + ] + }, + :on_demand + ) + + Queue.process_new_instances([%TokenInstance{token_instance | metadata: metadata}]) - {:fetched_metadata, error} -> + [%{error: error}] -> Logger.error(fn -> - "Error while refetching metadata for {#{token_instance.token_contract_address_hash}, #{token_id}}: #{inspect(error)}" + "Error while refetching metadata for {#{token_contract_address_hash}, #{token_id}}: #{inspect(error)}" end) + Publisher.broadcast( + %{ + not_fetched_token_instance_metadata: [ + to_string(token_contract_address_hash), + NFT.prepare_token_id(token_id), + "error" + ] + }, + :on_demand + ) + TokenInstanceMetadataRefetchAttempt.insert_retries_number( - token_instance.token_contract_address_hash, - token_instance.token_id + token_contract_address_hash, + token_id ) end end @@ -111,7 +115,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do @impl true def handle_cast({:refetch, token_instance}, state) do - fetch_metadata(token_instance, state) + fetch_metadata(token_instance) {:noreply, state} end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex index 74cbc08b7757..bec01eff8ddf 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex @@ -13,8 +13,55 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do @cryptokitties_address_hash "0x06012c8cf97bead5deae237070f9587f8e7a266d" - @spec batch_fetch_instances([%{}]) :: list() + @doc """ + Fetches and upserts a batch of token instances. + + This function takes a list of `token_instances`, prepares the parameters for insertion, + and attempts to upsert them into the database. If an error occurs during the upsert, + it rescues the exception. + + ## Parameters + + - `token_instances`: A list of token instance maps to be processed. + + ## Returns + + - The result of the upsert operation, which may vary depending on the implementation + of `upsert_with_rescue/1`. + """ + @spec batch_fetch_instances([map()]) :: nil | [map()] def batch_fetch_instances(token_instances) do + token_instances + |> batch_prepare_instances_insert_params() + |> upsert_with_rescue() + end + + @doc """ + Prepares a batch of token instance insert parameters. + + This function processes a list of token instances, grouping them by contract address hash, + and handles special logic for CryptoKitties tokens. It fetches token types for non-CryptoKitties + tokens, retrieves metadata, and formats the results for database insertion. + + ## Parameters + + - `token_instances`: A list of maps representing token instances. Each item should + contain at least `:contract_address_hash` and `:token_id`. + + ## Returns + + - A list of insert parameters for each token instance, ready for database insertion. + + ## Special Cases + + - CryptoKitties tokens are identified by a specific contract address hash and are handled + separately with a fixed API endpoint. + + - Errors during metadata retrieval are truncated and included in the result. + + """ + @spec batch_prepare_instances_insert_params([map()]) :: [map()] + def batch_prepare_instances_insert_params(token_instances) do token_instances = Enum.map(token_instances, fn %{contract_address_hash: hash, token_id: token_id} -> {hash, token_id} @@ -57,7 +104,6 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do ) end end) - |> upsert_with_rescue() end defp batch_fetch_instances_inner(token_instances, token_types_map, cryptokitties) do diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs index 79d6f527b8bc..617005781b7a 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs @@ -32,6 +32,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do describe "refetch token instance metadata behaviour" do setup do Subscriber.to(:fetched_token_instance_metadata, :on_demand) + Subscriber.to(:not_fetched_token_instance_metadata, :on_demand) Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) @@ -182,6 +183,11 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do {:chain_event, :fetched_token_instance_metadata, :on_demand, [^token_contract_address_hash_string, ^token_id, %{metadata: ^metadata}]} ) + + assert_receive( + {:chain_event, :not_fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, "error"]} + ) end end end From 72f72a85f85bcd2df308fbc4c63482ba1f0978bc Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 16 May 2025 12:37:56 +0300 Subject: [PATCH 0048/1006] perf: force index usage in `api/v2/addresses/:hash/transactions` (#12415) --- .../lib/block_scout_web/paging_helper.ex | 18 ++++- .../lib/explorer/chain/transaction.ex | 68 ++++++++++++------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 98e539817567..884f431c4c1f 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -292,8 +292,22 @@ defmodule BlockScoutWeb.PagingHelper do def address_transactions_sorting(_), do: [] - defp do_address_transaction_sorting("block_number", "asc"), do: [asc: :block_number, asc: :index] - defp do_address_transaction_sorting("block_number", "desc"), do: [desc: :block_number, desc: :index] + defp do_address_transaction_sorting("block_number", "asc"), + do: [ + asc: :block_number, + asc: :index, + asc: :inserted_at, + desc: :hash + ] + + defp do_address_transaction_sorting("block_number", "desc"), + do: [ + desc: :block_number, + desc: :index, + desc: :inserted_at, + asc: :hash + ] + defp do_address_transaction_sorting("value", "asc"), do: [asc: :value] defp do_address_transaction_sorting("value", "desc"), do: [desc: :value] defp do_address_transaction_sorting("fee", "asc"), do: [{:dynamic, :fee, :asc_nulls_first, Transaction.dynamic_fee()}] diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 6375e487d16d..c4e6b39d9316 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -1124,7 +1124,9 @@ defmodule Explorer.Chain.Transaction do Produces a list of queries starting from the given one and adding filters for transactions that are linked to the given address_hash through a direction. """ - def matching_address_queries_list(query, :from, address_hashes) when is_list(address_hashes) do + def matching_address_queries_list(query, direction, address_hashes, custom_sorting \\ []) + + def matching_address_queries_list(query, :from, address_hashes, _custom_sorting) when is_list(address_hashes) do [ from( a in fragment("SELECT unnest(?) as from_address_hash", type(^address_hashes, {:array, Hash.Address})), @@ -1140,7 +1142,7 @@ defmodule Explorer.Chain.Transaction do ] end - def matching_address_queries_list(query, :to, address_hashes) when is_list(address_hashes) do + def matching_address_queries_list(query, :to, address_hashes, _custom_sorting) when is_list(address_hashes) do [ from( a in fragment("SELECT unnest(?) as to_address_hash", type(^address_hashes, {:array, Hash.Address})), @@ -1173,30 +1175,51 @@ defmodule Explorer.Chain.Transaction do ] end - def matching_address_queries_list(query, _direction, address_hashes) when is_list(address_hashes) do + def matching_address_queries_list(query, _direction, address_hashes, _custom_sorting) when is_list(address_hashes) do matching_address_queries_list(query, :from, address_hashes) ++ matching_address_queries_list(query, :to, address_hashes) end - def matching_address_queries_list(query, :from, address_hash) do - [where(query, [t], t.from_address_hash == ^address_hash)] - end + # in ^[address_hash] addresses this issue: https://github.com/blockscout/blockscout/issues/12393 + def matching_address_queries_list(query, :from, address_hash, custom_sorting) do + order = + for {key, :block_number = value} <- custom_sorting do + {value, key} + end + |> Keyword.get(:block_number, :desc) - def matching_address_queries_list(query, :to, address_hash) do [ - where(query, [t], t.to_address_hash == ^address_hash), - where(query, [t], t.created_contract_address_hash == ^address_hash) + query + |> where([t], t.from_address_hash in ^[address_hash]) + |> prepend_order_by([t], [{^order, t.from_address_hash}]) ] end - def matching_address_queries_list(query, _direction, address_hash) do + def matching_address_queries_list(query, :to, address_hash, custom_sorting) do + order = + for {key, :block_number = value} <- custom_sorting do + {value, key} + end + |> Keyword.get(:block_number, :desc) + [ - where(query, [t], t.from_address_hash == ^address_hash), - where(query, [t], t.to_address_hash == ^address_hash), - where(query, [t], t.created_contract_address_hash == ^address_hash) + query + |> where([t], t.to_address_hash in ^[address_hash]) + |> prepend_order_by([t], [{^order, t.to_address_hash}]), + query + |> where( + [t], + t.created_contract_address_hash in ^[address_hash] + ) + |> prepend_order_by([t], [{^order, t.created_contract_address_hash}]) ] end + def matching_address_queries_list(query, _direction, address_hash, custom_sorting) do + matching_address_queries_list(query, :from, address_hash, custom_sorting) ++ + matching_address_queries_list(query, :to, address_hash, custom_sorting) + end + def not_pending_transactions(query) do where(query, [t], not is_nil(t.block_number)) end @@ -1577,13 +1600,14 @@ defmodule Explorer.Chain.Transaction do direction = Keyword.get(options, :direction) necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) old_ui? = old_ui? || is_tuple(Keyword.get(options, :paging_options, Chain.default_paging_options()).key) + sorting_options = Keyword.get(options, :sorting, []) options |> address_to_transactions_tasks_query(false, old_ui?) |> not_dropped_or_replaced_transactions() |> Chain.join_associations(necessity_by_association) |> put_has_token_transfers_to_transaction(old_ui?) - |> matching_address_queries_list(direction, address_hash) + |> matching_address_queries_list(direction, address_hash, sorting_options) |> Enum.map(fn query -> Task.async(fn -> Chain.select_repo(options).all(query) end) end) end @@ -1650,17 +1674,11 @@ defmodule Explorer.Chain.Transaction do end end - defp compare_custom_sorting([{block_order, :block_number}, {index_order, :index}]) do - fn a, b -> - case {Helper.compare(a.block_number, b.block_number), Helper.compare(a.index, b.index)} do - {:eq, :eq} -> compare_default_sorting(a, b) - {:eq, :gt} -> index_order == :desc - {:eq, :lt} -> index_order == :asc - {:gt, _} -> block_order == :desc - {:lt, _} -> block_order == :asc - end - end - end + defp compare_custom_sorting([{:desc, :block_number}, {:desc, :index}, {:desc, :inserted_at}, {:asc, :hash}]), + do: &compare_default_sorting/2 + + defp compare_custom_sorting([{:asc, :block_number}, {:asc, :index}, {:asc, :inserted_at}, {:desc, :hash}]), + do: &(!compare_default_sorting(&1, &2)) defp compare_custom_sorting([{:dynamic, :fee, order, _dynamic_fee}]) do fn a, b -> From ab224cea65dead369498b166c0de2a7ab6410a5c Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 16 May 2025 14:26:24 +0300 Subject: [PATCH 0049/1006] fix: Fix logs decoding issue for proxies (#12414) * fix: Fix logs decoding issue for proxies * Fix tests * Process review comments * Update apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex Co-authored-by: Victor Baranov * Fix corner case * Refactor topic filter value handling; Merge string_to_block_hash/1 and string_to_transaction_hash/1 * Process review comments --------- Co-authored-by: Victor Baranov --- apps/block_scout_web/lib/block_scout_web.ex | 4 +- .../lib/block_scout_web/chain.ex | 23 ++- .../controllers/address_logs_controller.ex | 26 +++- .../controllers/api/rpc/address_controller.ex | 2 +- .../api/rpc/transaction_controller.ex | 2 +- .../controllers/api/v2/address_controller.ex | 69 ++++----- .../controllers/api/v2/blob_controller.ex | 2 +- .../api/v2/internal_transaction_controller.ex | 2 +- .../controllers/api/v2/optimism_controller.ex | 2 +- .../proxy/account_abstraction_controller.ex | 4 +- .../api/v2/transaction_controller.ex | 6 +- .../controllers/transaction_controller.ex | 4 +- ...saction_internal_transaction_controller.ex | 4 +- .../controllers/transaction_log_controller.ex | 8 +- .../transaction_raw_trace_controller.ex | 2 +- .../transaction_state_controller.ex | 4 +- .../transaction_token_transfer_controller.ex | 4 +- .../lib/block_scout_web/graphql/schema.ex | 6 +- .../transaction_interpretation.ex | 2 +- .../lib/block_scout_web/paging_helper.ex | 4 +- .../lib/block_scout_web/views/address_view.ex | 8 +- .../views/api/v2/transaction_view.ex | 64 ++++++-- .../api/v2/address_controller_test.exs | 140 ++++++++++++++++++ .../views/api/v2/transaction_view_test.exs | 14 +- apps/explorer/lib/explorer/chain.ex | 49 ++---- .../chain/arbitrum/reader/api/settlement.ex | 2 +- .../explorer/chain/arbitrum/reader/common.ex | 4 +- .../lib/explorer/chain/beacon/reader.ex | 2 +- apps/explorer/lib/explorer/chain/log.ex | 122 +-------------- apps/explorer/lib/explorer/chain/search.ex | 2 +- .../proxy/models/implementation.ex | 53 ++++++- apps/explorer/lib/explorer/eth_rpc.ex | 2 +- apps/explorer/lib/explorer/etherscan/logs.ex | 2 +- .../explorer/account/notifier/email_test.exs | 4 +- .../explorer/test/explorer/chain/log_test.exs | 109 +++++++------- apps/explorer/test/explorer/chain_test.exs | 2 +- .../lib/indexer/fetcher/block_reward.ex | 2 +- 37 files changed, 437 insertions(+), 324 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web.ex b/apps/block_scout_web/lib/block_scout_web.ex index b7afec23a90d..e0fffdb280be 100644 --- a/apps/block_scout_web/lib/block_scout_web.ex +++ b/apps/block_scout_web/lib/block_scout_web.ex @@ -29,7 +29,9 @@ defmodule BlockScoutWeb do import BlockScoutWeb.ErrorHelper import BlockScoutWeb.Routers.AccountRouter.Helpers, except: [static_path: 2] import Plug.Conn - import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, + only: [proxy_implementations_association: 0, proxy_implementations_smart_contracts_association: 0] alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes end diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 7c5c13b54ddb..d02e57a4293d 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -10,8 +10,7 @@ defmodule BlockScoutWeb.Chain do hash_to_transaction: 1, number_to_block: 1, string_to_address_hash: 1, - string_to_block_hash: 1, - string_to_transaction_hash: 1 + string_to_full_hash: 1 ] import Explorer.PagingOptions, @@ -199,7 +198,7 @@ defmodule BlockScoutWeb.Chain do "inserted_at" => inserted_at_string, "hash" => hash_string }) do - with {:ok, hash} <- string_to_transaction_hash(hash_string), + with {:ok, hash} <- string_to_full_hash(hash_string), {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string) do [ paging_options: %{ @@ -289,8 +288,8 @@ defmodule BlockScoutWeb.Chain do with {block_number, ""} <- Integer.parse(block_number_string), {index, ""} <- Integer.parse(index_string), {index_in_batch, ""} <- Integer.parse(index_in_batch_string), - {:ok, batch_transaction_hash} <- string_to_transaction_hash(batch_transaction_hash_string), - {:ok, batch_block_hash} <- string_to_block_hash(batch_block_hash_string), + {:ok, batch_transaction_hash} <- string_to_full_hash(batch_transaction_hash_string), + {:ok, batch_block_hash} <- string_to_full_hash(batch_block_hash_string), {batch_log_index, ""} <- Integer.parse(batch_log_index_string) do [ paging_options: %{ @@ -314,8 +313,8 @@ defmodule BlockScoutWeb.Chain do when is_binary(batch_log_index_string) and is_binary(batch_block_hash_string) and is_binary(batch_transaction_hash_string) and is_binary(index_in_batch_string) do with {index_in_batch, ""} <- Integer.parse(index_in_batch_string), - {:ok, batch_transaction_hash} <- string_to_transaction_hash(batch_transaction_hash_string), - {:ok, batch_block_hash} <- string_to_block_hash(batch_block_hash_string), + {:ok, batch_transaction_hash} <- string_to_full_hash(batch_transaction_hash_string), + {:ok, batch_block_hash} <- string_to_full_hash(batch_block_hash_string), {batch_log_index, ""} <- Integer.parse(batch_log_index_string) do [ paging_options: %{ @@ -395,7 +394,7 @@ defmodule BlockScoutWeb.Chain do def paging_options(%{"inserted_at" => inserted_at_string, "hash" => hash_string}) when is_binary(inserted_at_string) and is_binary(hash_string) do with {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string), - {:ok, hash} <- string_to_transaction_hash(hash_string) do + {:ok, hash} <- string_to_full_hash(hash_string) do [paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_transaction: true}] else _ -> @@ -445,7 +444,7 @@ defmodule BlockScoutWeb.Chain do def paging_options(%{"l1_block_number" => block_number, "transaction_hash" => transaction_hash}) do with {block_number, ""} <- Integer.parse(block_number), - {:ok, transaction_hash} <- string_to_transaction_hash(transaction_hash) do + {:ok, transaction_hash} <- string_to_full_hash(transaction_hash) do [paging_options: %{@default_paging_options | key: {block_number, transaction_hash}}] else _ -> @@ -472,7 +471,7 @@ defmodule BlockScoutWeb.Chain do def paging_options(%{"timestamp" => timestamp, "init_transaction_hash" => init_transaction_hash}) do with {ts, ""} <- Integer.parse(timestamp), - {:ok, transaction_hash} <- string_to_transaction_hash(init_transaction_hash) do + {:ok, transaction_hash} <- string_to_full_hash(init_transaction_hash) do [paging_options: %{@default_paging_options | key: {ts, transaction_hash}}] else _ -> @@ -822,7 +821,7 @@ defmodule BlockScoutWeb.Chain do end defp block_or_transaction_or_operation_or_blob_from_param(param) do - with {:ok, hash} <- string_to_transaction_hash(param), + with {:ok, hash} <- string_to_full_hash(param), {:error, :not_found} <- hash_to_transaction(hash), {:error, :not_found} <- hash_to_block(hash), {:error, :not_found} <- hash_to_user_operation(hash), @@ -900,7 +899,7 @@ defmodule BlockScoutWeb.Chain do end def parse_block_hash_or_number_param("0x" <> _ = param) do - case string_to_block_hash(param) do + case string_to_full_hash(param) do {:ok, hash} -> {:ok, :hash, hash} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index febb8c95d31f..3e5a9a666ef3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.AddressLogsController do alias BlockScoutWeb.{AccessHelper, AddressLogsView, Controller} alias Explorer.{Chain, Market} alias Explorer.Chain.Address + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Phoenix.View @@ -21,7 +22,17 @@ defmodule BlockScoutWeb.AddressLogsController do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), :ok <- Address.check_address_exists(address_hash), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do - logs_plus_one = Chain.address_to_logs(address_hash, false, paging_options(params)) + options = + params + |> paging_options() + |> Keyword.merge( + necessity_by_association: %{ + [address: [:smart_contract, Implementation.proxy_implementations_smart_contracts_association()]] => + :optional + } + ) + + logs_plus_one = Chain.address_to_logs(address_hash, false, options) {results, next_page} = split_list_by_page(logs_plus_one) next_page_url = @@ -86,7 +97,18 @@ defmodule BlockScoutWeb.AddressLogsController do formatted_topic = if String.starts_with?(topic, "0x"), do: topic, else: "0x" <> topic - logs_plus_one = Chain.address_to_logs(address_hash, false, topic: formatted_topic) + options = + params + |> paging_options() + |> Keyword.merge( + necessity_by_association: %{ + [address: [:smart_contract, Implementation.proxy_implementations_smart_contracts_association()]] => + :optional + } + ) + |> Keyword.merge(topic: formatted_topic) + + logs_plus_one = Chain.address_to_logs(address_hash, false, options) {results, next_page} = split_list_by_page(logs_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index c95307249060..b15f8acf453b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -449,7 +449,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end defp to_transaction_hash(transaction_hash_string) do - {:format, Chain.string_to_transaction_hash(transaction_hash_string)} + {:format, Chain.string_to_full_hash(transaction_hash_string)} end defp put_order_by_direction(options, params) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex index 0d932e6c0741..2042fa6d7740 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do end defp to_transaction_hash(transaction_hash_string) do - {:format, Chain.string_to_transaction_hash(transaction_hash_string)} + {:format, Chain.string_to_full_hash(transaction_hash_string)} end defp to_transaction_status(transaction_hash) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 0a548140394c..925ed53f8885 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -458,57 +458,21 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec logs(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def logs(conn, %{"address_hash_param" => address_hash_string, "topic" => topic} = params) do - with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do - case Chain.hash_to_address(address_hash, @address_options) do + def logs(conn, %{"address_hash_param" => address_hash_string} = params) do + with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), + {:ok, topic} <- validate_optional_topic(params["topic"]) do + case Chain.hash_to_address(address_hash, @api_true) do {:ok, _address} -> - prepared_topic = String.trim(topic) - - formatted_topic = - if String.starts_with?(prepared_topic, "0x"), do: prepared_topic, else: "0x" <> prepared_topic - options = params |> paging_options() - |> Keyword.merge(topic: formatted_topic) |> Keyword.merge( necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional } ) |> Keyword.merge(@api_true) - - results_plus_one = Chain.address_to_logs(address_hash, false, options) - - {logs, next_page} = split_list_by_page(results_plus_one) - - next_page_params = next_page |> next_page_params(logs, delete_parameters_from_next_page_params(params)) - - conn - |> put_status(200) - |> put_view(TransactionView) - |> render(:logs, %{ - logs: logs |> maybe_preload_ens() |> maybe_preload_metadata(), - next_page_params: next_page_params - }) - - _ -> - conn - |> put_status(200) - |> put_view(TransactionView) - |> render(:logs, %{ - logs: [], - next_page_params: nil - }) - end - end - end - - def logs(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do - case Chain.hash_to_address(address_hash, @address_options) do - {:ok, _address} -> - options = params |> paging_options() |> Keyword.merge(@api_true) + |> Keyword.put(:topic, topic) results_plus_one = Chain.address_to_logs(address_hash, false, options) @@ -1043,4 +1007,25 @@ defmodule BlockScoutWeb.API.V2.AddressController do validate_address_hash(address_hash_string, params) end end + + @spec validate_optional_topic(nil | String.t()) :: {:ok, nil | Hash.Full.t()} | {:format, :error} + defp validate_optional_topic(topic) do + topic = if is_binary(topic), do: String.trim(topic), else: topic + + case topic do + nil -> + {:ok, nil} + + "" -> + {:ok, nil} + + "null" -> + {:ok, nil} + + _ -> + with {:format, {:ok, topic}} <- {:format, Chain.string_to_full_hash(topic)} do + {:ok, topic} + end + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex index f72a6424a7c3..f66c8c45107a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/blob_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.API.V2.BlobController do """ @spec blob(Plug.Conn.t(), map()) :: Plug.Conn.t() def blob(conn, %{"blob_hash_param" => blob_hash_string} = _params) do - with {:format, {:ok, blob_hash}} <- {:format, Chain.string_to_transaction_hash(blob_hash_string)} do + with {:format, {:ok, blob_hash}} <- {:format, Chain.string_to_full_hash(blob_hash_string)} do transaction_hashes = Reader.blob_hash_to_transactions(blob_hash, api?: true) {status, blob} = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 3a71ffe0542e..f936c03e135b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -79,7 +79,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do defp transaction_hash_from_params(params) do with transaction_hash_string when not is_nil(transaction_hash_string) <- params["transaction_hash"], - {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string) do + {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string) do transaction_hash else nil -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index d40fcdac6c3a..b965380de706 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -629,7 +629,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do defp interop_prepare_transaction_hash_filter(transaction_hash) when is_binary(transaction_hash) do transaction_hash |> String.trim() - |> Chain.string_to_transaction_hash() + |> Chain.string_to_full_hash() |> case do {:ok, hash} -> hash _ -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex index b5801beefcc8..22eab9056945 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex @@ -26,7 +26,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do @spec summary(Plug.Conn.t(), map()) :: {:error | :format | :transaction_interpreter_enabled | non_neg_integer(), any()} | Plug.Conn.t() def summary(conn, %{"operation_hash_param" => operation_hash_string, "just_request_body" => "true"}) do - with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)}, + with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_full_hash(operation_hash_string)}, {200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do conn |> json(TransactionInterpretationService.get_user_op_request_body(user_op)) @@ -34,7 +34,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do end def summary(conn, %{"operation_hash_param" => operation_hash_string}) do - with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)}, + with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_full_hash(operation_hash_string)}, {:transaction_interpreter_enabled, true} <- {:transaction_interpreter_enabled, TransactionInterpretationService.enabled?()}, {200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 848d82304390..ac6f140048a1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -269,7 +269,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec external_transactions(Plug.Conn.t(), %{required(String.t()) => String.t()}) :: Plug.Conn.t() def external_transactions(conn, %{"transaction_hash_param" => transaction_hash} = _params) do - with {:format, {:ok, hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash)} do + with {:format, {:ok, hash}} <- {:format, Chain.string_to_full_hash(transaction_hash)} do case NeonSolanaTransactions.maybe_fetch(hash) do {:ok, linked_transactions} -> conn @@ -539,7 +539,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do full_options = [ necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional } ] |> Keyword.merge(paging_options(params)) @@ -717,7 +717,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do | {:restricted_access, true} | {:ok, Transaction.t(), Hash.t()} def validate_transaction(transaction_hash_string, params, options \\ @api_true) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, + with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_full_hash(transaction_hash_string)}, {:not_found, {:ok, transaction}} <- {:not_found, Chain.hash_to_transaction(transaction_hash, options)}, {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index a170270449f6..ed4332c46236 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -129,7 +129,7 @@ defmodule BlockScoutWeb.TransactionController do end def show(conn, %{"id" => transaction_hash_string, "type" => "JSON"}) do - case Chain.string_to_transaction_hash(transaction_hash_string) do + case Chain.string_to_full_hash(transaction_hash_string) do {:ok, transaction_hash} -> if Chain.transaction_has_token_transfers?(transaction_hash) do TransactionTokenTransferController.index(conn, %{ @@ -149,7 +149,7 @@ defmodule BlockScoutWeb.TransactionController do end def show(conn, %{"id" => id} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(id), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(id), :ok <- Chain.check_transaction_exists(transaction_hash) do if Chain.transaction_has_token_transfers?(transaction_hash) do with {:ok, transaction} <- diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index 7af84eb1ea46..5ed7024a99c5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -12,7 +12,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do alias Phoenix.View def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), :ok <- Chain.check_transaction_exists(transaction_hash), {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash, []), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), @@ -84,7 +84,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do end def index(conn, %{"transaction_id" => transaction_hash_string} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction( transaction_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex index fe0c93ad5134..69379162c157 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.TransactionLogController do alias Phoenix.View def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash, necessity_by_association: %{[to_address: :smart_contract] => :optional} @@ -22,9 +22,7 @@ defmodule BlockScoutWeb.TransactionLogController do Keyword.merge( [ necessity_by_association: %{ - [address: :names] => :optional, - [address: :smart_contract] => :optional, - address: :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional } ], paging_options(params) @@ -75,7 +73,7 @@ defmodule BlockScoutWeb.TransactionLogController do end def index(conn, %{"transaction_id" => transaction_hash_string} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction( transaction_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex index 6beedaf261c9..751ec69b9de6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_raw_trace_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.TransactionRawTraceController do alias Indexer.Fetcher.OnDemand.FirstTrace, as: FirstTraceOnDemand def index(conn, %{"transaction_id" => hash_string} = params) do - with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string), + with {:ok, hash} <- Chain.string_to_full_hash(hash_string), {:ok, transaction} <- Chain.hash_to_transaction( hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex index 00671841fbcb..437b33af2769 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_state_controller.ex @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.TransactionStateController do @burn_address_hash burn_address_hash def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash), {:ok, false} <- @@ -83,7 +83,7 @@ defmodule BlockScoutWeb.TransactionStateController do end def index(conn, %{"transaction_id" => transaction_hash_string} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction( transaction_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex index 0db5840c9827..1ca50b21ca9d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex @@ -15,7 +15,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do @burn_address_hash burn_address_hash def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), :ok <- Chain.check_transaction_exists(transaction_hash), {:ok, transaction} <- Chain.hash_to_transaction( @@ -87,7 +87,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do end def index(conn, %{"transaction_id" => transaction_hash_string} = params) do - with {:ok, transaction_hash} <- Chain.string_to_transaction_hash(transaction_hash_string), + with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), {:ok, transaction} <- Chain.hash_to_transaction( transaction_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex index b579a9dbc58f..e94ed7ddffbc 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex @@ -50,16 +50,16 @@ defmodule BlockScoutWeb.GraphQL.Schema do resolve(fn %{type: :internal_transaction, id: id}, resolution -> %{"transaction_hash" => transaction_hash_string, "index" => index} = Jason.decode!(id) - {:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string) + {:ok, transaction_hash} = Chain.string_to_full_hash(transaction_hash_string) InternalTransaction.get_by(%{transaction_hash: transaction_hash, index: index}, resolution) %{type: :token_transfer, id: id}, resolution -> %{"transaction_hash" => transaction_hash_string, "log_index" => log_index} = Jason.decode!(id) - {:ok, transaction_hash} = Chain.string_to_transaction_hash(transaction_hash_string) + {:ok, transaction_hash} = Chain.string_to_full_hash(transaction_hash_string) TokenTransfer.get_by(%{transaction_hash: transaction_hash, log_index: log_index}, resolution) %{type: :transaction, id: transaction_hash_string}, resolution -> - {:ok, hash} = Chain.string_to_transaction_hash(transaction_hash_string) + {:ok, hash} = Chain.string_to_full_hash(transaction_hash_string) Transaction.get_by(%{}, %{hash: hash}, resolution) _, _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index f71cbaf14709..21c387fc63bb 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -394,7 +394,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do def decode_user_op_calldata(user_op_hash, call_data) do {:ok, input} = Data.cast(call_data) - {:ok, op_hash} = Chain.string_to_transaction_hash(user_op_hash) + {:ok, op_hash} = Chain.string_to_full_hash(user_op_hash) mock_transaction = %Transaction{ to_address: %NotLoaded{}, diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 884f431c4c1f..7eefc82b5d0b 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.PagingHelper do """ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - import Explorer.Chain, only: [string_to_transaction_hash: 1] + import Explorer.Chain, only: [string_to_full_hash: 1] import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] alias Explorer.Chain.InternalTransaction.CallType, as: InternalTransactionCallType @@ -57,7 +57,7 @@ defmodule BlockScoutWeb.PagingHelper do def paging_options(%{"inserted_at" => inserted_at_string, "hash" => hash_string}, [:pending | _]) do with {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string), - {:ok, hash} <- string_to_transaction_hash(hash_string) do + {:ok, hash} <- string_to_full_hash(hash_string) do [paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_transaction: true}] else _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 7b7ae76eeba0..74337756e236 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.AddressView do alias BlockScoutWeb.{AccessHelper, LayoutView} alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper + alias BlockScoutWeb.API.V2.TransactionView, as: APIV2TransactionView alias Explorer.Account.CustomABI alias Explorer.{Chain, CustomContractsHelper, Repo} alias Explorer.Chain.Address.Counters @@ -475,7 +476,12 @@ defmodule BlockScoutWeb.AddressView do | {:error, atom(), list()} | {{:error, :contract_not_verified, list()}, any()} def decode(log, transaction) do - {result, _full_abi_per_address_hash_contracts_acc, _events_acc} = Log.decode(log, transaction, [], true, false) + full_abi = + (APIV2TransactionView.extract_implementations_abi(log.address.proxy_implementations) ++ + APIV2TransactionView.try_to_get_abi(log.address.smart_contract)) + |> Enum.uniq() + + {result, _events_acc} = Log.decode(log, transaction, [], true, false, full_abi) result end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index f9420d9a12a7..bdac36950334 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -8,9 +8,22 @@ defmodule BlockScoutWeb.API.V2.TransactionView do alias BlockScoutWeb.{TransactionStateView, TransactionView} alias Ecto.Association.NotLoaded alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Block, DecodingHelper, Log, SignedAuthorization, Token, Transaction, Wei} + + alias Explorer.Chain.{ + Address, + Block, + DecodingHelper, + Log, + SignedAuthorization, + SmartContract, + Token, + Transaction, + Wei + } + alias Explorer.Chain.Block.Reward alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation, as: ProxyImplementation alias Explorer.Chain.Transaction.StateChange alias Timex.Duration @@ -221,35 +234,58 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> Enum.map(&prepare_signed_authorization/1) end + @doc """ + Returns the ABI of a smart contract or an empty list if the smart contract is nil + """ + @spec try_to_get_abi(SmartContract.t() | nil) :: [map()] + def try_to_get_abi(smart_contract) do + (smart_contract && smart_contract.abi) || [] + end + + @doc """ + Returns the ABI of a proxy implementations or an empty list if the proxy implementations is nil + """ + @spec extract_implementations_abi(ProxyImplementation.t() | nil) :: [map()] + def extract_implementations_abi(nil) do + [] + end + + def extract_implementations_abi(proxy_implementations) do + proxy_implementations.smart_contracts + |> Enum.flat_map(fn smart_contract -> + try_to_get_abi(smart_contract) + end) + end + @doc """ Decodes list of logs """ @spec decode_logs([Log.t()], boolean) :: [tuple] def decode_logs(logs, skip_sig_provider?) do - unique_log_address_hashes = - logs - |> Enum.map(fn log -> log.address_hash end) - |> Enum.uniq() - full_abi_per_address_hash = - Log.accumulate_abi_by_address_hashes(%{}, unique_log_address_hashes, @api_true) + Enum.reduce(logs, %{}, fn log, acc -> + full_abi = + (extract_implementations_abi(log.address.proxy_implementations) ++ + try_to_get_abi(log.address.smart_contract)) + |> Enum.uniq() + + Map.put(acc, log.address_hash, full_abi) + end) - {all_logs, _, _} = - Enum.reduce(logs, {[], full_abi_per_address_hash, %{}}, fn log, - {results, full_abi_per_address_hash_acc, events_acc} -> - {result, full_abi_per_address_hash_acc, events_acc} = + {all_logs, _} = + Enum.reduce(logs, {[], %{}}, fn log, {results, events_acc} -> + {result, events_acc} = Log.decode( log, %Transaction{hash: log.transaction_hash}, @api_true, skip_sig_provider?, true, - full_abi_per_address_hash_acc[log.address_hash], - full_abi_per_address_hash_acc, + full_abi_per_address_hash[log.address_hash], events_acc ) - {[result | results], full_abi_per_address_hash_acc, events_acc} + {[result | results], events_acc} end) all_logs_with_index = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 2a1ff83f662d..0a0310f5e997 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2380,6 +2380,146 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(log, Enum.at(response["items"], 0)) end + + test "log could be decoded via verified implementation", %{conn: conn} do + address = insert(:contract_address) + + contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, + address_hash: contract_address.hash, + abi: [ + %{ + "name" => "OptionSettled", + "type" => "event", + "inputs" => [ + %{"name" => "accountId", "type" => "uint256", "indexed" => true, "internalType" => "uint256"}, + %{"name" => "option", "type" => "address", "indexed" => false, "internalType" => "address"}, + %{"name" => "subId", "type" => "uint256", "indexed" => false, "internalType" => "uint256"}, + %{"name" => "amount", "type" => "int256", "indexed" => false, "internalType" => "int256"}, + %{"name" => "value", "type" => "int256", "indexed" => false, "internalType" => "int256"} + ], + "anonymous" => false + } + ] + ) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + + log_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + transaction = :transaction |> insert() |> with_block() + + log = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address + ) + + insert(:proxy_implementation, + proxy_address_hash: address.hash, + proxy_type: "eip1167", + address_hashes: [smart_contract.address_hash], + names: ["Test"] + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + log_from_api = Enum.at(response["items"], 0) + compare_item(log, log_from_api) + assert not is_nil(log_from_api["decoded"]) + + assert log_from_api["decoded"] == %{ + "method_call" => + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + "method_id" => "d20a68b2", + "parameters" => [ + %{ + "indexed" => true, + "name" => "accountId", + "type" => "uint256", + "value" => "23833" + }, + %{ + "indexed" => false, + "name" => "option", + "type" => "address", + "value" => "0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7" + }, + %{ + "indexed" => false, + "name" => "subId", + "type" => "uint256", + "value" => "20615843020801704441600" + }, + %{ + "indexed" => false, + "name" => "amount", + "type" => "int256", + "value" => "-120000000000000000" + }, + %{ + "indexed" => false, + "name" => "value", + "type" => "int256", + "value" => "-522838470013113778446" + } + ] + } + end + + test "test corner case, when preload functions face absent smart contract", %{conn: conn} do + address = insert(:contract_address) + + contract_address = insert(:contract_address) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + + log_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + transaction = :transaction |> insert() |> with_block() + + log = + insert(:log, + transaction: transaction, + first_topic: topic(topic1), + second_topic: topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address + ) + + insert(:proxy_implementation, + proxy_address_hash: address.hash, + proxy_type: "eip1167", + address_hashes: [contract_address.hash], + names: ["Test"] + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + log_from_api = Enum.at(response["items"], 0) + compare_item(log, log_from_api) + end end describe "/addresses/{address_hash}/tokens" do diff --git a/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs index a5ff0b18f6b9..c2f5ab8c2b6e 100644 --- a/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/api/v2/transaction_view_test.exs @@ -2,6 +2,8 @@ defmodule BlockScoutWeb.API.V2.TransactionViewTest do use BlockScoutWeb.ConnCase, async: true alias BlockScoutWeb.API.V2.TransactionView + alias Explorer.Repo + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation describe "decode_logs/2" do test "doesn't use decoding candidate event with different 2nd, 3d or 4th topic" do @@ -54,7 +56,11 @@ defmodule BlockScoutWeb.API.V2.TransactionViewTest do data: log2_data ) - logs = [log1, log2] + logs = + [log1, log2] + |> Repo.preload( + address: [:names, :smart_contract, Implementation.proxy_implementations_smart_contracts_association()] + ) assert [ {:ok, "d20a68b2", @@ -129,7 +135,11 @@ defmodule BlockScoutWeb.API.V2.TransactionViewTest do data: log2_data ) - logs = [log1, log2] + logs = + [log1, log2] + |> Repo.preload( + address: [:names, :smart_contract, Implementation.proxy_implementations_smart_contracts_association()] + ) assert [ {:ok, "d20a68b2", diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index f4c3a254da63..785ff4333d3d 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -954,7 +954,7 @@ defmodule Explorer.Chain do Returns `{:error, :not_found}` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_block_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.hash_to_block(hash) @@ -1025,7 +1025,7 @@ defmodule Explorer.Chain do Returns `{:error, :not_found}` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.hash_to_transaction(hash) @@ -1106,7 +1106,7 @@ defmodule Explorer.Chain do Returns `[]` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.hashes_to_transactions([hash]) @@ -2299,34 +2299,7 @@ defmodule Explorer.Chain do @doc """ The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.t/0`. - iex> Explorer.Chain.string_to_block_hash( - ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" - ...> ) - { - :ok, - %Explorer.Chain.Hash{ - byte_count: 32, - bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - } - } - - `String.t` format must always have 64 hexadecimal digits after the `0x` base prefix. - - iex> Explorer.Chain.string_to_block_hash("0x0") - :error - - """ - @spec string_to_block_hash(String.t()) :: {:ok, Hash.t()} | :error - def string_to_block_hash(string) when is_binary(string) do - Hash.Full.cast(string) - end - - def string_to_block_hash(_), do: :error - - @doc """ - The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.t/0`. - - iex> Explorer.Chain.string_to_transaction_hash( + iex> Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) { @@ -2339,16 +2312,16 @@ defmodule Explorer.Chain do `String.t` format must always have 64 hexadecimal digits after the `0x` base prefix. - iex> Explorer.Chain.string_to_transaction_hash("0x0") + iex> Explorer.Chain.string_to_full_hash("0x0") :error """ - @spec string_to_transaction_hash(String.t()) :: {:ok, Hash.t()} | :error - def string_to_transaction_hash(string) when is_binary(string) do + @spec string_to_full_hash(String.t()) :: {:ok, Hash.t()} | :error + def string_to_full_hash(string) when is_binary(string) do Hash.Full.cast(string) end - def string_to_transaction_hash(_), do: :error + def string_to_full_hash(_), do: :error @doc """ Constructs the base query `Ecto.Query.t()/0` to create requests to the transaction logs @@ -3718,7 +3691,7 @@ defmodule Explorer.Chain do Returns `:not_found` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.check_transaction_exists(hash) @@ -3742,7 +3715,7 @@ defmodule Explorer.Chain do Returns `false` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.transaction_exists?(hash) @@ -3846,7 +3819,7 @@ defmodule Explorer.Chain do {:ok, nil} end - {:ok, transaction_hash} = Chain.string_to_transaction_hash(first_trace.transaction_hash) + {:ok, transaction_hash} = Chain.string_to_full_hash(first_trace.transaction_hash) {:ok, call_type} = if Map.has_key?(first_trace, :call_type) do diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex index 3b376ec4ec76..be77593cfc2b 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex @@ -255,7 +255,7 @@ defmodule Explorer.Chain.Arbitrum.Reader.API.Settlement do """ @spec get_da_record_by_data_key(binary()) :: {:ok, {non_neg_integer(), map()}} | {:error, :not_found} def get_da_record_by_data_key("0x" <> _ = data_key) do - data_key_bytes = data_key |> Chain.string_to_block_hash() |> Kernel.elem(1) |> Map.get(:bytes) + data_key_bytes = data_key |> Chain.string_to_full_hash() |> Kernel.elem(1) |> Map.get(:bytes) get_da_record_by_data_key(data_key_bytes) end diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/common.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/common.ex index 3490ef79d948..cf390a1da8e5 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/common.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/common.ex @@ -27,7 +27,7 @@ defmodule Explorer.Chain.Arbitrum.Reader.Common do """ import Ecto.Query, only: [from: 2] - import Explorer.Chain, only: [select_repo: 1, string_to_block_hash: 1] + import Explorer.Chain, only: [select_repo: 1, string_to_full_hash: 1] alias Explorer.Chain.Arbitrum.{ BatchBlock, @@ -76,7 +76,7 @@ defmodule Explorer.Chain.Arbitrum.Reader.Common do """ @spec get_anytrust_keyset(binary(), api?: boolean()) :: map() def get_anytrust_keyset("0x" <> <<_::binary-size(64)>> = keyset_hash, options) do - get_anytrust_keyset(keyset_hash |> string_to_block_hash() |> Kernel.elem(1) |> Map.get(:bytes), options) + get_anytrust_keyset(keyset_hash |> string_to_full_hash() |> Kernel.elem(1) |> Map.get(:bytes), options) end def get_anytrust_keyset(keyset_hash, options) do diff --git a/apps/explorer/lib/explorer/chain/beacon/reader.ex b/apps/explorer/lib/explorer/chain/beacon/reader.ex index 5fb8189ec585..65fe20b977bc 100644 --- a/apps/explorer/lib/explorer/chain/beacon/reader.ex +++ b/apps/explorer/lib/explorer/chain/beacon/reader.ex @@ -33,7 +33,7 @@ defmodule Explorer.Chain.Beacon.Reader do Returns `{:error, :not_found}` if not found - iex> {:ok, hash} = Explorer.Chain.string_to_transaction_hash( + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" ...> ) iex> Explorer.Chain.Beacon.Reader.blob(hash, true) diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 49402eddcc58..2741f7201f59 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -87,8 +87,7 @@ defmodule Explorer.Chain.Log do alias ABI.{Event, FunctionSelector} alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Address, ContractMethod, Hash, Log, TokenTransfer, Transaction} - alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.{ContractMethod, Hash, Log, TokenTransfer, Transaction} alias Explorer.SmartContract.SigProviderInterface @required_attrs ~w(address_hash data block_hash index)a @@ -167,36 +166,25 @@ defmodule Explorer.Chain.Log do @doc """ Decode transaction log data. """ - @spec decode(Log.t(), Transaction.t(), any(), boolean(), boolean(), map(), map()) :: + @spec decode(Log.t(), Transaction.t(), any(), boolean(), boolean(), list(), map()) :: {{:ok, String.t(), String.t(), map()} | {:error, :could_not_decode} | {:error, atom(), list()} - | {{:error, :contract_not_verified | :try_with_sig_provider, [any()]}, any()}, map(), map()} + | {{:error, :contract_not_verified | :try_with_sig_provider, [any()]}, any()}, map()} def decode( log, transaction, db_options, skip_sig_provider?, decoding_from_list?, - full_abi \\ nil, - full_abi_per_address_hash_acc \\ %{}, + full_abi, events_acc \\ %{} ) do - {full_abi, full_abi_per_address_hash_acc} = - if decoding_from_list? do - {full_abi, full_abi_per_address_hash_acc} - else - full_abi_per_address_hash_acc = - accumulate_abi_by_address_hash(full_abi_per_address_hash_acc, log.address_hash, db_options) - - {full_abi_per_address_hash_acc[log.address_hash], full_abi_per_address_hash_acc} - end - with {:no_abi, false} <- {:no_abi, is_nil(full_abi)}, {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction.hash), identifier <- Base.encode16(selector.method_id, case: :lower), text <- function_call(selector.function, mapping) do - {{:ok, identifier, text, mapping}, full_abi_per_address_hash_acc, events_acc} + {{:ok, identifier, text, mapping}, events_acc} else {:error, _} = error -> handle_method_decode_error( @@ -206,7 +194,6 @@ defmodule Explorer.Chain.Log do skip_sig_provider?, decoding_from_list?, db_options, - full_abi_per_address_hash_acc, events_acc ) @@ -218,7 +205,6 @@ defmodule Explorer.Chain.Log do skip_sig_provider?, decoding_from_list?, db_options, - full_abi_per_address_hash_acc, events_acc ) end @@ -231,7 +217,6 @@ defmodule Explorer.Chain.Log do skip_sig_provider?, decoding_from_list?, db_options, - full_abi_per_address_hash_acc, events_acc ) do case error do @@ -240,7 +225,7 @@ defmodule Explorer.Chain.Log do find_method_candidates(log, transaction, db_options, events_acc), {true, events_acc} <- {is_list(candidates), events_acc}, {false, events_acc} <- {Enum.empty?(candidates), events_acc} do - {{:error, :contract_not_verified, candidates}, full_abi_per_address_hash_acc, events_acc} + {{:error, :contract_not_verified, candidates}, events_acc} else {_, events_acc} -> result = @@ -250,104 +235,11 @@ defmodule Explorer.Chain.Log do decode_event_via_sig_provider(log, transaction.hash, skip_sig_provider?) end - {result, full_abi_per_address_hash_acc, events_acc} + {result, events_acc} end end end - # Accumulates the ABI (Application Binary Interface) by the given address hash. - - # ## Parameters - - # - `acc` (map): The accumulator map where the ABIs are stored. - # - `address_hash` (binary): The address hash for which the ABI needs to be accumulated. - # - `db_options` (keyword list): Database options to be used for querying the contract address. - - # ## Returns - - # - `map`: The updated accumulator map with the ABI for the given address hash. - - # If the address hash is `nil` or already exists in the accumulator, it returns the accumulator as is. - # If the contract address is found, it combines the proxy implementation ABI and stores it in the accumulator. - # If the contract address is not found, it stores `nil` in the accumulator for the given address hash. - @spec accumulate_abi_by_address_hash(map(), Hash.t(), Keyword.t()) :: map() - defp accumulate_abi_by_address_hash(acc, address_hash, db_options) do - address_options = - [ - necessity_by_association: %{ - :smart_contract => :optional - } - ] - |> Keyword.merge(db_options) - - if !is_nil(address_hash) && Map.has_key?(acc, address_hash) do - acc - else - case Chain.find_contract_address(address_hash, address_options) do - {:ok, %{smart_contract: smart_contract}} -> - full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, db_options) - Map.put(acc, address_hash, full_abi) - - _ -> - Map.put(acc, address_hash, nil) - end - end - end - - @doc """ - Accumulates the ABI (Application Binary Interface) by the given list of address hashes. - - ## Parameters - - - `acc` (map): The accumulator map where the ABIs are stored. - - `address_hash` (binary): The address hash for which the ABI needs to be accumulated. - - `db_options` (keyword list): Database options to be used for querying the contract address. - - ## Returns - - - `map`: The updated accumulator map with the ABI for the given address hash. - - If the address hash is `nil` or already exists in the accumulator, it returns the accumulator as is. - If the contract address is found, it combines the proxy implementation ABI and stores it in the accumulator. - If the contract address is not found, it stores `nil` in the accumulator for the given address hash. - """ - @spec accumulate_abi_by_address_hashes(map(), [Hash.t()], Keyword.t()) :: map() - def accumulate_abi_by_address_hashes(address_hash_abi_map, address_hashes, db_options) do - address_options = - [ - necessity_by_association: %{ - :smart_contract => :optional - } - ] - |> Keyword.merge(db_options) - - address_hashes_without_abi = - address_hashes - |> Enum.filter(fn address_hash -> - is_nil(address_hash_abi_map[address_hash]) - end) - - if Enum.empty?(address_hashes_without_abi) do - address_hash_abi_map - else - case Address.find_contract_addresses(address_hashes_without_abi, address_options) do - {:ok, addresses} when is_list(addresses) -> - update_address_hash_abi_map_with_implementations_abi(address_hash_abi_map, addresses, db_options) - - _ -> - %{} - end - end - end - - defp update_address_hash_abi_map_with_implementations_abi(address_hash_abi_map, addresses, db_options) do - addresses - |> Enum.reduce(address_hash_abi_map, fn %{smart_contract: smart_contract} = address, acc -> - full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, db_options) - Map.put(acc, address.hash, full_abi) - end) - end - defp find_method_candidates(log, transaction, options, events_acc) do if is_nil(log.first_topic) do {{:error, :could_not_decode}, events_acc} diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 3674b8da9685..137e039c9174 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -220,7 +220,7 @@ defmodule Explorer.Chain.Search do defp prepare_search_query(query, {:some, prepared_term}) do address_hash_result = Chain.string_to_address_hash(query) filecoin_address_result = maybe_parse_filecoin_address(query) - full_hash_result = Chain.string_to_transaction_hash(query) + full_hash_result = Chain.string_to_full_hash(query) non_negative_integer_result = ExplorerHelper.safe_parse_non_negative_integer(query) query_length = String.length(query) diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index 7a03d5601004..98e4fa4f971e 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -63,6 +63,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do field(:names, {:array, :string}, null: false) has_many(:addresses, Address, foreign_key: :hash, references: :address_hashes) + has_many(:smart_contracts, SmartContract, foreign_key: :address_hash, references: :address_hashes) belongs_to( :address, @@ -471,6 +472,43 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do def names(_, _), do: [] + @doc """ + Fetches and associates smart contracts for a nested list of address hashes. + + This function takes a nested list of address hashes (`nested_address_hashes`), queries the database + for smart contracts matching the flattened list of address hashes, and then maps the results back + to the original nested structure. Each address hash is associated with its corresponding smart + contract, if found. + + ## Parameters + + - `nested_address_hashes`: A nested list of address hashes (e.g., `[[hash1, hash2], [hash3]]`). + + ## Returns + + - A list of tuples where each tuple contains: + - The original list of address hashes. + - A list of smart contracts corresponding to the address hashes. + + """ + @spec smart_contract_association_for_implementations([Hash.Address.t()]) :: [ + {[Hash.Address.t()], [SmartContract.t() | nil]} + ] + def smart_contract_association_for_implementations(nested_address_hashes) do + query = + from(smart_contract in SmartContract, where: smart_contract.address_hash in ^List.flatten(nested_address_hashes)) + + smart_contracts_map = + query + |> Repo.replica().all() + |> Map.new(&{&1.address_hash, &1}) + + for address_hashes <- nested_address_hashes, + smart_contracts when not is_nil(smart_contracts) <- address_hashes |> Enum.map(&smart_contracts_map[&1]) do + {address_hashes, smart_contracts} + end + end + if @chain_type == :filecoin do @doc """ Fetches associated addresses for Filecoin based on the provided nested IDs. @@ -494,7 +532,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do |> Map.new(&{&1.hash, &1}) for ids <- nested_ids, - address <- ids |> Enum.map(&addresses_map[&1]) do + address when not is_nil(address) <- ids |> Enum.map(&addresses_map[&1]) do {ids, address} end end @@ -531,6 +569,15 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do def proxy_implementations_addresses_association do [addresses: &__MODULE__.addresses_association_for_filecoin/1] end + + def proxy_implementations_smart_contracts_association do + [ + proxy_implementations: [ + addresses: &__MODULE__.addresses_association_for_filecoin/1, + smart_contracts: &__MODULE__.smart_contract_association_for_implementations/1 + ] + ] + end else @doc """ Returns the association for proxy implementations. @@ -564,5 +611,9 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do def proxy_implementations_addresses_association do [] end + + def proxy_implementations_smart_contracts_association do + [proxy_implementations: [smart_contracts: &__MODULE__.smart_contract_association_for_implementations/1]] + end end end diff --git a/apps/explorer/lib/explorer/eth_rpc.ex b/apps/explorer/lib/explorer/eth_rpc.ex index f73603b676b8..b51e5ed13f3c 100644 --- a/apps/explorer/lib/explorer/eth_rpc.ex +++ b/apps/explorer/lib/explorer/eth_rpc.ex @@ -1018,7 +1018,7 @@ defmodule Explorer.EthRPC do defp validate_and_render_transaction(transaction_hash_string, render_func, params) do with {:transaction_hash, {:ok, transaction_hash}} <- - {:transaction_hash, Chain.string_to_transaction_hash(transaction_hash_string)}, + {:transaction_hash, Chain.string_to_full_hash(transaction_hash_string)}, {:transaction, {:ok, transaction}} <- {:transaction, Chain.hash_to_transaction(transaction_hash, params)} do render_func.(transaction) else diff --git a/apps/explorer/lib/explorer/etherscan/logs.ex b/apps/explorer/lib/explorer/etherscan/logs.ex index cef67eeebc6e..1a925eb480fc 100644 --- a/apps/explorer/lib/explorer/etherscan/logs.ex +++ b/apps/explorer/lib/explorer/etherscan/logs.ex @@ -273,7 +273,7 @@ defmodule Explorer.Etherscan.Logs do end defp sanitize_string_topic_value(topic_value) do - case Chain.string_to_block_hash(topic_value) do + case Chain.string_to_full_hash(topic_value) do {:ok, _} -> topic_value diff --git a/apps/explorer/test/explorer/account/notifier/email_test.exs b/apps/explorer/test/explorer/account/notifier/email_test.exs index 387aab7e6fab..eb0f0413b553 100644 --- a/apps/explorer/test/explorer/account/notifier/email_test.exs +++ b/apps/explorer/test/explorer/account/notifier/email_test.exs @@ -11,7 +11,7 @@ defmodule Explorer.Account.Notifier.EmailTest do import Explorer.Chain, only: [ string_to_address_hash: 1, - string_to_transaction_hash: 1 + string_to_full_hash: 1 ] import Explorer.Account.Notifier.Email, @@ -43,7 +43,7 @@ defmodule Explorer.Account.Notifier.EmailTest do describe "composing email" do test "compose_email" do {:ok, transaction_hash} = - string_to_transaction_hash("0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d") + string_to_full_hash("0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d") {:ok, from_hash} = string_to_address_hash("0x092D537737E767Dae48c28aE509f34094496f030") diff --git a/apps/explorer/test/explorer/chain/log_test.exs b/apps/explorer/test/explorer/chain/log_test.exs index 853d327880d7..e3835bf057a2 100644 --- a/apps/explorer/test/explorer/chain/log_test.exs +++ b/apps/explorer/test/explorer/chain/log_test.exs @@ -5,7 +5,7 @@ defmodule Explorer.Chain.LogTest do alias Ecto.Changeset alias Explorer.Chain.{Log, SmartContract} - alias Explorer.{Repo, TestHelper} + alias Explorer.TestHelper @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" @@ -70,25 +70,27 @@ defmodule Explorer.Chain.LogTest do log = insert(:log, transaction: transaction) - assert {{:error, :could_not_decode}, _, _} = Log.decode(log, transaction, [], false, false) + assert {{:error, :could_not_decode}, _} = Log.decode(log, transaction, [], false, false, []) end test "that a contract call transaction that has a verified contract returns the decoded input data" do to_address = insert(:address, contract_code: "0x") + abi = [ + %{ + "anonymous" => false, + "inputs" => [ + %{"indexed" => true, "name" => "_from_human", "type" => "string"}, + %{"indexed" => false, "name" => "_number", "type" => "uint256"}, + %{"indexed" => true, "name" => "_belly", "type" => "bool"} + ], + "name" => "WantsPets", + "type" => "event" + } + ] + insert(:smart_contract, - abi: [ - %{ - "anonymous" => false, - "inputs" => [ - %{"indexed" => true, "name" => "_from_human", "type" => "string"}, - %{"indexed" => false, "name" => "_number", "type" => "uint256"}, - %{"indexed" => true, "name" => "_belly", "type" => "bool"} - ], - "name" => "WantsPets", - "type" => "event" - } - ], + abi: abi, address_hash: to_address.hash, contract_code_md5: "123" ) @@ -116,8 +118,6 @@ defmodule Explorer.Chain.LogTest do data: data ) - TestHelper.get_all_proxies_implementation_zero_addresses() - assert {{:ok, "eb9b3c4c", "WantsPets(string indexed _from_human, uint256 _number, bool indexed _belly)", [ {"_from_human", "string", true, @@ -126,25 +126,27 @@ defmodule Explorer.Chain.LogTest do 152, 206, 227, 92, 181, 213, 23, 242, 210, 150, 162>>}}, {"_number", "uint256", false, 0}, {"_belly", "bool", true, true} - ]}, _, _} = Log.decode(log, transaction, [], false, false) + ]}, _} = Log.decode(log, transaction, [], false, false, abi) end test "replace arg names with argN if it's empty string" do to_address = insert(:address, contract_code: "0x") + abi = [ + %{ + "anonymous" => false, + "inputs" => [ + %{"indexed" => true, "name" => "", "type" => "string"}, + %{"indexed" => false, "name" => "", "type" => "uint256"}, + %{"indexed" => true, "name" => "", "type" => "bool"} + ], + "name" => "WantsPets", + "type" => "event" + } + ] + insert(:smart_contract, - abi: [ - %{ - "anonymous" => false, - "inputs" => [ - %{"indexed" => true, "name" => "", "type" => "string"}, - %{"indexed" => false, "name" => "", "type" => "uint256"}, - %{"indexed" => true, "name" => "", "type" => "bool"} - ], - "name" => "WantsPets", - "type" => "event" - } - ], + abi: abi, address_hash: to_address.hash, contract_code_md5: "123" ) @@ -172,8 +174,6 @@ defmodule Explorer.Chain.LogTest do data: data ) - TestHelper.get_all_proxies_implementation_zero_addresses() - assert {{:ok, "eb9b3c4c", "WantsPets(string indexed arg0, uint256 arg1, bool indexed arg2)", [ {"arg0", "string", true, @@ -182,24 +182,26 @@ defmodule Explorer.Chain.LogTest do 152, 206, 227, 92, 181, 213, 23, 242, 210, 150, 162>>}}, {"arg1", "uint256", false, 0}, {"arg2", "bool", true, true} - ]}, _, _} = Log.decode(log, transaction, [], false, false) + ]}, _} = Log.decode(log, transaction, [], false, false, abi) end test "finds decoding candidates" do + abi = [ + %{ + "anonymous" => false, + "inputs" => [ + %{"indexed" => true, "name" => "_from_human", "type" => "string"}, + %{"indexed" => false, "name" => "_number", "type" => "uint256"}, + %{"indexed" => true, "name" => "_belly", "type" => "bool"} + ], + "name" => "WantsPets", + "type" => "event" + } + ] + params = params_for(:smart_contract, %{ - abi: [ - %{ - "anonymous" => false, - "inputs" => [ - %{"indexed" => true, "name" => "_from_human", "type" => "string"}, - %{"indexed" => false, "name" => "_number", "type" => "uint256"}, - %{"indexed" => true, "name" => "_belly", "type" => "bool"} - ], - "name" => "WantsPets", - "type" => "event" - } - ] + abi: abi }) # changeset has a callback to insert contract methods @@ -226,18 +228,15 @@ defmodule Explorer.Chain.LogTest do data: data ) - assert {{:error, :contract_not_verified, + assert {{:ok, "eb9b3c4c", "WantsPets(string indexed _from_human, uint256 _number, bool indexed _belly)", [ - {:ok, "eb9b3c4c", "WantsPets(string indexed _from_human, uint256 _number, bool indexed _belly)", - [ - {"_from_human", "string", true, - {:dynamic, - <<56, 228, 122, 123, 113, 157, 206, 99, 102, 42, 234, 244, 52, 64, 50, 111, 85, 27, 138, 126, 225, - 152, 206, 227, 92, 181, 213, 23, 242, 210, 150, 162>>}}, - {"_number", "uint256", false, 0}, - {"_belly", "bool", true, true} - ]} - ]}, _, _} = Log.decode(log, transaction, [], false, false) + {"_from_human", "string", true, + {:dynamic, + <<56, 228, 122, 123, 113, 157, 206, 99, 102, 42, 234, 244, 52, 64, 50, 111, 85, 27, 138, 126, 225, + 152, 206, 227, 92, 181, 213, 23, 242, 210, 150, 162>>}}, + {"_number", "uint256", false, 0}, + {"_belly", "bool", true, true} + ]}, _} = Log.decode(log, transaction, [], false, false, abi) end end end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 7732bd54bdcb..95f020982770 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -3224,7 +3224,7 @@ defmodule Explorer.ChainTest do {:ok, from_address_hash_bytes} = Chain.string_to_address_hash(from_address_hash) {:ok, created_contract_code_bytes} = Data.cast(created_contract_code) {:ok, init_bytes} = Data.cast(init) - {:ok, transaction_hash_bytes} = Chain.string_to_transaction_hash(transaction_hash) + {:ok, transaction_hash_bytes} = Chain.string_to_full_hash(transaction_hash) {:ok, type_bytes} = Type.load(type) value_wei = %Wei{value: Decimal.new(value)} diff --git a/apps/indexer/lib/indexer/fetcher/block_reward.ex b/apps/indexer/lib/indexer/fetcher/block_reward.ex index bbe62314b44b..af5bee84fa2d 100644 --- a/apps/indexer/lib/indexer/fetcher/block_reward.ex +++ b/apps/indexer/lib/indexer/fetcher/block_reward.ex @@ -192,7 +192,7 @@ defmodule Indexer.Fetcher.BlockReward do |> Chain.timestamp_by_block_hash() Enum.map(beneficiaries_params, fn %{block_hash: block_hash_str} = beneficiary -> - {:ok, block_hash} = Chain.string_to_block_hash(block_hash_str) + {:ok, block_hash} = Chain.string_to_full_hash(block_hash_str) case timestamp_by_block_hash do %{^block_hash => block_timestamp} -> From 3bfc9603570cee2fe56db25436b4f2a496091e7a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 16 May 2025 14:27:23 +0300 Subject: [PATCH 0050/1006] feat: Multichain Search DB export retry queue (#12377) * feat: Multichain Search DB export retry queue * Add tests for batch_import/2 and extract_batch_import_params_into_chunks/1 * Update common-blockscout.env * Fix formatting * Add test "returns multiple chunks with the correct structure when all types of data is provided" * Add tests for Indexer.Fetcher.MultichainSearchDbExport.Retry.init/3 and Indexer.Fetcher.MultichainSearchDbExport.Retry.run/2 * Add test "returns {:error, reason} when at least one chunk is failed" * Process AI agent review * Update apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> * Add INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT, INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE variables * Manage addresses chunk size of Multichain Search DB export via runtime variable * Fix tests after introducing nev variable * Remove duplicated call to multichain db export for realtime / catchup indexers * Process review comment * Replace reduce_while with reduce * Return back sync batch import to Multicahin Search --------- Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> --- apps/explorer/lib/explorer/chain.ex | 11 +- apps/explorer/lib/explorer/chain/block.ex | 17 + apps/explorer/lib/explorer/chain/import.ex | 2 +- ...multichain_search_db_export_retry_queue.ex | 68 +++ .../chain/pending_operations_helper.ex | 12 +- .../lib/explorer/chain/transaction.ex | 4 +- apps/explorer/lib/explorer/helper.ex | 23 + .../multichain_search.ex | 369 +++++++++--- apps/explorer/lib/test_helper.ex | 11 + ...ain_search_db_export_retry_queue_table.exs | 17 + .../multichain_search_test.exs | 559 ++++++++++++++++++ apps/explorer/test/support/factory.ex | 5 + apps/indexer/lib/indexer/block/fetcher.ex | 15 - .../multichain_search_db_export/retry.ex | 127 ++++ .../indexer/fetcher/token_instance/retry.ex | 17 +- apps/indexer/lib/indexer/helper.ex | 44 ++ apps/indexer/lib/indexer/supervisor.ex | 2 + .../retry_test.exs | 144 +++++ config/runtime.exs | 15 +- docker-compose/envs/common-blockscout.env | 8 + 20 files changed, 1329 insertions(+), 141 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250505123201_create_multichain_search_db_export_retry_queue_table.exs create mode 100644 apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex create mode 100644 apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 785ff4333d3d..71f17dec0db3 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1168,11 +1168,10 @@ defmodule Explorer.Chain do } do result else - # credo:disable-for-next-line Credo.Check.Refactor.Nesting - case MultichainSearch.batch_import(assets_to_import) do - {:ok, _} -> result - _ -> {:error, :insert_to_multichain_search_db_failed} - end + # todo: consider robust async way of importing + MultichainSearch.batch_import(assets_to_import) + + result end other_result -> @@ -1502,7 +1501,7 @@ defmodule Explorer.Chain do @spec get_transactions_by_hashes([Hash.t()]) :: [Transaction.t()] def get_transactions_by_hashes(transaction_hashes) do transaction_hashes - |> Transaction.transactions_by_hashes() + |> Transaction.by_hashes_query() |> Repo.all() end diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 685f3bee5fcd..2a76be09c641 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -580,4 +580,21 @@ defmodule Explorer.Chain.Block do end def set_refetch_needed(block_number), do: set_refetch_needed([block_number]) + + @doc """ + Generates a query to fetch blocks by their hashes. + + ## Parameters + + - `hashes`: A list of block hashes to filter by. + + ## Returns + + - An Ecto query that can be used to retrieve blocks matching the given hashes. + """ + @spec by_hashes_query([binary()]) :: Ecto.Query.t() + def by_hashes_query(hashes) do + __MODULE__ + |> where([block], block.hash in ^hashes) + end end diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index d8c20ed4756b..737bb0c66134 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -70,7 +70,7 @@ defmodule Explorer.Chain.Import do @type all_result :: {:ok, %{unquote_splicing(quoted_runner_imported)}} - | {:error, [Changeset.t()] | :timeout | :insert_to_multichain_search_db_failed} + | {:error, [Changeset.t()] | :timeout} | {:error, step :: Ecto.Multi.name(), failed_value :: any(), changes_so_far :: %{optional(Ecto.Multi.name()) => any()}} diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex new file mode 100644 index 000000000000..a47beec70818 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex @@ -0,0 +1,68 @@ +defmodule Explorer.Chain.MultichainSearchDbExportRetryQueue do + @moduledoc """ + Tracks data pending retry for export to the Multichain Service database after an initial failure. + """ + + use Explorer.Schema + import Ecto.Query + alias Explorer.{Chain, Repo} + + @required_attrs ~w(hash hash_type)a + + @primary_key false + typed_schema "multichain_search_db_export_retry_queue" do + field(:hash, :binary, null: true) + + field(:hash_type, Ecto.Enum, + values: [ + :block, + :transaction, + :address + ], + null: true + ) + + timestamps() + end + + def changeset(%__MODULE__{} = pending_ops, attrs) do + pending_ops + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end + + @spec stream_multichain_db_data_batch_to_retry_export( + initial :: accumulator, + reducer :: (entry :: map(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_multichain_db_data_batch_to_retry_export(initial, reducer, limited? \\ false) + when is_function(reducer, 2) do + __MODULE__ + |> select([export], %{ + hash: export.hash, + hash_type: export.hash_type + }) + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Builds a query to retrieve records from the `Explorer.Chain.MultichainSearchDbExportRetryQueue` module + where the `hash` field matches any of the given `hashes`. + + ## Parameters + + - `hashes`: A list of hash values to filter the records by. + + ## Returns + + - An Ecto query that can be executed to fetch the matching records. + """ + @spec by_hashes_query([binary()]) :: Ecto.Query.t() + def by_hashes_query(hashes) do + __MODULE__ + |> where([export], export.hash in ^hashes) + end +end diff --git a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex index 35d817ffbd0b..eb64abc5fc1f 100644 --- a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex +++ b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex @@ -4,7 +4,7 @@ defmodule Explorer.Chain.PendingOperationsHelper do import Ecto.Query alias Explorer.Chain.{Hash, PendingBlockOperation, PendingTransactionOperation, Transaction} - alias Explorer.Repo + alias Explorer.{Helper, Repo} @transactions_batch_size 1000 @blocks_batch_size 10 @@ -68,7 +68,7 @@ defmodule Explorer.Chain.PendingOperationsHelper do :finish pbo_params -> - Repo.insert_all(PendingBlockOperation, add_timestamps(pbo_params), on_conflict: :nothing) + Repo.insert_all(PendingBlockOperation, Helper.add_timestamps(pbo_params), on_conflict: :nothing) block_numbers_to_delete = Enum.map(pbo_params, & &1.block_number) @@ -101,7 +101,7 @@ defmodule Explorer.Chain.PendingOperationsHelper do |> where([t], t.block_number in ^pbo_block_numbers) |> select([t], %{transaction_hash: t.hash}) |> Repo.all() - |> add_timestamps() + |> Helper.add_timestamps() Repo.insert_all(PendingTransactionOperation, pto_params, on_conflict: :nothing) @@ -113,12 +113,6 @@ defmodule Explorer.Chain.PendingOperationsHelper do end end - defp add_timestamps(params) do - now = DateTime.utc_now() - - Enum.map(params, &Map.merge(&1, %{inserted_at: now, updated_at: now})) - end - @doc """ Generates a query to find pending block operations that match any of the given block hashes. diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index c4e6b39d9316..cf0de325a9c4 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -1386,8 +1386,8 @@ defmodule Explorer.Chain.Transaction do @doc """ Builds an `Ecto.Query` to fetch transactions by hashes """ - @spec transactions_by_hashes([Hash.t()]) :: Ecto.Query.t() - def transactions_by_hashes(hashes) do + @spec by_hashes_query([Hash.t()]) :: Ecto.Query.t() + def by_hashes_query(hashes) do from( t in Transaction, where: t.hash in ^hashes diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 672b0d2f9b1d..b0899de56d96 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -485,4 +485,27 @@ defmodule Explorer.Helper do |> DateTime.from_unix!(unit) |> DateTime.to_date() end + + @doc """ + Adds `inserted_at` and `updated_at` timestamps to a list of maps. + + This function takes a list of maps (`params`) and adds the current UTC + timestamp (`DateTime.utc_now/0`) as the values for the `:inserted_at` and + `:updated_at` keys in each map. + + ## Parameters + + - `params` - A list of maps to which the timestamps will be added. + + ## Returns + + - A list of maps, each containing the original keys and values along with + the `:inserted_at` and `:updated_at` keys set to the current UTC timestamp. + """ + @spec add_timestamps([map()]) :: [map()] + def add_timestamps(params) do + now = DateTime.utc_now() + + Enum.map(params, &Map.merge(&1, %{inserted_at: now, updated_at: now})) + end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 45d7a08102bb..b69bc2d10f29 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -6,16 +6,17 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do alias Ecto.Association.NotLoaded alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Address, Block, Hash, Transaction} - alias Explorer.Repo + alias Explorer.Chain.MultichainSearchDbExportRetryQueue + alias Explorer.{Helper, Repo} alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger - @addresses_chunk_size 7_000 @max_concurrency 5 @post_timeout :timer.minutes(5) - @request_error_msg "Error while sending request to Multichain Search Service" + @request_error_msg "Error while sending request to Multichain Search DB Service" + @unspecified "UNSPECIFIED" @doc """ Performs a batch import of addresses, blocks, and transactions to the Multichain Search microservice. @@ -31,131 +32,311 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do - `{:ok, result}`: If the import was successful. - `{:error, reason}`: If an error occurred. """ - @spec batch_import(%{ - addresses: [Address.t()], - blocks: [Block.t()], - transactions: [Transaction.t()] - }) :: {:error, :disabled | String.t() | Jason.DecodeError.t()} | {:ok, any()} - def batch_import(params) do + @spec batch_import( + %{ + addresses: [Address.t()], + blocks: [Block.t()], + transactions: [Transaction.t()] + }, + boolean() + ) :: {:error, :disabled | String.t() | Jason.DecodeError.t()} | {:ok, any()} + def batch_import(params, retry? \\ false) do if enabled?() do params_chunks = extract_batch_import_params_into_chunks(params) + url = batch_import_url() params_chunks |> Task.async_stream( - fn body -> http_post_request(batch_import_url(), body) end, + fn export_body -> http_post_request(url, export_body, retry?) end, max_concurrency: @max_concurrency, - timeout: @post_timeout + timeout: @post_timeout, + zip_input_on_exit: true ) - |> Enum.reduce_while({:ok, :chunks_processed}, fn - {:ok, _result}, acc -> {:cont, acc} - {:exit, _reason}, _acc -> {:halt, {:error, @request_error_msg}} - {:error, _reason}, _acc -> {:halt, {:error, @request_error_msg}} + |> Enum.reduce({:ok, {:chunks_processed, params_chunks}}, fn + {:ok, {:ok, _result}}, acc -> + acc + + {:ok, {:error, error}}, _acc -> + on_error(error, retry?) + {:error, @request_error_msg} + + {:exit, {export_body, reason}}, _acc -> + on_error( + %{ + url: url, + data_to_retry: export_body, + reason: reason + }, + retry? + ) + + {:error, @request_error_msg} end) else {:ok, :service_disabled} end end - defp http_post_request(url, body) do + defp log_error(%{ + url: url, + data_to_retry: data_to_retry, + reason: reason + }) do + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to microservice url: #{url}, ", + "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}, ", + "error_reason: #{inspect(reason, limit: :infinity, printable_limit: :infinity)}" + ] + end) + + Logger.configure(truncate: old_truncate) + end + + defp log_error(%{ + url: url, + data_to_retry: data_to_retry, + status_code: status_code, + response_body: response_body + }) do + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to microservice url: #{url}, ", + "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}, ", + "status_code: #{inspect(status_code)}, ", + "response_body: #{inspect(response_body, limit: :infinity, printable_limit: :infinity)}" + ] + end) + + Logger.configure(truncate: old_truncate) + end + + @spec on_error( + map(), + boolean() + ) :: {non_neg_integer(), nil | [term()]} | :ok + # sobelow_skip ["DOS.StringToAtom"] + defp on_error( + %{ + data_to_retry: %{addresses: addresses, hashes: hashes} + } = error, + false + ) do + log_error(error) + + hashes_to_retry = + hashes + |> Enum.map( + &%{ + hash: Helper.hash_to_binary(&1.hash), + hash_type: &1.hash_type |> String.downcase() |> String.to_atom() + } + ) + + addresses_to_retry = + addresses + |> Enum.map(fn %{hash: address_hash_string} -> + %{ + hash: Helper.hash_to_binary(address_hash_string), + hash_type: :address + } + end) + + prepared_data = hashes_to_retry ++ addresses_to_retry + + Repo.insert_all(MultichainSearchDbExportRetryQueue, Helper.add_timestamps(prepared_data), on_conflict: :nothing) + end + + defp on_error(_, _), do: :ignore + + @spec http_post_request(String.t(), map(), boolean()) :: {:ok, any()} | {:error, String.t()} + defp http_post_request(url, body, retry?) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout, hackney: [pool: false]) do + case Application.get_env(:explorer, :http_adapter).post(url, Jason.encode!(body), headers, + recv_timeout: @post_timeout, + hackney: [pool: false] + ) do {:ok, %Response{body: response_body, status_code: 200}} -> response_body |> Jason.decode() {:ok, %Response{body: response_body, status_code: status_code}} -> - old_truncate = Application.get_env(:logger, :truncate) - Logger.configure(truncate: :infinity) - - Logger.error(fn -> - [ - "Error while sending request to microservice url: #{url}, ", - "status_code: #{inspect(status_code)}, ", - "response_body: #{inspect(response_body, limit: :infinity, printable_limit: :infinity)}, ", - "request_body: #{inspect(body |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}" - ] - end) - - Logger.configure(truncate: old_truncate) - {:error, @request_error_msg} - - error -> - old_truncate = Application.get_env(:logger, :truncate) - Logger.configure(truncate: :infinity) - - Logger.error(fn -> - [ - "Error while sending request to microservice url: #{url}, request_body: #{inspect(body |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}: ", - inspect(error, limit: :infinity, printable_limit: :infinity) - ] - end) - - Logger.configure(truncate: old_truncate) - {:error, @request_error_msg} + {:error, + %{ + url: url, + data_to_retry: body, + retry?: retry?, + status_code: status_code, + response_body: response_body + }} + + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, + %{ + url: url, + data_to_retry: body, + retry?: retry?, + reason: reason + }} end end - defp extract_batch_import_params_into_chunks(%{ - addresses: raw_addresses, - blocks: blocks, - transactions: transactions - }) do + @doc """ + Extracts batch import parameters into chunks for processing. + + This function takes a map containing `addresses`, `blocks`, and `transactions`, + and processes them into chunks suitable for batch import. It performs the following steps: + + - Retrieves the chain ID. + - Computes block ranges from the given blocks. + - Preloads associated `:token` and `:smart_contract` data for the addresses, removes duplicates, and formats them. + - Formats the blocks and transactions into hashes. + - Combines block hashes and transaction hashes into a single list of `block_transaction_hashes`. + - Splits the formatted addresses into chunks of size `addresses_chunk_size()` and indexes them. + - Constructs a base data chunk containing the API key, chain ID, and block ranges. + + The function returns a list of data chunks. If there are no addresses, it returns a single chunk with only the `block_transaction_hashes`. Otherwise, it creates a chunk for each group of addresses, including the `block_transaction_hashes` only in the first chunk. + + ## Parameters + + - `%{addresses: raw_addresses, blocks: blocks, transactions: transactions}`: A map containing: + - `addresses`: A list of raw address data. + - `blocks`: A list of block data. + - `transactions`: A list of transaction data. + + ## Returns + + - A list of maps, where each map represents a chunk of data for batch import. Each chunk contains: + - `:api_key`: The API key for the request. + - `:chain_id`: The chain ID as a string. + - `:addresses`: A chunk of formatted addresses. + - `:block_ranges`: The computed block ranges. + - `:hashes`: A list of block and transaction hashes (only included in the first chunk). + + """ + @spec extract_batch_import_params_into_chunks(%{ + addresses: [Address.t()], + blocks: [Block.t()], + transactions: [Transaction.t()] + }) :: [ + %{ + api_key: String.t(), + chain_id: String.t(), + addresses: [String.t()], + block_ranges: [%{min_block_number: String.t(), max_block_number: String.t()}], + hashes: [%{hash: String.t(), hash_type: String.t()}] + } + ] + def extract_batch_import_params_into_chunks(%{ + addresses: raw_addresses, + blocks: blocks, + transactions: transactions + }) do chain_id = ChainId.get_id() block_ranges = get_block_ranges(blocks) addresses = raw_addresses - |> Repo.preload([:token, :smart_contract]) - |> Enum.map(fn address -> - %{ - hash: Hash.to_string(address.hash), - is_contract: !is_nil(address.contract_code), - is_verified_contract: address.verified, - is_token: token?(address.token), - ens_name: address.ens_domain_name, - token_name: get_token_name(address.token), - token_type: get_token_type(address.token), - contract_name: get_smart_contract_name(address.smart_contract) - } - end) |> Enum.uniq() + |> Repo.preload([:token, :smart_contract]) + |> Enum.map(&format_address(&1)) - block_hashes = - blocks - |> Enum.map( - &%{ - hash: Hash.to_string(&1.hash), - hash_type: "BLOCK" - } - ) + block_hashes = format_blocks(blocks) - transaction_hashes = - transactions - |> Enum.map( - &%{ - hash: Hash.to_string(&1.hash), - hash_type: "TRANSACTION" - } - ) + transaction_hashes = format_transactions(transactions) block_transaction_hashes = block_hashes ++ transaction_hashes indexed_addresses_chunks = addresses - |> Enum.chunk_every(@addresses_chunk_size) + |> Enum.chunk_every(addresses_chunk_size()) |> Enum.with_index() - Enum.map(indexed_addresses_chunks, fn {addresses_chunk, index} -> - hashes = if index == 0, do: block_transaction_hashes, else: [] + api_key = api_key() + chain_id_string = to_string(chain_id) + + base_data_chunk = %{ + api_key: api_key, + chain_id: chain_id_string, + addresses: [], + block_ranges: [] + } + + if Enum.empty?(indexed_addresses_chunks) do + [ + base_data_chunk + |> Map.put(:hashes, block_transaction_hashes) + |> Map.put(:block_ranges, block_ranges) + ] + else + Enum.map(indexed_addresses_chunks, fn {addresses_chunk, index} -> + # credo:disable-for-lines:2 Credo.Check.Refactor.Nesting + hashes_in_chunk = if index == 0, do: block_transaction_hashes, else: [] + block_ranges_in_chunk = if index == 0, do: block_ranges, else: [] + + base_data_chunk + |> Map.put(:addresses, addresses_chunk) + |> Map.put(:hashes, hashes_in_chunk) + |> Map.put(:block_ranges, block_ranges_in_chunk) + end) + end + end - %{ - api_key: api_key(), - chain_id: to_string(chain_id), - addresses: addresses_chunk, - block_ranges: block_ranges, - hashes: hashes - } - end) + defp format_address(address) do + %{ + hash: Hash.to_string(address.hash), + is_contract: !is_nil(address.contract_code), + is_verified_contract: address.verified, + is_token: token?(address.token), + ens_name: address.ens_domain_name, + token_name: get_token_name(address.token), + token_type: get_token_type(address.token), + contract_name: get_smart_contract_name(address.smart_contract) + } + end + + @spec format_blocks([Block.t() | %{hash: String.t(), hash_type: String.t()}]) :: [ + %{hash: String.t(), hash_type: String.t()} + ] + defp format_blocks(blocks) do + blocks + |> Enum.map(&format_block(to_string(&1.hash))) + end + + @spec format_transactions([Transaction.t() | %{hash: String.t(), hash_type: String.t()}]) :: [ + %{hash: String.t(), hash_type: String.t()} + ] + defp format_transactions(transactions) do + transactions + |> Enum.map(&format_transaction(to_string(&1.hash))) + end + + @spec format_block(String.t()) :: %{ + hash: String.t(), + hash_type: String.t() + } + defp format_block(block_hash_string) do + %{ + hash: block_hash_string, + hash_type: "BLOCK" + } + end + + @spec format_transaction(String.t()) :: %{ + hash: String.t(), + hash_type: String.t() + } + defp format_transaction(transaction_hash_string) do + %{ + hash: transaction_hash_string, + hash_type: "TRANSACTION" + } end defp token?(nil), do: false @@ -176,9 +357,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp get_smart_contract_name(smart_contract), do: smart_contract.name - defp get_token_type(nil), do: "UNSPECIFIED" + defp get_token_type(nil), do: @unspecified - defp get_token_type(%NotLoaded{}), do: "UNSPECIFIED" + defp get_token_type(%NotLoaded{}), do: @unspecified defp get_token_type(token), do: token.type @@ -233,4 +414,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @return `true` if the base URL is not nil, `false` otherwise. """ def enabled?, do: !is_nil(base_url()) + + defp addresses_chunk_size do + Application.get_env(:explorer, __MODULE__)[:addresses_chunk_size] + end end diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex index 7bb268a85c22..3aee82c8f697 100644 --- a/apps/explorer/lib/test_helper.ex +++ b/apps/explorer/lib/test_helper.ex @@ -286,4 +286,15 @@ defmodule Explorer.TestHelper do ]} end) end + + def get_chain_id_mock do + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn %{ + id: _id, + method: "eth_chainId", + params: [] + }, + _options -> + {:ok, "0x1"} + end) + end end diff --git a/apps/explorer/priv/repo/migrations/20250505123201_create_multichain_search_db_export_retry_queue_table.exs b/apps/explorer/priv/repo/migrations/20250505123201_create_multichain_search_db_export_retry_queue_table.exs new file mode 100644 index 000000000000..008401ce7ba8 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250505123201_create_multichain_search_db_export_retry_queue_table.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Migrations.CreateMultichainSearchDbExportRetryQueueTable do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE multichain_search_hash_type AS ENUM ('block', 'transaction', 'address')", + "DROP TYPE multichain_search_hash_type" + ) + + create table(:multichain_search_db_export_retry_queue, primary_key: false) do + add(:hash, :bytea, null: false, primary_key: true) + add(:hash_type, :multichain_search_hash_type, null: false, primary_key: true) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs new file mode 100644 index 000000000000..ede50d4719f9 --- /dev/null +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -0,0 +1,559 @@ +defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do + use ExUnit.Case + use Explorer.DataCase + import Mox + + alias Explorer.Chain.Cache.ChainId + alias Explorer.Chain.MultichainSearchDbExportRetryQueue + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.{Repo, TestHelper} + alias Plug.Conn + + setup :verify_on_exit! + + @error_msg "Error while sending request to Multichain Search DB Service" + + describe "batch_import/2" do + setup do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + + :ok + end + + test "returns {:ok, :service_disabled} when the service is disabled" do + params = %{ + addresses: [], + blocks: [], + transactions: [] + } + + assert MultichainSearch.batch_import(params) == {:ok, :service_disabled} + end + + test "processes chunks and returns {:ok, result} when the service is enabled" do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + + block_1 = insert(:block) + block_2 = insert(:block) + + transaction_1 = insert(:transaction) + transaction_2 = insert(:transaction) + + address_1 = insert(:address) + address_2 = insert(:address) + + params = %{ + addresses: [address_1, address_2], + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_import(params) + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + end + + test "returns {:error, reason} when an error occurs during processing and 'multichain_search_db_export_retry_queue' table is populated" do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + + address_1 = insert(:address) + address_2 = insert(:address) + block_1 = insert(:block) + block_2 = insert(:block) + transaction_1 = insert(:transaction) |> with_block(block_1) + transaction_2 = insert(:transaction) |> with_block(block_2) + + params = %{ + addresses: [address_1, address_2], + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + assert {:error, @error_msg} = MultichainSearch.batch_import(params) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 6 + records = Repo.all(MultichainSearchDbExportRetryQueue) + + assert Enum.all?(records, fn record -> + (record.hash == address_1.hash.bytes && record.hash_type == :address) || + (record.hash == address_2.hash.bytes && record.hash_type == :address) || + (record.hash == block_1.hash.bytes && record.hash_type == :block) || + (record.hash == block_2.hash.bytes && record.hash_type == :block) || + (record.hash == transaction_1.hash.bytes && record.hash_type == :transaction) || + (record.hash == transaction_2.hash.bytes && record.hash_type == :transaction) + end) + end + + test "returns {:error, reason} when at least one chunk is failed" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Application.put_env(:explorer, :http_adapter, HTTPoison) + end) + + TestHelper.get_chain_id_mock() + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + + block_1 = insert(:block) + block_2 = insert(:block) + + transaction_1 = insert(:transaction) + transaction_2 = insert(:transaction) + + # 7002 addresses (7000 in the first chunk and 2 in the second) + addresses = + for _ <- 0..7001 do + insert(:address) + end + + Explorer.Mox.HTTPoison + |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", _expected_body, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} + end) + + Explorer.Mox.HTTPoison + |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", + _expected_body, + [{"Content-Type", "application/json"}], + _options -> + {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + end) + + params = %{ + addresses: addresses, + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + assert {:error, @error_msg} = MultichainSearch.batch_import(params) + # 7000 addresses + 2 blocks + 2 transactions + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 7004 + end + + test "returns {:error, reason} when an error occurs in all chunks during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + + addresses = 10 |> insert_list(:address) + + params = %{ + addresses: addresses, + blocks: [], + transactions: [] + } + + assert {:error, @error_msg} = MultichainSearch.batch_import(params) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 10 + end + + test "returns {:error, reason} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + + addresses = 15 |> insert_list(:address) + + params = %{ + addresses: addresses, + blocks: [], + transactions: [] + } + + assert {:error, @error_msg} = MultichainSearch.batch_import(params) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 15 + end + end + + describe "extract_batch_import_params_into_chunks/1" do + setup do + TestHelper.get_chain_id_mock() + Application.put_env(:explorer, MultichainSearch, api_key: "12345", addresses_chunk_size: 7000) + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, api_key: nil, addresses_chunk_size: 7000) + end) + + :ok + end + + test "returns empty chunks when no data is provided" do + # filling chain_id cache + ChainId.get_id() + + assert MultichainSearch.extract_batch_import_params_into_chunks(%{ + addresses: [], + blocks: [], + transactions: [] + }) == [%{api_key: "12345", addresses: [], block_ranges: [], chain_id: "1", hashes: []}] + end + + test "returns chunks with transactions and blocks when no addresses provided" do + block_1 = insert(:block) + block_2 = insert(:block) + transaction_1 = insert(:transaction) + transaction_2 = insert(:transaction) + + params = %{ + addresses: [], + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) + + assert Enum.count(chunks) == 1 + + chunk = List.first(chunks) + + assert chunk[:api_key] == "12345" + assert chunk[:chain_id] == "1" + assert length(chunk[:addresses]) == 0 + + assert chunk[:block_ranges] == [ + %{ + max_block_number: to_string(max(block_1.number, block_2.number)), + min_block_number: to_string(min(block_1.number, block_2.number)) + } + ] + + assert chunk[:hashes] == [ + %{ + hash: "0x" <> Base.encode16(block_1.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(block_2.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(transaction_1.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + }, + %{ + hash: "0x" <> Base.encode16(transaction_2.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + } + ] + end + + test "returns chunks with the correct structure when all types of data is provided" do + address_1 = insert(:address) + address_2 = insert(:address) + block_1 = insert(:block) + block_2 = insert(:block) + transaction_1 = insert(:transaction) + transaction_2 = insert(:transaction) + + params = %{ + addresses: [address_1, address_2], + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) + + assert Enum.count(chunks) == 1 + + chunk = List.first(chunks) + + assert chunk[:api_key] == "12345" + assert chunk[:chain_id] == "1" + + assert Enum.all?(chunk[:addresses], fn item -> + item.hash == "0x" <> Base.encode16(address_1.hash.bytes, case: :lower) || + item.hash == "0x" <> Base.encode16(address_2.hash.bytes, case: :lower) + end) + + assert chunk[:block_ranges] == [ + %{ + max_block_number: to_string(max(block_1.number, block_2.number)), + min_block_number: to_string(min(block_1.number, block_2.number)) + } + ] + + assert chunk[:hashes] == [ + %{ + hash: "0x" <> Base.encode16(block_1.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(block_2.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(transaction_1.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + }, + %{ + hash: "0x" <> Base.encode16(transaction_2.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + } + ] + end + + test "returns multiple chunks with the correct structure when all types of data is provided" do + addresses = + for _ <- 0..7001 do + insert(:address) + end + + block_1 = insert(:block) + block_2 = insert(:block) + transaction_1 = insert(:transaction) + transaction_2 = insert(:transaction) + + params = %{ + addresses: addresses, + blocks: [block_1, block_2], + transactions: [transaction_1, transaction_2] + } + + chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) + + assert Enum.count(chunks) == 2 + + first_chunk = List.first(chunks) + second_chunk = List.last(chunks) + + assert first_chunk[:api_key] == "12345" + assert first_chunk[:chain_id] == "1" + + assert Enum.count(first_chunk[:addresses]) == 7000 + assert Enum.count(second_chunk[:addresses]) == 2 + + random_index_in_first_chunk = Enum.random(0..6999) + + assert Enum.any?(first_chunk[:addresses], fn item -> + item.hash == + "0x" <> Base.encode16(Enum.at(addresses, random_index_in_first_chunk).hash.bytes, case: :lower) + end) + + assert first_chunk[:block_ranges] == [ + %{ + max_block_number: to_string(max(block_1.number, block_2.number)), + min_block_number: to_string(min(block_1.number, block_2.number)) + } + ] + + assert first_chunk[:hashes] == [ + %{ + hash: "0x" <> Base.encode16(block_1.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(block_2.hash.bytes, case: :lower), + hash_type: "BLOCK" + }, + %{ + hash: "0x" <> Base.encode16(transaction_1.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + }, + %{ + hash: "0x" <> Base.encode16(transaction_2.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + } + ] + + assert second_chunk[:api_key] == "12345" + assert second_chunk[:chain_id] == "1" + + assert second_chunk[:block_ranges] == [] + assert second_chunk[:hashes] == [] + + assert Enum.all?(second_chunk[:addresses], fn item -> + item.hash == "0x" <> Base.encode16(Enum.at(addresses, -2).hash.bytes, case: :lower) || + item.hash == "0x" <> Base.encode16(List.last(addresses).hash.bytes, case: :lower) + end) + end + + test "returns chunks with the correct structure when only addresses are provided" do + address_1 = insert(:address) + address_2 = insert(:address) + + params = %{ + addresses: [address_1, address_2], + blocks: [], + transactions: [] + } + + chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) + + assert Enum.count(chunks) == 1 + + chunk = List.first(chunks) + + assert chunk[:api_key] == "12345" + assert chunk[:chain_id] == "1" + + assert Enum.all?(chunk[:addresses], fn item -> + item.hash == "0x" <> Base.encode16(address_1.hash.bytes, case: :lower) || + item.hash == "0x" <> Base.encode16(address_2.hash.bytes, case: :lower) + end) + + assert chunk[:block_ranges] == [] + assert chunk[:hashes] == [] + end + + test "returns chunks with the correct structure of addresses" do + address_1 = insert(:address, ens_domain_name: "te.eth") + address_2 = insert(:address, contract_code: "0x1234") + address_3 = insert(:address, contract_code: "0x1234", verified: true) + insert(:smart_contract, address_hash: address_3.hash, contract_code_md5: "123") + insert(:token, %{contract_address: address_3, name: "Main Token", type: "ERC-721"}) + + address_3_with_preloads = address_3 |> Repo.preload([:smart_contract, :token]) + + params = %{ + addresses: [address_1, address_2, address_3_with_preloads], + blocks: [], + transactions: [] + } + + chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) + + assert Enum.count(chunks) == 1 + + chunk = List.first(chunks) + + assert chunk[:api_key] == "12345" + assert chunk[:chain_id] == "1" + + assert chunk[:addresses] == [ + %{ + hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), + is_contract: false, + is_verified_contract: false, + contract_name: nil, + token_name: nil, + token_type: "UNSPECIFIED", + is_token: false, + ens_name: "te.eth" + }, + %{ + hash: "0x" <> Base.encode16(address_2.hash.bytes, case: :lower), + is_contract: true, + is_verified_contract: false, + contract_name: nil, + token_name: nil, + token_type: "UNSPECIFIED", + is_token: false, + ens_name: nil + }, + %{ + hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), + is_contract: true, + is_verified_contract: true, + contract_name: "SimpleStorage", + token_name: "Main Token", + token_type: "ERC-721", + is_token: true, + ens_name: nil + } + ] + + assert chunk[:block_ranges] == [] + assert chunk[:hashes] == [] + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index d8f52ebba90e..877cf04fb4f8 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -39,6 +39,7 @@ defmodule Explorer.Factory do Hash, InternalTransaction, Log, + MultichainSearchDbExportRetryQueue, PendingBlockOperation, PendingTransactionOperation, SmartContract, @@ -744,6 +745,10 @@ defmodule Explorer.Factory do %PendingTransactionOperation{} end + def multichain_search_db_export_retry_queue_factory do + %MultichainSearchDbExportRetryQueue{} + end + def internal_transaction_factory() do gas = Enum.random(21_000..100_000) gas_used = Enum.random(0..gas) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 8820faa47bb2..4a9bde3894ba 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -16,7 +16,6 @@ defmodule Indexer.Block.Fetcher do alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions, Uncles} alias Explorer.Chain.Filecoin.PendingAddressOperation, as: FilecoinPendingAddressOperation alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei} - alias Explorer.MicroserviceInterfaces.MultichainSearch alias Indexer.Block.Fetcher.Receipts alias Indexer.Fetcher.Arbitrum.MessagesToL2Matcher, as: ArbitrumMessagesToL2Matcher alias Indexer.Fetcher.Celo.EpochBlockOperations, as: CeloEpochBlockOperations @@ -281,12 +280,6 @@ defmodule Indexer.Block.Fetcher do update_uncles_cache(inserted[:block_second_degree_relations]) update_withdrawals_cache(inserted[:withdrawals]) - update_multichain_search_db(%{ - addresses: inserted[:addresses], - blocks: inserted[:blocks], - transactions: inserted[:transactions] - }) - async_match_arbitrum_messages_to_l2(arbitrum_transactions_for_further_handling) result @@ -412,14 +405,6 @@ defmodule Indexer.Block.Fetcher do :ok end - defp update_multichain_search_db(%{addresses: addresses, blocks: blocks, transactions: transactions}) do - MultichainSearch.batch_import(%{ - addresses: addresses || [], - blocks: blocks || [], - transactions: transactions || [] - }) - end - def import( %__MODULE__{broadcast: broadcast, callback_module: callback_module} = state, options diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex new file mode 100644 index 000000000000..133029733add --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex @@ -0,0 +1,127 @@ +defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do + @moduledoc """ + Retries exporting blockchain data to Multichain Search DB service. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Block, Hash, MultichainSearchDbExportRetryQueue} + alias Explorer.MicroserviceInterfaces.MultichainSearch + + alias Indexer.BufferedTask + alias Indexer.Helper, as: IndexerHelper + + @behaviour BufferedTask + + @default_max_batch_size 10 + @default_max_concurrency 10 + @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + MultichainSearchDbExportRetryQueue.stream_multichain_db_data_batch_to_retry_export( + initial_acc, + fn data, acc -> + IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end + ) + + acc + end + + @impl BufferedTask + def run(export_data, _json_rpc_named_arguments) when is_list(export_data) do + pre_prepared_export_data = + export_data + |> Enum.reduce(%{address_hashes: [], block_hashes: [], transactions: []}, fn %{ + hash: hash_bytes, + hash_type: hash_type + }, + acc -> + case hash_type do + :address -> + Map.update( + acc, + :address_hashes, + [%Hash{byte_count: 20, bytes: hash_bytes}], + &[%Hash{byte_count: 20, bytes: hash_bytes} | &1] + ) + + :block -> + Map.update( + acc, + :block_hashes, + [%Hash{byte_count: 32, bytes: hash_bytes}], + &[%Hash{byte_count: 32, bytes: hash_bytes} | &1] + ) + + :transaction -> + Map.update( + acc, + :transactions, + [%{hash: to_string(%Hash{byte_count: 32, bytes: hash_bytes})}], + &[%{hash: to_string(%Hash{byte_count: 32, bytes: hash_bytes})} | &1] + ) + end + end) + + addresses = + pre_prepared_export_data.address_hashes + |> Chain.hashes_to_addresses() + + blocks = + pre_prepared_export_data.block_hashes + |> Block.by_hashes_query() + |> Repo.all() + + prepared_export_data = + pre_prepared_export_data + |> Map.put(:addresses, addresses) + |> Map.put(:blocks, blocks) + |> Map.drop([:address_hashes]) + |> Map.drop([:block_hashes]) + + case MultichainSearch.batch_import(prepared_export_data, true) do + {:ok, result} -> + unless result == :service_disabled do + export_data + |> Enum.map(&Map.get(&1, :hash)) + |> MultichainSearchDbExportRetryQueue.by_hashes_query() + |> Repo.delete_all() + end + + :ok + + {:error, _} -> + Logger.error(fn -> + ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] + end) + + {:retry, export_data} + end + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/retry.ex b/apps/indexer/lib/indexer/fetcher/token_instance/retry.ex index 2613b2350fbb..6f47c68534e4 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/retry.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/retry.ex @@ -10,13 +10,12 @@ defmodule Indexer.Fetcher.TokenInstance.Retry do alias Explorer.Chain.Token.Instance alias Indexer.BufferedTask + alias Indexer.Helper, as: IndexerHelper @behaviour BufferedTask @default_max_batch_size 10 @default_max_concurrency 10 - @max_queue_size 5000 - @busy_waiting_timeout 500 @doc false def child_spec([init_options, gen_server_options]) do @@ -34,25 +33,13 @@ defmodule Indexer.Fetcher.TokenInstance.Retry do Instance.stream_token_instances_with_error( initial_acc, fn data, acc -> - reduce_if_queue_is_not_full(data, acc, reducer) + IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) end ) acc end - defp reduce_if_queue_is_not_full(data, acc, reducer) do - bound_queue = GenServer.call(__MODULE__, :state).bound_queue - - if bound_queue.size >= @max_queue_size or (bound_queue.maximum_size && bound_queue.size >= bound_queue.maximum_size) do - :timer.sleep(@busy_waiting_timeout) - - reduce_if_queue_is_not_full(data, acc, reducer) - else - reducer.(data, acc) - end - end - @impl BufferedTask def run(token_instances, _json_rpc_named_arguments) when is_list(token_instances) do batch_fetch_instances(token_instances) diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 3ff19561d8d4..1ffc1e002aef 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -883,4 +883,48 @@ defmodule Indexer.Helper do |> String.trim() |> String.trim_trailing("/") end + + @max_queue_size 5000 + @enqueue_busy_waiting_timeout 500 + @doc """ + Reduces the given `data` into an accumulator `acc` using the provided `reducer` function, + but only if the queue is not full. This function ensures that the processing respects + the queue's size constraints. + + If the queue is full (i.e., its size is greater than or equal to `@max_queue_size` or + its `maximum_size`), the function will pause for a duration defined by `@busy_waiting_timeout` + and retry until the queue has available space. + + ## Parameters + + - `data`: The data to be processed by the `reducer` function. + - `acc`: The accumulator that will be passed to the `reducer` function. + - `reducer`: A function that takes `data` and `acc` as arguments and returns the updated accumulator. + + ## Returns + + The result of applying the `reducer` function to the `data` and `acc`. + + ## Notes + + This function uses a recursive approach to wait for the queue to have available space. + Ensure that the `@busy_waiting_timeout` is set to an appropriate value to avoid excessive delays. + """ + @spec reduce_if_queue_is_not_full(any(), any(), (any(), any() -> any()), module()) :: any() + def reduce_if_queue_is_not_full(data, acc, reducer, module) do + bound_queue = GenServer.call(module, :state).bound_queue + + max_queue_size = Application.get_env(:indexer, module)[:max_queue_size] || @max_queue_size + + enqueue_busy_waiting_timeout = + Application.get_env(:indexer, module)[:enqueue_busy_waiting_timeout] || @enqueue_busy_waiting_timeout + + if bound_queue.size >= max_queue_size or (bound_queue.maximum_size && bound_queue.size >= bound_queue.maximum_size) do + :timer.sleep(enqueue_busy_waiting_timeout) + + reduce_if_queue_is_not_full(data, acc, reducer, module) + else + reducer.(data, acc) + end + end end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index ab96cb012731..4b849e75bb4d 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -23,6 +23,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.Blackfort.Validator, as: ValidatorBlackfort alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.MultichainSearchDbExport.Retry, as: MultichainSearchDbExportRetry alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry @@ -146,6 +147,7 @@ defmodule Indexer.Supervisor do {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbExportRetry.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, configure( Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs new file mode 100644 index 000000000000..0f3688ecb1ad --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs @@ -0,0 +1,144 @@ +defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do + use ExUnit.Case + use Explorer.DataCase + + import ExUnit.CaptureLog, only: [capture_log: 1] + + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.TestHelper + alias Indexer.Fetcher.MultichainSearchDbExport.Retry, as: MultichainSearchDbExportRetry + alias Plug.Conn + + @moduletag :capture_log + + setup do + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + Application.put_env(:indexer, MultichainSearchDbExportRetry.Supervisor, disabled?: false) + + on_exit(fn -> + Application.put_env(:indexer, MultichainSearchDbExportRetry.Supervisor, disabled?: true) + end) + + :ok + end + + describe "init/3" do + setup do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + end) + end + + test "initializes with data from the retry queue" do + address_hash_bytes = "66A9B160F6a06f53f23785F069882Ee7337180E8" |> Base.decode16!(case: :mixed) + + block_hash_bytes = + "bba915260f4859d2c908d31296e125368c01e05ee59e2e691ea8d69cceac6e1b" |> Base.decode16!(case: :mixed) + + transaction_hash_bytes = + "aba197aa8a13871bdd53861f7b5108394000fc0f72893661ae39610e9cd94019" |> Base.decode16!(case: :mixed) + + insert(:multichain_search_db_export_retry_queue, %{hash: address_hash_bytes, hash_type: :address}) + insert(:multichain_search_db_export_retry_queue, %{hash: block_hash_bytes, hash_type: :block}) + insert(:multichain_search_db_export_retry_queue, %{hash: transaction_hash_bytes, hash_type: :transaction}) + + reducer = fn data, acc -> [data | acc] end + + pid = + [] + |> MultichainSearchDbExportRetry.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + + results = MultichainSearchDbExportRetry.init([], reducer, nil) + + assert Enum.count(results) == 3 + assert Enum.member?(results, %{hash: address_hash_bytes, hash_type: :address}) + assert Enum.member?(results, %{hash: block_hash_bytes, hash_type: :block}) + assert Enum.member?(results, %{hash: transaction_hash_bytes, hash_type: :transaction}) + :timer.sleep(10) + GenServer.stop(pid) + end + end + + describe "run/2" do + setup do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Bypass.down(bypass) + end) + + {:ok, bypass: bypass} + end + + test "successfully processes multichain search db export retry queue data", %{bypass: bypass} do + address_hash_bytes = "66A9B160F6a06f53f23785F069882Ee7337180E8" |> Base.decode16!(case: :mixed) + + block_hash_bytes = + "bba915260f4859d2c908d31296e125368c01e05ee59e2e691ea8d69cceac6e1b" |> Base.decode16!(case: :mixed) + + transaction_hash_bytes = + "aba197aa8a13871bdd53861f7b5108394000fc0f72893661ae39610e9cd94019" |> Base.decode16!(case: :mixed) + + export_data = [ + %{hash: address_hash_bytes, hash_type: :address}, + %{hash: block_hash_bytes, hash_type: :block}, + %{hash: transaction_hash_bytes, hash_type: :transaction} + ] + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + assert :ok = MultichainSearchDbExportRetry.run(export_data, nil) + end + + test "returns {:retry, export_data} on error", %{bypass: bypass} do + address = insert(:address) + block = insert(:block) + transaction = insert(:transaction) |> with_block(block) + + export_data = [ + %{hash: address.hash.bytes, hash_type: :address}, + %{hash: block.hash.bytes, hash_type: :block}, + %{hash: transaction.hash.bytes, hash_type: :transaction} + ] + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + log = + capture_log(fn -> + assert {:retry, ^export_data} = MultichainSearchDbExportRetry.run(export_data, nil) + end) + + assert log =~ "Batch export retry to the Multichain Search DB failed" + end + end +end diff --git a/config/runtime.exs b/config/runtime.exs index a0c4744fd248..77476c2467ce 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -623,12 +623,20 @@ config :explorer, Explorer.SmartContract.StylusVerifierInterface, config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), - service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL") + service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL"), + addresses_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7000) config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), service_url: System.get_env("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") +config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY", 10), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE", 10), + enqueue_busy_waiting_timeout: + ConfigHelper.parse_time_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s"), + max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE", 1000) + config :explorer, :air_table_public_tags, table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") @@ -975,6 +983,11 @@ config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC1155, config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_ERC_721_SANITIZE_FETCHER", "false") +config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry.Supervisor, + disabled?: + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_FETCHER") || + is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 10), interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 410c0f915c1b..00710f936db9 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -180,6 +180,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER= # INDEXER_DISABLE_WITHDRAWALS_FETCHER= # INDEXER_DISABLE_REPLACED_TRANSACTION_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_FETCHER= # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -198,6 +199,10 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY= # INDEXER_TOKEN_INSTANCE_REFETCH_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_REFETCH_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= @@ -391,6 +396,7 @@ RE_CAPTCHA_DISABLED=false # MICROSERVICE_SC_VERIFIER_ENABLED=true # MICROSERVICE_SC_VERIFIER_URL=http://smart-contract-verifier:8050/ # MICROSERVICE_SC_VERIFIER_TYPE=sc_verifier +# MICROSERVICE_SC_VERIFIER_API_KEY= MICROSERVICE_SC_VERIFIER_TYPE=eth_bytecode_db # MICROSERVICE_ETH_BYTECODE_DB_INTERVAL_BETWEEN_LOOKUPS=10m # MICROSERVICE_ETH_BYTECODE_DB_MAX_LOOKUPS_CONCURRENCY=10 @@ -407,6 +413,8 @@ MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ # MICROSERVICE_METADATA_PROXY_REQUESTS_TIMEOUT= # MICROSERVICE_STYLUS_VERIFIER_URL= # MICROSERVICE_MULTICHAIN_SEARCH_URL= +# MICROSERVICE_MULTICHAIN_SEARCH_API_KEY= +# MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL= DECODE_NOT_A_CONTRACT_CALLS=true From 9c252a2d06e854d0c8baf083f94a715acea332ba Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Fri, 16 May 2025 14:35:57 +0300 Subject: [PATCH 0051/1006] 8.1.0 --- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-berachain.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-berachain.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-suave.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-berachain.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 44 +++++++++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 +- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 65 files changed, 109 insertions(+), 65 deletions(-) diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 6c31b17aa4c0..c778a5c7b473 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-berachain.yml b/.github/workflows/pre-release-berachain.yml index 196201c4bdc7..a787e4b9c511 100644 --- a/.github/workflows/pre-release-berachain.yml +++ b/.github/workflows/pre-release-berachain.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 9edc3fc1779e..e34c32f7aadf 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 6d1c68016c05..dd403cc4c9d6 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index e81fd2725326..8e1324321850 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 54e957d6a7ae..0622ffc073af 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 4925acd2c295..2102db7b1669 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 90cab9635343..2dfb5e168faa 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index fc3af1f9c80a..52746664e8cc 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 565438cd9ec4..c1d4ac5e4806 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 3e95ea0a4967..5bc1b586b262 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 9ad8a1b1499f..d7de11ac8c9b 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 41f1a3992c80..b8f22f9a822f 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 232975f6f9ed..1e33050cf153 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 9510bee606e1..520ff6476602 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index da97433f6603..e259e7566f74 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-berachain.yml b/.github/workflows/publish-docker-image-for-berachain.yml index edf43346bdba..445c3499afa2 100644 --- a/.github/workflows/publish-docker-image-for-berachain.yml +++ b/.github/workflows/publish-docker-image-for-berachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: berachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 1e869a7e8b8e..f7f839687a47 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 671fe4990934..0bcd7e8ef74a 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index e66cc53ef2ef..7eba777de967 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index d1b877ec2d15..e356a10d8fab 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 9030fb4bad6c..09a03273f0c9 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index a48aac635389..ff2e9f7da259 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 0226d10f9c3d..3a6220533e95 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index ab1dfc75d442..c80054f8dae7 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index be70ef364d52..9a681d9ef880 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 2ac79845cdd6..8b8dbb0a0fd3 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 747a8873f81c..f2e7fa7dcd85 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 15168c24839c..2dec504a5182 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 7cefb9c5debd..76725b017f02 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index c7d40769ca10..b6ffa39ea738 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 75bc869afafa..3d38b9bcedee 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 53e05c89bd47..c235861d58e2 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 88413afb8a99..cd85a5be90f3 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 861d647b125a..d6c2f6dda203 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 jobs: push_to_registry: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 6a7e81b542b0..95d47178700f 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 jobs: push_to_registry: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index f34e8ef721d1..c2703f5814d3 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-berachain.yml b/.github/workflows/release-berachain.yml index 24c946077f5f..d31fbf61db05 100644 --- a/.github/workflows/release-berachain.yml +++ b/.github/workflows/release-berachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index e8a96172e961..f736e61907d3 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 57ae67eaad35..8162edd3ed53 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 56348f11550f..13732c7de4d4 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index a7a4209c634f..a22177bcc60a 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index e691c8eed7b3..17176b9b5aac 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 207d80a26770..be244184cc41 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 2ee5ef966eca..b9baa1147aa7 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 9554b7a045b0..5df90dc493ef 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 29404c08185d..fdda264d411c 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index 1abddeea38a8..dc9428718310 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 53ce112e100b..a3642aec6b71 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index d88514d9ff88..5fd122bac329 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index cd95e5d03e90..61c4ecdb4308 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e4f2fd7bf5e..30431f728559 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index c45c582dde07..503bca8d4e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ # Changelog +## 8.1.0 + +### 🚀 Features + +- Multichain Search DB export retry queue ([#12377](https://github.com/blockscout/blockscout/issues/12377)) +- Add TAC operation search ([#12367](https://github.com/blockscout/blockscout/issues/12367)) +- Add `internal_transactions_count` prop in api/v2/blocks/:block endpoint ([#12405](https://github.com/blockscout/blockscout/issues/12405)) + +### 🐛 Bug Fixes + +- Fix logs decoding issue for proxies ([#12414](https://github.com/blockscout/blockscout/issues/12414)) +- Refactor TokenInstanceMetadataRefetch on demand fetcher ([#12419](https://github.com/blockscout/blockscout/issues/12419)) +- Fix for type output in ETH RPC API transaction by hash endpoint +- Frozen confirmations discovery on Arbitrum Nova ([#12385](https://github.com/blockscout/blockscout/issues/12385)) +- Add prague Solidity EVM version ([#12115](https://github.com/blockscout/blockscout/issues/12115)) +- Fix :checkout_timeout error ([#12406](https://github.com/blockscout/blockscout/issues/12406)) +- Force index usage on select current token balances ([#12390](https://github.com/blockscout/blockscout/issues/12390)) +- Fix retrieving max block number in MissingRangesCollector ([#12333](https://github.com/blockscout/blockscout/issues/12333)) +- Start PubSub before Endpoint ([#12274](https://github.com/blockscout/blockscout/issues/12274)) +- Fix FunctionClauseError on internal transactions indexing ([#12246](https://github.com/blockscout/blockscout/issues/12246)) +- Support updated zkSync calldata format in batch proof tracking ([#12234](https://github.com/blockscout/blockscout/issues/12234)) +- On demand bytecode fetcher for eip7702 addresses ([#12330](https://github.com/blockscout/blockscout/issues/12330)) +- Handle pending operations for empty blocks as well ([#12349](https://github.com/blockscout/blockscout/issues/12349)) + +### 🚜 Refactor + +- Eliminate join with internal_transactions table to get list logs in API v1 ([#12352](https://github.com/blockscout/blockscout/issues/12352)) +- Define pending block operations by set of block hashes query ([#12375](https://github.com/blockscout/blockscout/issues/12375)) +- Move `address_to_internal_transactions/2` to `Explorer.Chain.InternalTransaction` module ([#12346](https://github.com/blockscout/blockscout/issues/12346)) +- Single definition of smart-contract internal creation transaction query ([#12335](https://github.com/blockscout/blockscout/issues/12335)) + +### ⚡ Performance + +- Force index usage in `api/v2/addresses/:hash/transactions` ([#12415](https://github.com/blockscout/blockscout/issues/12415)) + +### ⚙️ Miscellaneous Tasks + +- Improve logic behind emerging of custom fields in the response of `eth_getTransactionByHash` ETH RPC API endpoint ([#12416](https://github.com/blockscout/blockscout/issues/12416)) +- Internal transactions unique index ([#12394](https://github.com/blockscout/blockscout/issues/12394)) +- Update blocks consensus in case of import failure ([#12243](https://github.com/blockscout/blockscout/issues/12243)) +- Sanitize ERC-1155 token balances without token ids ([#12305](https://github.com/blockscout/blockscout/issues/12305)) +- Support Celestia Alt-DA in OP batches indexer and Super Roots in OP withdrawals indexer ([#12332](https://github.com/blockscout/blockscout/issues/12332)) +- Send DB read queries to replica in on-demand fetchers ([#12383](https://github.com/blockscout/blockscout/issues/12383)) + ## 8.0.2 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 9dc909757654..7276929c2aaa 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.0.2", + version: "8.1.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 0b711163c806..4d730a6f9828 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.0.2" + version: "8.1.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 96cc4492eb20..007177a6dee4 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -14,7 +14,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-8.0.2"}] + @default_headers [{"User-Agent", "blockscout-8.1.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 1b48769c3c6d..6585746a4f5d 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.0.2", + version: "8.1.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index fcf29990ad9a..d7eb7ae23856 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "8.0.2", + version: "8.1.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 04baf65c2ffb..55f4b4f5ad41 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "8.0.2", + version: "8.1.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index bf03d2273557..8a54c524b08e 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "8.0.2", + version: "8.1.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5e594c342681..6e75df84bb5c 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index 236bcd4e137b..a2bdf95cbac8 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 8.0.2 + RELEASE_VERSION: 8.1.0 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index 781f502a5536..8e1d30af9bd5 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '8.0.2' +RELEASE_VERSION ?= '8.1.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index f00d5fed26d4..7c3ab9dbf1d2 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "8.0.2", + version: "8.1.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index a18e9baf1266..1aa75db3e528 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "8.0.2" + set version: "8.1.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From b19bee2fd707056d052c285140778d64a9c2c524 Mon Sep 17 00:00:00 2001 From: Viktor Baranov Date: Fri, 16 May 2025 14:46:43 +0300 Subject: [PATCH 0052/1006] Add new env varaiables for 8.1.0 to CHANGELOG --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 503bca8d4e65..3cc88069cb8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,23 @@ - Support Celestia Alt-DA in OP batches indexer and Super Roots in OP withdrawals indexer ([#12332](https://github.com/blockscout/blockscout/issues/12332)) - Send DB read queries to replica in on-demand fetchers ([#12383](https://github.com/blockscout/blockscout/issues/12383)) +### New ENV variables + +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `HACKNEY_DEFAULT_POOL_SIZE` | Size of `default` hackney pool. Implemented in [#12406](https://github.com/blockscout/blockscout/pull/12406). |

Version: v8.1.0+
Default: 1000
Applications: API, Indexer

| +| `MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_BATCH_SIZE` | Number of internal transactions to reindex in the batch. Implemented in [#12394](https://github.com/blockscout/blockscout/pull/12394). |

Version: v8.1.0+
Default: 100
Applications: Indexer

| +| `MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_CONCURRENCY` | Number of parallel reindexing internal transaction batches processing. Implemented in [#12394](https://github.com/blockscout/blockscout/pull/12394). |

Version: v8.1.0+
Default: 1
Applications: Indexer

| +| `MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_TIMEOUT` | Timeout between reindexing internal transaction batches processing. Implemented in [#12394](https://github.com/blockscout/blockscout/pull/12394). |

Version: v8.1.0+
Default: 0
Applications: Indexer

| +| `INDEXER_SCROLL_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL` | Defines a URL to Blockscout Blobs API to retrieve L1 blobs from that. Example for Sepolia: `https://eth-sepolia.blockscout.com/api/v2/blobs`. Implemented in [#12294](https://github.com/blockscout/blockscout/pull/12294). |

Version: v8.1.0+
Default: (empty)
Applications: Indexer

| +| `MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE` | Chunk size of addresses while exporting to Multichain Search DB. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377) |

Version: v8.1.0+
Default: (empty)
Applications: API, Indexer

| +| `INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_FETCHER` | If `true`, `retry` multichain search export fetcher doesn't run. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v8.1.0+
Default: false
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE` | Batch size for `retry` multichain search export fetcher. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v8.1.0+
Default: 10
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY` | Concurrency for `retry` multichain search export fetcher. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v8.1.0+
Default: 10
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to `retry` multichain search export queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v8.1.0+
Default: 1s
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE` | Maximum size of `retry` multichain search export queue. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v8.1.0+
Default: 1000
Applications: Indexer

| + + ## 8.0.2 ### 🚀 Features From f52e9aad3ea0ea90238f0c225c6c3bca7df533d6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 19 May 2025 20:08:11 +0300 Subject: [PATCH 0053/1006] chore: Add additional test for Universal proxy, duplicate all proxy endpoints at /3rdparty path (#12442) * chore: Add additional test for Universal proxy, duplicate all proxy endpoints at /3rdparty path * UNIVERSAL_PROXY_CONFIG_URL * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../lib/block_scout_web/routers/api_router.ex | 21 +++++++ .../universal_proxy.ex | 19 ++++--- .../universal_proxy_test.exs | 42 +++++++++++++- config/runtime.exs | 55 +++++++++++-------- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 3 +- 7 files changed, 108 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc88069cb8a..6a4dd623dec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ ### ⚙️ Miscellaneous Tasks +- Add additional test for Universal proxy, duplicate all proxy endpoints at /3rdparty ([#12442](https://github.com/blockscout/blockscout/pull/12442)) - Improve logic behind emerging of custom fields in the response of `eth_getTransactionByHash` ETH RPC API endpoint ([#12416](https://github.com/blockscout/blockscout/issues/12416)) - Internal transactions unique index ([#12394](https://github.com/blockscout/blockscout/issues/12394)) - Update blocks consensus in case of import failure ([#12243](https://github.com/blockscout/blockscout/issues/12243)) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 256f7cc5274a..0ea378d0d286 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -358,6 +358,27 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end scope "/proxy" do + scope "/3rdparty" do + get("/:platform_id", V2.Proxy.UniversalProxyController, :index) + + scope "/noves-fi" do + get("/transactions/:transaction_hash_param", V2.Proxy.NovesFiController, :transaction) + + get("/addresses/:address_hash_param/transactions", V2.Proxy.NovesFiController, :address_transactions) + + get("/transaction-descriptions", V2.Proxy.NovesFiController, :describe_transactions) + end + + scope "/xname" do + get("/addresses/:address_hash_param", V2.Proxy.XnameController, :address) + end + + scope "/solidityscan" do + get("/smart-contracts/:address_hash/report", V2.SmartContractController, :solidityscan_report) + end + end + + # deprecate in the next major/minor release after 9.0.0 scope "/3dparty" do get("/:platform_id", V2.Proxy.UniversalProxyController, :index) diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex index 0e9750d4d0e4..8d6ef169cb5b 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex @@ -1,6 +1,6 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @moduledoc """ - Module for universal proxying 3dparty API endpoints + Module for universal proxying 3rd party API endpoints """ use Tesla @@ -8,7 +8,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @recv_timeout 60_000 - @type api_request :: %{ + @type api_request_params :: %{ url: String.t() | nil, body: String.t(), headers: [{String.t(), String.t()}], @@ -31,7 +31,6 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @allowed_methods [:get, :post, :put, :patch, :delete] @reserved_param_types ~w(address chain_id chain_id_dependent) - @config_url "https://raw.githubusercontent.com/blockscout/backend-configs/refs/heads/main/universal-proxy-config.json" @cache_name :universal_proxy_config @doc """ @@ -154,7 +153,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do - The function delegates further processing of the API key and endpoint parameters to `parse_endpoint_api_key/3` and `parse_endpoint_params/3`. """ - @spec parse_proxy_params(map()) :: api_request() + @spec parse_proxy_params(map()) :: api_request_params() def parse_proxy_params(proxy_params) do config = config() platform_id = proxy_params["platform_id"] @@ -210,7 +209,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do end end - @spec parse_endpoint_api_key(api_request(), api_key() | nil, String.t() | nil) :: api_request() + @spec parse_endpoint_api_key(api_request_params(), api_key() | nil, String.t() | nil) :: api_request_params() defp parse_endpoint_api_key(%{url: _url, headers: _headers} = map, nil, _platform_id), do: map defp parse_endpoint_api_key( @@ -248,7 +247,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do defp parse_endpoint_api_key(map, _endpoint_api_key, _platform_id), do: map - @spec parse_endpoint_params(api_request(), [endpoint_param()], map()) :: api_request() + @spec parse_endpoint_params(api_request_params(), [endpoint_param()], map()) :: api_request_params() defp parse_endpoint_params(api_request_map, endpoint_params, proxy_params) do endpoint_params |> Enum.reduce(api_request_map, fn param, map -> @@ -275,7 +274,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do end) end - @spec parse_param_location(api_request(), endpoint_param(), String.t()) :: api_request() + @spec parse_param_location(api_request_params(), endpoint_param(), String.t()) :: api_request_params() defp parse_param_location( %{body: body, url: url, headers: headers} = api_request_map, %{"location" => location} = params, @@ -334,7 +333,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do safe_parse_config_string(config_string) nil -> - case Tesla.get(@config_url) do + case Tesla.get(config_url()) do {:ok, %Tesla.Env{status: 200, body: config_string}} -> safe_parse_config_string(config_string, true) @@ -354,4 +353,8 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do %{} end end + + defp config_url do + Application.get_env(:explorer, __MODULE__)[:config_url] + end end diff --git a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs index 75b468152940..cbe2d2b13f0a 100644 --- a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs +++ b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs @@ -221,7 +221,47 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxyTest do }).url end - test "correctly parsing chain_id - dependent param in the path" do + test "correctly parsing chain_id - dependent param in the path when chain id is NOT present as param" do + Tesla.Test.expect_tesla_call( + times: 1, + returns: %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "platforms" => %{ + "test_platform" => %{ + "base_url" => "https://api.test.com", + "endpoints" => %{ + "base" => %{ + "path" => "/test/:endpoint_platform_id", + "method" => "get", + "params" => [ + %{ + "location" => "path", + "type" => "chain_id_dependent", + "name" => "endpoint_platform_id", + "mapping" => %{ + "1" => "first_endpoint_platform_id", + "100500" => "another_endpoint_platform_id" + } + } + ] + } + } + } + } + }) + } + ) + + assert "https://api.test.com/test/another_endpoint_platform_id" = + UniversalProxy.parse_proxy_params(%{ + "platform_id" => "test_platform", + "chain_id" => "100500" + }).url + end + + test "correctly parsing chain_id - dependent param in the path when chain id is present as param as well" do Tesla.Test.expect_tesla_call( times: 1, returns: %Tesla.Env{ diff --git a/config/runtime.exs b/config/runtime.exs index 77476c2467ce..c9eae240aea7 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -630,13 +630,6 @@ config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), service_url: System.get_env("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") -config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry, - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY", 10), - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE", 10), - enqueue_busy_waiting_timeout: - ConfigHelper.parse_time_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s"), - max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE", 1000) - config :explorer, :air_table_public_tags, table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") @@ -785,14 +778,6 @@ config :explorer, Explorer.Migrator.BackfillMetadataURL, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_BACKFILL_METADATA_URL_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_BACKFILL_METADATA_URL_CONCURRENCY", 5) -config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, - concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_CONCURRENCY", 5), - batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BATCH_SIZE", 50), - timeout: ConfigHelper.parse_time_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_TIMEOUT", "0s"), - blocks_batch_size: - ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BLOCKS_BATCH_SIZE", 100_000), - high_verbosity: ConfigHelper.parse_bool_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY", "true") - config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), @@ -838,8 +823,24 @@ config :explorer, Explorer.Utility.RateLimiter, limitation_period: ConfigHelper.parse_time_env_var("RATE_LIMITER_ON_DEMAND_LIMITATION_PERIOD", "1h") ] +config :explorer, Explorer.ThirdPartyIntegrations.UniversalProxy, + config_url: + System.get_env( + "UNIVERSAL_PROXY_CONFIG_URL", + "https://raw.githubusercontent.com/blockscout/backend-configs/refs/heads/main/universal-proxy-config.json" + ) + config :explorer, Explorer.Chain.Mud, enabled: ConfigHelper.parse_bool_env_var("MUD_INDEXER_ENABLED") +config :explorer, Explorer.Chain.Scroll.L1FeeParam, + curie_upgrade_block: ConfigHelper.parse_integer_env_var("SCROLL_L2_CURIE_UPGRADE_BLOCK", 0), + scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_SCALAR_INIT", 0), + overhead_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_OVERHEAD_INIT", 0), + commit_scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_COMMIT_SCALAR_INIT", 0), + blob_scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BLOB_SCALAR_INIT", 0), + l1_base_fee_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BASE_FEE_INIT", 0), + l1_blob_base_fee_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BLOB_BASE_FEE_INIT", 0) + ############### ### Indexer ### ############### @@ -1061,6 +1062,21 @@ config :indexer, Indexer.Fetcher.CoinBalance.Realtime, batch_size: coin_balances_batch_size, concurrency: coin_balances_concurrency +config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, + concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_CONCURRENCY", 5), + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BATCH_SIZE", 50), + timeout: ConfigHelper.parse_time_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_TIMEOUT", "0s"), + blocks_batch_size: + ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BLOCKS_BATCH_SIZE", 100_000), + high_verbosity: ConfigHelper.parse_bool_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY", "true") + +config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY", 10), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE", 10), + enqueue_busy_waiting_timeout: + ConfigHelper.parse_time_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s"), + max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE", 1000) + config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.DisputeGame.Supervisor, enabled: ConfigHelper.chain_type() == :optimism @@ -1346,15 +1362,6 @@ config :indexer, Indexer.Fetcher.Scroll, config :indexer, Indexer.Fetcher.Scroll.L1FeeParam, gas_oracle: System.get_env("INDEXER_SCROLL_L2_GAS_ORACLE_CONTRACT") -config :explorer, Explorer.Chain.Scroll.L1FeeParam, - curie_upgrade_block: ConfigHelper.parse_integer_env_var("SCROLL_L2_CURIE_UPGRADE_BLOCK", 0), - scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_SCALAR_INIT", 0), - overhead_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_OVERHEAD_INIT", 0), - commit_scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_COMMIT_SCALAR_INIT", 0), - blob_scalar_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BLOB_SCALAR_INIT", 0), - l1_base_fee_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BASE_FEE_INIT", 0), - l1_blob_base_fee_init: ConfigHelper.parse_integer_env_var("SCROLL_L1_BLOB_BASE_FEE_INIT", 0) - config :indexer, Indexer.Fetcher.Scroll.L1FeeParam.Supervisor, disabled?: ConfigHelper.chain_type() != :scroll config :indexer, Indexer.Fetcher.Scroll.BridgeL1, diff --git a/cspell.json b/cspell.json index 12c474e39da4..a998afe515d7 100644 --- a/cspell.json +++ b/cspell.json @@ -537,6 +537,7 @@ "returnaddress", "retval", "reuseaddr", + "rdparty", "rollup", "rollups", "RPC's", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 00710f936db9..5704ffe559ee 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -576,4 +576,5 @@ RELEASE_COOKIE=secret_cookie # NETWORK_PATH=/ # RE_CAPTCHA_CLIENT_KEY= # RE_CAPTCHA_V3_CLIENT_KEY= -# HACKNEY_DEFAULT_POOL_SIZE=1000 \ No newline at end of file +# HACKNEY_DEFAULT_POOL_SIZE=1000 +# UNIVERSAL_PROXY_CONFIG_URL= \ No newline at end of file From 109ee612c92f8b06d5040955dd591f790fb94789 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 20 May 2025 20:47:27 +0300 Subject: [PATCH 0054/1006] feat: Add lower bound for base fee (#12370) * feat: add lower bound for base fee * Add CHANGELOG entry * Process review comment --------- Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain/block.ex | 34 +++++++++++++++---- .../chain/cache/gas_price_oracle_test.exs | 20 +++++++++++ config/runtime.exs | 1 + docker-compose/envs/common-blockscout.env | 2 ++ 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4dd623dec8..b8c8d24f8d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Add lower bound for base fee ([#12370](https://github.com/blockscout/blockscout/pull/12370)) - Multichain Search DB export retry queue ([#12377](https://github.com/blockscout/blockscout/issues/12377)) - Add TAC operation search ([#12367](https://github.com/blockscout/blockscout/issues/12367)) - Add `internal_transactions_count` prop in api/v2/blocks/:block endpoint ([#12405](https://github.com/blockscout/blockscout/issues/12405)) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 2a76be09c641..2817210b42eb 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -546,18 +546,38 @@ defmodule Explorer.Chain.Block do def next_block_base_fee_per_gas(block) do {base_fee_max_change_denominator, elasticity_multiplier} = get_eip1559_config(block.number) - gas_target = Decimal.div(block.gas_limit, elasticity_multiplier) + gas_target = Decimal.div_int(block.gas_limit, elasticity_multiplier) - gas_used_delta = Decimal.sub(block.gas_used, gas_target) + lower_bound = Application.get_env(:explorer, :base_fee_lower_bound) base_fee_per_gas_decimal = block.base_fee_per_gas |> Wei.to(:wei) base_fee_per_gas_decimal && - base_fee_per_gas_decimal - |> Decimal.mult(gas_used_delta) - |> Decimal.div(gas_target) - |> Decimal.div(base_fee_max_change_denominator) - |> Decimal.add(base_fee_per_gas_decimal) + block.gas_used + |> Decimal.gt?(gas_target) + |> if do + gas_used_delta = Decimal.sub(block.gas_used, gas_target) + + base_fee_per_gas_decimal + |> get_base_fee_per_gas_delta(gas_used_delta, gas_target, base_fee_max_change_denominator) + |> Decimal.max(Decimal.new(1)) + |> Decimal.add(base_fee_per_gas_decimal) + else + gas_used_delta = Decimal.sub(gas_target, block.gas_used) + + base_fee_per_gas_decimal + |> get_base_fee_per_gas_delta(gas_used_delta, gas_target, base_fee_max_change_denominator) + |> Decimal.negate() + |> Decimal.add(base_fee_per_gas_decimal) + end + |> Decimal.max(lower_bound) + end + + defp get_base_fee_per_gas_delta(base_fee_per_gas_decimal, gas_used_delta, gas_target, base_fee_max_change_denominator) do + base_fee_per_gas_decimal + |> Decimal.mult(gas_used_delta) + |> Decimal.div_int(gas_target) + |> Decimal.div_int(base_fee_max_change_denominator) end @spec set_refetch_needed(integer | [integer]) :: :ok diff --git a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs index 59c14b1847ff..a42910336666 100644 --- a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs +++ b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs @@ -640,5 +640,25 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do assert {{:ok, %{average: nil, fast: nil, slow: nil}}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end + + test "does take into account EIP_1559_BASE_FEE_LOWER_BOUND_WEI env" do + old_config = Application.get_env(:explorer, :base_fee_lower_bound) + + Application.put_env(:explorer, :base_fee_lower_bound, 1_000_000_000) + + on_exit(fn -> + Application.put_env(:explorer, :base_fee_lower_bound, old_config) + end) + + insert(:block, + number: 1, + base_fee_per_gas: Wei.from(Decimal.new(1), :gwei), + gas_used: Decimal.new(0), + gas_limit: Decimal.new(1000) + ) + + assert {{:ok, %{average: %{price: 1.0}, fast: %{base_fee: 1.0}, slow: %{base_fee: 1.0}}}, _} = + GasPriceOracle.get_average_gas_price(1, 35, 60, 90) + end end end diff --git a/config/runtime.exs b/config/runtime.exs index c9eae240aea7..31f180caa8e9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -267,6 +267,7 @@ config :explorer, addresses_blacklist_key: System.get_env("ADDRESSES_BLACKLIST_KEY"), elasticity_multiplier: ConfigHelper.parse_integer_env_var("EIP_1559_ELASTICITY_MULTIPLIER", 2), base_fee_max_change_denominator: ConfigHelper.parse_integer_env_var("EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR", 8), + base_fee_lower_bound: ConfigHelper.parse_integer_env_var("EIP_1559_BASE_FEE_LOWER_BOUND_WEI", 0), csv_export_limit: ConfigHelper.parse_integer_env_var("CSV_EXPORT_LIMIT", 10_000), shrink_internal_transactions_enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), replica_max_lag: ConfigHelper.parse_time_env_var("REPLICA_MAX_LAG", "5m"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 5704ffe559ee..ed2e55326d51 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -438,6 +438,8 @@ ACCOUNT_CLOAK_KEY= ACCOUNT_ENABLED=false ACCOUNT_REDIS_URL=redis://redis-db:6379 # EIP_1559_ELASTICITY_MULTIPLIER=2 +# EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR=8 +# EIP_1559_BASE_FEE_LOWER_BOUND_WEI=0 # IPFS_GATEWAY_URL= # IPFS_GATEWAY_URL_PARAM_KEY= # IPFS_GATEWAY_URL_PARAM_VALUE= From e2b9752d22d7ef4d0a324e7ef2e75bc84d2fa01f Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 20 May 2025 21:03:49 +0300 Subject: [PATCH 0055/1006] fix: Handle mismatched `0x` prefixed bytes (#12453) * fix: handle mismatched `0x` prefixed bytes * Add CHANGELOG entry --------- Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + .../api/v2/transaction_controller_test.exs | 36 +++++++++++++++++++ .../lib/explorer/chain/decoding_helper.ex | 5 ++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8c8d24f8d56..e7d946a86949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ ### 🐛 Bug Fixes +- Handle mismatched 0x prefixed bytes ([#12453](https://github.com/blockscout/blockscout/pull/12453)) - Fix logs decoding issue for proxies ([#12414](https://github.com/blockscout/blockscout/issues/12414)) - Refactor TokenInstanceMetadataRefetch on demand fetcher ([#12419](https://github.com/blockscout/blockscout/issues/12419)) - Fix for type output in ETH RPC API transaction by hash endpoint diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index f3179efd3a15..74c919407d72 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -282,6 +282,42 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert is_map(Enum.at(response["token_transfers"], 0)["total"]) assert compare_item(%TokenTransfer{tt | amount: 2}, Enum.at(response["token_transfers"], 0)) end + + test "return transaction with input starting with 0x", %{conn: conn} do + contract = + insert(:smart_contract, + contract_code_md5: "123", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "", "type" => "bytes"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ] + ) + |> Repo.preload(:address) + + input_data = + "set(bytes)" + |> ABI.encode([ + <<48, 120, 253, 69, 39, 88, 49, 136, 89, 142, 21, 123, 116, 129, 248, 32, 77, 29, 224, 121, 49, 137, 216, 8, + 212, 195, 239, 11, 174, 75, 56, 126>> + ]) + |> Base.encode16(case: :lower) + + transaction = + :transaction + |> insert(to_address: contract.address, input: "0x" <> input_data) + |> Repo.preload(to_address: :smart_contract) + + request = get(conn, "/api/v2/transactions/" <> to_string(transaction.hash)) + + assert response = json_response(request, 200) + end end describe "/transactions/{transaction_hash}/internal-transactions" do diff --git a/apps/explorer/lib/explorer/chain/decoding_helper.ex b/apps/explorer/lib/explorer/chain/decoding_helper.ex index e35a8a68c40b..c7b9d16ab0b2 100644 --- a/apps/explorer/lib/explorer/chain/decoding_helper.ex +++ b/apps/explorer/lib/explorer/chain/decoding_helper.ex @@ -3,7 +3,6 @@ defmodule Explorer.Chain.DecodingHelper do Data decoding functions """ alias ABI.FunctionSelector - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Address, Hash} require Logger @@ -55,7 +54,7 @@ defmodule Explorer.Chain.DecodingHelper do end defp base_value_json(_, {:dynamic, value}) do - ExplorerHelper.add_0x_prefix(value) + "0x" <> Base.encode16(value, case: :lower) end defp base_value_json(:address, value) do @@ -66,7 +65,7 @@ defmodule Explorer.Chain.DecodingHelper do end defp base_value_json(:bytes, value) do - ExplorerHelper.add_0x_prefix(value) + "0x" <> Base.encode16(value, case: :lower) end defp base_value_json(_, value), do: to_string(value) From eb15f645a3dc7a078c28945c368b2abe05ad7187 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 22 May 2025 08:40:10 +0300 Subject: [PATCH 0056/1006] Bump Erlang version to 27.3.4 --- .tool-versions | 2 +- docker/Dockerfile | 4 ++-- docker/oldUI.Dockerfile | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.tool-versions b/.tool-versions index 639a1a07cd2e..cf38ec3ee8cf 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.17.3-otp-27 -erlang 27.1 +erlang 27.3.4 nodejs 20.17.0 diff --git a/docker/Dockerfile b/docker/Dockerfile index fea705f5f579..b8c539128091 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 AS builder-deps +FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 AS builder-deps WORKDIR /app @@ -49,7 +49,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 +FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 WORKDIR /app diff --git a/docker/oldUI.Dockerfile b/docker/oldUI.Dockerfile index 3033af0092c4..94c3418ef869 100644 --- a/docker/oldUI.Dockerfile +++ b/docker/oldUI.Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 AS builder-deps +FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 AS builder-deps WORKDIR /app @@ -64,7 +64,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.17.3-erlang-27.1-alpine-3.20.3 +FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 WORKDIR /app From ed05278ac95fc4b37f3599e24033acbae0ad1604 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 26 May 2025 16:35:04 +0300 Subject: [PATCH 0057/1006] chore: Add updated-gas-oracle to Access-Control-Allow-Headers (#12473) * chore: Add updated-gas-oracle to Access-Control-Allow-Headers * Add comment * Changelog --- CHANGELOG.md | 1 + apps/block_scout_web/lib/block_scout_web/endpoint.ex | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7d946a86949..f172dac46b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ ### ⚙️ Miscellaneous Tasks +- Add updated-gas-oracle to Access-Control-Allow-Headers ([#12473](https://github.com/blockscout/blockscout/pull/12473)) - Add additional test for Universal proxy, duplicate all proxy endpoints at /3rdparty ([#12442](https://github.com/blockscout/blockscout/pull/12442)) - Improve logic behind emerging of custom fields in the response of `eth_getTransactionByHash` ETH RPC API endpoint ([#12416](https://github.com/blockscout/blockscout/issues/12416)) - Internal transactions unique index ([#12394](https://github.com/blockscout/blockscout/issues/12394)) diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index 2bf0b6c7afae..d02ba286f1ee 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -77,7 +77,8 @@ defmodule BlockScoutWeb.Endpoint do plug(BlockScoutWeb.Prometheus.PublicExporter) # 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint - plug(CORSPlug, headers: ["x-apollo-tracing" | CORSPlug.defaults()[:headers]]) + # 'updated-gas-oracle' header for /api/v2/stats endpoint, added to support cross-origin requests (e.g. multichain search explorer) + plug(CORSPlug, headers: ["x-apollo-tracing", "updated-gas-oracle"] ++ CORSPlug.defaults()[:headers]) plug(BlockScoutWeb.Router) end From 4c397ddd29ecbcaee9056a94a5b8d3b8765aa39d Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 30 May 2025 14:21:06 +0300 Subject: [PATCH 0058/1006] =?UTF-8?q?fix:=20Add=20missing=20preload=20for?= =?UTF-8?q?=20logs=20in=20/api/v2/transactions/:transactio=E2=80=A6=20(#12?= =?UTF-8?q?491)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Add missing preload for logs in /api/v2/transactions/:transaction_hash_param/summary * Add preload to user ops logs * alias -> import --- .../transaction_interpretation.ex | 8 +- .../api/v2/address_controller_test.exs | 15 +- .../api/v2/transaction_controller_test.exs | 462 +++++++++++++++++- apps/explorer/lib/test_helper.ex | 5 + 4 files changed, 466 insertions(+), 24 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 21c387fc63bb..069bb4ca6ba3 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -10,7 +10,9 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do alias Explorer.Chain.{Data, InternalTransaction, Log, TokenTransfer, Transaction} alias HTTPoison.Response - import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, + only: [proxy_implementations_association: 0, proxy_implementations_smart_contracts_association: 0] + import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] import Explorer.Utility.Microservice, only: [base_url: 2, check_enabled: 2] require Logger @@ -233,7 +235,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do full_options = [ necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional } ] |> Keyword.merge(@api_true) @@ -255,7 +257,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do log_options = [ necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional }, limit: @items_limit ] diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 0a0310f5e997..25b2a9220332 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -49,11 +49,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do :ok end - defp topic(topic_hex_string) do - {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) - topic - end - describe "/addresses/{address_hash}" do test "get 200 on non existing address", %{conn: conn} do address = build(:address) @@ -2371,7 +2366,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block: transaction.block, block_number: transaction.block_number, address: address, - first_topic: topic(@first_topic_hex_string_1) + first_topic: TestHelper.topic(@first_topic_hex_string_1) ) request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=#{@first_topic_hex_string_1}") @@ -2417,8 +2412,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do log = insert(:log, transaction: transaction, - first_topic: topic(topic1), - second_topic: topic(topic2), + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), third_topic: nil, fourth_topic: nil, data: log_data, @@ -2497,8 +2492,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do log = insert(:log, transaction: transaction, - first_topic: topic(topic1), - second_topic: topic(topic2), + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), third_topic: nil, fourth_topic: nil, data: log_data, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 74c919407d72..66f506f7d1d2 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -4,13 +4,14 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox - require Logger alias Explorer.Account.{Identity, WatchlistAddress} alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} - alias Explorer.Repo - import Ecto.Query, only: [from: 2] + alias Explorer.{Repo, TestHelper} + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + require Logger + setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) @@ -316,7 +317,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = get(conn, "/api/v2/transactions/" <> to_string(transaction.hash)) - assert response = json_response(request, 200) + assert json_response(request, 200) end end @@ -1412,11 +1413,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do if Application.compile_env(:explorer, :chain_type) == :stability do @first_topic_hex_string_1 "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" - defp topic(topic_hex_string) do - {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) - topic - end - describe "stability fees" do test "check stability fees", %{conn: conn} do transaction = insert(:transaction) |> with_block() @@ -1427,7 +1423,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: 1, block: transaction.block, block_number: transaction.block_number, - first_topic: topic(@first_topic_hex_string_1), + first_topic: TestHelper.topic(@first_topic_hex_string_1), data: "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" ) @@ -1490,7 +1486,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: 1, block: transaction.block, block_number: transaction.block_number, - first_topic: topic(@first_topic_hex_string_1), + first_topic: TestHelper.topic(@first_topic_hex_string_1), data: "0x000000000000000000000000dc2b93f3291030f3f7a6d9363ac37757f7ad5c4300000000000000000000000000000000000000000000000000002824369a100000000000000000000000000046b555cb3962bf9533c437cbd04a2f702dfdb999000000000000000000000000000000000000000000000000000014121b4d0800000000000000000000000000faf7a981360c2fab3a5ab7b3d6d8d0cf97a91eb9000000000000000000000000000000000000000000000000000014121b4d0800" ) @@ -1649,6 +1645,450 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do defp check_total(_, _, _), do: true + describe "/transactions/{transaction_hash}/summary?just_request_body=true" do + setup do + original_config = + Application.get_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation) + + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: true, + service_url: "http://localhost:4000" + ) + + on_exit(fn -> + Application.put_env( + :block_scout_web, + BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + original_config + ) + end) + end + + test "return 404 on non existing transaction", %{conn: conn} do + transaction = build(:transaction) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "return 422 on invalid transaction hash", %{conn: conn} do + request = get(conn, "/api/v2/transactions/0x/summary?just_request_body=true") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return 403 when transaction interpretation service is disabled", %{conn: conn} do + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: false + ) + + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert %{"message" => "Transaction Interpretation Service is disabled"} = json_response(request, 403) + end + + test "return request body for existing transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + + # Verify the structure of the request body + assert Map.has_key?(response, "data") + assert Map.has_key?(response, "logs_data") + assert Map.has_key?(response, "chain_id") + + # Verify data structure + data = response["data"] + assert Map.has_key?(data, "to") + assert Map.has_key?(data, "from") + assert Map.has_key?(data, "hash") + assert Map.has_key?(data, "type") + assert Map.has_key?(data, "value") + assert Map.has_key?(data, "method") + assert Map.has_key?(data, "status") + assert Map.has_key?(data, "transaction_types") + assert Map.has_key?(data, "raw_input") + assert Map.has_key?(data, "decoded_input") + assert Map.has_key?(data, "token_transfers") + assert Map.has_key?(data, "internal_transactions") + + # Verify logs_data structure + logs_data = response["logs_data"] + assert Map.has_key?(logs_data, "items") + assert is_list(logs_data["items"]) + + # Verify chain_id is present and is an integer + assert is_integer(response["chain_id"]) + + # Verify transaction data matches + assert to_string(transaction.hash) == data["hash"] + assert transaction.type == data["type"] + assert to_string(transaction.value.value) == data["value"] + assert to_string(transaction.status) == data["status"] + end + + test "return request body with token transfers", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert is_list(response["data"]["token_transfers"]) + assert Enum.count(response["data"]["token_transfers"]) >= 1 + end + + test "return request body with internal transactions", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction_hash: transaction.hash, + index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: 1, + type: :reward + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert is_list(response["data"]["internal_transactions"]) + assert Enum.count(response["data"]["internal_transactions"]) >= 1 + end + + test "return request body with logs", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + insert(:log, + transaction: transaction, + index: 1, + block: transaction.block, + block_number: transaction.block_number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert is_list(response["logs_data"]["items"]) + assert Enum.count(response["logs_data"]["items"]) >= 1 + end + + test "return request body with smart contract transaction", %{conn: conn} do + contract = + insert(:smart_contract, + contract_code_md5: "123", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "", "type" => "bytes"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ] + ) + |> Repo.preload(:address) + + input_data = + "set(bytes)" + |> ABI.encode([ + <<48, 120, 253, 69, 39, 88, 49, 136, 89, 142, 21, 123, 116, 129, 248, 32, 77, 29, 224, 121, 49, 137, 216, 8, + 212, 195, 239, 11, 174, 75, 56, 126>> + ]) + |> Base.encode16(case: :lower) + + transaction = + :transaction + |> insert(to_address: contract.address, input: "0x" <> input_data) + |> with_block() + |> Repo.preload(to_address: :smart_contract) + + TestHelper.get_all_proxies_implementation_zero_addresses() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + + # Verify that the transaction has input data + assert response["data"]["raw_input"] == "0x" <> input_data + + assert response["data"]["decoded_input"] == %{ + "method_call" => "set(bytes arg0)", + "method_id" => "0399321e", + "parameters" => [ + %{ + "name" => "arg0", + "type" => "bytes", + "value" => "0x3078fd4527583188598e157b7481f8204d1de0793189d808d4c3ef0bae4b387e" + } + ] + } + end + + test "return request body with proper address information", %{conn: conn} do + from_address = insert(:address) + to_address = insert(:address) + + transaction = + :transaction + |> insert(from_address: from_address, to_address: to_address) + |> with_block() + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + + # Verify from address + from_data = response["data"]["from"] + assert Map.has_key?(from_data, "hash") + assert from_data["hash"] == Address.checksum(from_address.hash) + + # Verify to address + to_data = response["data"]["to"] + assert Map.has_key?(to_data, "hash") + assert to_data["hash"] == Address.checksum(to_address.hash) + end + + test "limits token transfers to 50 items", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + # Insert more than 50 token transfers + insert_list(60, :token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert Enum.count(response["data"]["token_transfers"]) <= 50 + end + + test "limits internal transactions to 50 items", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + # Insert more than 50 internal transactions + for index <- 1..60 do + insert(:internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert Enum.count(response["data"]["internal_transactions"]) <= 50 + end + + test "limits logs to 50 items", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + # Insert more than 50 logs + for index <- 1..60 do + insert(:log, + transaction: transaction, + index: index, + block: transaction.block, + block_number: transaction.block_number + ) + end + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert Enum.count(response["logs_data"]["items"]) <= 50 + end + + test "log could be decoded via verified implementation", %{conn: conn} do + address = insert(:contract_address) + + contract_address = insert(:contract_address) + + smart_contract = + insert(:smart_contract, + address_hash: contract_address.hash, + abi: [ + %{ + "name" => "OptionSettled", + "type" => "event", + "inputs" => [ + %{"name" => "accountId", "type" => "uint256", "indexed" => true, "internalType" => "uint256"}, + %{"name" => "option", "type" => "address", "indexed" => false, "internalType" => "address"}, + %{"name" => "subId", "type" => "uint256", "indexed" => false, "internalType" => "uint256"}, + %{"name" => "amount", "type" => "int256", "indexed" => false, "internalType" => "int256"}, + %{"name" => "value", "type" => "int256", "indexed" => false, "internalType" => "int256"} + ], + "anonymous" => false + } + ] + ) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + + log_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + transaction = :transaction |> insert() |> with_block() + + log = + insert(:log, + transaction: transaction, + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:proxy_implementation, + proxy_address_hash: address.hash, + proxy_type: "eip1167", + address_hashes: [smart_contract.address_hash], + names: ["Test"] + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert is_list(response["logs_data"]["items"]) + assert Enum.count(response["logs_data"]["items"]) == 1 + + log_from_api = Enum.at(response["logs_data"]["items"], 0) + assert not is_nil(log_from_api["decoded"]) + + assert log_from_api["decoded"] == %{ + "method_call" => + "OptionSettled(uint256 indexed accountId, address option, uint256 subId, int256 amount, int256 value)", + "method_id" => "d20a68b2", + "parameters" => [ + %{ + "indexed" => true, + "name" => "accountId", + "type" => "uint256", + "value" => "23833" + }, + %{ + "indexed" => false, + "name" => "option", + "type" => "address", + "value" => "0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7" + }, + %{ + "indexed" => false, + "name" => "subId", + "type" => "uint256", + "value" => "20615843020801704441600" + }, + %{ + "indexed" => false, + "name" => "amount", + "type" => "int256", + "value" => "-120000000000000000" + }, + %{ + "indexed" => false, + "name" => "value", + "type" => "int256", + "value" => "-522838470013113778446" + } + ] + } + end + + test "test corner case, when preload functions face absent smart contract", %{conn: conn} do + address = insert(:contract_address) + + contract_address = insert(:contract_address) + + topic1_bytes = ExKeccak.hash_256("OptionSettled(uint256,address,uint256,int256,int256)") + topic1 = "0x" <> Base.encode16(topic1_bytes, case: :lower) + topic2 = "0x0000000000000000000000000000000000000000000000000000000000005d19" + + log_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + transaction = :transaction |> insert() |> with_block() + + log = + insert(:log, + transaction: transaction, + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:proxy_implementation, + proxy_address_hash: address.hash, + proxy_type: "eip1167", + address_hashes: [contract_address.hash], + names: ["Test"] + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") + + assert response = json_response(request, 200) + assert is_list(response["logs_data"]["items"]) + assert Enum.count(response["logs_data"]["items"]) == 1 + + log_from_api = Enum.at(response["logs_data"]["items"], 0) + # In this case, the log should not be decoded since the smart contract is absent + assert is_nil(log_from_api["decoded"]) + end + end + if @chain_type == :neon do describe "neon linked transactions service" do test "fetches data from the node and caches in the db", %{conn: conn} do diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex index 3aee82c8f697..8f7406485190 100644 --- a/apps/explorer/lib/test_helper.ex +++ b/apps/explorer/lib/test_helper.ex @@ -297,4 +297,9 @@ defmodule Explorer.TestHelper do {:ok, "0x1"} end) end + + def topic(topic_hex_string) do + {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) + topic + end end From 6189e9a830b04df64ba90873d57c520b1d9a74ee Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 30 May 2025 14:56:45 +0300 Subject: [PATCH 0059/1006] v8.1.1 --- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-berachain.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-berachain.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .github/workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .github/workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-suave.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-berachain.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 6 ++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 ++-- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 65 files changed, 71 insertions(+), 65 deletions(-) diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index c778a5c7b473..68fbaaf3b4ca 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-berachain.yml b/.github/workflows/pre-release-berachain.yml index a787e4b9c511..a35dad7120ca 100644 --- a/.github/workflows/pre-release-berachain.yml +++ b/.github/workflows/pre-release-berachain.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index e34c32f7aadf..d63c64081cf4 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index dd403cc4c9d6..d773345831b8 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 8e1324321850..5f2e04dfbf1d 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 0622ffc073af..7b1ff0e355e2 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 2102db7b1669..0b4de86a4c4e 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 2dfb5e168faa..6c6f4a550b9e 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 52746664e8cc..8248639410c2 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index c1d4ac5e4806..89d944047e3a 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 5bc1b586b262..4287007611d6 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index d7de11ac8c9b..c8565c2276a8 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index b8f22f9a822f..9b72a2493f23 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 1e33050cf153..14c35fb81f0d 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 520ff6476602..19b44a3138ed 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index e259e7566f74..677d1985daa4 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-berachain.yml b/.github/workflows/publish-docker-image-for-berachain.yml index 445c3499afa2..e8672c3b41c8 100644 --- a/.github/workflows/publish-docker-image-for-berachain.yml +++ b/.github/workflows/publish-docker-image-for-berachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: berachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index f7f839687a47..70873b756193 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 0bcd7e8ef74a..61175ca3cf33 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 7eba777de967..01ec6f9f073a 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index e356a10d8fab..4470c3d6e1c4 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 09a03273f0c9..43a36d3a498f 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index ff2e9f7da259..0be53a189d87 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 3a6220533e95..dbda667a22bc 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index c80054f8dae7..f59660eb9a89 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 9a681d9ef880..9897e3474815 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 8b8dbb0a0fd3..400d21141914 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index f2e7fa7dcd85..a610990a42cd 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 2dec504a5182..e74c8dcc5ce8 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 76725b017f02..a62bdf1b3bdf 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index b6ffa39ea738..cda7292ad77b 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 3d38b9bcedee..5354aa4870ff 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index c235861d58e2..7bb9140123d9 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index cd85a5be90f3..e663a18bdadc 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index d6c2f6dda203..6e37e9206303 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 jobs: push_to_registry: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 95d47178700f..4fb18189ba25 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 jobs: push_to_registry: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index c2703f5814d3..110eab734496 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-berachain.yml b/.github/workflows/release-berachain.yml index d31fbf61db05..689b9d5eea80 100644 --- a/.github/workflows/release-berachain.yml +++ b/.github/workflows/release-berachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index f736e61907d3..30de3e0ebe4b 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 8162edd3ed53..505f1bba4d51 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 13732c7de4d4..1ebafe653406 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index a22177bcc60a..1a13ecc51cb5 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 17176b9b5aac..a3add02d8b01 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index be244184cc41..2c5797b38e81 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index b9baa1147aa7..e0853990c675 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 5df90dc493ef..f61da399d229 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index fdda264d411c..4c91a8b7ded1 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index dc9428718310..0e7f99bf16ac 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index a3642aec6b71..bfb962c0b8cc 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 5fd122bac329..815a1a48ba93 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 61c4ecdb4308..178599bcfb89 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30431f728559..f1a48f8afe96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index f172dac46b26..1c400a173f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 8.1.1 + +### 🐛 Bug Fixes + +- Add missing preload for logs in /api/v2/transactions/:transactio… ([#12491](https://github.com/blockscout/blockscout/issues/12491)) + ## 8.1.0 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 7276929c2aaa..9514e1ae25c9 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.0", + version: "8.1.1", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 4d730a6f9828..d40e1bd62320 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.0" + version: "8.1.1" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 007177a6dee4..18c619da3019 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -14,7 +14,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-8.1.0"}] + @default_headers [{"User-Agent", "blockscout-8.1.1"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 6585746a4f5d..6b68866b1976 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.0", + version: "8.1.1", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index d7eb7ae23856..7fd34a1f73b5 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "8.1.0", + version: "8.1.1", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 55f4b4f5ad41..bb9419abd8b8 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "8.1.0", + version: "8.1.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 8a54c524b08e..57abfd1884f9 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "8.1.0", + version: "8.1.1", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 6e75df84bb5c..95bf984eeee1 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index a2bdf95cbac8..9795021cfb5c 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 8.1.1 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index 8e1d30af9bd5..b4e3bbfaad7b 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '8.1.0' +RELEASE_VERSION ?= '8.1.1' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 7c3ab9dbf1d2..35626ec936bc 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "8.1.0", + version: "8.1.1", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 1aa75db3e528..13b9d0281300 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "8.1.0" + set version: "8.1.1" set applications: [ :runtime_tools, block_scout_web: :permanent, From 6c25fe6e9bf105223f1ae3c2fffff5fdb61e81f1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 30 May 2025 15:02:10 +0300 Subject: [PATCH 0060/1006] Pre-release workflow for Neon --- .github/workflows/pre-release-neon.yml | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/pre-release-neon.yml diff --git a/.github/workflows/pre-release-neon.yml b/.github/workflows/pre-release-neon.yml new file mode 100644 index 000000000000..0be38a2c551b --- /dev/null +++ b/.github/workflows/pre-release-neon.yml @@ -0,0 +1,62 @@ +name: Pre-release for Neon + +on: + workflow_dispatch: + inputs: + number: + type: number + required: true + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 8.1.1 + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Neon (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-neon:latest, ghcr.io/blockscout/blockscout-neon:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=neon + + - name: Build and push Docker image for Neon (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-neon:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=neon From df42e331a1ced7654ed2b0f284778a41423191a3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 30 May 2025 15:04:42 +0300 Subject: [PATCH 0061/1006] Delete pre-release workflow for Neon (it is supported in runtime) --- .github/workflows/pre-release-neon.yml | 62 -------------------------- 1 file changed, 62 deletions(-) delete mode 100644 .github/workflows/pre-release-neon.yml diff --git a/.github/workflows/pre-release-neon.yml b/.github/workflows/pre-release-neon.yml deleted file mode 100644 index 0be38a2c551b..000000000000 --- a/.github/workflows/pre-release-neon.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Pre-release for Neon - -on: - workflow_dispatch: - inputs: - number: - type: number - required: true - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 8.1.1 - steps: - - uses: actions/checkout@v4 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Neon (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-neon:latest, ghcr.io/blockscout/blockscout-neon:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=neon - - - name: Build and push Docker image for Neon (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-neon:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=neon From e2311bf48ef6c5d31092c9e34a728d94429934f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:54:11 +0300 Subject: [PATCH 0062/1006] chore(deps): bump hackney from 1.23.0 to 1.24.1 (#12479) Bumps [hackney](https://github.com/benoitc/hackney) from 1.23.0 to 1.24.1. - [Release notes](https://github.com/benoitc/hackney/releases) - [Changelog](https://github.com/benoitc/hackney/blob/master/NEWS.md) - [Commits](https://github.com/benoitc/hackney/compare/1.23.0...1.24.1) --- updated-dependencies: - dependency-name: hackney dependency-version: 1.24.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index 0438251e2c3f..5f253984f747 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, - "certifi": {:hex, :certifi, "2.14.0", "ed3bef654e69cde5e6c022df8070a579a79e8ba2368a00acf3d75b82d9aceeed", [:rebar3], [], "hexpm", "ea59d87ef89da429b8e905264fdec3419f84f2215bb3d81e07a18aac919026c3"}, + "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, @@ -77,7 +77,7 @@ "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, - "hackney": {:hex, :hackney, "1.23.0", "55cc09077112bcb4a69e54be46ed9bc55537763a96cd4a80a221663a7eafd767", [:rebar3], [{:certifi, "~> 2.14.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "6cd1c04cd15c81e5a493f167b226a15f0938a84fc8f0736ebe4ddcab65c0b44e"}, + "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"}, "hammer": {:hex, :hammer, "6.2.1", "5ae9c33e3dceaeb42de0db46bf505bd9c35f259c8defb03390cd7556fea67ee2", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b9476d0c13883d2dc0cc72e786bac6ac28911fba7cc2e04b70ce6a6d9c4b2bdc"}, "hammer_backend_redis": {:hex, :hammer_backend_redis, "6.2.0", "f39a9c8491387cdf719a38593311537e3e0251ca54725b6ee9145406821f39d2", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "9965d55705d7ca7412bb0685f5cd44fc47d103bf388abc50438e71974c36c9fa"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, @@ -103,7 +103,7 @@ "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, - "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, + "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, From 60c2b92c1dbe2bab15452817d8a13d2da4c70751 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:54:43 +0300 Subject: [PATCH 0063/1006] chore(deps): bump evision from 0.2.12 to 0.2.13 (#12446) Bumps [evision](https://github.com/cocoa-xu/evision) from 0.2.12 to 0.2.13. - [Release notes](https://github.com/cocoa-xu/evision/releases) - [Changelog](https://github.com/cocoa-xu/evision/blob/main/CHANGELOG.md) - [Commits](https://github.com/cocoa-xu/evision/compare/v0.2.12...v0.2.13) --- updated-dependencies: - dependency-name: evision dependency-version: 0.2.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 5f253984f747..04ebc10cfdc4 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.0", "f9a9123feac30df2b75260c9a74903c0bd7ec54f345f5f6f60c7830b81ead43a", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "77f9417a5549e424bdbfeea3f8060841384c39c0b5f563a8f62f1f2c2b6d2345"}, - "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, + "castore": {:hex, :castore, "1.0.13", "b735ea45d87f027128debaf662c6cdf618604f530bec554cc60c195f2a55b506", [:mix], [], "hexpm", "ec09e81a9c3db92d27c6651d119d8adc6d1cbbb3d90f8c1293eee2af590bf55d"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, @@ -47,7 +47,7 @@ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "evision": {:hex, :evision, "0.2.12", "bdb06e54e05a27a58cd42bd3c390e4e0ee288b3d463a60bb998f044650c63b38", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "f96bbbcb3fa09b9a9bec863345e4e717452000fef410e53ac35af850723b4253"}, + "evision": {:hex, :evision, "0.2.13", "15ce6e1ba70c2485ff6ea1fba67fa9d32abb7442c9189fa65f8cda20357e079e", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "49ff677cbbeaa1870841b5ac9dc4211e62d32d9585253c02a95dc98e30981e7a"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, "ex_aws": {:hex, :ex_aws, "2.5.9", "8e2455172f0e5cbe2f56dd68de514f0dae6bb26d6b6e2f435a06434cf9dbb412", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbdb6ffb0e6c6368de05ed8641fe1376298ba23354674428e5b153a541f23359"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"}, From 7f958a832fd549ce0bdd64c05410063f9e45dc8b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:55:09 +0300 Subject: [PATCH 0064/1006] chore(deps): bump bcrypt_elixir from 3.3.1 to 3.3.2 (#12444) Bumps [bcrypt_elixir](https://github.com/riverrun/bcrypt_elixir) from 3.3.1 to 3.3.2. - [Changelog](https://github.com/riverrun/bcrypt_elixir/blob/master/CHANGELOG.md) - [Commits](https://github.com/riverrun/bcrypt_elixir/compare/v3.3.1...v3.3.2) --- updated-dependencies: - dependency-name: bcrypt_elixir dependency-version: 3.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 04ebc10cfdc4..7c462bf8bb51 100644 --- a/mix.lock +++ b/mix.lock @@ -5,7 +5,7 @@ "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "bamboo": {:hex, :bamboo, "2.4.0", "d26bd9f22a18cfa4448dac9b305debfd533b73064aeaea10d251400e543b472b", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "85e15dfce007faa04ef3baa46fe0737aba955cba5843bad17514519f9cb86592"}, - "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.1", "9f2e7e00f661a6acfae1431f1bc590e28698aaecc962c2a7b33150dfe9289c3d", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9f539e9d3828fad4ffc8152dadc0d27c6d78cb2853a9a1d6518cfe8a5adb7f50"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, "blake2": {:hex, :blake2, "1.0.4", "8263c69a191142922bc2510f1ffc0de0ae96e8c3bd5e2ad3fac7e87aed94c8b1", [:mix], [], "hexpm", "e9f4120d163ba14d86304195e50745fa18483e6ad2be94c864ae449bbdd6a189"}, From fe6241725cdc2ddab3e1968ce91d019fab5239d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 1 Jun 2025 21:55:51 +0300 Subject: [PATCH 0065/1006] chore(deps): bump ex_json_schema from 0.11.0 to 0.11.1 (#12478) Bumps [ex_json_schema](https://github.com/jonasschmidt/ex_json_schema) from 0.11.0 to 0.11.1. - [Commits](https://github.com/jonasschmidt/ex_json_schema/compare/v0.11.0...v0.11.1) --- updated-dependencies: - dependency-name: ex_json_schema dependency-version: 0.11.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 7c462bf8bb51..51d935010f34 100644 --- a/mix.lock +++ b/mix.lock @@ -59,7 +59,7 @@ "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.11.0", "4909d7ae1f0546b6f3c8f73b71732115c86d2198cc7f4e09fda908e7ca86aa9e", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "23ea702a36d46906d3fd723a8aeac0b80e5d76c6165fdcb37d83b2df94062f28"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, From b2059a3e26235e0a6bc97a3b2cab76116e59df7c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 2 Jun 2025 09:28:22 +0300 Subject: [PATCH 0066/1006] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c400a173f92..49b6ae3d5b05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### 🐛 Bug Fixes -- Add missing preload for logs in /api/v2/transactions/:transactio… ([#12491](https://github.com/blockscout/blockscout/issues/12491)) +- Add missing preload for logs in /api/v2/transactions/:transaction_hash_param/summary ([#12491](https://github.com/blockscout/blockscout/issues/12491)) ## 8.1.0 From e7ba1dcaa7e3d805a018abe0642f04acbbc3ed54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:41:52 +0300 Subject: [PATCH 0067/1006] chore(deps): bump mixpanel-browser in /apps/block_scout_web/assets (#12507) Bumps [mixpanel-browser](https://github.com/mixpanel/mixpanel-js) from 2.63.0 to 2.65.0. - [Release notes](https://github.com/mixpanel/mixpanel-js/releases) - [Changelog](https://github.com/mixpanel/mixpanel-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/mixpanel/mixpanel-js/compare/v2.63.0...v2.65.0) --- updated-dependencies: - dependency-name: mixpanel-browser dependency-version: 2.65.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 6aa75c9d7dc8..2c47be5adf6d 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -46,7 +46,7 @@ "lodash.reduce": "^4.6.0", "luxon": "^3.6.1", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.63.0", + "mixpanel-browser": "^2.65.0", "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", @@ -12033,9 +12033,9 @@ "license": "MIT" }, "node_modules/mixpanel-browser": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.63.0.tgz", - "integrity": "sha512-h7M0J/LR/5YLWCVuvPaYuzwV7CgV9jkJz0m94uaTDPebWkhNQPEir63rf/ZpBZgntyvYjO1yMZp2pIpwQ1sBMQ==", + "version": "2.65.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.65.0.tgz", + "integrity": "sha512-BtrVYqilloAqx3TIhoIpNikHznTocEy/z3QIf6WEiz4PFxrgI6LgSMFIVKqLqGZJ8svrPlHbpp/CJp5wQYUZWw==", "license": "Apache-2.0", "dependencies": { "rrweb": "2.0.0-alpha.18" @@ -25189,9 +25189,9 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, "mixpanel-browser": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.63.0.tgz", - "integrity": "sha512-h7M0J/LR/5YLWCVuvPaYuzwV7CgV9jkJz0m94uaTDPebWkhNQPEir63rf/ZpBZgntyvYjO1yMZp2pIpwQ1sBMQ==", + "version": "2.65.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.65.0.tgz", + "integrity": "sha512-BtrVYqilloAqx3TIhoIpNikHznTocEy/z3QIf6WEiz4PFxrgI6LgSMFIVKqLqGZJ8svrPlHbpp/CJp5wQYUZWw==", "requires": { "rrweb": "2.0.0-alpha.18" } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 35f4c18e3973..4c2771546d07 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -58,7 +58,7 @@ "lodash.reduce": "^4.6.0", "luxon": "^3.6.1", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.63.0", + "mixpanel-browser": "^2.65.0", "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", From fc32aa5d13ba46e0037832185165d491058ce7c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 11:44:18 +0300 Subject: [PATCH 0068/1006] chore(deps): bump solc from 0.8.29 to 0.8.30 in /apps/explorer (#12502) Bumps [solc](https://github.com/ethereum/solc-js) from 0.8.29 to 0.8.30. - [Commits](https://github.com/ethereum/solc-js/compare/v0.8.29...v0.8.30) --- updated-dependencies: - dependency-name: solc dependency-version: 0.8.30 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/explorer/package-lock.json | 14 +++++++------- apps/explorer/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/explorer/package-lock.json b/apps/explorer/package-lock.json index c27dd57478f0..6fb65c57db57 100644 --- a/apps/explorer/package-lock.json +++ b/apps/explorer/package-lock.json @@ -7,7 +7,7 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "solc": "0.8.29" + "solc": "0.8.30" }, "engines": { "node": "18.x", @@ -76,9 +76,9 @@ } }, "node_modules/solc": { - "version": "0.8.29", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.29.tgz", - "integrity": "sha512-M1hmcsejAtT1RWC+wy45Oi8a82nSnDl65wDzA3/8uWTC7R7VcnrMVIHv6/LHqnQPZ7OM2RIeKh3zpNo4E5lYbw==", + "version": "0.8.30", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.30.tgz", + "integrity": "sha512-9Srk/gndtBmoUbg4CE6ypAzPQlElv8ntbnl6SigUBAzgXKn35v87sj04uZeoZWjtDkdzT0qKFcIo/wl63UMxdw==", "license": "MIT", "dependencies": { "command-exists": "^1.2.8", @@ -145,9 +145,9 @@ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" }, "solc": { - "version": "0.8.29", - "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.29.tgz", - "integrity": "sha512-M1hmcsejAtT1RWC+wy45Oi8a82nSnDl65wDzA3/8uWTC7R7VcnrMVIHv6/LHqnQPZ7OM2RIeKh3zpNo4E5lYbw==", + "version": "0.8.30", + "resolved": "https://registry.npmjs.org/solc/-/solc-0.8.30.tgz", + "integrity": "sha512-9Srk/gndtBmoUbg4CE6ypAzPQlElv8ntbnl6SigUBAzgXKn35v87sj04uZeoZWjtDkdzT0qKFcIo/wl63UMxdw==", "requires": { "command-exists": "^1.2.8", "commander": "^8.1.0", diff --git a/apps/explorer/package.json b/apps/explorer/package.json index 26858ef1b45b..fe64f0e1bb1b 100644 --- a/apps/explorer/package.json +++ b/apps/explorer/package.json @@ -13,6 +13,6 @@ }, "scripts": {}, "dependencies": { - "solc": "0.8.29" + "solc": "0.8.30" } } From 99cd79bd7673a76eb281d414c02fa74a524f838f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:38:07 +0300 Subject: [PATCH 0069/1006] chore(deps-dev): bump webpack in /apps/block_scout_web/assets (#12510) Bumps [webpack](https://github.com/webpack/webpack) from 5.98.0 to 5.99.9. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v5.98.0...v5.99.9) --- updated-dependencies: - dependency-name: webpack dependency-version: 5.99.9 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 32 ++++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 2c47be5adf6d..49946d3916fb 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -93,7 +93,7 @@ "sass": "^1.87.0", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", - "webpack": "^5.98.0", + "webpack": "^5.99.9", "webpack-cli": "^6.0.1" }, "engines": { @@ -14143,9 +14143,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15884,14 +15884,15 @@ } }, "node_modules/webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -15908,7 +15909,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", @@ -26656,9 +26657,9 @@ } }, "schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", @@ -27971,13 +27972,14 @@ "dev": true }, "webpack": { - "version": "5.98.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", - "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", + "version": "5.99.9", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.9.tgz", + "integrity": "sha512-brOPwM3JnmOa+7kd3NsmOUOwbDAj8FT9xDsG3IW0MgbN9yZV7Oi/s/+MNQ/EcSMqw7qfoRyXPoeEWT8zLVdVGg==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -27994,7 +27996,7 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^4.3.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 4c2771546d07..729c7d22bc61 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -105,7 +105,7 @@ "sass": "^1.87.0", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", - "webpack": "^5.98.0", + "webpack": "^5.99.9", "webpack-cli": "^6.0.1" }, "jest": { From 56798f3e1b957b99ef0fd01835eb82b5b28da80c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:38:31 +0300 Subject: [PATCH 0070/1006] chore(deps): bump sweetalert2 in /apps/block_scout_web/assets (#12509) Bumps [sweetalert2](https://github.com/sweetalert2/sweetalert2) from 11.19.1 to 11.22.0. - [Release notes](https://github.com/sweetalert2/sweetalert2/releases) - [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md) - [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.19.1...v11.22.0) --- updated-dependencies: - dependency-name: sweetalert2 dependency-version: 11.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 49946d3916fb..68bf86cede04 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -61,7 +61,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.19.1", + "sweetalert2": "^11.22.0", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", @@ -14767,9 +14767,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.19.1.tgz", - "integrity": "sha512-+8yws3Sc1srAZbrgdhmEIZny1I1UOYhJOIOdtOlv4TYaP5kkwQ9Zm8/BT23Qg+KdByCNOazltxEJAHzXVu8mhA==", + "version": "11.22.0", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.0.tgz", + "integrity": "sha512-pSMuRGDULhh+wrFkO22O0YsIXxs8yFE0O+WVYXcqc/sTa1oRnf0JlR+vfQIRY1QM1UeFfnCjyw6DYnG75/oxiQ==", "license": "MIT", "funding": { "type": "individual", @@ -27108,9 +27108,9 @@ } }, "sweetalert2": { - "version": "11.19.1", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.19.1.tgz", - "integrity": "sha512-+8yws3Sc1srAZbrgdhmEIZny1I1UOYhJOIOdtOlv4TYaP5kkwQ9Zm8/BT23Qg+KdByCNOazltxEJAHzXVu8mhA==" + "version": "11.22.0", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.0.tgz", + "integrity": "sha512-pSMuRGDULhh+wrFkO22O0YsIXxs8yFE0O+WVYXcqc/sTa1oRnf0JlR+vfQIRY1QM1UeFfnCjyw6DYnG75/oxiQ==" }, "symbol-tree": { "version": "3.2.4", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 729c7d22bc61..0b14ac3c61bf 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -73,7 +73,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.19.1", + "sweetalert2": "^11.22.0", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", From e7abd486718e474e972868705a59a69db2299b36 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 2 Jun 2025 12:46:42 +0300 Subject: [PATCH 0071/1006] feat: add `/api/v2/config/celo` convenience endpoint (#12238) --- .../controllers/api/v2/config_controller.ex | 8 ++++++++ .../lib/block_scout_web/routers/api_router.ex | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 543b43d4f8dd..a39bfda1c026 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -24,4 +24,12 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> put_status(200) |> json(%{update_period_hours: public_metrics_update_period_hours}) end + + def celo(conn, _params) do + config = Application.get_env(:explorer, :celo) + + conn + |> put_status(200) + |> json(%{l2_migration_block: config[:l2_migration_block]}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 0ea378d0d286..77321e71f313 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -141,6 +141,10 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/backend-version", V2.ConfigController, :backend_version) get("/csv-export", V2.ConfigController, :csv_export) get("/public-metrics", V2.ConfigController, :public_metrics) + + chain_scope :celo do + get("/celo", V2.ConfigController, :celo) + end end scope "/transactions" do From dfb9b6e171db5347ab60377b6879ef9474716d7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:04:12 +0300 Subject: [PATCH 0072/1006] chore(deps-dev): bump postcss in /apps/block_scout_web/assets (#12503) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.3 to 8.5.4. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.3...8.5.4) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 30 +++++++++---------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 68bf86cede04..8385b35912a7 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -88,7 +88,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.3", + "postcss": "^8.5.4", "postcss-loader": "^8.1.1", "sass": "^1.87.0", "sass-loader": "^14.2.1", @@ -12060,9 +12060,9 @@ "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" }, "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -12672,9 +12672,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "funding": [ { "type": "opencollective", @@ -12691,7 +12691,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -25213,9 +25213,9 @@ "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" }, "nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==" + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "nanomorph": { "version": "5.4.3", @@ -25644,11 +25644,11 @@ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" }, "postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", "requires": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 0b14ac3c61bf..9fbb0d7cc166 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -100,7 +100,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.3", + "postcss": "^8.5.4", "postcss-loader": "^8.1.1", "sass": "^1.87.0", "sass-loader": "^14.2.1", From 85934e522ab8bcadb214edecbdcd5b6184cec3db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 13:08:13 +0300 Subject: [PATCH 0073/1006] chore(deps-dev): bump eslint in /apps/block_scout_web/assets (#12505) Bumps [eslint](https://github.com/eslint/eslint) from 9.23.0 to 9.28.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.23.0...v9.28.0) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.28.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 89 ++++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 47 insertions(+), 44 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 8385b35912a7..009d95495539 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -78,7 +78,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.2", - "eslint": "^9.23.0", + "eslint": "^9.28.0", "eslint-formatter-junit": "^8.40.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-node": "^11.1.0", @@ -2086,9 +2086,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2111,9 +2111,9 @@ } }, "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2181,13 +2181,16 @@ } }, "node_modules/@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { @@ -2201,13 +2204,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -7097,20 +7100,20 @@ } }, "node_modules/eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -17689,9 +17692,9 @@ "dev": true }, "@eslint/config-array": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", - "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "requires": { "@eslint/object-schema": "^2.1.6", @@ -17706,9 +17709,9 @@ "dev": true }, "@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "requires": { "@types/json-schema": "^7.0.15" @@ -17755,9 +17758,9 @@ } }, "@eslint/js": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", - "integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true }, "@eslint/object-schema": { @@ -17767,12 +17770,12 @@ "dev": true }, "@eslint/plugin-kit": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", - "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "requires": { - "@eslint/core": "^0.12.0", + "@eslint/core": "^0.14.0", "levn": "^0.4.1" } }, @@ -21398,19 +21401,19 @@ } }, "eslint": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", - "integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.19.2", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.23.0", - "@eslint/plugin-kit": "^0.2.7", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 9fbb0d7cc166..9ff246e2173c 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -90,7 +90,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.2", - "eslint": "^9.23.0", + "eslint": "^9.28.0", "eslint-formatter-junit": "^8.40.0", "eslint-plugin-import": "^2.31.0", "eslint-plugin-node": "^11.1.0", From b0f416693c00632a9367f4612be857a67dd0acee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:18:42 +0300 Subject: [PATCH 0074/1006] chore(deps-dev): bump sass in /apps/block_scout_web/assets (#12512) Bumps [sass](https://github.com/sass/dart-sass) from 1.87.0 to 1.89.1. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.87.0...1.89.1) --- updated-dependencies: - dependency-name: sass dependency-version: 1.89.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 009d95495539..24b21ef26b78 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -90,7 +90,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.4", "postcss-loader": "^8.1.1", - "sass": "^1.87.0", + "sass": "^1.89.1", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.99.9", @@ -14064,9 +14064,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.87.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", - "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "version": "1.89.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz", + "integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==", "dev": true, "license": "MIT", "dependencies": { @@ -26621,9 +26621,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.87.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", - "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", + "version": "1.89.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz", + "integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==", "dev": true, "requires": { "@parcel/watcher": "^2.4.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 9ff246e2173c..a2fbed812ef1 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -102,7 +102,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.4", "postcss-loader": "^8.1.1", - "sass": "^1.87.0", + "sass": "^1.89.1", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.99.9", From 441c461bd669e7b7e1e03217137effaa0d9122b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:19:15 +0300 Subject: [PATCH 0075/1006] chore(deps-dev): bump @babel/preset-env in /apps/block_scout_web/assets (#12511) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.27.1 to 7.27.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.2/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-version: 7.27.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 84 ++++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 24b21ef26b78..279521abbaeb 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -72,7 +72,7 @@ }, "devDependencies": { "@babel/core": "^7.27.1", - "@babel/preset-env": "^7.27.1", + "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", @@ -272,9 +272,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -344,12 +344,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.1", + "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -1102,9 +1102,9 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", "dev": true, "license": "MIT", "dependencies": { @@ -1450,14 +1450,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", - "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", + "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { @@ -1813,14 +1814,14 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", - "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", @@ -1862,7 +1863,7 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", @@ -16505,9 +16506,9 @@ } }, "@babel/compat-data": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.1.tgz", - "integrity": "sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A==" + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", + "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==" }, "@babel/core": { "version": "7.27.1", @@ -16559,11 +16560,11 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz", - "integrity": "sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "requires": { - "@babel/compat-data": "^7.27.1", + "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -17054,9 +17055,9 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz", - "integrity": "sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.27.1" @@ -17255,13 +17256,14 @@ } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz", - "integrity": "sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", + "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", "@babel/plugin-transform-parameters": "^7.27.1" } }, @@ -17471,13 +17473,13 @@ } }, "@babel/preset-env": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.1.tgz", - "integrity": "sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", @@ -17519,7 +17521,7 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index a2fbed812ef1..def609f33b2e 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -84,7 +84,7 @@ }, "devDependencies": { "@babel/core": "^7.27.1", - "@babel/preset-env": "^7.27.1", + "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", From 35f5c6f21b38172514689d20eba919a132ccc571 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:19:59 +0300 Subject: [PATCH 0076/1006] chore(deps-dev): bump globals in /apps/block_scout_web/assets (#12506) Bumps [globals](https://github.com/sindresorhus/globals) from 16.0.0 to 16.2.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v16.0.0...v16.2.0) --- updated-dependencies: - dependency-name: globals dependency-version: 16.2.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 279521abbaeb..586f5b32b8a8 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -84,7 +84,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", - "globals": "^16.0.0", + "globals": "^16.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", @@ -8542,9 +8542,9 @@ } }, "node_modules/globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true, "license": "MIT", "engines": { @@ -22552,9 +22552,9 @@ } }, "globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", "dev": true }, "globalthis": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index def609f33b2e..17d5aaa73a6f 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -96,7 +96,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", - "globals": "^16.0.0", + "globals": "^16.2.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", From a3c244ec2dd168008079d645a10287b1128d2eb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 15:47:31 +0300 Subject: [PATCH 0077/1006] chore(deps-dev): bump @babel/core in /apps/block_scout_web/assets (#12504) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.27.1 to 7.27.4. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.4/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.27.4 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 174 +++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 88 insertions(+), 88 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 586f5b32b8a8..495623dd8078 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -71,7 +71,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.1", + "@babel/core": "^7.27.4", "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", @@ -281,21 +281,21 @@ } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -316,13 +316,13 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -456,14 +456,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -587,25 +587,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -1966,13 +1966,13 @@ } }, "node_modules/@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" }, "engines": { @@ -1980,16 +1980,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2007,9 +2007,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -16511,20 +16511,20 @@ "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==" }, "@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -16540,12 +16540,12 @@ } }, "@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", + "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", "requires": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/parser": "^7.27.3", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -16645,13 +16645,13 @@ } }, "@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", "requires": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.27.3" } }, "@babel/helper-optimise-call-expression": { @@ -16727,20 +16727,20 @@ } }, "@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", "requires": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" } }, "@babel/parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", - "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", + "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", "requires": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -17604,25 +17604,25 @@ } }, "@babel/template": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", - "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "requires": { "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.1", + "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", "requires": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -17635,9 +17635,9 @@ } }, "@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 17d5aaa73a6f..6f49541d5d08 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -83,7 +83,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.1", + "@babel/core": "^7.27.4", "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", From 5f437a18e07eb3dd5cf2f4504f8f165d9a12e084 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Jun 2025 17:52:15 +0300 Subject: [PATCH 0078/1006] chore(deps): bump @amplitude/analytics-browser (#12508) Bumps [@amplitude/analytics-browser](https://github.com/amplitude/Amplitude-TypeScript) from 2.16.1 to 2.17.6. - [Release notes](https://github.com/amplitude/Amplitude-TypeScript/releases) - [Commits](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@2.16.1...@amplitude/analytics-browser@2.17.6) --- updated-dependencies: - dependency-name: "@amplitude/analytics-browser" dependency-version: 2.17.6 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 142 +++++++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 90 insertions(+), 54 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 495623dd8078..86559606e5f5 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -7,7 +7,7 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "@amplitude/analytics-browser": "^2.16.1", + "@amplitude/analytics-browser": "^2.17.6", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", @@ -122,15 +122,16 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.16.1.tgz", - "integrity": "sha512-ssd5MPff6tm8BmzHq2j1HJ0/5KmupFX9ztlrVq0Kj5T3vJhDNgEOIyBZkdjsJAyEPyQITZ790wLZNVm6uJBKfg==", + "version": "2.17.6", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.6.tgz", + "integrity": "sha512-1Lbm5KNXG/6Zpg1Zr0qAmft562kXWGIXZ1ah12oEf6Wcp2ePxIPlbkKgagOTzeS8hwOlfuKVeBy9oCuOdf8yFw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.10.1", + "@amplitude/analytics-core": "^2.12.0", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.1.10", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.20", + "@amplitude/plugin-autocapture-browser": "^1.2.6", + "@amplitude/plugin-network-capture-browser": "^1.1.6", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.27", "tslib": "^2.4.1" } }, @@ -140,14 +141,14 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/@amplitude/analytics-client-common": { - "version": "2.3.17", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.17.tgz", - "integrity": "sha512-NCOEQzRn4bIMFLICtfg72KHF9KZyCSHYmALlOMh9wXmgBndPF4e2vrhXwNu65rIfP21Vb9Zm/MbPh8RKJ+bJ5w==", + "version": "2.3.21", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.21.tgz", + "integrity": "sha512-7KfSIr9d+Dnn+jW3UxQS/VlTaWpQz0nxX4LCi006x7YtjFgReFU2dxwB/48ZkfORLBEQokhm7cKilvD+JZveeg==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.10.1", - "@amplitude/analytics-types": "^2.9.1", + "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" } }, @@ -163,9 +164,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.10.1.tgz", - "integrity": "sha512-QWCGNVDYIsQKoEMJF3o79QlWFytYxowUYtfEQshTQcV9DXo7+sbuntkPbX0kEgZJRAiIwO1NA1bMMQZiSTeuXg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.12.0.tgz", + "integrity": "sha512-mfA6J2krFionypPHka65fK13eFi058EjSpLk8XgGmGS6Yvw+eidkgHvbouNDKwxBkhQFDH7cY8vOZeIAdkrktw==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -194,18 +195,18 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/@amplitude/analytics-types": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.1.tgz", - "integrity": "sha512-Hw0pgUQCDV6AgvZD8ymBF3mzyBnPrIvXiPxfTNTg/bAkg1h+GVNdJIL6VAXd53kYuO7Lx/vjdhZS54i+RYtgMA==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.2.tgz", + "integrity": "sha512-juhTz396dDP/jLJYP9zDOEAZBtJM0JVvP8G10p1OxUDBVwVIprpQL598F9GRQwVFyqV4CEhDmNyAY0HqqU5bhA==", "license": "MIT" }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.10.tgz", - "integrity": "sha512-Wgnl9BFmM0fG+ZbE92UwEeFgJ27YzqYOeMZJno9zja8kdaLrHoaakZrmr2d8tie4DcHVZtMjQ8j/NAMmVEJycQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.2.6.tgz", + "integrity": "sha512-+WB6DSGrKOxWC0muT/z0bcc5ftzjPY9L1D3CB5nHIHJTHAzxkTuc1+TFRtjc8L5Tdly9h2YADPNo1H8dkWNOkg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.10.1", + "@amplitude/analytics-core": "^2.12.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" } @@ -216,14 +217,31 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@amplitude/plugin-network-capture-browser": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.1.6.tgz", + "integrity": "sha512-Ck39vvvizJ7qlBUxkOqq8k4/hrD9yCdvceBpSDfCr9jGzd3R3VisYUHsChOVChHd1fOoLtpoKF+HsgfrXmn17g==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": "^2.12.0", + "rxjs": "^7.8.1", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/plugin-network-capture-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.20", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.20.tgz", - "integrity": "sha512-gZIgaTY21sm3x5NXxVv8Pe8xc244+rMHFCWttu2HF0Hkd90qlCFBSqJmBqmw+RfpfjsB6+2JJs0LKO5zTRQopA==", + "version": "2.3.27", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.27.tgz", + "integrity": "sha512-iJeuCn+jvBymQwV2EEkZhmJg+bitpZulubCxRMI0ihIdh/Nvk1XWhwmSfcgKaxMdk2qpa9CYXD9/pDewI88IDg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-client-common": "^2.3.17", - "@amplitude/analytics-types": "^2.9.1", + "@amplitude/analytics-client-common": "^2.3.21", + "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" } }, @@ -16361,14 +16379,15 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "@amplitude/analytics-browser": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.16.1.tgz", - "integrity": "sha512-ssd5MPff6tm8BmzHq2j1HJ0/5KmupFX9ztlrVq0Kj5T3vJhDNgEOIyBZkdjsJAyEPyQITZ790wLZNVm6uJBKfg==", + "version": "2.17.6", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.6.tgz", + "integrity": "sha512-1Lbm5KNXG/6Zpg1Zr0qAmft562kXWGIXZ1ah12oEf6Wcp2ePxIPlbkKgagOTzeS8hwOlfuKVeBy9oCuOdf8yFw==", "requires": { - "@amplitude/analytics-core": "^2.10.1", + "@amplitude/analytics-core": "^2.12.0", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.1.10", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.20", + "@amplitude/plugin-autocapture-browser": "^1.2.6", + "@amplitude/plugin-network-capture-browser": "^1.1.6", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.27", "tslib": "^2.4.1" }, "dependencies": { @@ -16380,13 +16399,13 @@ } }, "@amplitude/analytics-client-common": { - "version": "2.3.17", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.17.tgz", - "integrity": "sha512-NCOEQzRn4bIMFLICtfg72KHF9KZyCSHYmALlOMh9wXmgBndPF4e2vrhXwNu65rIfP21Vb9Zm/MbPh8RKJ+bJ5w==", + "version": "2.3.21", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.21.tgz", + "integrity": "sha512-7KfSIr9d+Dnn+jW3UxQS/VlTaWpQz0nxX4LCi006x7YtjFgReFU2dxwB/48ZkfORLBEQokhm7cKilvD+JZveeg==", "requires": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.10.1", - "@amplitude/analytics-types": "^2.9.1", + "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" }, "dependencies": { @@ -16403,9 +16422,9 @@ "integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==" }, "@amplitude/analytics-core": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.10.1.tgz", - "integrity": "sha512-QWCGNVDYIsQKoEMJF3o79QlWFytYxowUYtfEQshTQcV9DXo7+sbuntkPbX0kEgZJRAiIwO1NA1bMMQZiSTeuXg==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.12.0.tgz", + "integrity": "sha512-mfA6J2krFionypPHka65fK13eFi058EjSpLk8XgGmGS6Yvw+eidkgHvbouNDKwxBkhQFDH7cY8vOZeIAdkrktw==", "requires": { "@amplitude/analytics-connector": "^1.6.4", "tslib": "^2.4.1" @@ -16437,16 +16456,33 @@ } }, "@amplitude/analytics-types": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.1.tgz", - "integrity": "sha512-Hw0pgUQCDV6AgvZD8ymBF3mzyBnPrIvXiPxfTNTg/bAkg1h+GVNdJIL6VAXd53kYuO7Lx/vjdhZS54i+RYtgMA==" + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-2.9.2.tgz", + "integrity": "sha512-juhTz396dDP/jLJYP9zDOEAZBtJM0JVvP8G10p1OxUDBVwVIprpQL598F9GRQwVFyqV4CEhDmNyAY0HqqU5bhA==" }, "@amplitude/plugin-autocapture-browser": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.1.10.tgz", - "integrity": "sha512-Wgnl9BFmM0fG+ZbE92UwEeFgJ27YzqYOeMZJno9zja8kdaLrHoaakZrmr2d8tie4DcHVZtMjQ8j/NAMmVEJycQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.2.6.tgz", + "integrity": "sha512-+WB6DSGrKOxWC0muT/z0bcc5ftzjPY9L1D3CB5nHIHJTHAzxkTuc1+TFRtjc8L5Tdly9h2YADPNo1H8dkWNOkg==", + "requires": { + "@amplitude/analytics-core": "^2.12.0", + "rxjs": "^7.8.1", + "tslib": "^2.4.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, + "@amplitude/plugin-network-capture-browser": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.1.6.tgz", + "integrity": "sha512-Ck39vvvizJ7qlBUxkOqq8k4/hrD9yCdvceBpSDfCr9jGzd3R3VisYUHsChOVChHd1fOoLtpoKF+HsgfrXmn17g==", "requires": { - "@amplitude/analytics-core": "^2.10.1", + "@amplitude/analytics-core": "^2.12.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" }, @@ -16459,12 +16495,12 @@ } }, "@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.20", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.20.tgz", - "integrity": "sha512-gZIgaTY21sm3x5NXxVv8Pe8xc244+rMHFCWttu2HF0Hkd90qlCFBSqJmBqmw+RfpfjsB6+2JJs0LKO5zTRQopA==", + "version": "2.3.27", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.27.tgz", + "integrity": "sha512-iJeuCn+jvBymQwV2EEkZhmJg+bitpZulubCxRMI0ihIdh/Nvk1XWhwmSfcgKaxMdk2qpa9CYXD9/pDewI88IDg==", "requires": { - "@amplitude/analytics-client-common": "^2.3.17", - "@amplitude/analytics-types": "^2.9.1", + "@amplitude/analytics-client-common": "^2.3.21", + "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" }, "dependencies": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 6f49541d5d08..613f477a56e5 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -19,7 +19,7 @@ "eslint": "eslint js/**" }, "dependencies": { - "@amplitude/analytics-browser": "^2.16.1", + "@amplitude/analytics-browser": "^2.17.6", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", From abe9db5735fa14751495506cb30ea16d0e5f495c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:38:56 +0300 Subject: [PATCH 0079/1006] chore(deps-dev): bump ex_doc from 0.38.1 to 0.38.2 (#12524) Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.38.1 to 0.38.2. - [Release notes](https://github.com/elixir-lang/ex_doc/releases) - [Changelog](https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md) - [Commits](https://github.com/elixir-lang/ex_doc/compare/v0.38.1...v0.38.2) --- updated-dependencies: - dependency-name: ex_doc dependency-version: 0.38.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 51d935010f34..626caafa3748 100644 --- a/mix.lock +++ b/mix.lock @@ -57,7 +57,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, - "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, + "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, From acd01689fc2a20d839cffdf6a1dfef7b95017005 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:34:47 +0300 Subject: [PATCH 0080/1006] chore(deps): bump mime from 2.0.6 to 2.0.7 (#12445) Bumps [mime](https://github.com/elixir-plug/mime) from 2.0.6 to 2.0.7. - [Changelog](https://github.com/elixir-plug/mime/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-plug/mime/compare/v2.0.6...v2.0.7) --- updated-dependencies: - dependency-name: mime dependency-version: 2.0.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 626caafa3748..a2e7217c1d21 100644 --- a/mix.lock +++ b/mix.lock @@ -102,7 +102,7 @@ "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, From 9412ba5dc6d0b42d676ea7f944b0768c8517d38f Mon Sep 17 00:00:00 2001 From: Dmitry <98899785+mdqst@users.noreply.github.com> Date: Tue, 3 Jun 2025 17:53:09 +0300 Subject: [PATCH 0081/1006] chore: set home directory for blockscout user (#12337) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b8c539128091..20232af3064f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -60,7 +60,7 @@ ARG BLOCKSCOUT_GID=10001 RUN apk --no-cache --update add jq curl && \ addgroup --system --gid ${BLOCKSCOUT_GID} ${BLOCKSCOUT_GROUP} && \ - adduser --system --uid ${BLOCKSCOUT_UID} --ingroup ${BLOCKSCOUT_GROUP} --disabled-password ${BLOCKSCOUT_USER} + adduser --system --uid ${BLOCKSCOUT_UID} --ingroup ${BLOCKSCOUT_GROUP} --disabled-password --home /app ${BLOCKSCOUT_USER} ENV DISABLE_WEBAPP=true ENV ADMIN_PANEL_ENABLED=false From be7f2e2abbad7010fa06bee0316cf9771ab29192 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 5 Jun 2025 13:57:04 +0400 Subject: [PATCH 0082/1006] fix: Fix for `add_0x_prefix` function (#12514) * fix: Fix for add_0x_prefix function * Refactoring * Enhance add_0x_prefix function --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- apps/explorer/lib/explorer/helper.ex | 2 +- .../lib/indexer/fetcher/optimism/dispute_game.ex | 16 +++++----------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index b0899de56d96..1e649ea99c60 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -353,7 +353,7 @@ defmodule Explorer.Helper do end def add_0x_prefix(binary_hash) when is_binary(binary_hash) do - if String.starts_with?(binary_hash, "0x") do + if String.starts_with?(binary_hash, "0x") and String.printable?(binary_hash) do binary_hash else "0x" <> Base.encode16(binary_hash, case: :lower) diff --git a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex index 10f730a3efd0..580e5fac296d 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex @@ -16,6 +16,7 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do alias EthereumJSONRPC.Contract alias Explorer.Application.Constants alias Explorer.{Chain, Helper, Repo} + alias Explorer.Chain.Hash.Address alias Explorer.Chain.Optimism.{DisputeGame, Withdrawal} alias Explorer.Helper, as: ExplorerHelper alias Indexer.Fetcher.Optimism @@ -387,10 +388,12 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do |> Enum.map(fn response -> [game_type, created_at, address_hash] = Helper.decode_data(response.result, [{:uint, 32}, {:uint, 64}, :address]) + {:ok, address} = Address.cast(address_hash) + %{ index: response.id, game_type: game_type, - address_hash: address_hash, + address_hash: address, created_at: Timex.from_unix(created_at) } end) @@ -399,16 +402,7 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do defp read_extra_data(method_id, method_name, games, json_rpc_named_arguments, retries \\ 10) do requests = games - |> Enum.map(fn game -> - address_hash = - if is_binary(game.address_hash) do - ExplorerHelper.add_0x_prefix(game.address_hash) - else - game.address_hash - end - - Contract.eth_call_request(method_id, address_hash, game.index, nil, nil) - end) + |> Enum.map(&Contract.eth_call_request(method_id, &1.address_hash, &1.index, nil, nil)) error_message = &"Cannot call #{method_name} public getter of FaultDisputeGame. Error: #{inspect(&1)}" From 7612a38414150f375773f80b7fc1a2df66a7c7ca Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 5 Jun 2025 16:48:39 +0300 Subject: [PATCH 0083/1006] chore: remove `is_self_destructed` field in `/api/v2/smart-contracts/{address_hash}` response (#12239) --- .../views/api/v2/smart_contract_view.ex | 3 --- .../api/v2/smart_contract_controller_test.exs | 15 --------------- 2 files changed, 18 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index 4a1295728936..d487946df075 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -265,7 +265,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do case AddressContractView.contract_creation_code(address) do {:selfdestructed, init} -> %{ - "is_self_destructed" => true, "deployed_bytecode" => nil, "creation_bytecode" => init, "status" => "selfdestructed" @@ -273,7 +272,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do {:failed, creation_code} -> %{ - "is_self_destructed" => false, "deployed_bytecode" => "0x", "creation_bytecode" => creation_code, "status" => "failed" @@ -281,7 +279,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do {:ok, contract_code} -> %{ - "is_self_destructed" => false, "deployed_bytecode" => contract_code, "creation_bytecode" => AddressContractView.creation_code(address), "status" => "success" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 74d1fb63230f..8cd003ac14f8 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -60,7 +60,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => nil, "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => nil, "status" => "success" @@ -82,7 +81,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => nil, "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -162,7 +160,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "external_libraries" => [%{"name" => "ABC", "address_hash" => Address.checksum(lib_address)}], "constructor_args" => target_contract.constructor_arguments, "decoded_constructor_args" => nil, - "is_self_destructed" => false, "deployed_bytecode" => "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "creation_bytecode" => @@ -275,7 +272,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{"name" => "_implementationAddress", "type" => "address"} ] ], - "is_self_destructed" => false, "deployed_bytecode" => "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "creation_bytecode" => @@ -380,7 +376,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "external_libraries" => [%{"name" => "ABC", "address_hash" => Address.checksum(lib_address)}], "constructor_args" => nil, "decoded_constructor_args" => nil, - "is_self_destructed" => false, "deployed_bytecode" => "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "creation_bytecode" => @@ -478,7 +473,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do |> with_block(status: :ok) correct_response = %{ - "is_self_destructed" => false, "deployed_bytecode" => proxy_deployed_bytecode, "creation_bytecode" => proxy_transaction_input, "proxy_type" => "eip1167", @@ -540,7 +534,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "external_libraries" => target_contract.external_libraries, "constructor_args" => target_contract.constructor_arguments, "decoded_constructor_args" => nil, - "is_self_destructed" => false, "deployed_bytecode" => "0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", "creation_bytecode" => @@ -644,7 +637,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "name" => implementation_contract.name } ], - "is_self_destructed" => false, "deployed_bytecode" => proxy_deployed_bytecode, "creation_bytecode" => proxy_transaction_input } @@ -733,7 +725,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => nil, "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -819,7 +810,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => nil, "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -952,7 +942,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => "unknown", "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -1040,7 +1029,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do %{ "proxy_type" => nil, "implementations" => [], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -1161,7 +1149,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "name" => nil }) ], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -1299,7 +1286,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "name" => nil }) ], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", @@ -1437,7 +1423,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "name" => nil }) ], - "is_self_destructed" => false, "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", From 80d44956ba28c5ec325759623076de7de7f43a71 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 5 Jun 2025 21:33:59 +0400 Subject: [PATCH 0084/1006] fix: Fix typo in ondemand token balance request (#12495) * fix: Fix typo in ondemand token balance request * Add tests for on demand token balance fetcher --- .../fetcher/on_demand/token_balance.ex | 2 +- .../fetcher/on_demand/token_balance_test.exs | 148 ++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex index 84323374c927..3defaa43d75b 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex @@ -230,7 +230,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do {{:ok, balance}, request}, acc -> params = %{ address_hash: request.address_hash, - token_contract_address_hash: request.contract_address_hash, + token_contract_address_hash: request.token_contract_address_hash, token_type: request.token_type, token_id: request.token_id, block_number: request.block_number, diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs new file mode 100644 index 000000000000..ac853b38fdee --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs @@ -0,0 +1,148 @@ +defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Events.Subscriber + alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Indexer.Fetcher.OnDemand.TokenBalance, as: TokenBalanceOnDemand + + setup :set_mox_global + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!(AverageBlockTime) + + configuration = Application.get_env(:indexer, TokenBalanceOnDemand.Supervisor) + Application.put_env(:indexer, TokenBalanceOnDemand.Supervisor, disabled?: false) + + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + + TokenBalanceOnDemand.Supervisor.Case.start_supervised!(json_rpc_named_arguments: mocked_json_rpc_named_arguments) + + on_exit(fn -> + Application.put_env(:indexer, TokenBalanceOnDemand.Supervisor, configuration) + Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) + end) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end + + describe "update behaviour" do + setup do + Subscriber.to(:address_current_token_balances, :on_demand) + Subscriber.to(:address_token_balances, :on_demand) + + now = Timex.now() + + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 50)) + end) + + insert(:block, number: 102, timestamp: now) + AverageBlockTime.refresh() + + :ok + end + + test "current token balances are imported and broadcasted for a stale address" do + %{address: address, token_contract_address_hash: token_contract_address_hash} = + insert(:address_current_token_balance, + value_fetched_at: nil, + value: nil, + token_type: "ERC-20", + block_number: 101 + ) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + } + ]} + end + ) + + TokenBalanceOnDemand.trigger_fetch(address.hash) + + Process.sleep(100) + + [%{value: updated_value} = updated_ctb] = Repo.all(CurrentTokenBalance) + + assert updated_value == Decimal.new(1_000_000_000_000_000_000_000_000) + refute is_nil(updated_ctb.value_fetched_at) + + address_hash = to_string(address.hash) + + assert_receive( + {:chain_event, :address_current_token_balances, :on_demand, + %{ + address_hash: ^address_hash, + address_current_token_balances: [ + %{value: ^updated_value, token_contract_address_hash: ^token_contract_address_hash} + ] + }} + ) + end + + test "historic balances are imported and broadcasted" do + token_balance = insert(:token_balance, value_fetched_at: nil, value: nil, token_type: "ERC-20", block_number: 101) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + } + ]} + end + ) + + TokenBalanceOnDemand.trigger_historic_fetch( + token_balance.address_hash, + token_balance.token_contract_address_hash, + token_balance.token_type, + token_balance.token_id, + token_balance.block_number + ) + + Process.sleep(100) + + [%{value: updated_value} = updated_tb] = Repo.all(TokenBalance) + + assert updated_value == Decimal.new(1_000_000_000_000_000_000_000_000) + refute is_nil(updated_tb.value_fetched_at) + + address_hash = token_balance.address_hash + token_contract_address_hash = token_balance.token_contract_address_hash + + assert_receive( + {:chain_event, :address_token_balances, :on_demand, + [ + %{ + address_hash: ^address_hash, + token_contract_address_hash: ^token_contract_address_hash, + value: ^updated_value + } + ]} + ) + end + end +end From 5c6074665a452eed97533c8ac59a9641a4b164e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Jun 2025 12:21:12 +0300 Subject: [PATCH 0085/1006] chore(deps): bump tesla from 1.14.1 to 1.14.2 (#12447) Bumps [tesla](https://github.com/elixir-tesla/tesla) from 1.14.1 to 1.14.2. - [Release notes](https://github.com/elixir-tesla/tesla/releases) - [Changelog](https://github.com/elixir-tesla/tesla/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-tesla/tesla/compare/v1.14.1...v1.14.2) --- updated-dependencies: - dependency-name: tesla dependency-version: 1.14.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index a2e7217c1d21..26c15f6ff3c0 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.0", "f9a9123feac30df2b75260c9a74903c0bd7ec54f345f5f6f60c7830b81ead43a", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "77f9417a5549e424bdbfeea3f8060841384c39c0b5f563a8f62f1f2c2b6d2345"}, - "castore": {:hex, :castore, "1.0.13", "b735ea45d87f027128debaf662c6cdf618604f530bec554cc60c195f2a55b506", [:mix], [], "hexpm", "ec09e81a9c3db92d27c6651d119d8adc6d1cbbb3d90f8c1293eee2af590bf55d"}, + "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, @@ -127,7 +127,7 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, + "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, @@ -159,13 +159,13 @@ "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "tesla": {:hex, :tesla, "1.14.1", "71c5b031b4e089c0fbfb2b362e24b4478465773ae4ef569760a8c2899ad1e73c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "c1dde8140a49a3bef5bb622356e77ac5a24ad0c8091f12c3b7fc1077ce797155"}, + "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.1.0", "0632d5844049fa2f26823f15e1120aa32f27df6f27ce515a4b04641736594bf4", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "8d3b30fa27c95c9e82c30c4afb016251405706d2e9627e603c3c9787fd1314fc"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, "vix": {:hex, :vix, "0.33.1", "72717f9b7a2ccd314c0b320a91d8cae67f100c0df378251acea19151fbf6e4b6", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "d9143e03fcd7f2f0e4db9a86d4b64aca0e39e99c303059d5841450cc4481b29c"}, From 1cae89964c12e6563da6b39b19b66849c68cdcd3 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 9 Jun 2025 12:24:50 +0300 Subject: [PATCH 0086/1006] fix: prevent constant refetching of celo epoch blocks (#12498) * fix: prevent constant refetching of celo epoch blocks * chore: add regression test * refactor: move celo-specific existence check under chain type --- .../lib/explorer/chain/token_transfer.ex | 42 +++++++++++++--- .../explorer/chain/token_transfer_test.exs | 50 +++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 874181d75b81..36c8876a787f 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -135,6 +135,7 @@ defmodule Explorer.Chain.TokenTransfer do use Explorer.Schema use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] require Explorer.Chain.TokenTransfer.Schema @@ -633,13 +634,7 @@ defmodule Explorer.Chain.TokenTransfer do l.first_topic == ^@constant or l.first_topic == ^@erc1155_single_transfer_signature or l.first_topic == ^@erc1155_batch_transfer_signature, - where: - not exists( - from(tf in TokenTransfer, - where: tf.transaction_hash == parent_as(:log).transaction_hash, - where: tf.log_index == parent_as(:log).index - ) - ), + where: not exists(token_transfer_exists_query()), select: l.block_number, distinct: l.block_number ) @@ -647,6 +642,39 @@ defmodule Explorer.Chain.TokenTransfer do Repo.stream_reduce(query, [], &[&1 | &2]) end + # Builds a query to check if a token transfer exists for a given log. Handles + # chain-specific logic for transaction_hash comparison. + # + # For Celo epoch blocks, `transaction_hash` can be `nil` in both `Log` and + # `TokenTransfer`. A direct SQL comparison `NULL = NULL` evaluates to + # `UNKNOWN` (effectively false in this context). Therefore, we need a + # NULL-safe comparison for `transaction_hash`. Additionally, `block_hash` is + # included in the join condition to uniquely identify the token transfer, as + # `transaction_hash` (when nil) and `log_index` alone are insufficient. + @spec token_transfer_exists_query() :: Ecto.Query.t() + defp token_transfer_exists_query do + query = + from(tt in TokenTransfer, + where: tt.block_hash == parent_as(:log).block_hash, + where: tt.log_index == parent_as(:log).index + ) + + chain_type() + |> case do + :celo -> + query + |> where( + [tt], + tt.transaction_hash == parent_as(:log).transaction_hash or + (is_nil(parent_as(:log).transaction_hash) and is_nil(tt.transaction_hash)) + ) + + _ -> + query + |> where([tt], tt.transaction_hash == parent_as(:log).transaction_hash) + end + end + @doc """ Returns ecto query to fetch consensus token transfers with ERC-721 token type """ diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 3139f318cb39..0ca06af2cb54 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -1,6 +1,9 @@ defmodule Explorer.Chain.TokenTransferTest do use Explorer.DataCase + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type] + import Explorer.Factory alias Explorer.PagingOptions @@ -349,4 +352,51 @@ defmodule Explorer.Chain.TokenTransferTest do assert {:ok, [^block_number]} = TokenTransfer.uncataloged_token_transfer_block_numbers() end end + + if @chain_type == :celo do + test "returns block numbers for Celo epoch blocks with nil transaction_hash" do + log = + insert(:token_transfer_log, + transaction: nil, + transaction_hash: nil + ) + + block_number = log.block_number + assert {:ok, [^block_number]} = TokenTransfer.uncataloged_token_transfer_block_numbers() + end + + test "does not return block numbers when matching token transfer exists for Celo epoch blocks" do + log = + insert(:token_transfer_log, + transaction: nil, + transaction_hash: nil + ) + + from_address_hash = + log.second_topic + |> to_string() + |> String.replace_prefix("0x000000000000000000000000", "0x") + + to_address_hash = + log.third_topic + |> to_string() + |> String.replace_prefix("0x000000000000000000000000", "0x") + + token_contract_address = insert(:address, hash: log.address_hash) + to_address = insert(:address, hash: to_address_hash) + from_address = insert(:address, hash: from_address_hash) + + insert(:token_transfer, + transaction: nil, + transaction_hash: nil, + block: log.block, + log_index: log.index, + token_contract_address: token_contract_address, + from_address: from_address, + to_address: to_address + ) + + assert {:ok, []} = TokenTransfer.uncataloged_token_transfer_block_numbers() + end + end end From d03847ee1245d1d7f218c8e6ef0f395b52c9de17 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 9 Jun 2025 13:54:51 +0300 Subject: [PATCH 0087/1006] feat: celo l2 epochs (#12373) * feat: chunked rpc requests in celo epochs fetcher * chore: format * feat: do not create postmigration epoch block operations * fix: remove test * feat: add function to convert epoch number to block range * Add L2 epochs fetching * Preload epoch number in API responses * Fix credo & add `/api/v2/celo/epochs` endpoint * fix: epochs cache * refactor: `/api/v2/addresses/:hash/celo/election-rewards` * fix: config * refactor: use block number cache * fix: tests & credo * fix: update is_epoch_block to false for frontend compatibility * fix: process review by @coderabbitai * chore: remove unused file * chore: remove comment * fix: process review by @sl1depengwyn and fix some bugs * fix: remove TODO comment regarding block number comparison * fix: process review by @sl1depengwyn * fix: add deletion of epoch rewards that were fetched after l2 transition * fix: propagated `number` param in `next_page_params` * fix: prevent constant refetching of celo epoch blocks * chore: add regression test * feat: bring missing fields to API * chore: add new variables to common-blockscout.env --- .../controllers/api/v2/address_controller.ex | 117 +++-- .../controllers/api/v2/block_controller.ex | 124 ----- .../controllers/api/v2/celo_controller.ex | 199 +++++++ .../controllers/api/v2/stats_controller.ex | 4 +- .../lib/block_scout_web/routers/api_router.ex | 16 +- .../block_scout_web/views/api/v2/celo_view.ex | 362 +++++++++---- apps/explorer/lib/explorer/application.ex | 2 + apps/explorer/lib/explorer/chain/block.ex | 11 +- .../lib/explorer/chain/cache/celo_epochs.ex | 98 ++++ .../explorer/chain/celo/election_reward.ex | 492 +++++++----------- .../explorer/lib/explorer/chain/celo/epoch.ex | 333 ++++++++++++ .../lib/explorer/chain/celo/epoch_reward.ex | 99 +--- .../lib/explorer/chain/celo/helper.ex | 109 ++-- .../celo/pending_epoch_block_operation.ex | 87 ---- .../lib/explorer/chain/celo/reader.ex | 213 -------- .../chain/celo/reader/epoch_manager.ex | 92 ++++ .../chain/csv_export/celo/election_rewards.ex | 4 +- apps/explorer/lib/explorer/chain/import.ex | 34 ++ .../explorer/chain/import/runner/blocks.ex | 89 ++-- .../import/runner/celo/election_rewards.ex | 4 +- .../chain/import/runner/celo/epoch_rewards.ex | 65 +-- .../chain/import/runner/celo/epochs.ex | 104 ++++ .../chain/import/stage/chain_type_specific.ex | 1 + apps/explorer/lib/explorer/helper.ex | 24 + .../lib/explorer/migrator/celo_l2_epochs.ex | 102 ++++ .../20250416085705_create_epoch.exs | 108 ++++ ..._remove_pending_epoch_block_operations.exs | 7 + ...s_replace_block_hash_with_epoch_number.exs | 40 ++ ...s_replace_block_hash_with_epoch_number.exs | 36 ++ .../chain/import/runner/blocks_test.exs | 203 ++++++-- .../explorer/migrator/celo_l2_epochs_test.ex | 179 +++++++ apps/explorer/test/support/factory.ex | 5 - apps/indexer/lib/indexer/block/fetcher.ex | 14 +- .../fetcher/celo/epoch_block_operations.ex | 231 ++++++-- .../core_contract_version.ex | 2 +- ... delegated_payments_prior_l2_migration.ex} | 43 +- .../epoch_block_operations/distributions.ex | 59 ++- .../validator_and_group_payments.ex | 67 --- ...or_and_group_payments_post_l2_migration.ex | 327 ++++++++++++ ...r_and_group_payments_prior_l2_migration.ex | 58 +++ .../epoch_block_operations/voter_payments.ex | 98 ++-- .../lib/indexer/fetcher/celo/epoch_logs.ex | 4 +- .../lib/indexer/fetcher/celo/helper.ex | 29 -- .../fetcher/celo/validator_group_votes.ex | 2 +- apps/indexer/lib/indexer/helper.ex | 50 +- .../lib/indexer/transform/celo/l1_epochs.ex | 50 ++ .../lib/indexer/transform/celo/l2_epochs.ex | 67 +++ .../test/indexer/block/fetcher_test.exs | 1 + .../celo/epoch_block_operations_test.exs | 64 --- config/runtime.exs | 17 +- docker-compose/envs/common-blockscout.env | 5 +- 51 files changed, 3071 insertions(+), 1480 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex create mode 100644 apps/explorer/lib/explorer/chain/cache/celo_epochs.ex create mode 100644 apps/explorer/lib/explorer/chain/celo/epoch.ex delete mode 100644 apps/explorer/lib/explorer/chain/celo/pending_epoch_block_operation.ex delete mode 100644 apps/explorer/lib/explorer/chain/celo/reader.ex create mode 100644 apps/explorer/lib/explorer/chain/celo/reader/epoch_manager.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/celo/epochs.ex create mode 100644 apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex create mode 100644 apps/explorer/priv/celo/migrations/20250416085705_create_epoch.exs create mode 100644 apps/explorer/priv/celo/migrations/20250418141539_remove_pending_epoch_block_operations.exs create mode 100644 apps/explorer/priv/celo/migrations/20250420130550_epoch_rewards_replace_block_hash_with_epoch_number.exs create mode 100644 apps/explorer/priv/celo/migrations/20250420140107_election_rewards_replace_block_hash_with_epoch_number.exs create mode 100644 apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex rename apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/{delegated_payments.ex => delegated_payments_prior_l2_migration.ex} (83%) delete mode 100644 apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments.ex create mode 100644 apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_post_l2_migration.ex create mode 100644 apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/celo/helper.ex create mode 100644 apps/indexer/lib/indexer/transform/celo/l1_epochs.ex create mode 100644 apps/indexer/lib/indexer/transform/celo/l2_epochs.ex delete mode 100644 apps/indexer/test/indexer/fetcher/celo/epoch_block_operations_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 925ed53f8885..b1092a2f5700 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -24,12 +24,14 @@ defmodule BlockScoutWeb.API.V2.AddressController do nft_types_options: 1 ] + import Explorer.Helper, only: [safe_parse_non_negative_integer: 1] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1, maybe_preload_ens_to_address: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} - alias Explorer.{Chain, Market} + alias Explorer.{Chain, Market, PagingOptions} alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction} alias Explorer.Chain.Address.Counters @@ -38,7 +40,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.API.V2.CeloView alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward - alias Explorer.Chain.Celo.Reader, as: CeloReader alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand @@ -112,7 +113,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do :smart_contract, proxy_implementations_association() ] - ] => :optional + ] => :optional, + [epoch: [:end_processing_block]] => :optional }, api?: true ] @@ -937,42 +939,81 @@ defmodule BlockScoutWeb.API.V2.AddressController do """ @spec celo_election_rewards(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() def celo_election_rewards(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do - case Chain.hash_to_address(address_hash, @address_options) do - {:ok, _address} -> - full_options = - @celo_election_rewards_options - |> Keyword.merge(CeloElectionReward.address_paging_options(params)) - - results_plus_one = CeloReader.address_hash_to_election_rewards(address_hash, full_options) - - {rewards, next_page} = split_list_by_page(results_plus_one) - - next_page_params = - next_page_params( - next_page, - rewards, - delete_parameters_from_next_page_params(params), - &CeloElectionReward.to_address_paging_params/1 - ) - - conn - |> put_status(200) - |> put_view(CeloView) - |> render(:celo_election_rewards, %{ - rewards: rewards, - next_page_params: next_page_params - }) + with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), + {:ok, _address} <- Chain.hash_to_address(address_hash, api?: true) do + full_options = + @celo_election_rewards_options + |> Keyword.put( + :paging_options, + celo_election_rewards_paging_options(params) + ) + + results_plus_one = CeloElectionReward.address_hash_to_rewards(address_hash, full_options) + + {rewards, next_page} = split_list_by_page(results_plus_one) + + filtered_params = + params + |> delete_parameters_from_next_page_params() + |> Map.drop([ + "epoch_number", + "amount", + "associated_account_address_hash", + "type" + ]) + + next_page_params = + next_page_params( + next_page, + rewards, + filtered_params, + &%{ + epoch_number: &1.epoch_number, + amount: &1.amount, + associated_account_address_hash: &1.associated_account_address_hash, + type: &1.type + } + ) + + conn + |> put_status(200) + |> put_view(CeloView) + |> render(:celo_address_election_rewards, %{ + rewards: rewards, + next_page_params: next_page_params + }) + end + end - _ -> - conn - |> put_status(200) - |> put_view(CeloView) - |> render(:celo_election_rewards, %{ - rewards: [], - next_page_params: nil - }) - end + @spec celo_election_rewards_paging_options(map()) :: PagingOptions.t() + defp celo_election_rewards_paging_options(params) do + with %{ + "epoch_number" => epoch_number_string, + "amount" => amount_string, + "associated_account_address_hash" => associated_account_address_hash_string, + "type" => type_string + } + when is_binary(epoch_number_string) and + is_binary(amount_string) and + is_binary(associated_account_address_hash_string) and + is_binary(type_string) <- params, + {:ok, epoch_number} <- safe_parse_non_negative_integer(epoch_number_string), + {amount, ""} <- Decimal.parse(amount_string), + {:ok, associated_account_address_hash} <- + Hash.Address.cast(associated_account_address_hash_string), + {:ok, type} <- CeloElectionReward.type_from_string(type_string) do + %{ + PagingOptions.default_paging_options() + | key: %{ + epoch_number: epoch_number, + amount: amount, + associated_account_address_hash: associated_account_address_hash, + type: type + } + } + else + _ -> + PagingOptions.default_paging_options() end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 0cd1c39b4b75..aec5124daf63 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -24,23 +24,13 @@ defmodule BlockScoutWeb.API.V2.BlockController do import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] - import Explorer.Chain.Celo.Helper, - only: [ - validate_epoch_block_number: 1, - block_number_to_epoch_number: 1 - ] - alias BlockScoutWeb.API.V2.{ - CeloView, TransactionView, WithdrawalView } alias Explorer.Chain alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader - alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward - alias Explorer.Chain.Celo.EpochReward, as: CeloEpochReward - alias Explorer.Chain.Celo.Reader, as: CeloReader alias Explorer.Chain.InternalTransaction alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Scroll.Reader, as: ScrollReader @@ -379,123 +369,9 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - @doc """ - Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number/epoch` endpoint. - """ - @spec celo_epoch(Plug.Conn.t(), map()) :: - {:error, :not_found | {:invalid, :hash | :number | :celo_election_reward_type}} - | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}} - | Plug.Conn.t() - def celo_epoch(conn, %{"block_hash_or_number" => block_hash_or_number}) do - params = [ - necessity_by_association: %{ - :celo_epoch_reward => :optional - }, - api?: true - ] - - with {:ok, block} <- block_param_to_block(block_hash_or_number, params), - :ok <- validate_epoch_block_number(block.number) do - epoch_number = block_number_to_epoch_number(block.number) - - epoch_distribution = - block - |> Map.get(:celo_epoch_reward) - |> case do - %CeloEpochReward{} = epoch_reward -> - CeloEpochReward.load_token_transfers(epoch_reward, api?: true) - - _ -> - nil - end - - aggregated_election_rewards = - CeloReader.block_hash_to_aggregated_election_rewards_by_type( - block.hash, - api?: true - ) - - conn - |> put_status(200) - |> put_view(CeloView) - |> render(:celo_epoch, %{ - epoch_number: epoch_number, - epoch_distribution: epoch_distribution, - aggregated_election_rewards: aggregated_election_rewards - }) - end - end - - @doc """ - Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number/election-rewards/:reward_type` endpoint. - """ - @spec celo_election_rewards(Plug.Conn.t(), map()) :: - {:error, :not_found | {:invalid, :hash | :number | :celo_election_reward_type}} - | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}} - | Plug.Conn.t() - def celo_election_rewards( - conn, - %{"block_hash_or_number" => block_hash_or_number, "reward_type" => reward_type} = params - ) do - with {:ok, reward_type_atom} <- celo_reward_type_to_atom(reward_type), - {:ok, block} <- - block_param_to_block(block_hash_or_number) do - address_associations = [:names, :smart_contract, proxy_implementations_association()] - - full_options = - [ - necessity_by_association: %{ - [account_address: address_associations] => :optional, - [associated_account_address: address_associations] => :optional - } - ] - |> Keyword.merge(CeloElectionReward.block_paging_options(params)) - |> Keyword.merge(@api_true) - - rewards_plus_one = - CeloReader.block_hash_to_election_rewards_by_type( - block.hash, - reward_type_atom, - full_options - ) - - {rewards, next_page} = split_list_by_page(rewards_plus_one) - - filtered_params = - params - |> delete_parameters_from_next_page_params() - |> Map.delete("reward_type") - - next_page_params = - next_page_params( - next_page, - rewards, - filtered_params, - &CeloElectionReward.to_block_paging_params/1 - ) - - conn - |> put_status(200) - |> put_view(CeloView) - |> render(:celo_election_rewards, %{ - rewards: rewards, - next_page_params: next_page_params - }) - end - end - defp block_param_to_block(block_hash_or_number, options \\ @api_true) do with {:ok, type, value} <- parse_block_hash_or_number_param(block_hash_or_number) do fetch_block(type, value, options) end end - - defp celo_reward_type_to_atom(reward_type_string) do - reward_type_string - |> CeloElectionReward.type_from_url_string() - |> case do - {:ok, type} -> {:ok, type} - :error -> {:error, {:invalid, :celo_election_reward_type}} - end - end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex new file mode 100644 index 000000000000..aa567feb12d4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -0,0 +1,199 @@ +defmodule BlockScoutWeb.API.V2.CeloController do + use BlockScoutWeb, :controller + + import Explorer.Helper, only: [safe_parse_non_negative_integer: 1] + + import BlockScoutWeb.Chain, + only: [ + next_page_params: 4, + split_list_by_page: 1 + ] + + import Explorer.PagingOptions, only: [default_paging_options: 0] + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + + alias Explorer.Chain.Hash + alias Explorer.Chain.Celo.{ElectionReward, Epoch} + alias Explorer.PagingOptions + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + @doc """ + Handles GET requests to `/api/v2/celo/epochs` endpoint. + """ + @spec epochs(Plug.Conn.t(), map()) :: Plug.Conn.t() + def epochs(conn, params) do + paging_options = + with {:ok, number_string} <- Map.fetch(params, "number"), + {:ok, number} <- parse_epoch_number(number_string) do + %{default_paging_options() | key: %{number: number}} + else + _ -> default_paging_options() + end + + options = [ + necessity_by_association: %{ + :end_processing_block => :optional, + :distribution => :optional + }, + paging_options: paging_options, + api?: true + ] + + {epochs, next_page} = + options + |> Epoch.fetched_epochs() + |> split_list_by_page() + + filtered_params = + params + |> delete_parameters_from_next_page_params() + |> Map.drop(["number"]) + + next_page_params = + next_page_params( + next_page, + epochs, + filtered_params, + &%{number: &1.number} + ) + + conn + |> render(:celo_epochs, %{ + epochs: epochs, + next_page_params: next_page_params + }) + end + + @doc """ + Handles GET requests to `/api/v2/celo/epochs/:number` endpoint. + """ + @spec epoch(Plug.Conn.t(), map()) :: Plug.Conn.t() + def epoch(conn, %{"number" => number_string}) do + options = [ + necessity_by_association: %{ + :distribution => :optional, + :start_processing_block => :optional, + :end_processing_block => :optional + }, + api?: true + ] + + with {:ok, number} <- parse_epoch_number(number_string), + {:ok, epoch} <- Epoch.from_number(number, options) do + aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, options) + + conn + |> render(:celo_epoch, %{ + epoch: epoch, + aggregated_election_rewards: aggregated_rewards + }) + end + end + + @doc """ + Handles GET requests to `/api/v2/celo/epochs/:number/election-rewards/:type` + endpoint. + """ + @spec election_rewards(Plug.Conn.t(), map()) :: Plug.Conn.t() + def election_rewards(conn, %{"number" => epoch_number_string, "type" => reward_type} = params) do + with {:ok, number} <- parse_epoch_number(epoch_number_string), + {:ok, reward_type_atom} <- parse_celo_reward_type(reward_type) do + address_associations = [:names, :smart_contract, proxy_implementations_association()] + + full_options = [ + necessity_by_association: %{ + [account_address: address_associations] => :optional, + [associated_account_address: address_associations] => :optional + }, + paging_options: election_rewards_paging_options(params), + api?: true + ] + + rewards_plus_one = + ElectionReward.epoch_number_and_type_to_rewards( + number, + reward_type_atom, + full_options + ) + + {rewards, next_page} = split_list_by_page(rewards_plus_one) + + filtered_params = + params + |> Map.drop([ + "number", + "type", + "amount", + "account_address_hash", + "associated_account_address_hash" + ]) + + next_page_params = + next_page_params( + next_page, + rewards, + filtered_params, + &%{ + amount: &1.amount, + account_address_hash: &1.account_address_hash, + associated_account_address_hash: &1.associated_account_address_hash + } + ) + + conn + |> render(:celo_epoch_election_rewards, %{ + rewards: rewards, + next_page_params: next_page_params + }) + end + end + + @spec election_rewards_paging_options(map()) :: PagingOptions.t() + defp election_rewards_paging_options(params) do + with %{ + "amount" => amount_string, + "account_address_hash" => account_address_hash_string, + "associated_account_address_hash" => associated_account_address_hash_string + } + when is_binary(amount_string) and + is_binary(account_address_hash_string) and + is_binary(associated_account_address_hash_string) <- params, + {amount, ""} <- Decimal.parse(amount_string), + true <- Decimal.compare(amount, Decimal.new(0)) == :gt, + {:ok, account_address_hash} <- Hash.Address.cast(account_address_hash_string), + {:ok, associated_account_address_hash} <- + Hash.Address.cast(associated_account_address_hash_string) do + %{ + default_paging_options() + | key: %{ + amount: amount, + account_address_hash: account_address_hash, + associated_account_address_hash: associated_account_address_hash + } + } + else + _ -> default_paging_options() + end + end + + @spec parse_epoch_number(String.t()) :: + {:ok, non_neg_integer()} | {:error, {:invalid, :number}} + defp parse_epoch_number(number) do + case safe_parse_non_negative_integer(number) do + {:ok, epoch_number} -> {:ok, epoch_number} + _ -> {:error, {:invalid, :number}} + end + end + + @spec parse_celo_reward_type(String.t()) :: + {:ok, ElectionReward.type()} | {:error, {:invalid, :celo_election_reward_type}} + defp parse_celo_reward_type(reward_type_string) do + reward_type_string + |> ElectionReward.type_from_url_string() + |> case do + {:ok, type} -> {:ok, type} + :error -> {:error, {:invalid, :celo_election_reward_type}} + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 298ba07f2e06..d6b6f81b9ec8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -197,8 +197,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do :celo -> defp add_chain_type_fields(response) do - import Explorer.Chain.Celo.Reader, only: [last_block_epoch_number: 0] - response |> Map.put("celo", %{"epoch_number" => last_block_epoch_number()}) + alias Explorer.Chain.Cache.CeloEpochs + response |> Map.put("celo", %{"epoch_number" => CeloEpochs.last_block_epoch_number()}) end _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 77321e71f313..ded1bfd258ed 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -248,9 +248,9 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/:address_hash_param/nft", V2.AddressController, :nft_list) get("/:address_hash_param/nft/collections", V2.AddressController, :nft_collections) - if @chain_type == :celo do - get("/:address_hash_param/election-rewards", V2.AddressController, :celo_election_rewards) - get("/:address_hash_param/election-rewards/csv", V2.CsvExportController, :celo_election_rewards_csv) + chain_scope :celo do + get("/:address_hash_param/celo/election-rewards", V2.AddressController, :celo_election_rewards) + get("/:address_hash_param/celo/election-rewards/csv", V2.CsvExportController, :celo_election_rewards_csv) end end @@ -314,6 +314,16 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end + scope "/celo" do + chain_scope :celo do + scope "/epochs" do + get("/", V2.CeloController, :epochs) + get("/:number", V2.CeloController, :epoch) + get("/:number/election-rewards/:type", V2.CeloController, :election_rewards) + end + end + end + scope "/polygon-edge" do chain_scope :polygon_edge do get("/deposits", V2.PolygonEdgeController, :deposits) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index ebde6640a938..3dcde14044ff 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -8,13 +8,11 @@ defmodule BlockScoutWeb.API.V2.CeloView do import Explorer.Chain.SmartContract, only: [dead_address_hash_string: 0] - alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView} - alias Ecto.Association.NotLoaded + alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView, TransactionView} alias Explorer.Chain - alias Explorer.Chain.Cache.CeloCoreContracts + alias Explorer.Chain.Cache.{CeloCoreContracts, CeloEpochs} alias Explorer.Chain.Celo.Helper, as: CeloHelper - alias Explorer.Chain.Celo.{ElectionReward, EpochReward} - alias Explorer.Chain.Hash + alias Explorer.Chain.Celo.{Epoch, EpochReward} alias Explorer.Chain.{Block, Token, Transaction, Wei} @address_params [ @@ -26,64 +24,57 @@ defmodule BlockScoutWeb.API.V2.CeloView do api?: true ] - def render("celo_epoch.json", %{epoch_number: epoch_number, epoch_distribution: nil}) do + def render("celo_epochs.json", %{ + epochs: epochs, + next_page_params: next_page_params + }) do %{ - number: epoch_number, - distribution: nil, - aggregated_election_rewards: nil + items: Enum.map(epochs, &prepare_epoch/1), + next_page_params: next_page_params } end - def render( - "celo_epoch.json", - %{ - epoch_number: epoch_number, - epoch_distribution: %EpochReward{ - reserve_bolster_transfer: reserve_bolster_transfer, - community_transfer: community_transfer, - carbon_offsetting_transfer: carbon_offsetting_transfer - }, - aggregated_election_rewards: aggregated_election_rewards - } - ) do + def render("celo_epoch.json", %{ + epoch: epoch, + aggregated_election_rewards: aggregated_election_rewards + }) do distribution_json = - Map.new( - [ - reserve_bolster_transfer: reserve_bolster_transfer, - community_transfer: community_transfer, - carbon_offsetting_transfer: carbon_offsetting_transfer - ], - fn {field, token_transfer} -> - token_transfer_json = - token_transfer && - TransactionView.render( - "token_transfer.json", - %{token_transfer: token_transfer, conn: nil} - ) - - {field, token_transfer_json} - end - ) + epoch.distribution + |> prepare_distribution() aggregated_election_rewards_json = - Map.new( - aggregated_election_rewards, - fn {type, %{total: total, count: count, token: token}} -> - {type, - %{ - total: total, - count: count, - token: - TokenView.render("token.json", %{ - token: token, - contract_address_hash: token && token.contract_address_hash - }) - }} - end - ) + aggregated_election_rewards + |> Map.new(fn {type, %{total: total, count: count, token: token}} -> + {type, + %{ + total: total, + count: count, + token: + TokenView.render("token.json", %{ + token: token, + contract_address_hash: token && token.contract_address_hash + }) + }} + end) + # For L2, delegated payments are implemented differently. They're + # distributed on-demand via direct payments rather than through epoch + # processing, so we need to handle them separately. + |> (&(if CeloHelper.premigration_epoch_number?(epoch.number) do + &1 + else + &1 + |> Map.put(:delegated_payment, nil) + end)).() %{ - number: epoch_number, + number: epoch.number, + start_block_number: epoch.start_block_number, + end_block_number: epoch.end_block_number, + timestamp: epoch.end_processing_block.timestamp, + start_processing_block_hash: epoch.start_processing_block.hash, + start_processing_block_number: epoch.start_processing_block.number, + end_processing_block_hash: epoch.end_processing_block.hash, + end_processing_block_number: epoch.end_processing_block.number, distribution: distribution_json, aggregated_election_rewards: aggregated_election_rewards_json } @@ -112,12 +103,66 @@ defmodule BlockScoutWeb.API.V2.CeloView do end end - def render("celo_election_rewards.json", %{ + def render("celo_epoch_election_rewards.json", %{ rewards: rewards, next_page_params: next_page_params }) do + rewards_json = + rewards + |> Enum.map(fn reward -> + %{ + amount: reward.amount, + account: + Helper.address_with_info( + reward.account_address, + reward.account_address_hash + ), + associated_account: + Helper.address_with_info( + reward.associated_account_address, + reward.associated_account_address_hash + ) + } + end) + + %{ + items: rewards_json, + next_page_params: next_page_params + } + end + + def render("celo_address_election_rewards.json", %{ + rewards: rewards, + next_page_params: next_page_params + }) do + rewards_json = + rewards + |> Enum.map(fn reward -> + %{ + amount: reward.amount, + epoch_number: reward.epoch_number, + block_timestamp: reward.epoch.end_processing_block.timestamp, + account: + Helper.address_with_info( + reward.account_address, + reward.account_address_hash + ), + associated_account: + Helper.address_with_info( + reward.associated_account_address, + reward.associated_account_address_hash + ), + type: reward.type, + token: + TokenView.render("token.json", %{ + token: reward.token, + contract_address_hash: reward.token.contract_address_hash + }) + } + end) + %{ - "items" => Enum.map(rewards, &prepare_election_reward/1), + "items" => rewards_json, "next_page_params" => next_page_params } end @@ -135,15 +180,28 @@ defmodule BlockScoutWeb.API.V2.CeloView do ## Returns - A map extended with data related to Celo. """ - def extend_block_json_response(out_json, %Block{} = block, single_block?) do + def extend_block_json_response(out_json, block, single_block?) do + epoch_number = CeloEpochs.block_number_to_epoch_number(block.number) + + l1_era_finalized_epoch_number = + if CeloHelper.pre_migration_block_number?(block.number) and + CeloHelper.epoch_block_number?(block.number) do + epoch_number - 1 + else + nil + end + celo_json = %{ - "is_epoch_block" => CeloHelper.epoch_block_number?(block.number), - "epoch_number" => CeloHelper.block_number_to_epoch_number(block.number) + # todo: keep `is_epoch_block = false` for compatibility with frontend and remove + # when new frontend is bound to `l1_era_finalized_epoch_number` property + is_epoch_block: false, + l1_era_finalized_epoch_number: l1_era_finalized_epoch_number, + epoch_number: epoch_number } |> maybe_add_base_fee_info(block, single_block?) - Map.put(out_json, "celo", celo_json) + Map.put(out_json, :celo, celo_json) end @doc """ @@ -189,55 +247,149 @@ defmodule BlockScoutWeb.API.V2.CeloView do Map.put(out_json, "celo", %{"gas_token" => token_json}) end - @spec prepare_election_reward(Explorer.Chain.Celo.ElectionReward.t()) :: %{ - :account => nil | %{optional(String.t()) => any()}, - :amount => Decimal.t(), - :associated_account => nil | %{optional(String.t()) => any()}, - optional(:block_hash) => Hash.Full.t(), - optional(:block_number) => Block.block_number(), - optional(:epoch_number) => non_neg_integer(), - optional(:type) => ElectionReward.type() - } - defp prepare_election_reward(%ElectionReward{block: %NotLoaded{}} = reward) do + @spec prepare_epoch(Epoch.t()) :: map() + defp prepare_epoch(epoch) do + community_transfer = + epoch.distribution.community_transfer |> TokenTransferView.prepare_token_transfer_total() + + carbon_offsetting_transfer = + epoch.distribution.carbon_offsetting_transfer |> TokenTransferView.prepare_token_transfer_total() + %{ - amount: reward.amount, - account: - Helper.address_with_info( - reward.account_address, - reward.account_address_hash - ), - associated_account: - Helper.address_with_info( - reward.associated_account_address, - reward.associated_account_address_hash - ) + number: epoch.number, + start_block_number: epoch.start_block_number, + end_block_number: epoch.end_block_number, + timestamp: epoch.end_processing_block.timestamp, + distribution: %{ + community_transfer: community_transfer, + carbon_offsetting_transfer: carbon_offsetting_transfer, + transfers_total: %{ + decimals: "18", + value: + Decimal.add( + community_transfer["value"], + carbon_offsetting_transfer["value"] + ) + } + } } end - defp prepare_election_reward(%ElectionReward{token: %Token{}, block: %Block{}} = reward) do - %{ - amount: reward.amount, - block_number: reward.block.number, - block_hash: reward.block_hash, - block_timestamp: reward.block.timestamp, - epoch_number: reward.block.number |> CeloHelper.block_number_to_epoch_number(), - account: - Helper.address_with_info( - reward.account_address, - reward.account_address_hash - ), - associated_account: - Helper.address_with_info( - reward.associated_account_address, - reward.associated_account_address_hash - ), - type: reward.type, - token: - TokenView.render("token.json", %{ - token: reward.token, - contract_address_hash: reward.token.contract_address_hash - }) - } + @spec prepare_distribution(EpochReward.t() | nil) :: + %{ + optional(:reserve_bolster_transfer) => nil | %{optional(String.t()) => any()}, + optional(:community_transfer) => nil | %{optional(String.t()) => any()}, + optional(:carbon_offsetting_transfer) => nil | %{optional(String.t()) => any()} + } + | nil + defp prepare_distribution(%EpochReward{} = distribution) do + transfers_json = + Map.new( + [ + reserve_bolster_transfer: distribution.reserve_bolster_transfer, + community_transfer: distribution.community_transfer, + carbon_offsetting_transfer: distribution.carbon_offsetting_transfer + ], + fn {field, token_transfer} -> + token_transfer_json = + token_transfer && + TransactionView.render( + "token_transfer.json", + %{token_transfer: token_transfer, conn: nil} + ) + + {field, token_transfer_json} + end + ) + + total = calculate_total_epoch_rewards(transfers_json) + + transfers_json + |> Map.put(:transfers_total, total) + end + + defp prepare_distribution(_), do: nil + + @doc """ + Calculates the total sum of all epoch reward transfers with token information. + + This function sums up all non-nil token transfers (reserve_bolster_transfer, + community_transfer, carbon_offsetting_transfer) and ensures they all use the + same token. If different tokens are found, it raises an error. + + ## Parameters + - `transfers_map` (`map()`): Map containing the rendered token transfers. + + ## Returns + - `%{token: map(), total: %{decimals: Decimal.t(), value: Decimal.t()}}`: + Token info and total sum, or `nil` if no transfers exist. + + ## Raises + - `ArgumentError`: If transfers use different tokens. + + ## Example + iex> transfers = %{ + ...> reserve_bolster_transfer: %{"token" => %{"address" => "0xABC..."}, "total" => %{"value" => Decimal.new("100")}}, + ...> community_transfer: %{"token" => %{"address" => "0xABC..."}, "total" => %{"value" => Decimal.new("200")}} + ...> } + iex> calculate_total_epoch_rewards(transfers) + %{ + token: %{"address" => "0xABC...", ...}, + total: %{decimals: Decimal.new("18"), value: Decimal.new("300")} + } + """ + @spec calculate_total_epoch_rewards(map()) :: map() | nil + def calculate_total_epoch_rewards(transfers) do + transfers = + transfers + |> Map.values() + |> Enum.reject(&is_nil/1) + + case transfers do + [] -> + nil + + [first_transfer | rest_transfers] -> + token_info = validate_and_extract_token(first_transfer, rest_transfers) + + total_value = + transfers + |> Enum.map(fn transfer -> + get_in(transfer, ["total", "value"]) || Decimal.new(0) + end) + |> Enum.reduce(Decimal.new(0), &Decimal.add/2) + + %{ + token: token_info, + total: %{ + decimals: token_info["decimals"], + value: total_value + } + } + end + end + + @spec validate_and_extract_token(map(), [map()]) :: map() + defp validate_and_extract_token(first_transfer, rest_transfers) do + first_token = get_in(first_transfer, ["token"]) + first_token_address = get_in(first_token, ["address"]) + + if is_nil(first_token_address) do + raise ArgumentError, "Token transfer missing token address information" + end + + # Validate all transfers use the same token + Enum.each(rest_transfers, fn transfer -> + token_address = get_in(transfer, ["token", "address"]) + + if token_address != first_token_address do + raise ArgumentError, + "Inconsistent token addresses found in epoch rewards. " <> + "Expected: #{first_token_address}, Found: #{token_address}" + end + end) + + first_token end # Get the breakdown of the base fee for the case when FeeHandler is a contract diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 8bbe921094d3..b25fe85d24f9 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -147,6 +147,7 @@ defmodule Explorer.Application do configure_sc_microservice(Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand), configure(Explorer.Chain.Cache.Counters.Rootstock.LockedBTCCount), configure(Explorer.Chain.Cache.OptimismFinalizationPeriod), + configure(Explorer.Chain.Cache.CeloEpochs), configure(Explorer.Migrator.TransactionsDenormalization), configure(Explorer.Migrator.AddressCurrentTokenBalanceTokenType), configure(Explorer.Migrator.AddressTokenBalanceTokenType), @@ -159,6 +160,7 @@ defmodule Explorer.Application do configure(Explorer.Migrator.RestoreOmittedWETHTransfers), configure(Explorer.Migrator.FilecoinPendingAddressOperations), configure(Explorer.Migrator.SmartContractLanguage), + configure(Explorer.Migrator.CeloL2Epochs), configure(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds), Explorer.Migrator.BackfillMultichainSearchDB |> configure_mode_dependent_process(:indexer) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 2817210b42eb..2a269a3ddd63 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -20,7 +20,7 @@ defmodule Explorer.Chain.Block.Schema do alias Explorer.Chain.Arbitrum.BatchBlock, as: ArbitrumBatchBlock alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} - alias Explorer.Chain.Celo.EpochReward, as: CeloEpochReward + alias Explorer.Chain.Celo.Epoch, as: CeloEpoch alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Zilliqa.AggregateQuorumCertificate, as: ZilliqaAggregateQuorumCertificate alias Explorer.Chain.Zilliqa.QuorumCertificate, as: ZilliqaQuorumCertificate @@ -76,10 +76,13 @@ defmodule Explorer.Chain.Block.Schema do :celo -> elem( quote do - has_one(:celo_epoch_reward, CeloEpochReward, foreign_key: :block_hash, references: :hash) + has_one(:celo_initiated_epoch, CeloEpoch, + foreign_key: :start_processing_block_hash, + references: :hash + ) - has_many(:celo_epoch_election_rewards, CeloEpochReward, - foreign_key: :block_hash, + has_one(:celo_terminated_epoch, CeloEpoch, + foreign_key: :end_processing_block_hash, references: :hash ) end, diff --git a/apps/explorer/lib/explorer/chain/cache/celo_epochs.ex b/apps/explorer/lib/explorer/chain/cache/celo_epochs.ex new file mode 100644 index 000000000000..56145b0e37e6 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/celo_epochs.ex @@ -0,0 +1,98 @@ +defmodule Explorer.Chain.Cache.CeloEpochs do + @moduledoc """ + Cache for efficiently mapping block numbers to epoch numbers in Celo blockchain. + + This implementation uses: + 1. A direct mathematical calculation for pre-migration epochs + 2. An ordered cache of post-migration epochs for efficient lookups + """ + + use Explorer.Chain.OrderedCache, + name: :celo_epochs_cache, + ttl_check_interval: :timer.minutes(1), + global_ttl: :timer.minutes(5), + # Adjust based on expected number of post-migration epochs + max_size: 256 + + @type element :: %{ + number: non_neg_integer(), + start_block_number: non_neg_integer(), + end_block_number: non_neg_integer() | nil + } + + @type id :: non_neg_integer() + + import Ecto.Query, only: [select: 3] + + alias Explorer.Chain + alias Explorer.Chain.Block + alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Chain.Celo.{Epoch, Helper} + + @impl Explorer.Chain.OrderedCache + def element_to_id(epoch) when is_map(epoch), do: epoch.number + + @doc """ + Gets the epoch number for a given block number. Uses mathematical formula for + pre-migration blocks and cached data for post-migration blocks. + """ + def block_number_to_epoch_number(block_number) do + block_number + |> Helper.pre_migration_block_number?() + |> if do + # For pre-migration blocks, use the mathematical formula + Helper.block_number_to_epoch_number(block_number) + else + # For post-migration blocks, use the ordered cache + fetch_post_migration_epoch_number(block_number) + end + end + + @doc """ + Retrieves the epoch number of the last fetched block. + """ + @spec last_block_epoch_number() :: Block.block_number() + def last_block_epoch_number do + BlockNumber.get_max() |> block_number_to_epoch_number() + end + + @spec fetch_post_migration_epoch_number(non_neg_integer()) :: non_neg_integer() | nil + defp fetch_post_migration_epoch_number(block_number) do + with {:cache, nil} <- {:cache, fetch_epoch_from_cache(block_number)}, + {:db, nil} <- {:db, fetch_epoch_from_db(block_number)} do + nil + else + {source, epoch} -> + # If the epoch is found in the database, update the cache + if source == :db do + update(epoch) + end + + epoch.number + end + end + + @spec fetch_epoch_from_cache(non_neg_integer()) :: element() | nil + defp fetch_epoch_from_cache(block_number) do + Enum.find(all(), fn + %{end_block_number: nil} = epoch -> + epoch.start_block_number <= block_number + + epoch -> + epoch.start_block_number <= block_number and + block_number <= epoch.end_block_number + end) + end + + @spec fetch_epoch_from_db(non_neg_integer()) :: element() | nil + defp fetch_epoch_from_db(block_number) do + block_number + |> Epoch.block_number_to_epoch_query() + |> select([e], %{ + number: e.number, + start_block_number: e.start_block_number, + end_block_number: e.end_block_number + }) + |> Chain.select_repo(api?: true).one() + end +end diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index 6b69d02e2b37..7102621f98cd 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -29,12 +29,11 @@ defmodule Explorer.Chain.Celo.ElectionReward do use Explorer.Schema import Explorer.PagingOptions, only: [default_paging_options: 0] - import Ecto.Query, only: [from: 2, where: 3] - import Explorer.Helper, only: [safe_parse_non_negative_integer: 1] + import Ecto.Query, only: [from: 2, where: 3, group_by: 3, select: 3] alias Explorer.Chain.Cache.CeloCoreContracts - alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Address, Block, Hash, Token, Wei} + alias Explorer.{Chain, SortingHelper} + alias Explorer.Chain.{Address, Celo.Epoch, Hash, Token, Wei} @type type :: :voter | :validator | :group | :delegated_payment @types_enum ~w(voter validator group delegated_payment)a @@ -60,7 +59,7 @@ defmodule Explorer.Chain.Celo.ElectionReward do :delegated_payment => :usd_token } - @required_attrs ~w(amount type block_hash account_address_hash associated_account_address_hash)a + @required_attrs ~w(amount type epoch_number account_address_hash associated_account_address_hash)a @primary_key false typed_schema "celo_election_rewards" do @@ -74,14 +73,11 @@ defmodule Explorer.Chain.Celo.ElectionReward do primary_key: true ) - belongs_to( - :block, - Block, + belongs_to(:epoch, Epoch, primary_key: true, - foreign_key: :block_hash, - references: :hash, - type: Hash.Full, - null: false + foreign_key: :epoch_number, + references: :number, + type: :integer ) belongs_to( @@ -151,99 +147,207 @@ defmodule Explorer.Chain.Celo.ElectionReward do end @doc """ - Returns a map of reward type atoms to their corresponding token atoms. + Converts a reward type string to its corresponding atom. + + ## Parameters + - `type_string` (`String.t()`): The string representation of the reward type. ## Returns - - A map where the keys are reward type atoms and the values are token atoms. + - `{:ok, type}` if the string is valid, `:error` otherwise. ## Examples - iex> ElectionReward.reward_type_atom_to_token_atom() - %{voter: :celo_token, validator: :usd_token, group: :usd_token, delegated_payment: :usd_token} + iex> ElectionReward.type_from_string("voter") + {:ok, :voter} + + iex> ElectionReward.type_from_string("invalid") + :error """ - @spec reward_type_atom_to_token_atom() :: %{type => atom()} - def reward_type_atom_to_token_atom, do: @reward_type_atom_to_token_atom + @spec type_from_string(String.t()) :: {:ok, type} | :error + def type_from_string(type_string) do + Map.fetch(@reward_type_string_to_atom, type_string) + end @doc """ - Builds a query to aggregate rewards by type for a given block hash. + Retrieves aggregated election rewards by block hash. ## Parameters - - `block_hash` (`Hash.Full.t()`): The block hash to filter rewards. + - `block_hash` (`Hash.Full.t()`): The block hash to aggregate election + rewards. + - `options` (`Keyword.t()`): Optional parameters for fetching data. ## Returns - - An Ecto query. + - `%{atom() => Wei.t() | nil}`: A map of aggregated election rewards by type. + + ## Examples + + iex> block_hash = %Hash.Full{ + ...> byte_count: 32, + ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> + ...> } + iex> Explorer.Chain.Celo.Reader.epoch_number_to_rewards_aggregated_by_type(block_hash) + %{voter_reward: %{total: %Decimal{}, count: 2}, ...} """ - @spec block_hash_to_aggregated_rewards_by_type_query(Hash.Full.t()) :: Ecto.Query.t() - def block_hash_to_aggregated_rewards_by_type_query(block_hash) do - from( - r in __MODULE__, - where: r.block_hash == ^block_hash, - select: {r.type, sum(r.amount), count(r)}, - group_by: r.type - ) + @spec epoch_number_to_rewards_aggregated_by_type(integer(), Keyword.t()) :: + %{atom() => %{total: Decimal.t(), count: integer(), token: map() | nil}} + def epoch_number_to_rewards_aggregated_by_type(epoch_number, options \\ []) do + reward_type_to_aggregated_rewards = + __MODULE__ + |> where([r], r.epoch_number == ^epoch_number) + |> group_by([r], r.type) + |> select([r], {r.type, sum(r.amount), count(r)}) + |> Chain.select_repo(options).all() + |> Map.new(fn {type, total, count} -> + {type, %{total: total, count: count}} + end) + + reward_type_to_token = election_reward_tokens_by_type(options) + + @types_enum + |> Map.new(&{&1, %{total: Decimal.new(0), count: 0}}) + |> Map.merge(reward_type_to_aggregated_rewards) + |> Map.new(fn {type, aggregated_reward} -> + token = reward_type_to_token[type] + aggregated_reward_with_token = Map.put(aggregated_reward, :token, token) + {type, aggregated_reward_with_token} + end) + end + + # Retrieves the token for each type of election reward. + # + # ## Parameters + # - `options` (`Keyword.t()`): Optional parameters for fetching data. + # + # ## Returns + # - `%{atom() => Token.t() | nil}`: A map of reward types to token. + # + # ## Examples + # + # iex> epoch_number = %Hash.Full{ + # ...> byte_count: 32, + # ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> + # ...> } + # iex> Explorer.Chain.Celo.ElectionReward.election_reward_token_addresses_by_type(epoch_number) + # %{voter_reward: %Token{}, ...} + @spec election_reward_tokens_by_type(Keyword.t()) :: %{atom() => Token.t() | nil} + defp election_reward_tokens_by_type(options) do + reward_type_to_token_address_hash = reward_type_to_token_address_hash() + + tokens = + reward_type_to_token_address_hash + |> Map.values() + |> Token.get_by_contract_address_hashes(options) + + reward_type_to_token_address_hash + |> Map.new(fn {type, address_hash} -> + token = Enum.find(tokens, &(&1.contract_address_hash == address_hash)) + {type, token} + end) end @doc """ - Builds a query to get rewards by type for a given block hash. + Retrieves election rewards by epoch number and reward type. ## Parameters - - `block_hash` (`Hash.Full.t()`): The block hash to filter rewards. - - `reward_type` (`type`): The type of reward to filter. + - `epoch_number` (`Hash.t()`): The epoch number to search for election rewards. + - `reward_type` (`ElectionReward.type()`): The type of reward to filter. + - `options` (`Keyword.t()`): Optional parameters for fetching data. ## Returns - - An Ecto query. + - `[ElectionReward.t()]`: A list of election rewards filtered by epoch number + and reward type. + """ - @spec block_hash_to_rewards_by_type_query(Hash.Full.t(), type) :: Ecto.Query.t() - def block_hash_to_rewards_by_type_query(block_hash, reward_type) do - from( - r in __MODULE__, - where: r.block_hash == ^block_hash and r.type == ^reward_type, - select: r, - order_by: [ - desc: :amount, - asc: :account_address_hash, - asc: :associated_account_address_hash - ] - ) + @spec epoch_number_and_type_to_rewards(integer(), type(), Keyword.t()) :: [__MODULE__.t()] + def epoch_number_and_type_to_rewards(epoch_number, reward_type, options \\ []) + when reward_type in @types_enum do + default_sorting = [ + desc: :amount, + asc: :account_address_hash, + asc: :associated_account_address_hash + ] + + paging_options = Keyword.get(options, :paging_options, default_paging_options()) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + sorting_options = Keyword.get(options, :sorting, []) + + __MODULE__ + |> where([r], r.epoch_number == ^epoch_number) + |> where([r], r.type == ^reward_type) + |> SortingHelper.apply_sorting(sorting_options, default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() end - @doc """ - Builds a query to get rewards by account address hash. - """ - @spec address_hash_to_rewards_query(Hash.Address.t()) :: Ecto.Query.t() def address_hash_to_rewards_query(address_hash) do - from( - r in __MODULE__, - where: r.account_address_hash == ^address_hash, - select: r - ) + __MODULE__ + |> where([r], r.account_address_hash == ^address_hash) end @doc """ - Builds a query to get ordered rewards by account address hash. + Retrieves election rewards associated with a given address hash. ## Parameters - - `address_hash` (`Hash.Address.t()`): The account address hash to filter + - `address_hash` (`Hash.Address.t()`): The address hash to search for election rewards. + - `options` (`Keyword.t()`): Optional parameters for fetching data. ## Returns - - An Ecto query. + - `[ElectionReward.t()]`: A list of election rewards associated with the + address hash. + + ## Examples + + iex> address_hash = %Hash.Address{ + ...> byte_count: 20, + ...> bytes: <<0x1d1f7f0e1441c37e28b89e0b5e1edbbd34d77649 :: size(160)>> + ...> } + iex> Explorer.Chain.Celo.ElectionReward.address_hash_to_rewards(address_hash) + [%ElectionReward{}, ...] """ - @spec address_hash_to_ordered_rewards_query(Hash.Address.t()) :: Ecto.Query.t() - def address_hash_to_ordered_rewards_query(address_hash) do - from( - r in __MODULE__, - join: b in assoc(r, :block), - as: :block, - preload: [block: b], - where: r.account_address_hash == ^address_hash, - select: r, - order_by: [ - desc: b.number, - desc: r.amount, - asc: r.associated_account_address_hash, - asc: r.type - ] + @spec address_hash_to_rewards(Hash.Address.t(), Keyword.t()) :: [__MODULE__.t()] + def address_hash_to_rewards(address_hash, options \\ []) do + default_sorting = [ + desc: :epoch_number, + asc: :type, + desc: :amount, + asc: :associated_account_address_hash + ] + + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, default_paging_options()) + sorting_options = Keyword.get(options, :sorting, []) + + address_hash + |> address_hash_to_rewards_query() + |> join_token() + |> SortingHelper.apply_sorting(sorting_options, default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + + defp reward_type_to_token_address_hash do + Map.new( + @reward_type_atom_to_token_atom, + fn {type, token_atom} -> + addresses = + token_atom + |> CeloCoreContracts.get_address_updates() + |> case do + {:ok, addresses} -> addresses + _ -> [] + end + |> Enum.map(fn %{"address" => address_hash_string} -> + {:ok, address_hash} = Hash.Address.cast(address_hash_string) + address_hash + end) + + # This match should never fail + [address] = addresses + {type, address} + end ) end @@ -258,31 +362,7 @@ defmodule Explorer.Chain.Celo.ElectionReward do """ @spec join_token(Ecto.Query.t()) :: Ecto.Query.t() def join_token(query) do - # This match should never fail - %{ - voter: [voter_token_address_hash], - validator: [validator_token_address_hash], - group: [group_token_address_hash], - delegated_payment: [delegated_payment_token_address_hash] - } = - Map.new( - @reward_type_atom_to_token_atom, - fn {type, token_atom} -> - addresses = - token_atom - |> CeloCoreContracts.get_address_updates() - |> case do - {:ok, addresses} -> addresses - _ -> [] - end - |> Enum.map(fn %{"address" => address_hash_string} -> - {:ok, address_hash} = Hash.Address.cast(address_hash_string) - address_hash - end) - - {type, addresses} - end - ) + reward_type_to_token_address_hash = reward_type_to_token_address_hash() from( r in query, @@ -301,228 +381,18 @@ defmodule Explorer.Chain.Celo.ElectionReward do """, r.type, ^"voter", - ^voter_token_address_hash.bytes, + ^reward_type_to_token_address_hash.voter.bytes, ^"validator", - ^validator_token_address_hash.bytes, + ^reward_type_to_token_address_hash.validator.bytes, ^"group", - ^group_token_address_hash.bytes, + ^reward_type_to_token_address_hash.group.bytes, ^"delegated_payment", - ^delegated_payment_token_address_hash.bytes + ^reward_type_to_token_address_hash.delegated_payment.bytes ), select_merge: %{token: t} ) end - @doc """ - Makes Explorer.PagingOptions map for election rewards. - """ - @spec block_paging_options(map()) :: [Chain.paging_options()] - def block_paging_options(params) do - with %{ - "amount" => amount_string, - "account_address_hash" => account_address_hash_string, - "associated_account_address_hash" => associated_account_address_hash_string - } - when is_binary(amount_string) and - is_binary(account_address_hash_string) and - is_binary(associated_account_address_hash_string) <- params, - {amount, ""} <- Decimal.parse(amount_string), - {:ok, account_address_hash} <- Hash.Address.cast(account_address_hash_string), - {:ok, associated_account_address_hash} <- - Hash.Address.cast(associated_account_address_hash_string) do - [ - paging_options: %{ - default_paging_options() - | key: {amount, account_address_hash, associated_account_address_hash} - } - ] - else - _ -> - [paging_options: default_paging_options()] - end - end - - @doc """ - Makes Explorer.PagingOptions map for election rewards. - """ - @spec address_paging_options(map()) :: [Chain.paging_options()] - def address_paging_options(params) do - with %{ - "block_number" => block_number_string, - "amount" => amount_string, - "associated_account_address_hash" => associated_account_address_hash_string, - "type" => type_string - } - when is_binary(block_number_string) and - is_binary(amount_string) and - is_binary(associated_account_address_hash_string) and - is_binary(type_string) <- params, - {:ok, block_number} <- safe_parse_non_negative_integer(block_number_string), - {amount, ""} <- Decimal.parse(amount_string), - {:ok, associated_account_address_hash} <- - Hash.Address.cast(associated_account_address_hash_string), - {:ok, type} <- Map.fetch(@reward_type_string_to_atom, type_string) do - [ - paging_options: %{ - default_paging_options() - | key: {block_number, amount, associated_account_address_hash, type} - } - ] - else - _ -> - [paging_options: default_paging_options()] - end - end - - @doc """ - Paginates the given query based on the provided `PagingOptions`. - - ## Parameters - - `query` (`Ecto.Query.t()`): The query to paginate. - - `paging_options` (`PagingOptions.t()`): The pagination options. - - ## Returns - - An Ecto query with pagination applied. - """ - def paginate(query, %PagingOptions{key: nil}), do: query - - def paginate(query, %PagingOptions{key: {0 = _amount, account_address_hash, associated_account_address_hash}}) do - where( - query, - [reward], - reward.amount == 0 and - (reward.account_address_hash > ^account_address_hash or - (reward.account_address_hash == ^account_address_hash and - reward.associated_account_address_hash > ^associated_account_address_hash)) - ) - end - - def paginate(query, %PagingOptions{key: {amount, account_address_hash, associated_account_address_hash}}) do - where( - query, - [reward], - reward.amount < ^amount or - (reward.amount == ^amount and - reward.account_address_hash > ^account_address_hash) or - (reward.amount == ^amount and - reward.account_address_hash == ^account_address_hash and - reward.associated_account_address_hash > ^associated_account_address_hash) - ) - end - - def paginate(query, %PagingOptions{key: {0 = _block_number, 0 = _amount, associated_account_address_hash, type}}) do - where( - query, - [reward, block], - block.number == 0 and reward.amount == 0 and - (reward.associated_account_address_hash > ^associated_account_address_hash or - (reward.associated_account_address_hash == ^associated_account_address_hash and - reward.type > ^type)) - ) - end - - def paginate(query, %PagingOptions{key: {0 = _block_number, amount, associated_account_address_hash, type}}) do - where( - query, - [reward, block], - block.number == 0 and - (reward.amount < ^amount or - (reward.amount == ^amount and - reward.associated_account_address_hash > ^associated_account_address_hash) or - (reward.amount == ^amount and - reward.associated_account_address_hash == ^associated_account_address_hash and - reward.type > ^type)) - ) - end - - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - def paginate(query, %PagingOptions{key: {block_number, 0 = _amount, associated_account_address_hash, type}}) do - where( - query, - [reward, block], - block.number < ^block_number or - (block.number == ^block_number and - reward.amount == 0 and - reward.associated_account_address_hash > ^associated_account_address_hash) or - (block.number == ^block_number and - reward.amount == 0 and - reward.associated_account_address_hash == ^associated_account_address_hash and - reward.type > ^type) - ) - end - - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - def paginate(query, %PagingOptions{key: {block_number, amount, associated_account_address_hash, type}}) do - where( - query, - [reward, block], - block.number < ^block_number or - (block.number == ^block_number and - reward.amount < ^amount) or - (block.number == ^block_number and - reward.amount == ^amount and - reward.associated_account_address_hash > ^associated_account_address_hash) or - (block.number == ^block_number and - reward.amount == ^amount and - reward.associated_account_address_hash == ^associated_account_address_hash and - reward.type > ^type) - ) - end - - @doc """ - Converts an `ElectionReward` struct to paging parameters on the block view. - - ## Parameters - - `reward` (`%__MODULE__{}`): The election reward struct. - - ## Returns - - A map representing the block paging parameters. - - ## Examples - - iex> ElectionReward.to_block_paging_params(%ElectionReward{amount: 1000, account_address_hash: "0x123", associated_account_address_hash: "0x456"}) - %{"amount" => 1000, "account_address_hash" => "0x123", "associated_account_address_hash" => "0x456"} - """ - def to_block_paging_params(%__MODULE__{ - amount: amount, - account_address_hash: account_address_hash, - associated_account_address_hash: associated_account_address_hash - }) do - %{ - "amount" => amount, - "account_address_hash" => account_address_hash, - "associated_account_address_hash" => associated_account_address_hash - } - end - - @doc """ - Converts an `ElectionReward` struct to paging parameters on the address view. - - ## Parameters - - `reward` (`%__MODULE__{}`): The election reward struct. - - ## Returns - - A map representing the address paging parameters. - - ## Examples - - iex> ElectionReward.to_address_paging_params(%ElectionReward{block_number: 1, amount: 1000, associated_account_address_hash: "0x456", type: :voter}) - %{"block_number" => 1, "amount" => 1000, "associated_account_address_hash" => "0x456", "type" => :voter} - """ - def to_address_paging_params(%__MODULE__{ - block: %Block{number: block_number}, - amount: amount, - associated_account_address_hash: associated_account_address_hash, - type: type - }) do - %{ - "block_number" => block_number, - "amount" => amount, - "associated_account_address_hash" => associated_account_address_hash, - "type" => type - } - end - @doc """ Custom filter for `ElectionReward`, inspired by `Chain.where_block_number_in_period/3`. diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex new file mode 100644 index 000000000000..43542b2cd789 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -0,0 +1,333 @@ +defmodule Explorer.Chain.Celo.Epoch do + @moduledoc """ + Schema for Celo blockchain epochs. + """ + + use Explorer.Schema + + import Ecto.Query, only: [from: 2, where: 2] + + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, + only: [proxy_implementations_association: 0] + + alias Explorer.{Chain, Repo, SortingHelper} + + alias Explorer.Chain.{ + Block, + Celo.ElectionReward, + Celo.EpochReward, + Hash, + TokenTransfer + } + + @required_attrs ~w(number)a + @optional_attrs ~w(fetched? start_block_number end_block_number start_processing_block_hash end_processing_block_hash)a + + @default_paging_options Chain.default_paging_options() + + @typedoc """ + * `number` - The epoch number. + * `fetched?` - Indicates whether the epoch has been fetched. + * `start_block_number` - The starting block number of the epoch. + * `end_block_number` - The ending block number of the epoch. + * `start_processing_block_hash` - The hash of the block where the epoch + processing starts. + * `end_processing_block_hash` - The hash of the block where the epoch + processing ends. + """ + @primary_key false + typed_schema "celo_epochs" do + field(:number, :integer, primary_key: true) + + field(:fetched?, :boolean, + source: :is_fetched, + default: false, + null: false + ) + + field(:start_block_number, :integer) + field(:end_block_number, :integer) + + belongs_to(:start_processing_block, Block, + foreign_key: :start_processing_block_hash, + references: :hash, + type: Hash.Full + ) + + belongs_to(:end_processing_block, Block, + foreign_key: :end_processing_block_hash, + references: :hash, + type: Hash.Full + ) + + has_one(:distribution, EpochReward, + foreign_key: :epoch_number, + references: :number + ) + + has_many(:election_reward, ElectionReward, + foreign_key: :epoch_number, + references: :number + ) + + timestamps() + end + + @spec changeset( + __MODULE__.t(), + :invalid | %{optional(:__struct__) => none, optional(atom | binary) => any} + ) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = vote, attrs \\ %{}) do + vote + |> cast(attrs, @required_attrs ++ @optional_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:start_processing_block_hash) + |> foreign_key_constraint(:end_processing_block_hash) + end + + @doc """ + Returns a stream of epochs with unfetched rewards. + """ + @spec stream_unfetched_epochs( + initial :: accumulator, + reducer :: (entry :: term(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_unfetched_epochs(initial, reducer, limited? \\ false) + when is_function(reducer, 2) do + query = + from( + epoch in __MODULE__, + join: start_processing_block in assoc(epoch, :start_processing_block), + join: end_processing_block in assoc(epoch, :end_processing_block), + where: + epoch.fetched? == false and + start_processing_block.consensus == true and + end_processing_block.consensus == true, + order_by: [desc: epoch.number] + ) + + query + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Retrieves all epochs that have been marked as fetched. + + ## Parameters + - `options` (`Keyword.t()`): Options for filtering and ordering the epochs. + - `:paging_options` - pagination parameters + - `:necessity_by_association` - associations that need to be loaded + - `:sorting` - sorting parameters + + ## Returns + - `list(__MODULE__.t())`: A list of fetched epochs. + + ## Examples + + iex> Explorer.Chain.Celo.Epoch.fetched_epochs([]) + [%Explorer.Chain.Celo.Epoch{number: 42, fetched?: true, ...}, ...] + + iex> Explorer.Chain.Celo.Epoch.fetched_epochs(sorting: [asc: :number], paging_options: %{page_size: 10}) + [%Explorer.Chain.Celo.Epoch{number: 1, fetched?: true, ...}, ...] + """ + @spec fetched_epochs(Keyword.t()) :: [__MODULE__.t()] + def fetched_epochs(options) do + default_sorting = [desc: :number] + + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + sorting_options = Keyword.get(options, :sorting, []) + + __MODULE__ + |> where(fetched?: true) + |> SortingHelper.apply_sorting(sorting_options, default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + |> Enum.map(&with_loaded_distribution_token_transfers(&1, options)) + end + + @doc """ + Returns a query to find an epoch by its number. + + ## Parameters + - `number` (`integer()`): The epoch number to search for. + + ## Returns + - `Ecto.Query.t()`: The query to find the epoch. + + ## Examples + + iex> Repo.one(epoch_by_number_query(42)) + %Epoch{number: 42, start_block_number: 123400, end_block_number: 123799} + + iex> Repo.one(epoch_by_number_query(999999)) + nil + """ + @spec epoch_by_number_query(integer()) :: Ecto.Query.t() + def epoch_by_number_query(number) do + __MODULE__ + |> where(number: ^number) + end + + @doc """ + Retrieves an epoch by its number. This function fetches the epoch from the + database and preloads its associated data based on the provided options. It + always preloads distribution token transfers. + + ## Parameters + - `number` (`integer()`): The epoch number to search for. + - `options` (`Keyword.t()`): Options for filtering and ordering the epochs. + - `:necessity_by_association` - associations that need to be loaded + - `:sorting` - sorting parameters + ## Returns + - `{:ok, __MODULE__.t()}`: The epoch struct if found. + - `{:error, :not_found}`: If the epoch is not found. + + ## Examples + iex> Explorer.Chain.Celo.Epoch.from_number(42, []) + {:ok, %Epoch{number: 42, start_block_number: 123400, end_block_number: 123799}} + + iex> Explorer.Chain.Celo.Epoch.from_number(999999, []) + {:error, :not_found} + """ + @spec from_number(integer(), Keyword.t()) :: + {:ok, __MODULE__.t()} | {:error, :not_found} + def from_number(number, options) when is_integer(number) and is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + number + |> epoch_by_number_query() + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).one() + |> with_loaded_distribution_token_transfers(options) + |> case do + nil -> + {:error, :not_found} + + epoch -> + {:ok, epoch} + end + end + + # Loads the token transfers for the epoch with preloaded epoch reward. + # + # This function retrieves token transfers related to the specified epoch reward + # by knowing the index of the log of the token transfer and populates the + # virtual fields in the `EpochReward` struct. We manually preload token + # transfers since Ecto does not support automatically preloading objects by + # composite key (i.e., `log_index` and `block_hash`). + # + # ## Parameters + # - `epoch_reward` (`EpochReward.t()`): The epoch reward struct. + # - `options` (`Keyword.t()`): Optional parameters for selecting the repository. + # + # ## Returns + # - `EpochReward.t()`: The epoch reward struct with the token transfers loaded. + # + # ## Example + # + # iex> epoch_reward = %Explorer.Chain.Celo.EpochReward{block_hash: "some_hash", reserve_bolster_transfer_log_index: 1} + # iex> Explorer.Chain.Celo.EpochReward.load_token_transfers(epoch_reward) + # %Explorer.Chain.Celo.EpochReward{ + # block_hash: "some_hash", + # reserve_bolster_transfer_log_index: 1, + # reserve_bolster_transfer: %Explorer.Chain.TokenTransfer{log_index: 1, ...} + # } + # + @spec with_loaded_distribution_token_transfers(__MODULE__.t() | nil, api?: boolean()) :: + __MODULE__.t() | nil + defp with_loaded_distribution_token_transfers( + %__MODULE__{ + end_processing_block_hash: block_hash, + distribution: %EpochReward{ + reserve_bolster_transfer_log_index: reserve_bolster_transfer_log_index, + community_transfer_log_index: community_transfer_log_index, + carbon_offsetting_transfer_log_index: carbon_offsetting_transfer_log_index + } + } = epoch, + options + ) do + virtual_field_to_log_index = [ + reserve_bolster_transfer: reserve_bolster_transfer_log_index, + community_transfer: community_transfer_log_index, + carbon_offsetting_transfer: carbon_offsetting_transfer_log_index + ] + + log_indexes = + virtual_field_to_log_index + |> Enum.map(fn {_, index} -> index end) + |> Enum.reject(&is_nil/1) + + query = + from( + tt in TokenTransfer.only_consensus_transfers_query(), + where: tt.log_index in ^log_indexes and tt.block_hash == ^block_hash, + select: {tt.log_index, tt}, + preload: [ + :token, + [from_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]], + [to_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]] + ] + ) + + log_index_to_token_transfer = + query + |> Chain.select_repo(options).all() + |> Map.new() + + with_token_transfers = + Enum.reduce(virtual_field_to_log_index, epoch.distribution, fn + {field, log_index}, acc -> + token_transfer = Map.get(log_index_to_token_transfer, log_index) + Map.put(acc, field, token_transfer) + end) + + %__MODULE__{epoch | distribution: with_token_transfers} + end + + defp with_loaded_distribution_token_transfers(epoch, _options), do: epoch + + @doc """ + Returns a query to find an epoch containing the given block number. + + When multiple epochs with nil end_block_number match the block number, the one + with the maximum start_block_number is selected. + + ## Parameters + - `block_number` (`non_neg_integer()`): The block number to search for. + + ## Returns + - `Ecto.Query.t()`: The query to find the epoch. + + ## Examples + + iex> Repo.one(block_number_to_epoch_query(123456)) + %Epoch{number: 42, start_block_number: 123400, end_block_number: 123799} + + iex> Repo.one(block_number_to_epoch_query(123800)) + %Epoch{number: 43, start_block_number: 123800, end_block_number: nil} + + iex> Repo.one(block_number_to_epoch_query(999999)) + nil + """ + @spec block_number_to_epoch_query(non_neg_integer()) :: Ecto.Query.t() + def block_number_to_epoch_query(block_number) do + from(e in __MODULE__, + # First, find epochs where the block_number falls within a defined range + # OR it's an open-ended epoch with start_block_number <= block_number + where: + e.start_block_number <= ^block_number and + (is_nil(e.end_block_number) or + e.end_block_number >= ^block_number), + # Order by end_block_number nulls last (closed epochs first) + # Then by start_block_number descending (most recent open epoch) + order_by: [asc_nulls_last: e.end_block_number, desc: e.start_block_number], + # Limit to just one result + limit: 1 + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex index 85ed60fb1fa5..3b3a8b4f0275 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex @@ -6,34 +6,37 @@ defmodule Explorer.Chain.Celo.EpochReward do """ use Explorer.Schema - import Ecto.Query, only: [from: 2] - import Explorer.Chain, only: [select_repo: 1] - import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + alias Explorer.Chain.{Celo.Epoch, TokenTransfer} - alias Explorer.Chain.Celo.EpochReward - alias Explorer.Chain.{Block, Hash, TokenTransfer} - - @required_attrs ~w(block_hash)a + @required_attrs ~w(epoch_number)a @optional_attrs ~w(reserve_bolster_transfer_log_index community_transfer_log_index carbon_offsetting_transfer_log_index)a + # @optional_attrs [ + # :reserve_bolster_transfer_log_index, + # :reserve_bolster_transfer_value, + # :community_transfer_log_index, + # :community_transfer_value, + # :carbon_offsetting_transfer_log_index, + # :carbon_offsetting_transfer_value + # ] @allowed_attrs @required_attrs ++ @optional_attrs @primary_key false typed_schema "celo_epoch_rewards" do field(:reserve_bolster_transfer_log_index, :integer) + # field(:reserve_bolster_transfer_value, :integer) field(:community_transfer_log_index, :integer) + # field(:community_transfer_value, :integer) field(:carbon_offsetting_transfer_log_index, :integer) + # field(:carbon_offsetting_transfer_value, :integer) field(:reserve_bolster_transfer, :any, virtual: true) :: TokenTransfer.t() | nil field(:community_transfer, :any, virtual: true) :: TokenTransfer.t() | nil field(:carbon_offsetting_transfer, :any, virtual: true) :: TokenTransfer.t() | nil - belongs_to( - :block, - Block, + belongs_to(:epoch, Epoch, primary_key: true, - foreign_key: :block_hash, - references: :hash, - type: Hash.Full, - null: false + foreign_key: :epoch_number, + references: :number, + type: :integer ) timestamps() @@ -47,72 +50,4 @@ defmodule Explorer.Chain.Celo.EpochReward do |> foreign_key_constraint(:block_hash) |> unique_constraint(:block_hash) end - - @doc """ - Loads the token transfers for the given epoch reward. - - This function retrieves token transfers related to the specified epoch reward - by knowing the index of the log of the token transfer and populates the - virtual fields in the `EpochReward` struct. We manually preload token - transfers since Ecto does not support automatically preloading objects by - composite key (i.e., `log_index` and `block_hash`). - - ## Parameters - - `epoch_reward` (`EpochReward.t()`): The epoch reward struct. - - `options` (`Keyword.t()`): Optional parameters for selecting the repository. - - ## Returns - - `EpochReward.t()`: The epoch reward struct with the token transfers loaded. - - ## Example - - iex> epoch_reward = %Explorer.Chain.Celo.EpochReward{block_hash: "some_hash", reserve_bolster_transfer_log_index: 1} - iex> Explorer.Chain.Celo.EpochReward.load_token_transfers(epoch_reward) - %Explorer.Chain.Celo.EpochReward{ - block_hash: "some_hash", - reserve_bolster_transfer_log_index: 1, - reserve_bolster_transfer: %Explorer.Chain.TokenTransfer{log_index: 1, ...} - } - """ - - @spec load_token_transfers(EpochReward.t()) :: EpochReward.t() - def load_token_transfers( - %EpochReward{ - reserve_bolster_transfer_log_index: reserve_bolster_transfer_log_index, - community_transfer_log_index: community_transfer_log_index, - carbon_offsetting_transfer_log_index: carbon_offsetting_transfer_log_index - } = epoch_reward, - options \\ [] - ) do - virtual_field_to_log_index = [ - reserve_bolster_transfer: reserve_bolster_transfer_log_index, - community_transfer: community_transfer_log_index, - carbon_offsetting_transfer: carbon_offsetting_transfer_log_index - ] - - log_indexes = - virtual_field_to_log_index - |> Enum.map(&elem(&1, 1)) - |> Enum.reject(&is_nil/1) - - query = - from( - tt in TokenTransfer.only_consensus_transfers_query(), - where: tt.log_index in ^log_indexes and tt.block_hash == ^epoch_reward.block_hash, - select: {tt.log_index, tt}, - preload: [ - :token, - [from_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]], - [to_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]] - ] - ) - - log_index_to_token_transfer = query |> select_repo(options).all() |> Map.new() - - Enum.reduce(virtual_field_to_log_index, epoch_reward, fn - {field, log_index}, acc -> - token_transfer = Map.get(log_index_to_token_transfer, log_index) - Map.put(acc, field, token_transfer) - end) - end end diff --git a/apps/explorer/lib/explorer/chain/celo/helper.ex b/apps/explorer/lib/explorer/chain/celo/helper.ex index e85d2be760ac..b2b92f69aa7a 100644 --- a/apps/explorer/lib/explorer/chain/celo/helper.ex +++ b/apps/explorer/lib/explorer/chain/celo/helper.ex @@ -3,53 +3,15 @@ defmodule Explorer.Chain.Celo.Helper do Common helper functions for Celo. """ - import Explorer.Chain.Cache.CeloCoreContracts, only: [atom_to_contract_name: 0] - alias Explorer.Chain.Block @blocks_per_epoch 17_280 - @core_contract_atoms atom_to_contract_name() |> Map.keys() @doc """ - Returns the number of blocks per epoch in the Celo network. - """ - @spec blocks_per_epoch() :: non_neg_integer() - def blocks_per_epoch, do: @blocks_per_epoch - - defguard is_epoch_block_number(block_number) - when is_integer(block_number) and - block_number > 0 and - rem(block_number, @blocks_per_epoch) == 0 + Checks if a block number belongs to a block that finalized an L1-era epoch. - defguard is_core_contract_atom(atom) - when atom in @core_contract_atoms - - @doc """ - Validates if a block number is an epoch block number. - - ## Parameters - - `block_number` (`Block.block_number()`): The block number to validate. - - ## Returns - - `:ok` if the block number is an epoch block number. - - `{:error, :not_found}` if the block number is not an epoch block number. - - ## Examples - - iex> Explorer.Chain.Celo.Helper.validate_epoch_block_number(17280) - :ok - - iex> Explorer.Chain.Celo.Helper.validate_epoch_block_number(17281) - {:error, :not_found} - """ - @spec validate_epoch_block_number(Block.block_number()) :: :ok | {:error, :not_found} - def validate_epoch_block_number(block_number) when is_epoch_block_number(block_number), - do: :ok - - def validate_epoch_block_number(_block_number), do: {:error, :not_found} - - @doc """ - Checks if a block number belongs to a block that finalized an epoch. + This function should only be used for pre-L2 migration blocks, as the concept + of epoch blocks no longer exists after L2 migration. ## Parameters - `block_number` (`Block.block_number()`): The block number to check. @@ -67,7 +29,12 @@ defmodule Explorer.Chain.Celo.Helper do false """ @spec epoch_block_number?(block_number :: Block.block_number()) :: boolean() - def epoch_block_number?(block_number) when is_epoch_block_number(block_number), do: true + def epoch_block_number?(block_number) + when is_integer(block_number) and + block_number > 0 and + rem(block_number, @blocks_per_epoch) == 0, + do: true + def epoch_block_number?(_), do: false @doc """ @@ -81,15 +48,49 @@ defmodule Explorer.Chain.Celo.Helper do ## Examples - iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17280) + iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17279) 1 + iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17280) + 2 + iex> Explorer.Chain.Celo.Helper.block_number_to_epoch_number(17281) 2 """ @spec block_number_to_epoch_number(block_number :: Block.block_number()) :: non_neg_integer() def block_number_to_epoch_number(block_number) when is_integer(block_number) do - (block_number / @blocks_per_epoch) |> Float.ceil() |> trunc() + (block_number / @blocks_per_epoch) |> Float.floor() |> trunc() |> Kernel.+(1) + end + + @doc """ + Converts an epoch number to a block range for L1-era epochs. + + This function should only be used for pre-L2 migration epochs, as epoch block + ranges are deterministic only in L1 era. + + ## Parameters + - `epoch_number` (`non_neg_integer()`): The epoch number to convert. + + ## Returns + - `{Block.block_number(), Block.block_number()}`: A tuple containing the start + and end block numbers of the epoch. + + ## Examples + + iex> Explorer.Chain.Celo.Helper.epoch_number_to_block_range(1) + {0, 17279} + + iex> Explorer.Chain.Celo.Helper.epoch_number_to_block_range(2) + {17280, 34559} + """ + @spec epoch_number_to_block_range(epoch_number :: non_neg_integer()) :: + {Block.block_number(), Block.block_number()} + def epoch_number_to_block_range(epoch_number) + when is_integer(epoch_number) and epoch_number > 0 do + start_block = (epoch_number - 1) * @blocks_per_epoch + end_block = epoch_number * @blocks_per_epoch - 1 + + {start_block, end_block} end @doc """ @@ -111,12 +112,26 @@ defmodule Explorer.Chain.Celo.Helper do @doc """ Checks if a block with given number appeared prior to Celo L2 migration. """ - @spec premigration_block_number?(Block.block_number()) :: boolean() - def premigration_block_number?(block_number) do + @spec pre_migration_block_number?(Block.block_number()) :: boolean() + def pre_migration_block_number?(block_number) do + l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] + + if l2_migration_block_number do + block_number < l2_migration_block_number + else + true + end + end + + @doc """ + Checks if an epoch number is prior to Celo L2 migration. + """ + @spec premigration_epoch_number?(non_neg_integer()) :: boolean() + def premigration_epoch_number?(epoch_number) do l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] if l2_migration_block_number do - block_number <= l2_migration_block_number + epoch_number < block_number_to_epoch_number(l2_migration_block_number) else true end diff --git a/apps/explorer/lib/explorer/chain/celo/pending_epoch_block_operation.ex b/apps/explorer/lib/explorer/chain/celo/pending_epoch_block_operation.ex deleted file mode 100644 index 458360d6d9d9..000000000000 --- a/apps/explorer/lib/explorer/chain/celo/pending_epoch_block_operation.ex +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Explorer.Chain.Celo.PendingEpochBlockOperation do - @moduledoc """ - Tracks an epoch block that has pending operation. - """ - - use Explorer.Schema - - import Explorer.Chain, only: [add_fetcher_limit: 2] - alias Explorer.Chain.{Block, Hash} - alias Explorer.Repo - - @required_attrs ~w(block_hash)a - - @typedoc """ - * `block_hash` - the hash of the block that has pending epoch operations. - """ - @primary_key false - typed_schema "celo_pending_epoch_block_operations" do - belongs_to(:block, Block, - foreign_key: :block_hash, - primary_key: true, - references: :hash, - type: Hash.Full, - null: false - ) - - timestamps() - end - - def changeset(%__MODULE__{} = pending_ops, attrs) do - pending_ops - |> cast(attrs, @required_attrs) - |> validate_required(@required_attrs) - |> foreign_key_constraint(:block_hash) - |> unique_constraint(:block_hash, name: :pending_epoch_block_operations_pkey) - end - - @doc """ - Returns a stream of all blocks prior to Celo L2 migration with unfetched - epochs, using the `celo_pending_epoch_block_operations` table. - - iex> unfetched = insert(:block, number: 1 * blocks_per_epoch()) - iex> insert(:celo_pending_epoch_block_operation, block: unfetched) - iex> {:ok, blocks} = PendingEpochBlockOperation.stream_premigration_epoch_blocks_with_unfetched_rewards( - ...> [], - ...> fn block, acc -> - ...> [block | acc] - ...> end - ...> ) - iex> [{unfetched.number, unfetched.hash}] == blocks - true - """ - @spec stream_premigration_epoch_blocks_with_unfetched_rewards( - initial :: accumulator, - reducer :: (entry :: term(), accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_premigration_epoch_blocks_with_unfetched_rewards(initial, reducer, limited? \\ false) - when is_function(reducer, 2) do - query = - from( - op in __MODULE__, - join: block in assoc(op, :block), - select: %{block_number: block.number, block_hash: block.hash}, - where: block.consensus == true, - order_by: [desc: block.number] - ) - - query - |> maybe_filter_premigration_epoch_blocks() - |> add_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - - @spec maybe_filter_premigration_epoch_blocks(Ecto.Query.t()) :: Ecto.Query.t() - defp maybe_filter_premigration_epoch_blocks(query) do - l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] - - if l2_migration_block_number do - query - |> where([op, block], block.number <= ^l2_migration_block_number) - else - query - end - end -end diff --git a/apps/explorer/lib/explorer/chain/celo/reader.ex b/apps/explorer/lib/explorer/chain/celo/reader.ex deleted file mode 100644 index 8a76aed80e23..000000000000 --- a/apps/explorer/lib/explorer/chain/celo/reader.ex +++ /dev/null @@ -1,213 +0,0 @@ -defmodule Explorer.Chain.Celo.Reader do - @moduledoc """ - Read functions for Celo modules. - """ - - import Ecto.Query, only: [limit: 2] - - alias Explorer.Chain - alias Explorer.Chain.Block - alias Explorer.Chain.Cache.{Blocks, CeloCoreContracts} - alias Explorer.Chain.Celo.{ElectionReward, Helper} - alias Explorer.Chain.{Hash, Token, Wei} - - @election_reward_types ElectionReward.types() - @default_paging_options Chain.default_paging_options() - - @doc """ - Retrieves election rewards associated with a given address hash. - - ## Parameters - - `address_hash` (`Hash.Address.t()`): The address hash to search for election - rewards. - - `options` (`Keyword.t()`): Optional parameters for fetching data. - - ## Returns - - `[ElectionReward.t()]`: A list of election rewards associated with the - address hash. - - ## Examples - - iex> address_hash = %Hash.Address{ - ...> byte_count: 20, - ...> bytes: <<0x1d1f7f0e1441c37e28b89e0b5e1edbbd34d77649 :: size(160)>> - ...> } - iex> Explorer.Chain.Celo.Reader.address_hash_to_election_rewards(address_hash) - [%ElectionReward{}, ...] - """ - @spec address_hash_to_election_rewards( - Hash.Address.t(), - Keyword.t() - ) :: [ElectionReward.t()] - def address_hash_to_election_rewards(address_hash, options \\ []) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - from_block = Chain.from_block(options) - to_block = Chain.to_block(options) - - address_hash - |> ElectionReward.address_hash_to_ordered_rewards_query() - |> ElectionReward.where_block_number_in_period(from_block, to_block) - |> ElectionReward.join_token() - |> ElectionReward.paginate(paging_options) - |> limit(^paging_options.page_size) - |> Chain.join_associations(necessity_by_association) - |> Chain.select_repo(options).all() - end - - @doc """ - Retrieves election rewards by block hash and reward type. - - ## Parameters - - `block_hash` (`Hash.t()`): The block hash to search for election rewards. - - `reward_type` (`ElectionReward.type()`): The type of reward to filter. - - `options` (`Keyword.t()`): Optional parameters for fetching data. - - ## Returns - - `[ElectionReward.t()]`: A list of election rewards filtered by block hash - and reward type. - - ## Examples - - iex> block_hash = %Hash.Full{ - ...> byte_count: 32, - ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - ...> } - iex> Explorer.Chain.Celo.Reader.block_hash_to_election_rewards_by_type(block_hash, :voter_reward) - [%ElectionReward{}, ...] - """ - @spec block_hash_to_election_rewards_by_type( - Hash.t(), - ElectionReward.type(), - Keyword.t() - ) :: [ElectionReward.t()] - def block_hash_to_election_rewards_by_type(block_hash, reward_type, options \\ []) - when reward_type in @election_reward_types do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - block_hash - |> ElectionReward.block_hash_to_rewards_by_type_query(reward_type) - |> ElectionReward.paginate(paging_options) - |> limit(^paging_options.page_size) - |> Chain.join_associations(necessity_by_association) - |> Chain.select_repo(options).all() - end - - @doc """ - Retrieves aggregated election rewards by block hash. - - ## Parameters - - `block_hash` (`Hash.Full.t()`): The block hash to aggregate election - rewards. - - `options` (`Keyword.t()`): Optional parameters for fetching data. - - ## Returns - - `%{atom() => Wei.t() | nil}`: A map of aggregated election rewards by type. - - ## Examples - - iex> block_hash = %Hash.Full{ - ...> byte_count: 32, - ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - ...> } - iex> Explorer.Chain.Celo.Reader.block_hash_to_aggregated_election_rewards_by_type(block_hash) - %{voter_reward: %{total: %Decimal{}, count: 2}, ...} - """ - @spec block_hash_to_aggregated_election_rewards_by_type( - Hash.Full.t(), - Keyword.t() - ) :: %{atom() => Wei.t() | nil} - def block_hash_to_aggregated_election_rewards_by_type(block_hash, options \\ []) do - reward_type_to_token = - block_hash_to_election_reward_token_addresses_by_type( - block_hash, - options - ) - - reward_type_to_aggregated_rewards = - block_hash - |> ElectionReward.block_hash_to_aggregated_rewards_by_type_query() - |> Chain.select_repo(options).all() - |> Map.new(fn {type, total, count} -> - {type, %{total: total, count: count}} - end) - - ElectionReward.types() - |> Map.new(&{&1, %{total: Decimal.new(0), count: 0}}) - |> Map.merge(reward_type_to_aggregated_rewards) - |> Map.new(fn {type, aggregated_reward} -> - token = Map.get(reward_type_to_token, type) - aggregated_reward_with_token = Map.put(aggregated_reward, :token, token) - {type, aggregated_reward_with_token} - end) - end - - # Retrieves the token for each type of election reward on the given block. - # - # ## Parameters - # - `block_hash` (`Hash.Full.t()`): The block hash to search for token - # addresses. - # - `options` (`Keyword.t()`): Optional parameters for fetching data. - # - # ## Returns - # - `%{atom() => Token.t() | nil}`: A map of reward types to token. - # - # ## Examples - # - # iex> block_hash = %Hash.Full{ - # ...> byte_count: 32, - # ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - # ...> } - # iex> Explorer.Chain.Celo.Reader.block_hash_to_election_reward_token_addresses_by_type(block_hash) - # %{voter_reward: %Token{}, ...} - @spec block_hash_to_election_reward_token_addresses_by_type( - Hash.Full.t(), - Keyword.t() - ) :: %{atom() => Token.t() | nil} - defp block_hash_to_election_reward_token_addresses_by_type(block_hash, options) do - contract_address_hash_to_atom = - ElectionReward.reward_type_atom_to_token_atom() - |> Map.values() - |> Map.new(fn token_atom -> - {:ok, contract_address_hash} = CeloCoreContracts.get_address(token_atom, block_hash) - {contract_address_hash, token_atom} - end) - - token_atom_to_token = - contract_address_hash_to_atom - |> Map.keys() - |> Token.get_by_contract_address_hashes(options) - |> Map.new(fn token -> - hash = to_string(token.contract_address_hash) - atom = contract_address_hash_to_atom[hash] - {atom, token} - end) - - ElectionReward.reward_type_atom_to_token_atom() - |> Map.new(fn {reward_type_atom, token_atom} -> - {reward_type_atom, Map.get(token_atom_to_token, token_atom)} - end) - end - - @doc """ - Retrieves the epoch number of the last fetched block. - """ - @spec last_block_epoch_number(Keyword.t()) :: Block.block_number() | nil - def last_block_epoch_number(options \\ []) do - block_number = - 1 - |> Blocks.atomic_take_enough() - |> case do - [%Block{number: number}] -> {:ok, number} - nil -> Chain.max_consensus_block_number(options) - end - |> case do - {:ok, number} -> number - _ -> nil - end - - block_number && Helper.block_number_to_epoch_number(block_number) - end -end diff --git a/apps/explorer/lib/explorer/chain/celo/reader/epoch_manager.ex b/apps/explorer/lib/explorer/chain/celo/reader/epoch_manager.ex new file mode 100644 index 000000000000..f3635795af13 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/reader/epoch_manager.ex @@ -0,0 +1,92 @@ +defmodule Explorer.Chain.Celo.Reader.EpochManager do + @moduledoc """ + Functions for interacting with the Celo Epoch Manager contract. + """ + + use Utils.RuntimeEnvHelper, + epoch_manager_contract_address_hash: [ + :explorer, + [:celo, :epoch_manager_contract_address] + ] + + @get_first_block_at_epoch_abi [ + %{ + "inputs" => [%{"type" => "uint256"}], + "name" => "getFirstBlockAtEpoch", + "outputs" => [%{"type" => "uint256"}], + "stateMutability" => "view", + "type" => "function" + } + ] + + @get_last_block_at_epoch_abi [ + %{ + "inputs" => [%{"type" => "uint256"}], + "name" => "getLastBlockAtEpoch", + "outputs" => [%{"type" => "uint256"}], + "stateMutability" => "view", + "type" => "function" + } + ] + + alias Explorer.Helper + alias Explorer.SmartContract.Reader + + @doc """ + Retrieves the first block number in a given epoch. + + ## Parameters + + * `block_number` - The epoch number to retrieve epoch for. + + ## Returns + + * `{:ok, number}` - The first block number in the epoch. + * `:error` - If the request fails. + """ + @spec fetch_first_block_at_epoch(non_neg_integer()) :: + {:ok, non_neg_integer()} | :error + def fetch_first_block_at_epoch(block_number) do + method_id = Helper.abi_to_method_id(@get_first_block_at_epoch_abi) + + epoch_manager_contract_address_hash() + |> Reader.query_contract( + @get_first_block_at_epoch_abi, + %{method_id => [block_number]}, + false + ) + |> case do + %{^method_id => {:ok, [number]}} -> {:ok, number} + _ -> :error + end + end + + @doc """ + Retrieves the last block number in a given epoch. + + ## Parameters + + * `block_number` - The epoch number to retrieve epoch for. + + ## Returns + + * `{:ok, number}` - The last block number in the epoch. + * `:error` - If the request fails. + """ + @spec fetch_last_block_at_epoch(non_neg_integer()) :: + {:ok, non_neg_integer()} | :error + def fetch_last_block_at_epoch(block_number) do + method_id = Helper.abi_to_method_id(@get_last_block_at_epoch_abi) + + epoch_manager_contract_address_hash() + |> Reader.query_contract( + @get_last_block_at_epoch_abi, + %{method_id => [block_number]}, + false + ) + |> case do + %{^method_id => {:ok, [number]}} -> {:ok, number} + _ -> :error + end + end +end diff --git a/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex b/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex index 61613dbc6a7d..867eafbf0073 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.CsvExport.Address.Celo.ElectionRewards do block_number_to_epoch_number: 1 ] - alias Explorer.Chain.Celo.Reader + alias Explorer.Chain.Celo.ElectionReward alias Explorer.Chain.CsvExport.Helper alias Explorer.Chain.{Hash, Wei} @@ -22,7 +22,7 @@ defmodule Explorer.Chain.CsvExport.Address.Celo.ElectionRewards do ] address_hash - |> Reader.address_hash_to_election_rewards(options) + |> ElectionReward.address_hash_to_rewards(options) |> to_csv_format() |> Helper.dump_to_stream() end diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 737bb0c66134..9e7468fca7e5 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -7,6 +7,7 @@ defmodule Explorer.Chain.Import do alias Explorer.Account.Notify alias Explorer.Chain.Events.Publisher alias Explorer.Chain.{Block, Import} + alias Explorer.Chain.Import.Stage alias Explorer.Repo require Logger @@ -147,6 +148,39 @@ defmodule Explorer.Chain.Import do end end + @doc """ + Prepares a bulk import transaction without executing it. + + This function follows the same validation steps as `all/1` but instead of executing the transaction, + it returns the prepared `Ecto.Multi` struct. This allows the caller to compose the transaction with + additional operations before executing it. + + ## Parameters + + - `runners`: List of runner modules to prepare the multi for + - `options`: The import options map (same structure as in `all/1`) + + ## Returns + + - `{:ok, multi}` - The prepared transaction that can be executed later + - `{:error, [Changeset.t()]}` - Validation errors for the provided options + - `{:error, {:unknown_options, map()}}` - Unknown options were provided + """ + @spec all_single_multi([module()], all_options()) :: + {:ok, Ecto.Multi.t()} + | {:error, [Changeset.t()]} + | {:error, {:unknown_options, map()}} + def all_single_multi(runners, options) do + with {:ok, runner_options_pairs} <- validate_options(options), + {:ok, valid_runner_option_pairs} <- validate_runner_options_pairs(runner_options_pairs), + {:ok, runner_to_changes_list} <- runner_to_changes_list(valid_runner_option_pairs) do + timestamps = timestamps() + full_options = Map.put(options, :timestamps, timestamps) + {multi, _remaining_runner_to_changes_list} = Stage.single_multi(runners, runner_to_changes_list, full_options) + {:ok, multi} + end + end + defp configured_runners do # in order so that foreign keys are inserted before being referenced Enum.flat_map(@stages, fn stage_batch -> diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index fe110686d523..d6f862ef246d 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -26,9 +26,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do Transaction } - alias Explorer.Chain.Celo.Helper, as: CeloHelper - alias Explorer.Chain.Celo.PendingEpochBlockOperation, as: CeloPendingEpochBlockOperation - alias Explorer.Chain.Block.Reward alias Explorer.Chain.Import.Runner alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances @@ -37,6 +34,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward + alias Explorer.Chain.Celo.Epoch, as: CeloEpoch + alias Explorer.Chain.Celo.EpochReward, as: CeloEpochReward + @behaviour Runner # milliseconds @@ -232,16 +233,18 @@ defmodule Explorer.Chain.Import.Runner.Blocks do end) |> chain_type_dependent_import( :celo, - &Multi.run(&1, :celo_pending_epoch_block_operations, fn repo, %{blocks: blocks} -> - Instrumenter.block_import_stage_runner( - fn -> - celo_pending_epoch_block_operations(repo, blocks, insert_options) - end, - :address_referencing, - :blocks, - :celo_pending_epoch_block_operations - ) - end) + &Multi.run( + &1, + :celo_delete_epoch_rewards, + fn repo, %{lose_consensus: non_consensus_blocks} -> + Instrumenter.block_import_stage_runner( + fn -> celo_delete_epoch_rewards(repo, non_consensus_blocks, insert_options) end, + :address_referencing, + :blocks, + :celo_delete_epoch_rewards + ) + end + ) ) end @@ -1068,23 +1071,49 @@ defmodule Explorer.Chain.Import.Runner.Blocks do where(invalid_neighbors_query, [block], block.consensus) end - defp celo_pending_epoch_block_operations(repo, inserted_blocks, %{timeout: timeout, timestamps: timestamps}) do - ordered_epoch_blocks = - inserted_blocks - |> Enum.filter(fn block -> CeloHelper.epoch_block_number?(block.number) && block.consensus end) - |> Enum.map(&%{block_hash: &1.hash}) - |> Enum.sort_by(& &1.block_hash) - |> Enum.dedup_by(& &1.block_hash) + defp celo_delete_epoch_rewards(_repo, [], _options), do: {:ok, []} - Import.insert_changes_list( - repo, - ordered_epoch_blocks, - conflict_target: :block_hash, - on_conflict: :nothing, - for: CeloPendingEpochBlockOperation, - returning: true, - timeout: timeout, - timestamps: timestamps - ) + defp celo_delete_epoch_rewards(repo, non_consensus_blocks, %{timeout: timeout}) do + non_consensus_block_hashes = Enum.map(non_consensus_blocks, fn {_number, hash} -> hash end) + + ordered_query = + from(epoch in CeloEpoch, + where: + epoch.start_processing_block_hash in ^non_consensus_block_hashes or + epoch.end_processing_block_hash in ^non_consensus_block_hashes, + # Enforce Epoch ShareLocks order (see docs: sharelocks.md) + order_by: epoch.number, + lock: "FOR UPDATE" + ) + + repo.all(ordered_query, timeout: timeout) + + epoch_rewards_query = + from(reward in CeloEpochReward, + join: epoch in subquery(ordered_query), + on: reward.epoch_number == epoch.number + ) + + election_rewards_query = + from(reward in CeloElectionReward, + join: epoch in subquery(ordered_query), + on: reward.epoch_number == epoch.number + ) + + repo.delete_all(epoch_rewards_query, timeout: timeout) + repo.delete_all(election_rewards_query, timeout: timeout) + + {_count, updated_epochs} = + repo.update_all( + from(epoch in CeloEpoch, + join: ordered_epoch in subquery(ordered_query), + on: epoch.number == ordered_epoch.number, + select: epoch + ), + [set: [fetched?: false]], + timeout: timeout + ) + + {:ok, updated_epochs} end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex index b7ff06f78157..8f32c87fee83 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex @@ -64,7 +64,7 @@ defmodule Explorer.Chain.Import.Runner.Celo.ElectionRewards do Enum.sort_by( changes_list, &{ - &1.block_hash, + &1.epoch_number, &1.type, &1.account_address_hash, &1.associated_account_address_hash @@ -80,7 +80,7 @@ defmodule Explorer.Chain.Import.Runner.Celo.ElectionRewards do timeout: timeout, timestamps: timestamps, conflict_target: [ - :block_hash, + :epoch_number, :type, :account_address_hash, :associated_account_address_hash diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/epoch_rewards.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/epoch_rewards.ex index 7c7cd5752e68..5846be4d9d04 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/celo/epoch_rewards.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/epoch_rewards.ex @@ -6,12 +6,10 @@ defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do require Ecto.Query alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Celo.{EpochReward, PendingEpochBlockOperation} + alias Explorer.Chain.Celo.EpochReward alias Explorer.Chain.Import alias Explorer.Prometheus.Instrumenter - import Ecto.Query - @behaviour Import.Runner # milliseconds @@ -45,14 +43,6 @@ defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do |> Map.put(:timestamps, timestamps) multi - |> Multi.run(:acquire_pending_epoch_block_operations, fn repo, _ -> - Instrumenter.block_import_stage_runner( - fn -> acquire_pending_epoch_block_operations(repo, changes_list) end, - :block_pending, - :celo_epoch_rewards, - :acquire_pending_epoch_block_operations - ) - end) |> Multi.run(:insert_celo_epoch_rewards, fn repo, _ -> Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, @@ -61,20 +51,6 @@ defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do :celo_epoch_rewards ) end) - |> Multi.run( - :delete_pending_epoch_block_operations, - fn repo, - %{ - acquire_pending_epoch_block_operations: pending_block_hashes - } -> - Instrumenter.block_import_stage_runner( - fn -> delete_pending_epoch_block_operations(repo, pending_block_hashes) end, - :block_pending, - :celo_epoch_rewards, - :delete_pending_epoch_block_operations - ) - end - ) end @impl Import.Runner @@ -85,7 +61,7 @@ defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do | {:error, [Changeset.t()]} def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do # Enforce Celo.EpochReward ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.block_hash) + ordered_changes_list = Enum.sort_by(changes_list, & &1.epoch_number) {:ok, inserted} = Import.insert_changes_list( @@ -95,45 +71,10 @@ defmodule Explorer.Chain.Import.Runner.Celo.EpochRewards do returning: true, timeout: timeout, timestamps: timestamps, - conflict_target: :block_hash, + conflict_target: :epoch_number, on_conflict: :replace_all ) {:ok, inserted} end - - def acquire_pending_epoch_block_operations(repo, changes_list) do - block_hashes = Enum.map(changes_list, & &1.block_hash) - - query = - from( - pending_ops in PendingEpochBlockOperation, - where: pending_ops.block_hash in ^block_hashes, - select: pending_ops.block_hash, - # Enforce PendingBlockOperation ShareLocks order (see docs: sharelocks.md) - order_by: [asc: pending_ops.block_hash], - lock: "FOR UPDATE" - ) - - {:ok, repo.all(query)} - end - - def delete_pending_epoch_block_operations(repo, block_hashes) do - delete_query = - from( - pending_ops in PendingEpochBlockOperation, - where: pending_ops.block_hash in ^block_hashes - ) - - try do - # ShareLocks order already enforced by - # `acquire_pending_epoch_block_operations` (see docs: sharelocks.md) - {_count, deleted} = repo.delete_all(delete_query, []) - - {:ok, deleted} - rescue - postgrex_error in Postgrex.Error -> - {:error, %{exception: postgrex_error, pending_hashes: block_hashes}} - end - end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/epochs.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/epochs.ex new file mode 100644 index 000000000000..9fdf3e868120 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/epochs.ex @@ -0,0 +1,104 @@ +defmodule Explorer.Chain.Import.Runner.Celo.Epochs do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Celo.Epoch.t/0`. + """ + + require Ecto.Query + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Celo.Epoch + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Epoch.t()] + + @impl Import.Runner + def ecto_schema_module, do: Epoch + + @impl Import.Runner + def option_key, do: :celo_epochs + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :celo_epochs, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :celo_epochs, + :celo_epochs + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [Epoch.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce Celo.Epoch ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.number) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: Epoch, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: [:number], + on_conflict: default_on_conflict() + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from(epoch in Epoch, + update: [ + set: [ + start_block_number: fragment("COALESCE(EXCLUDED.start_block_number, ?)", epoch.start_block_number), + end_block_number: fragment("COALESCE(EXCLUDED.end_block_number, ?)", epoch.end_block_number), + start_processing_block_hash: + fragment("COALESCE(EXCLUDED.start_processing_block_hash, ?)", epoch.start_processing_block_hash), + end_processing_block_hash: + fragment("COALESCE(EXCLUDED.end_processing_block_hash, ?)", epoch.end_processing_block_hash), + fetched?: fragment("EXCLUDED.is_fetched"), + inserted_at: fragment("LEAST(EXCLUDED.inserted_at, ?)", epoch.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", epoch.updated_at) + ] + ], + where: + fragment("EXCLUDED.start_block_number IS DISTINCT FROM ?", epoch.start_block_number) or + fragment("EXCLUDED.end_block_number IS DISTINCT FROM ?", epoch.end_block_number) or + fragment("EXCLUDED.start_processing_block_hash IS DISTINCT FROM ?", epoch.start_processing_block_hash) or + fragment("EXCLUDED.end_processing_block_hash IS DISTINCT FROM ?", epoch.end_processing_block_hash) or + fragment("EXCLUDED.is_fetched IS DISTINCT FROM ?", epoch.fetched?) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 1b4d182cc841..d9e428e7f75a 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -63,6 +63,7 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do ], celo: [ Runner.Celo.ValidatorGroupVotes, + Runner.Celo.Epochs, Runner.Celo.ElectionRewards, Runner.Celo.EpochRewards ], diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 1e649ea99c60..cdc613766971 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -486,6 +486,30 @@ defmodule Explorer.Helper do |> DateTime.to_date() end + @doc """ + Extracts the method ID from an ABI specification. + + ## Parameters + - `method` ([map()] | map()): The ABI specification, either as a single map + or a list containing one map. + + ## Returns + - `binary()`: The method ID extracted from the ABI specification. + + ## Examples + + iex> Indexer.Fetcher.Celo.Helper.abi_to_method_id([%{"name" => "transfer", "type" => "function", "inputs" => [%{"name" => "to", "type" => "address"}]}]) + <<26, 105, 82, 48>> + + """ + @spec abi_to_method_id([map()] | map()) :: binary() + def abi_to_method_id([method]), do: abi_to_method_id(method) + + def abi_to_method_id(method) when is_map(method) do + [parsed_method] = ABI.parse_specification([method]) + parsed_method.method_id + end + @doc """ Adds `inserted_at` and `updated_at` timestamps to a list of maps. diff --git a/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex b/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex new file mode 100644 index 000000000000..c3789927be49 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex @@ -0,0 +1,102 @@ +defmodule Explorer.Migrator.CeloL2Epochs do + @moduledoc """ + Backfills Celo L2 epochs data. It processes logs related to epoch processing + and fills the `Epoch` table with the relevant data. + """ + + use Explorer.Migrator.FillingMigration + + use Utils.RuntimeEnvHelper, + epoch_manager_contract_address_hash: [ + :explorer, + [:celo, :epoch_manager_contract_address] + ] + + import Ecto.Query + + alias Explorer.Chain.Celo.Epoch + alias Explorer.{Helper, Repo} + alias Explorer.Chain.{Import, Log} + alias Explorer.Chain.Import.Runner.Celo.Epochs + alias Explorer.Migrator.FillingMigration + + @migration_name "celo_l2_epochs" + + # Events from the EpochManager contract + @epoch_processing_started_topic "0xae58a33f8b8d696bcbaca9fa29d9fdc336c140e982196c2580db3d46f3e6d4b6" + @epoch_processing_ended_topic "0xc8e58d8e6979dd5e68bad79d4a4368a1091f6feb2323e612539b1b84e0663a8f" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> select([log], log) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + epochs_start_processing_block_hashes = + from(epoch in Epoch, select: epoch.start_processing_block_hash) + + epochs_end_processing_block_hashes = + from(epoch in Epoch, select: epoch.end_processing_block_hash) + + from( + log in Log, + where: + log.address_hash == ^epoch_manager_contract_address_hash() and + ((log.first_topic == ^@epoch_processing_started_topic and + log.block_hash not in subquery(epochs_start_processing_block_hashes)) or + (log.first_topic == ^@epoch_processing_ended_topic and + log.block_hash not in subquery(epochs_end_processing_block_hashes))), + order_by: [asc: log.block_number] + ) + end + + @impl FillingMigration + def update_batch(logs) do + changes_list = + logs + |> Enum.reduce(%{}, fn log, epochs_acc -> + # Extract epoch number from the log + [epoch_number] = log.second_topic |> to_string() |> Helper.decode_data([{:uint, 256}]) + + current_epoch = Map.get(epochs_acc, epoch_number, %{number: epoch_number}) + + updated_epoch = + log.first_topic + |> to_string() + |> case do + @epoch_processing_started_topic -> + Map.put(current_epoch, :start_processing_block_hash, log.block_hash) + + @epoch_processing_ended_topic -> + Map.put(current_epoch, :end_processing_block_hash, log.block_hash) + end + + Map.put(epochs_acc, epoch_number, updated_epoch) + end) + |> Map.values() + + Epochs.insert( + Repo, + changes_list, + %{ + timeout: :infinity, + timestamps: Import.timestamps() + } + ) + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/priv/celo/migrations/20250416085705_create_epoch.exs b/apps/explorer/priv/celo/migrations/20250416085705_create_epoch.exs new file mode 100644 index 000000000000..73b531721e0e --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250416085705_create_epoch.exs @@ -0,0 +1,108 @@ +defmodule Explorer.Repo.Celo.Migrations.CreateEpoch do + use Ecto.Migration + + def change do + create table(:celo_epochs, primary_key: false) do + add(:number, :smallint, null: false, primary_key: true) + add(:is_fetched, :boolean, null: false, default: false) + + add(:start_block_number, :integer) + add(:end_block_number, :integer) + + add( + :start_processing_block_hash, + references( + :blocks, + column: :hash, + type: :bytea, + on_delete: :delete_all + ) + ) + + add( + :end_processing_block_hash, + references( + :blocks, + column: :hash, + type: :bytea, + on_delete: :delete_all + ) + ) + + timestamps() + end + + l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] + + if l2_migration_block_number do + execute(""" + WITH epoch_blocks AS ( + SELECT + b.number AS block_number, + b.hash AS block_hash, + FLOOR(b.number / 17280) AS epoch_number + FROM celo_pending_epoch_block_operations op + JOIN blocks b ON op.block_hash = b.hash + WHERE b.consensus = true AND b.number > 0 AND b.number < #{l2_migration_block_number} + ) + INSERT INTO celo_epochs ( + number, + start_processing_block_hash, + end_processing_block_hash, + start_block_number, + end_block_number, + inserted_at, + updated_at + ) + SELECT + epoch_number, + block_hash AS start_processing_block_hash, + block_hash AS end_processing_block_hash, + ((epoch_number - 1) * 17280) AS start_block_number, + (block_number - 1) AS end_block_number, + NOW(), + NOW() + FROM epoch_blocks + """) + + execute(""" + WITH epoch_blocks AS ( + SELECT + b.number AS block_number, + b.hash AS block_hash, + FLOOR(b.number / 17280) AS epoch_number + FROM blocks b + WHERE + b.consensus = true AND + b.number > 0 AND + b.number < #{l2_migration_block_number} AND + b.number % 17280 = 0 AND + NOT EXISTS ( + SELECT 1 FROM celo_epochs e + WHERE e.number = FLOOR(b.number / 17280) + ) + ) + INSERT INTO celo_epochs ( + number, + is_fetched, + start_processing_block_hash, + end_processing_block_hash, + start_block_number, + end_block_number, + inserted_at, + updated_at + ) + SELECT + epoch_number, + true AS is_fetched, + block_hash AS start_processing_block_hash, + block_hash AS end_processing_block_hash, + ((epoch_number - 1) * 17280) AS start_block_number, + (epoch_number * 17280 - 1) AS end_block_number, -- End at last block of epoch + NOW(), + NOW() + FROM epoch_blocks + """) + end + end +end diff --git a/apps/explorer/priv/celo/migrations/20250418141539_remove_pending_epoch_block_operations.exs b/apps/explorer/priv/celo/migrations/20250418141539_remove_pending_epoch_block_operations.exs new file mode 100644 index 000000000000..6121c118bb17 --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250418141539_remove_pending_epoch_block_operations.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Celo.Migrations.RemovePendingEpochBlockOperations do + use Ecto.Migration + + def change do + drop(table(:celo_pending_epoch_block_operations)) + end +end diff --git a/apps/explorer/priv/celo/migrations/20250420130550_epoch_rewards_replace_block_hash_with_epoch_number.exs b/apps/explorer/priv/celo/migrations/20250420130550_epoch_rewards_replace_block_hash_with_epoch_number.exs new file mode 100644 index 000000000000..c9bd94a00dc7 --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250420130550_epoch_rewards_replace_block_hash_with_epoch_number.exs @@ -0,0 +1,40 @@ +defmodule Explorer.Repo.Celo.Migrations.EpochRewardsReplaceBlockHashWithEpochNumber do + use Ecto.Migration + + def change do + l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] + + if l2_migration_block_number do + execute(""" + DELETE FROM celo_epoch_rewards er + USING blocks b + WHERE er.block_hash = b.hash + AND b.number >= #{l2_migration_block_number} + """) + end + + alter table(:celo_epoch_rewards) do + add(:epoch_number, references(:celo_epochs, column: :number, on_delete: :delete_all)) + end + + execute(""" + UPDATE celo_epoch_rewards er + SET epoch_number = e.number + FROM celo_epochs e + WHERE er.block_hash = e.end_processing_block_hash + """) + + execute(""" + ALTER TABLE celo_epoch_rewards DROP CONSTRAINT celo_epoch_rewards_pkey; + """) + + execute(""" + ALTER TABLE celo_epoch_rewards ADD PRIMARY KEY (epoch_number); + """) + + # Drop the block_hash column + alter table(:celo_epoch_rewards) do + remove(:block_hash) + end + end +end diff --git a/apps/explorer/priv/celo/migrations/20250420140107_election_rewards_replace_block_hash_with_epoch_number.exs b/apps/explorer/priv/celo/migrations/20250420140107_election_rewards_replace_block_hash_with_epoch_number.exs new file mode 100644 index 000000000000..76215c792d9b --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250420140107_election_rewards_replace_block_hash_with_epoch_number.exs @@ -0,0 +1,36 @@ +defmodule Explorer.Repo.Migrations.ElectionRewardsReplaceBlockHashWithEpochNumber do + use Ecto.Migration + + def change do + alter table(:celo_election_rewards) do + add( + :epoch_number, + references( + :celo_epochs, + column: :number, + on_delete: :delete_all + ) + ) + end + + execute(""" + UPDATE celo_election_rewards er + SET epoch_number = e.number + FROM celo_epochs e + WHERE er.block_hash = e.end_processing_block_hash + """) + + execute(""" + ALTER TABLE celo_election_rewards DROP CONSTRAINT celo_election_rewards_pkey; + """) + + execute(""" + ALTER TABLE celo_election_rewards + ADD PRIMARY KEY (type, epoch_number, account_address_hash, associated_account_address_hash); + """) + + alter table(:celo_election_rewards) do + remove(:block_hash) + end + end +end diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index a2386f8fe09c..b24ea5623968 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -1,5 +1,6 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do use Explorer.DataCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] import Ecto.Query, only: [from: 2, select: 2, where: 2] @@ -8,12 +9,9 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do alias Ecto.Multi alias Explorer.Chain.Import.Runner.{Blocks, Transactions} alias Explorer.Chain.{Address, Block, Transaction, PendingBlockOperation} - alias Explorer.Chain.Celo.PendingEpochBlockOperation alias Explorer.{Chain, Repo} alias Explorer.Utility.MissingBlockRange - alias Explorer.Chain.Celo.Helper, as: CeloHelper - describe "run/1" do setup do miner = insert(:address) @@ -456,60 +454,6 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do assert %{block_number: ^number, block_hash: ^hash} = Repo.one(PendingBlockOperation) end - if Application.compile_env(:explorer, :chain_type) == :celo do - test "inserts pending_epoch_block_operations only for epoch blocks", - %{consensus_block: %{miner_hash: miner_hash}, options: options} do - epoch_block_number = CeloHelper.blocks_per_epoch() - - %{hash: hash} = - epoch_block_params = - params_for( - :block, - miner_hash: miner_hash, - consensus: true, - number: epoch_block_number - ) - - non_epoch_block_params = - params_for( - :block, - miner_hash: miner_hash, - consensus: true, - number: epoch_block_number + 1 - ) - - insert_block(epoch_block_params, options) - insert_block(non_epoch_block_params, options) - - assert %{block_hash: ^hash} = Repo.one(PendingEpochBlockOperation) - end - - test "inserts pending_epoch_block_operations only for consensus epoch blocks", - %{consensus_block: %{miner_hash: miner_hash}, options: options} do - %{hash: hash} = - first_epoch_block_params = - params_for( - :block, - miner_hash: miner_hash, - consensus: true, - number: CeloHelper.blocks_per_epoch() - ) - - second_epoch_block_params = - params_for( - :block, - miner_hash: miner_hash, - consensus: false, - number: CeloHelper.blocks_per_epoch() * 2 - ) - - insert_block(first_epoch_block_params, options) - insert_block(second_epoch_block_params, options) - - assert %{block_hash: ^hash} = Repo.one(PendingEpochBlockOperation) - end - end - test "change instance owner if was token transfer in older blocks", %{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do block_number = block_number + 2 @@ -674,6 +618,151 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do ] }} = Multi.new() |> Blocks.run(changes_list, options) |> Repo.transaction() end + + if @chain_type == :celo do + test "removes celo epoch rewards and sets fetched? = false when starting block loses consensus", %{ + consensus_block: %{miner_hash: miner_hash} = parent_block, + options: options + } do + start_processing_block = insert(:block, number: 1, consensus: true, parent_hash: parent_block.hash) + end_processing_block = insert(:block, number: 2, consensus: true, parent_hash: start_processing_block.hash) + address = insert(:address) + + epoch = %{ + number: 1, + start_processing_block_hash: start_processing_block.hash, + end_processing_block_hash: end_processing_block.hash, + fetched?: true + } + + election_reward = %{ + epoch_number: epoch.number, + account_address_hash: address.hash, + amount: 100, + associated_account_address_hash: address.hash, + type: :validator + } + + distribution = %{ + epoch_number: epoch.number, + community_transfer_log_index: 1 + } + + {:ok, _imported} = + Chain.import(%{ + celo_epochs: %{params: [epoch]}, + celo_election_rewards: %{params: [election_reward]}, + celo_epoch_rewards: %{params: [distribution]} + }) + + assert Repo.get_by(Explorer.Chain.Celo.Epoch, number: epoch.number) + assert Repo.get_by(Explorer.Chain.Celo.EpochReward, epoch_number: epoch.number) + assert Repo.get_by(Explorer.Chain.Celo.ElectionReward, epoch_number: epoch.number) + + block_params = + params_for(:block, + number: start_processing_block.number, + miner_hash: miner_hash, + parent_hash: parent_block.hash + ) + + start_processing_block_hash = start_processing_block.hash + start_processing_block_number = start_processing_block.number + end_processing_block_hash = end_processing_block.hash + end_processing_block_number = end_processing_block.number + + assert {:ok, + %{ + celo_delete_epoch_rewards: [ + %Explorer.Chain.Celo.Epoch{ + number: 1, + fetched?: false, + start_processing_block_hash: ^start_processing_block_hash, + end_processing_block_hash: ^end_processing_block_hash + } + ], + lose_consensus: [ + {^start_processing_block_number, ^start_processing_block_hash}, + {^end_processing_block_number, ^end_processing_block_hash} + ] + }} = insert_block(block_params, options) + + assert Repo.get_by(Explorer.Chain.Celo.Epoch, number: epoch.number).fetched? == false + assert Repo.get_by(Explorer.Chain.Celo.EpochReward, epoch_number: epoch.number) == nil + assert Repo.get_by(Explorer.Chain.Celo.ElectionReward, epoch_number: epoch.number) == nil + end + + test "removes celo epoch rewards and sets fetched? = false when ending block loses consensus", %{ + consensus_block: %{miner_hash: miner_hash}, + options: options + } do + start_processing_block = insert(:block, number: 0, consensus: true) + intermediate_block = insert(:block, number: 1, consensus: true) + end_processing_block = insert(:block, number: 2, consensus: true, parent_hash: intermediate_block.hash) + address = insert(:address) + + epoch = %{ + number: 1, + start_processing_block_hash: start_processing_block.hash, + end_processing_block_hash: end_processing_block.hash, + fetched?: true + } + + election_reward = %{ + epoch_number: epoch.number, + account_address_hash: address.hash, + amount: 100, + associated_account_address_hash: address.hash, + type: :validator + } + + distribution = %{ + epoch_number: epoch.number, + community_transfer_log_index: 1 + } + + {:ok, _imported} = + Chain.import(%{ + celo_epochs: %{params: [epoch]}, + celo_election_rewards: %{params: [election_reward]}, + celo_epoch_rewards: %{params: [distribution]} + }) + + assert Repo.get_by(Explorer.Chain.Celo.Epoch, number: epoch.number) + assert Repo.get_by(Explorer.Chain.Celo.EpochReward, epoch_number: epoch.number) + assert Repo.get_by(Explorer.Chain.Celo.ElectionReward, epoch_number: epoch.number) + + block_params = + params_for(:block, + number: end_processing_block.number, + miner_hash: miner_hash, + parent_hash: intermediate_block.hash + ) + + start_processing_block_hash = start_processing_block.hash + end_processing_block_hash = end_processing_block.hash + end_processing_block_number = end_processing_block.number + + assert {:ok, + %{ + celo_delete_epoch_rewards: [ + %Explorer.Chain.Celo.Epoch{ + number: 1, + fetched?: false, + start_processing_block_hash: ^start_processing_block_hash, + end_processing_block_hash: ^end_processing_block_hash + } + ], + lose_consensus: [ + {^end_processing_block_number, ^end_processing_block_hash} + ] + }} = insert_block(block_params, options) + + assert Repo.get_by(Explorer.Chain.Celo.Epoch, number: epoch.number).fetched? == false + assert Repo.get_by(Explorer.Chain.Celo.EpochReward, epoch_number: epoch.number) == nil + assert Repo.get_by(Explorer.Chain.Celo.ElectionReward, epoch_number: epoch.number) == nil + end + end end describe "lose_consensus/5" do diff --git a/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex b/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex new file mode 100644 index 000000000000..fe01497d2961 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex @@ -0,0 +1,179 @@ +defmodule Explorer.Migrator.CeloL2EpochsTest do + use Explorer.DataCase, async: false + + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type] + + if @chain_type == :celo do + alias Explorer.Migrator.{CeloL2Epochs, MigrationStatus} + alias Explorer.Chain.Celo.Epoch + alias Explorer.Repo + + import Ecto.Query + + describe "celo_l2_epochs migration" do + test "backfills epochs from logs" do + epoch_manager_address = insert(:address, hash: "0x8d3436d48e1e3d915a0a6948049b0f58a79c9bbb") + + old_env = Application.get_env(:explorer, :celo) + + Application.put_env( + :explorer, + :celo, + Keyword.put( + old_env, + :epoch_manager_contract_address, + to_string(epoch_manager_address.hash) + ) + ) + + on_exit(fn -> + Application.put_env(:explorer, :celo, old_env) + end) + + # Create test logs for epoch processing events + epoch_number = 42 + epoch_number_hex = "0x000000000000000000000000000000000000000000000000000000000000002a" + + # Add epoch_processing_started event log + start_log = + insert(:log, + address: epoch_manager_address, + # epoch_processing_started + first_topic: "0xae58a33f8b8d696bcbaca9fa29d9fdc336c140e982196c2580db3d46f3e6d4b6", + second_topic: epoch_number_hex + ) + + # Add epoch_processing_ended event log + end_log = + insert(:log, + address: epoch_manager_address, + # epoch_processing_ended + first_topic: "0xc8e58d8e6979dd5e68bad79d4a4368a1091f6feb2323e612539b1b84e0663a8f", + second_topic: epoch_number_hex + ) + + # Ensure migration has not run yet + assert MigrationStatus.get_status("celo_l2_epochs") == nil + + # Run migration process + {:ok, _} = CeloL2Epochs.start_link([]) + + # Wait for migration to complete + Process.sleep(500) + + # Verify epoch was properly created + epochs = Repo.all(from(e in Epoch, where: e.number == ^epoch_number)) + + assert length(epochs) == 1 + epoch = List.first(epochs) + + # Check epoch data matches our logs + assert epoch.number == epoch_number + assert epoch.start_processing_block_hash == start_log.block_hash + assert epoch.end_processing_block_hash == end_log.block_hash + assert epoch.fetched? == false + + # Confirm migration status is completed + assert MigrationStatus.get_status("celo_l2_epochs") == "completed" + end + + test "backfills multiple epochs from logs" do + epoch_manager_address = insert(:address, hash: "0x8d3436d48e1e3d915a0a6948049b0f58a79c9bbb") + + old_env = Application.get_env(:explorer, :celo) + + Application.put_env(:explorer, :celo, [ + { + :epoch_manager_contract_address, + to_string(epoch_manager_address.hash) + } + | old_env + ]) + + on_exit(fn -> + Application.put_env(:explorer, :celo, old_env) + end) + + # Create data for 3 epochs with different patterns + epoch_data = [ + # Standard epoch with start and end events + %{ + number: 42, + number_hex: "0x000000000000000000000000000000000000000000000000000000000000002a" + }, + # Another complete epoch + %{ + number: 43, + number_hex: "0x000000000000000000000000000000000000000000000000000000000000002b" + }, + # Epoch with only a start event (incomplete) + %{ + number: 44, + number_hex: "0x000000000000000000000000000000000000000000000000000000000000002c" + } + ] + + # Insert logs for each epoch + start_logs = + Enum.map(epoch_data, fn epoch -> + insert(:log, + address: epoch_manager_address, + first_topic: "0xae58a33f8b8d696bcbaca9fa29d9fdc336c140e982196c2580db3d46f3e6d4b6", + second_topic: epoch.number_hex + ) + end) + + # Insert end logs for all except the last epoch + end_logs = + Enum.slice(epoch_data, 0..1) + |> Enum.map(fn epoch -> + insert(:log, + address: epoch_manager_address, + first_topic: "0xc8e58d8e6979dd5e68bad79d4a4368a1091f6feb2323e612539b1b84e0663a8f", + second_topic: epoch.number_hex + ) + end) + + # Ensure migration has not run yet + assert MigrationStatus.get_status("celo_l2_epochs") == nil + + # Run migration process + {:ok, _} = CeloL2Epochs.start_link([]) + + # Wait for migration to complete + Process.sleep(1000) + + # Verify all epochs were properly created + created_epochs = Repo.all(from(e in Epoch, order_by: [asc: e.number])) + + # Should have created all three epochs + assert length(created_epochs) == 3 + + # Check first epoch (complete) + first_epoch = Enum.at(created_epochs, 0) + assert first_epoch.number == 42 + assert first_epoch.start_processing_block_hash == Enum.at(start_logs, 0).block_hash + assert first_epoch.end_processing_block_hash == Enum.at(end_logs, 0).block_hash + assert first_epoch.fetched? == false + + # Check second epoch (complete) + second_epoch = Enum.at(created_epochs, 1) + assert second_epoch.number == 43 + assert second_epoch.start_processing_block_hash == Enum.at(start_logs, 1).block_hash + assert second_epoch.end_processing_block_hash == Enum.at(end_logs, 1).block_hash + assert second_epoch.fetched? == false + + # Check third epoch (incomplete - only has start) + third_epoch = Enum.at(created_epochs, 2) + assert third_epoch.number == 44 + assert third_epoch.start_processing_block_hash == Enum.at(start_logs, 2).block_hash + assert third_epoch.end_processing_block_hash == nil + assert third_epoch.fetched? == false + + # Confirm migration status is completed + assert MigrationStatus.get_status("celo_l2_epochs") == "completed" + end + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 877cf04fb4f8..4bf35356e615 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -25,7 +25,6 @@ defmodule Explorer.Factory do alias Explorer.Chain.Beacon.{Blob, BlobTransaction} alias Explorer.Chain.Block.{EmissionReward, Range, Reward} alias Explorer.Chain.Stability.Validator, as: ValidatorStability - alias Explorer.Chain.Celo.PendingEpochBlockOperation, as: CeloPendingEpochBlockOperation alias Explorer.Chain.{ Address, @@ -1352,10 +1351,6 @@ defmodule Explorer.Factory do to_string(bls_public_key) end - def celo_pending_epoch_block_operation_factory do - %CeloPendingEpochBlockOperation{} - end - def withdrawal_log_factory(params) do weth_log(TokenTransfer.weth_withdrawal_signature(), params) end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 4a9bde3894ba..366b73d5bbaa 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -62,6 +62,8 @@ defmodule Indexer.Block.Fetcher do alias Indexer.Transform.Blocks, as: TransformBlocks alias Indexer.Transform.PolygonZkevm.Bridge, as: PolygonZkevmBridge + alias Indexer.Transform.Celo.L1Epochs, as: CeloL1Epochs + alias Indexer.Transform.Celo.L2Epochs, as: CeloL2Epochs alias Indexer.Transform.Celo.TransactionGasTokens, as: CeloTransactionGasTokens alias Indexer.Transform.Celo.TransactionTokenTransfers, as: CeloTransactionTokenTransfers @@ -168,6 +170,8 @@ defmodule Indexer.Block.Fetcher do CeloTransactionTokenTransfers.parse_transactions(transactions_with_receipts), celo_gas_tokens = CeloTransactionGasTokens.parse(transactions_with_receipts), token_transfers = token_transfers ++ celo_native_token_transfers, + celo_l1_epochs = CeloL1Epochs.parse(blocks), + celo_l2_epochs = CeloL2Epochs.parse(logs), tokens = Enum.uniq(tokens ++ celo_tokens), %{transaction_actions: transaction_actions} = TransactionActions.parse(logs), %{mint_transfers: mint_transfers} = MintTransfers.parse(logs), @@ -257,6 +261,7 @@ defmodule Indexer.Block.Fetcher do scroll_l1_fee_params: scroll_l1_fee_params, shibarium_bridge_operations: shibarium_bridge_operations, celo_gas_tokens: celo_gas_tokens, + celo_epochs: celo_l1_epochs ++ celo_l2_epochs, arbitrum_messages: arbitrum_xlevel_messages } |> extend_with_zilliqa_import_options(fetched_blocks), @@ -332,13 +337,14 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:shibarium_bridge_operations, %{params: shibarium_bridge_operations}) end - defp do_import_options(:celo, basic_import_options, %{celo_gas_tokens: celo_gas_tokens}) do + defp do_import_options(:celo, basic_import_options, %{celo_gas_tokens: celo_gas_tokens, celo_epochs: celo_epochs}) do tokens = basic_import_options |> Map.get(:tokens, %{}) |> Map.get(:params, []) basic_import_options + |> Map.put_new(:celo_epochs, %{params: celo_epochs}) |> Map.put( :tokens, %{params: (tokens ++ celo_gas_tokens) |> Enum.uniq()} @@ -539,10 +545,8 @@ defmodule Indexer.Block.Fetcher do def async_import_polygon_zkevm_bridge_l1_tokens(_), do: :ok - def async_import_celo_epoch_block_operations(%{blocks: operations}, realtime?) do - operations - |> Enum.map(&%{block_number: &1.number, block_hash: &1.hash}) - |> CeloEpochBlockOperations.async_fetch(realtime?) + def async_import_celo_epoch_block_operations(%{celo_epochs: epochs}, realtime?) do + CeloEpochBlockOperations.async_fetch(epochs, realtime?) end def async_import_celo_epoch_block_operations(_, _), do: :ok diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex index 11cd35dd6a36..21adea918d25 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex @@ -3,23 +3,27 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do Tracks epoch blocks awaiting processing by the epoch fetcher. """ - import Explorer.Chain.Celo.Helper, - only: [ - epoch_block_number?: 1, - premigration_block_number?: 1 - ] + import Ecto.Query, only: [from: 2] - alias Explorer.Chain - alias Explorer.Chain.Block - alias Explorer.Chain.Celo.PendingEpochBlockOperation - alias Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, as: EpochBlockOperationsSupervisor - alias Indexer.Transform.Addresses + alias Ecto.Multi + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Block, Import} + alias Explorer.Chain.Celo.{Epoch, Helper} + alias Explorer.Chain.Celo.Reader.EpochManager alias Indexer.{BufferedTask, Tracer} + alias Indexer.Transform.Addresses + + alias Explorer.Chain.Import.Runner.Celo.{ + ElectionRewards, + EpochRewards, + Epochs + } alias Indexer.Fetcher.Celo.EpochBlockOperations.{ - DelegatedPayments, + DelegatedPaymentsPriorL2Migration, Distributions, - ValidatorAndGroupPayments, + ValidatorAndGroupPaymentsPostL2Migration, + ValidatorAndGroupPaymentsPriorL2Migration, VoterPayments } @@ -53,11 +57,11 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do def defaults do [ - poll: false, + poll: true, flush_interval: :timer.seconds(3), max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, - task_supervisor: Indexer.Fetcher.Celo.EpochBlockOperations.TaskSupervisor, + task_supervisor: __MODULE__.TaskSupervisor, metadata: [fetcher: :celo_epoch_rewards] ] end @@ -68,15 +72,12 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do integer() ) :: :ok def async_fetch(entries, realtime?, timeout \\ 5000) when is_list(entries) do - if EpochBlockOperationsSupervisor.disabled?() do + if __MODULE__.Supervisor.disabled?() do :ok else filtered_entries = - Enum.filter( - entries, - &(epoch_block_number?(&1.block_number) && - premigration_block_number?(&1.block_number)) - ) + entries + |> Enum.filter(&(&1.start_processing_block_hash && &1.end_processing_block_hash)) BufferedTask.buffer(__MODULE__, filtered_entries, realtime?, timeout) end @@ -85,7 +86,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do @impl BufferedTask def init(initial, reducer, _json_rpc_named_arguments) do {:ok, final} = - PendingEpochBlockOperation.stream_premigration_epoch_blocks_with_unfetched_rewards( + Epoch.stream_unfetched_epochs( initial, reducer, true @@ -101,56 +102,176 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do service: :indexer, tracer: Tracer ) - def run(pending_operations, json_rpc_named_arguments) do - Enum.each(pending_operations, fn pending_operation -> - fetch(pending_operation, json_rpc_named_arguments) + def run(epochs, json_rpc_named_arguments) do + epochs + |> Repo.preload([ + :start_processing_block, + :end_processing_block + ]) + |> Enum.each(fn epoch -> + epoch + |> fetch(json_rpc_named_arguments) end) :ok end - defp fetch(pending_operation, json_rpc_named_arguments) do - {:ok, distributions} = Distributions.fetch(pending_operation) - {:ok, validator_and_group_payments} = ValidatorAndGroupPayments.fetch(pending_operation) + defp fetch(epoch, json_rpc_named_arguments) do + election_rewards_params = fetch_election_rewards_params(epoch, json_rpc_named_arguments) + epoch_params = fetch_epoch_params(epoch) + {:ok, distributions_params} = Distributions.fetch(epoch) + + epochs_params = + (epoch.number + 1) + |> Epoch.epoch_by_number_query() + |> Repo.exists?() + |> if do + [epoch_params] + else + next_epoch_params = %{ + number: epoch_params.number + 1, + start_block_number: epoch_params.end_block_number + 1 + } + + [epoch_params, next_epoch_params] + end + + addresses_params = + Addresses.extract_addresses(%{ + celo_election_rewards: election_rewards_params + }) + + {:ok, _imported_addresses} = Chain.import(%{addresses: %{params: addresses_params}}) + + {:ok, import_multi} = + Import.all_single_multi( + [ + Epochs, + ElectionRewards, + EpochRewards + ], + %{ + celo_epoch_rewards: %{params: [distributions_params]}, + celo_election_rewards: %{params: election_rewards_params}, + celo_epochs: %{params: epochs_params} + } + ) + + Multi.new() + |> Multi.run( + :acquire_processing_blocks, + fn repo, _changes -> + acquire_processing_blocks(repo, epoch) + end + ) + |> Multi.append(import_multi) + |> Repo.transaction() + |> case do + {:ok, results} -> + Logger.info("Successfully fetched and imported epoch rewards for epoch number: #{epoch.number}") + {:ok, results} + + {:error, :acquire_processing_blocks, :processing_blocks_not_consensus, _changes} -> + Logger.error( + "Skipped importing epoch rewards for epoch #{epoch.number} since processing blocks are not consensus" + ) + + {:error, :processing_blocks_not_consensus} + + {:error, operation, reason, _changes} -> + Logger.error("Failed importing epoch rewards for epoch #{epoch.number} on #{operation}: #{inspect(reason)}") + {:error, reason} + end + end + defp fetch_election_rewards_params(epoch, json_rpc_named_arguments) do {:ok, voter_payments} = VoterPayments.fetch( - pending_operation, + epoch, json_rpc_named_arguments ) - {:ok, delegated_payments} = - validator_and_group_payments - |> Enum.filter(&(&1.type == :validator)) - |> Enum.map(& &1.account_address_hash) - |> DelegatedPayments.fetch( - pending_operation, - json_rpc_named_arguments + {:ok, validator_and_group_payments} = + if Helper.pre_migration_block_number?(epoch.start_processing_block.number) do + ValidatorAndGroupPaymentsPriorL2Migration.fetch(epoch) + else + ValidatorAndGroupPaymentsPostL2Migration.fetch(epoch) + end + + {:ok, delegated_payments_prior_l2_migration} = + if Helper.pre_migration_block_number?(epoch.start_processing_block.number) do + validator_and_group_payments + |> Enum.filter(&(&1.type == :validator)) + |> Enum.map(& &1.account_address_hash) + |> DelegatedPaymentsPriorL2Migration.fetch( + epoch, + json_rpc_named_arguments + ) + else + {:ok, []} + end + + [ + voter_payments, + validator_and_group_payments, + delegated_payments_prior_l2_migration + ] + |> Enum.concat() + |> Enum.filter(&(&1.amount > 0)) + end + + defp acquire_processing_blocks(repo, epoch) do + # First verify start block consensus and lock it to prevent changes during our operation + + query = + from(b in Block.consensus_blocks_query(), + where: + b.hash in ^[ + epoch.start_processing_block_hash, + epoch.end_processing_block_hash + ], + order_by: [asc: b.hash], + lock: "FOR SHARE" ) - election_rewards = - [ - validator_and_group_payments, - voter_payments, - delegated_payments - ] - |> Enum.concat() - |> Enum.filter(&(&1.amount > 0)) + premigration? = Helper.pre_migration_block_number?(epoch.start_processing_block.number) - addresses_params = - Addresses.extract_addresses(%{ - celo_election_rewards: election_rewards - }) + query + |> repo.all() + |> case do + [_] = blocks when premigration? -> + {:ok, blocks} - {:ok, imported} = - Chain.import(%{ - addresses: %{params: addresses_params}, - celo_election_rewards: %{params: election_rewards}, - celo_epoch_rewards: %{params: [distributions]} - }) + [_, _] = blocks -> + {:ok, blocks} + + [] -> + {:error, :processing_blocks_not_consensus} + end + end - Logger.info("Fetched epoch rewards for block number: #{pending_operation.block_number}") + defp fetch_epoch_params(epoch) do + params = %{number: epoch.number, fetched?: true} - {:ok, imported} + if Helper.pre_migration_block_number?(epoch.start_processing_block.number) do + { + start_block_number, + end_block_number + } = Helper.epoch_number_to_block_range(epoch.number) + + params + |> Map.put(:start_block_number, start_block_number) + |> Map.put(:end_block_number, end_block_number) + else + {:ok, start_block_number} = + EpochManager.fetch_first_block_at_epoch(epoch.number) + + {:ok, end_block_number} = + EpochManager.fetch_last_block_at_epoch(epoch.number) + + params + |> Map.put(:start_block_number, start_block_number) + |> Map.put(:end_block_number, end_block_number) + end end end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/core_contract_version.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/core_contract_version.ex index d6c011de9b12..9b3a5d237048 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/core_contract_version.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/core_contract_version.ex @@ -2,7 +2,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.CoreContractVersion do @moduledoc """ Fetches the version of the celo core contract. """ - import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] + import Explorer.Helper, only: [abi_to_method_id: 1] import Indexer.Helper, only: [read_contracts_with_retries: 5] @repeated_request_max_retries 3 diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex similarity index 83% rename from apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments.ex rename to apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex index 8569019a3b42..3005e057988f 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex @@ -1,14 +1,21 @@ -defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do +defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPaymentsPriorL2Migration do @moduledoc """ Fetches delegated validator payments for the epoch block. """ import Ecto.Query, only: [from: 2] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] - import Indexer.Helper, only: [read_contracts_with_retries: 4] + import Explorer.Helper, only: [abi_to_method_id: 1] + import Indexer.Helper, + only: [ + read_contracts_with_retries_by_chunks: 3, + read_contracts_with_retries: 4 + ] + + alias Explorer.Chain.{Block, Hash, TokenTransfer} alias Explorer.Chain.Cache.CeloCoreContracts - alias Explorer.Chain.{Hash, TokenTransfer} + alias Explorer.Chain.Celo.Epoch + alias Explorer.Chain.Wei alias Explorer.Repo alias Indexer.Fetcher.Celo.EpochBlockOperations.CoreContractVersion @@ -17,6 +24,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do @mint_address_hash_string burn_address_hash_string() @repeated_request_max_retries 3 + @requests_chunk_size 100 # The method `getPaymentDelegation` was introduced in the following. Thus, we # set version hardcoded in `getVersionNumber` method. @@ -43,17 +51,14 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do @spec fetch( [EthereumJSONRPC.address()], - %{ - :block_hash => EthereumJSONRPC.hash(), - :block_number => EthereumJSONRPC.block_number() - }, + Epoch.t(), EthereumJSONRPC.json_rpc_named_arguments() ) :: {:ok, list()} | {:error, any()} def fetch( validator_addresses, - %{block_number: block_number, block_hash: block_hash} = _pending_operation, + %Epoch{start_processing_block: %Block{number: block_number, hash: block_hash}} = epoch, json_rpc_named_arguments ) do with {:ok, accounts_contract_address} <- @@ -98,12 +103,12 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do |> Enum.filter(&match?({_, {:ok, [_, fraction]}} when fraction > 0, &1)) |> Enum.map(fn {validator_address, {:ok, [beneficiary_address, _]}} -> - amount = Map.get(beneficiary_address_to_amount, beneficiary_address, 0) + amount = beneficiary_address_to_amount |> Map.get(beneficiary_address, 0) %{ - block_hash: block_hash, + epoch_number: epoch.number, account_address_hash: beneficiary_address, - amount: amount, + amount: %Wei{value: amount}, associated_account_address_hash: validator_address, type: :delegated_payment } @@ -157,10 +162,16 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPayments do block_number: block_number } ) - |> read_contracts_with_retries( - @get_payment_delegation_abi, - json_rpc_named_arguments, - @repeated_request_max_retries + |> read_contracts_with_retries_by_chunks( + @requests_chunk_size, + fn requests -> + read_contracts_with_retries( + requests, + @get_payment_delegation_abi, + json_rpc_named_arguments, + @repeated_request_max_retries + ) + end ) end end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex index e21ce3f535b4..9ce3cc842b67 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex @@ -3,42 +3,37 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do Fetches Reserve bolster, Community, and Carbon offsetting distributions for the epoch block. """ + use Utils.RuntimeEnvHelper, + celo_unreleased_treasury_contract_address: [ + :explorer, + [:celo, :celo_unreleased_treasury_contract_address] + ] + import Ecto.Query, only: [from: 2, subquery: 1] + import Explorer.Chain.Celo.Helper, only: [pre_migration_block_number?: 1] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - alias Explorer.Repo - alias Explorer.Chain.{ + Block, Cache.CeloCoreContracts, Hash, TokenTransfer } - @mint_address_hash_string burn_address_hash_string() + alias Explorer.Chain.Celo.Epoch + alias Explorer.Repo - @spec fetch(%{ - :block_hash => EthereumJSONRPC.hash(), - :block_number => EthereumJSONRPC.block_number() - }) :: + @spec fetch(Epoch.t()) :: {:ok, map()} | {:error, :multiple_transfers_to_same_address} - def fetch(%{block_number: block_number, block_hash: block_hash} = _pending_operation) do - {:ok, celo_token_contract_address_hash} = CeloCoreContracts.get_address(:celo_token, block_number) + def fetch(%{end_processing_block: %Block{number: block_number, hash: block_hash}} = epoch) do {:ok, reserve_contract_address_hash} = CeloCoreContracts.get_address(:reserve, block_number) {:ok, community_contract_address_hash} = CeloCoreContracts.get_address(:governance, block_number) {:ok, %{"address" => carbon_offsetting_contract_address_hash}} = CeloCoreContracts.get_event(:epoch_rewards, :carbon_offsetting_fund_set, block_number) - celo_mint_transfers_query = - from( - tt in TokenTransfer.only_consensus_transfers_query(), - where: - tt.block_hash == ^block_hash and - tt.token_contract_address_hash == ^celo_token_contract_address_hash and - tt.from_address_hash == ^@mint_address_hash_string and - is_nil(tt.transaction_hash) - ) + celo_distributions_query = celo_distributions_query(block_hash, block_number) # Every epoch has at least one CELO transfer from the zero address to the # reserve. This is how cUSD is minted before it is distributed to @@ -49,7 +44,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do from( tt in subquery( from( - tt in subquery(celo_mint_transfers_query), + tt in subquery(celo_distributions_query), where: tt.to_address_hash == ^reserve_contract_address_hash, order_by: tt.log_index, offset: 1 @@ -60,7 +55,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do query = from( - tt in subquery(celo_mint_transfers_query), + tt in subquery(celo_distributions_query), where: tt.to_address_hash in ^[ community_contract_address_hash, @@ -91,11 +86,33 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do key = Map.get(address_to_key, address |> Hash.to_string()) Map.put(acc, key, log_index) end) - |> Map.put(:block_hash, block_hash) + |> Map.put(:epoch_number, epoch.number) {:ok, distributions} else {:error, :multiple_transfers_to_same_address} end end + + defp celo_distributions_query(block_hash, block_number) do + {:ok, celo_token_contract_address_hash} = CeloCoreContracts.get_address(:celo_token, block_number) + + query = + from( + tt in TokenTransfer.only_consensus_transfers_query(), + where: + tt.block_hash == ^block_hash and + tt.token_contract_address_hash == ^celo_token_contract_address_hash + ) + + if pre_migration_block_number?(block_number) do + celo_sender_address_hash = burn_address_hash_string() + + from(tt in query, where: tt.from_address_hash == ^celo_sender_address_hash and is_nil(tt.transaction_hash)) + else + celo_sender_address_hash = celo_unreleased_treasury_contract_address() + + from(tt in query, where: tt.from_address_hash == ^celo_sender_address_hash) + end + end end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments.ex deleted file mode 100644 index f677d7439442..000000000000 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Indexer.Fetcher.Celo.EpochBlockOperations.ValidatorAndGroupPayments do - @moduledoc """ - Fetches validator and group payments for the epoch block. - """ - import Ecto.Query, only: [from: 2] - - alias Explorer.Chain.Cache.CeloCoreContracts - alias Explorer.Chain.Log - alias Explorer.Repo - alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions - - @spec fetch(%{ - :block_hash => EthereumJSONRPC.hash(), - :block_number => EthereumJSONRPC.block_number() - }) :: {:ok, list()} - def fetch(%{block_number: block_number, block_hash: block_hash} = _pending_operation) do - epoch_payment_distributions_signature = ValidatorEpochPaymentDistributions.signature() - {:ok, validators_contract_address} = CeloCoreContracts.get_address(:validators, block_number) - - query = - from( - log in Log, - where: - log.block_hash == ^block_hash and - log.address_hash == ^validators_contract_address and - log.first_topic == ^epoch_payment_distributions_signature and - is_nil(log.transaction_hash), - select: log - ) - - payments = - query - |> Repo.all() - |> ValidatorEpochPaymentDistributions.parse() - |> process_distribution_events(block_hash) - - {:ok, payments} - end - - defp process_distribution_events(distribution_events, block_hash) do - distribution_events - |> Enum.map(fn %{ - validator_address: validator_address, - validator_payment: validator_payment, - group_address: group_address, - group_payment: group_payment - } -> - [ - %{ - block_hash: block_hash, - account_address_hash: validator_address, - amount: validator_payment, - associated_account_address_hash: group_address, - type: :validator - }, - %{ - block_hash: block_hash, - account_address_hash: group_address, - amount: group_payment, - associated_account_address_hash: validator_address, - type: :group - } - ] - end) - |> Enum.concat() - end -end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_post_l2_migration.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_post_l2_migration.ex new file mode 100644 index 000000000000..3feb70abace2 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_post_l2_migration.ex @@ -0,0 +1,327 @@ +defmodule Indexer.Fetcher.Celo.EpochBlockOperations.ValidatorAndGroupPaymentsPostL2Migration do + @moduledoc """ + Fetches validator and group payments for the epoch post L2 migration. + """ + require Logger + + use Utils.RuntimeEnvHelper, + epoch_manager_contract_address_hash: [ + :explorer, + [:celo, :epoch_manager_contract_address] + ], + validators_contract_address_hash: [ + :explorer, + [:celo, :validators_contract_address] + ], + json_rpc_named_arguments: [:indexer, :json_rpc_named_arguments] + + import Indexer.Helper, + only: [ + read_contracts_with_retries_by_chunks: 3, + read_contracts_with_retries: 4 + ] + + import Ecto.Query, only: [from: 2] + import Explorer.Helper, only: [abi_to_method_id: 1] + + alias Explorer.Chain.{Block, Log} + alias Explorer.Chain.Celo.Epoch + alias Explorer.Repo + + @repeated_request_max_retries 3 + @requests_chunk_size 100 + + @number_of_elected_in_current_set_abi [ + %{ + "name" => "numberOfElectedInCurrentSet", + "type" => "function", + "stateMutability" => "view", + "inputs" => [], + "outputs" => [%{"type" => "uint256"}] + } + ] + + @get_elected_account_by_index_abi [ + %{ + "name" => "getElectedAccountByIndex", + "type" => "function", + "stateMutability" => "view", + "inputs" => [%{"type" => "uint256"}], + "outputs" => [%{"type" => "address"}] + } + ] + + @validator_pending_payments_abi [ + %{ + "name" => "validatorPendingPayments", + "type" => "function", + "stateMutability" => "view", + "inputs" => [%{"type" => "address"}], + "outputs" => [%{"type" => "uint256"}] + } + ] + + @get_validators_group_abi [ + %{ + "name" => "getValidatorsGroup", + "type" => "function", + "stateMutability" => "view", + "inputs" => [%{"type" => "address"}], + "outputs" => [%{"type" => "address"}] + } + ] + + @get_validator_group_abi [ + %{ + "name" => "getValidatorGroup", + "type" => "function", + "stateMutability" => "view", + "inputs" => [%{"type" => "address"}], + "outputs" => [ + %{"type" => "address[]"}, + %{"type" => "uint256"}, + %{"type" => "uint256"}, + %{"type" => "uint256"}, + %{"type" => "uint256[]"}, + %{"type" => "uint256"}, + %{"type" => "uint256"} + ] + } + ] + + @validator_epoch_payment_distributed_event_topic "0xee2788e7abedfc61d9608e143b172de1a608a4298b06ed8c84838aa0ad6bd136" + + def fetch(%Epoch{number: epoch_number, start_processing_block: %Block{number: block_number, hash: block_hash}}) do + with false <- + check_if_validator_payment_distributed_events_exist(block_hash), + {:ok, length} <- number_of_elected_in_current_set(block_number), + {:ok, account_address_hashes} <- get_elected_accounts(block_number, length), + {:ok, payments_before} <- get_allocated_payments(account_address_hashes, block_number - 1), + {:ok, payments_after} <- get_allocated_payments(account_address_hashes, block_number), + {:ok, group_address_hashes} <- get_validator_groups(account_address_hashes, block_number), + unique_group_address_hashes = Enum.uniq(group_address_hashes), + {:ok, group_commissions} <- get_validator_group_commissions(unique_group_address_hashes, block_number) do + account_address_hash_to_group_address_hash = + account_address_hashes + |> Enum.zip(group_address_hashes) + |> Map.new() + + group_address_hash_to_commission = + unique_group_address_hashes + |> Enum.zip(group_commissions) + |> Map.new() + + payments = Enum.zip_with(payments_after, payments_before, &(&1 - &2)) + + params = + account_address_hashes + |> Enum.zip(payments) + |> Enum.flat_map(fn {account_address_hash, payment} -> + group_address_hash = Map.get(account_address_hash_to_group_address_hash, account_address_hash) + + base = Decimal.new(1, 1, 24) + + commission = + group_address_hash_to_commission + |> Map.get(group_address_hash) + |> Decimal.new() + |> Decimal.div(base) + + payment = payment |> Decimal.new() + group_payment = payment |> Decimal.mult(commission) + validator_payment = payment |> Decimal.sub(group_payment) + + [ + %{ + epoch_number: epoch_number, + account_address_hash: account_address_hash, + amount: validator_payment, + associated_account_address_hash: group_address_hash, + type: :validator + }, + %{ + epoch_number: epoch_number, + account_address_hash: group_address_hash, + amount: group_payment, + associated_account_address_hash: account_address_hash, + type: :group + } + ] + end) + + {:ok, params} + else + true -> + # TODO: As invariant, we assume that the validator payment distributed + # event is not present in the block. If it is present, this is a corner + # case that should be addressed in the future. + Logger.error("Validator payment distributed event exists for block hash: #{block_hash}. Aborting.") + {:error, "Validator payment distributed event exists"} + + {:error, reason} -> + Logger.error("Failed to fetch validator and group payments: #{inspect(reason)}") + {:error, reason} + end + end + + @spec number_of_elected_in_current_set(EthereumJSONRPC.block_number()) :: + {:ok, integer()} | {:error, any()} + defp number_of_elected_in_current_set(block_number) do + method_id = @number_of_elected_in_current_set_abi |> abi_to_method_id() + + [ + %{ + contract_address: epoch_manager_contract_address_hash(), + method_id: method_id, + args: [], + block_number: block_number + } + ] + |> read_contracts_with_retries( + @number_of_elected_in_current_set_abi, + json_rpc_named_arguments(), + @repeated_request_max_retries + ) + |> case do + {[ok: [value]], []} -> + {:ok, value} + + {_, errors} -> + {:error, errors} + end + end + + defp get_elected_accounts(block_number, length) when length > 0 do + method_id = @get_elected_account_by_index_abi |> abi_to_method_id() + + 0..(length - 1) + |> Enum.map(fn index -> + %{ + contract_address: epoch_manager_contract_address_hash(), + method_id: method_id, + args: [index], + block_number: block_number + } + end) + |> read_contract(@get_elected_account_by_index_abi) + |> case do + {responses, []} -> + address_hashes = + responses + |> Enum.map(fn {:ok, [address_hash]} -> address_hash end) + + {:ok, address_hashes} + + {_, errors} -> + {:error, errors} + end + end + + defp get_allocated_payments(account_address_hashes, block_number) do + method_id = @validator_pending_payments_abi |> abi_to_method_id() + + account_address_hashes + |> Enum.map(fn address_hash -> + %{ + contract_address: epoch_manager_contract_address_hash(), + method_id: method_id, + args: [address_hash], + block_number: block_number + } + end) + |> read_contract(@validator_pending_payments_abi) + |> case do + {responses, []} -> + values = + responses + |> Enum.map(fn {:ok, [value]} -> value end) + + {:ok, values} + + {_, errors} -> + {:error, errors} + end + end + + defp get_validator_groups(account_address_hashes, block_number) do + method_id = @get_validators_group_abi |> abi_to_method_id() + + account_address_hashes + |> Enum.map(fn address_hash -> + %{ + contract_address: validators_contract_address_hash(), + method_id: method_id, + args: [address_hash], + block_number: block_number + } + end) + |> read_contract(@get_validators_group_abi) + |> case do + {responses, []} -> + values = + responses + |> Enum.map(fn {:ok, [value]} -> value end) + + {:ok, values} + + {_, errors} -> + {:error, errors} + end + end + + defp get_validator_group_commissions(unique_group_address_hashes, block_number) do + method_id = @get_validator_group_abi |> abi_to_method_id() + + unique_group_address_hashes + |> Enum.map(fn address_hash -> + %{ + contract_address: validators_contract_address_hash(), + method_id: method_id, + args: [address_hash], + block_number: block_number + } + end) + |> read_contract(@get_validator_group_abi) + |> case do + {responses, []} -> + values = + responses + |> Enum.map(fn {:ok, [_, value | _]} -> value end) + + {:ok, values} + + {_, errors} -> + {:error, errors} + end + end + + defp read_contract(requests, abi) do + read_contracts_with_retries_by_chunks( + requests, + @requests_chunk_size, + fn chunk -> + read_contracts_with_retries( + chunk, + abi, + json_rpc_named_arguments(), + @repeated_request_max_retries + ) + end + ) + end + + defp check_if_validator_payment_distributed_events_exist(block_hash) do + query = + from( + log in Log, + where: [ + block_hash: ^block_hash, + address_hash: ^epoch_manager_contract_address_hash(), + first_topic: ^@validator_epoch_payment_distributed_event_topic + ] + ) + + query + |> Repo.exists?() + end +end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex new file mode 100644 index 000000000000..91d3ab9289b3 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex @@ -0,0 +1,58 @@ +defmodule Indexer.Fetcher.Celo.EpochBlockOperations.ValidatorAndGroupPaymentsPriorL2Migration do + @moduledoc """ + Fetches validator and group payments for the epoch block. + """ + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.Cache.CeloCoreContracts + alias Explorer.Chain.{Block, Celo.Epoch, Log} + alias Explorer.Repo + alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions + + @spec fetch(Epoch.t()) :: {:ok, list()} + def fetch(%Epoch{start_processing_block: %Block{number: block_number, hash: block_hash}} = epoch) do + epoch_payment_distributions_signature = ValidatorEpochPaymentDistributions.signature() + {:ok, validators_contract_address} = CeloCoreContracts.get_address(:validators, block_number) + + query = + from( + log in Log, + where: + log.block_hash == ^block_hash and + log.address_hash == ^validators_contract_address and + log.first_topic == ^epoch_payment_distributions_signature and + is_nil(log.transaction_hash), + select: log + ) + + payments = + query + |> Repo.all() + |> ValidatorEpochPaymentDistributions.parse() + |> Enum.flat_map(fn %{ + validator_address: validator_address, + validator_payment: validator_payment, + group_address: group_address, + group_payment: group_payment + } -> + [ + %{ + epoch_number: epoch.number, + account_address_hash: validator_address, + amount: validator_payment, + associated_account_address_hash: group_address, + type: :validator + }, + %{ + epoch_number: epoch.number, + account_address_hash: group_address, + amount: group_payment, + associated_account_address_hash: validator_address, + type: :group + } + ] + end) + + {:ok, payments} + end +end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/voter_payments.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/voter_payments.ex index d526feb0c7e7..684840bb596c 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/voter_payments.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/voter_payments.ex @@ -2,17 +2,26 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do @moduledoc """ Fetches voter payments for the epoch block. """ - import Ecto.Query, only: [from: 2] + import Ecto.Query, only: [from: 2, select: 3] - import Explorer.Helper, only: [decode_data: 2] - import Indexer.Fetcher.Celo.Helper, only: [abi_to_method_id: 1] - import Indexer.Helper, only: [read_contracts_with_retries: 4] + import Explorer.Helper, + only: [ + decode_data: 2, + abi_to_method_id: 1 + ] + + import Indexer.Helper, + only: [ + read_contracts_with_retries_by_chunks: 3, + read_contracts_with_retries: 4 + ] alias Explorer.Repo alias Indexer.Fetcher.Celo.ValidatorGroupVotes alias Explorer.Chain.{ Cache.CeloCoreContracts, + Celo.Epoch, Celo.ValidatorGroupVote, Hash, Log @@ -22,6 +31,8 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do @repeated_request_max_retries 3 + @requests_chunk_size 100 + @epoch_rewards_distributed_to_voters_topic "0x91ba34d62474c14d6c623cd322f4256666c7a45b7fdaa3378e009d39dfcec2a7" @get_active_votes_for_group_by_account_abi [ @@ -44,38 +55,29 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do @get_active_votes_for_group_by_account_method_id @get_active_votes_for_group_by_account_abi |> abi_to_method_id() - @spec fetch( - %{ - :block_hash => EthereumJSONRPC.hash(), - :block_number => EthereumJSONRPC.block_number() - }, - EthereumJSONRPC.json_rpc_named_arguments() - ) :: {:error, list()} | {:ok, list()} + @spec fetch(Epoch.t(), EthereumJSONRPC.json_rpc_named_arguments()) :: + {:error, list()} | {:ok, list()} def fetch( - %{block_number: block_number, block_hash: block_hash} = pending_operation, + %Epoch{start_processing_block: start_block, end_processing_block: end_block} = epoch, json_rpc_named_arguments ) do - :ok = ValidatorGroupVotes.fetch(block_number) + :ok = ValidatorGroupVotes.fetch(end_block.number) - {:ok, election_contract_address} = CeloCoreContracts.get_address(:election, block_number) + {:ok, election_contract_address} = CeloCoreContracts.get_address(:election, start_block.number) elected_groups_query = - from( - l in Log, - where: - l.block_hash == ^block_hash and - l.address_hash == ^election_contract_address and - l.first_topic == ^@epoch_rewards_distributed_to_voters_topic and - is_nil(l.transaction_hash), - select: fragment("SUBSTRING(? from 13)", l.second_topic) - ) + start_block.number + |> epoch_rewards_distributed_to_voters_query(end_block.number) + |> select([l], fragment("SUBSTRING(? from 13)", l.second_topic)) + + end_block_number = end_block.number query = from( v in ValidatorGroupVote, where: v.group_address_hash in subquery(elected_groups_query) and - v.block_number <= ^block_number, + v.block_number <= ^end_block_number, distinct: true, select: {v.account_address_hash, v.group_address_hash} ) @@ -94,7 +96,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do requests = accounts_with_activated_votes |> Enum.map(fn {account_address_hash, group_address_hash} -> - (block_number - 1)..block_number + (end_block_number - 1)..end_block_number |> Enum.map(fn block_number -> %{ contract_address: election_contract_address, @@ -110,11 +112,17 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do |> Enum.concat() {responses, []} = - read_contracts_with_retries( + read_contracts_with_retries_by_chunks( requests, - @get_active_votes_for_group_by_account_abi, - json_rpc_named_arguments, - @repeated_request_max_retries + @requests_chunk_size, + fn requests -> + read_contracts_with_retries( + requests, + @get_active_votes_for_group_by_account_abi, + json_rpc_named_arguments, + @repeated_request_max_retries + ) + end ) diffs = @@ -138,7 +146,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do diffs, fn {account_address_hash, group_address_hash}, diff -> %{ - block_hash: block_hash, + epoch_number: epoch.number, account_address_hash: account_address_hash, amount: diff, associated_account_address_hash: group_address_hash, @@ -147,29 +155,35 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.VoterPayments do end ) - ok_or_error = validate_voter_rewards(pending_operation, rewards) + ok_or_error = validate_voter_rewards(start_block.number, end_block.number, rewards) {ok_or_error, rewards} end + defp epoch_rewards_distributed_to_voters_query(start_block_number, end_block_number) do + {:ok, election_contract_address} = CeloCoreContracts.get_address(:election, start_block_number) + + from( + l in Log, + where: + l.block_number >= ^start_block_number and + l.block_number <= ^end_block_number and + l.address_hash == ^election_contract_address and + l.first_topic == ^@epoch_rewards_distributed_to_voters_topic + ) + end + # Validates voter rewards by comparing the sum of what we got from the # `EpochRewardsDistributedToVoters` event and the sum of what we calculated # manually by fetching the votes for each account that has or had an activated # vote. - defp validate_voter_rewards(pending_operation, voter_rewards) do + defp validate_voter_rewards(start_block_number, end_block_number, voter_rewards) do manual_voters_total = voter_rewards |> Enum.map(& &1.amount) |> Enum.sum() - {:ok, election_contract_address} = CeloCoreContracts.get_address(:election, pending_operation.block_number) query = - from( - l in Log, - where: - l.block_hash == ^pending_operation.block_hash and - l.address_hash == ^election_contract_address and - l.first_topic == ^@epoch_rewards_distributed_to_voters_topic and - is_nil(l.transaction_hash), - select: l.data - ) + start_block_number + |> epoch_rewards_distributed_to_voters_query(end_block_number) + |> select([l], l.data) voter_rewards_from_event_total = query diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex index d7ab7b592494..f34f6c614f62 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex @@ -6,7 +6,7 @@ defmodule Indexer.Fetcher.Celo.EpochLogs do import Explorer.Chain.Celo.Helper, only: [ epoch_block_number?: 1, - premigration_block_number?: 1 + pre_migration_block_number?: 1 ] alias EthereumJSONRPC.{Logs, Transport} @@ -58,7 +58,7 @@ defmodule Indexer.Fetcher.Celo.EpochLogs do defp do_fetch(blocks, json_rpc_named_arguments) do requests = blocks - |> Enum.filter(&premigration_block_number?(&1.number)) + |> Enum.filter(&pre_migration_block_number?(&1.number)) |> Enum.reduce({[], 0}, &blocks_reducer/2) |> elem(0) |> Enum.reverse() diff --git a/apps/indexer/lib/indexer/fetcher/celo/helper.ex b/apps/indexer/lib/indexer/fetcher/celo/helper.ex deleted file mode 100644 index ec178397cd1f..000000000000 --- a/apps/indexer/lib/indexer/fetcher/celo/helper.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Indexer.Fetcher.Celo.Helper do - @moduledoc """ - Helper functions for the Celo fetchers. - """ - - @doc """ - Extracts the method ID from an ABI specification. - - ## Parameters - - `method` ([map()] | map()): The ABI specification, either as a single map - or a list containing one map. - - ## Returns - - `binary()`: The method ID extracted from the ABI specification. - - ## Examples - - iex> Indexer.Fetcher.Celo.Helper.abi_to_method_id([%{"name" => "transfer", "type" => "function", "inputs" => [%{"name" => "to", "type" => "address"}]}]) - <<26, 105, 82, 48>> - - """ - @spec abi_to_method_id([map()] | map()) :: binary() - def abi_to_method_id([method]), do: abi_to_method_id(method) - - def abi_to_method_id(method) when is_map(method) do - [parsed_method] = ABI.parse_specification([method]) - parsed_method.method_id - end -end diff --git a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex index 0369dfba071d..eb59b0d726b5 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex @@ -30,7 +30,7 @@ defmodule Indexer.Fetcher.Celo.ValidatorGroupVotes do @spec fetch(block_number :: EthereumJSONRPC.block_number()) :: :ok def fetch(block_number) do - GenServer.call(__MODULE__, {:fetch, block_number}) + GenServer.call(__MODULE__, {:fetch, block_number}, :infinity) end def child_spec(start_link_arguments) do diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 1ffc1e002aef..0c27faaa9045 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -380,6 +380,53 @@ defmodule Indexer.Helper do end end + @doc """ + Processes a large batch of contract calls by splitting them into smaller + chunks for efficiency and resiliency. + + This function takes a potentially large list of contract call requests, + divides them into manageable chunks, applies the provided reader function to + each chunk, and then consolidates the results. + + The chunking approach helps prevent timeouts that can occur when making too + many simultaneous RPC calls. + + ## Parameters + - `requests`: A list of `EthereumJSONRPC.Contract.call()` instances + representing contract calls to execute. + - `chunk_size`: The maximum number of contract calls to include in each chunk. + - `reader`: A function that takes a chunk of contract call requests and + returns `{responses, errors}` tuple. + + ## Returns + - A tuple `{responses, errors}` where: + - `responses`: A concatenated list of all response tuples `{:ok, result}` or + `{:error, reason}` from all chunks, maintaining the original + order. + - `errors`: A deduplicated list of all error messages encountered across all + chunks. + """ + @spec read_contracts_with_retries_by_chunks( + [EthereumJSONRPC.Contract.call()], + integer(), + ([EthereumJSONRPC.Contract.call()] -> + {[{:ok | :error, any()}], list()}) + ) :: + {[{:ok | :error, any()}], list()} + def read_contracts_with_retries_by_chunks(requests, chunk_size, reader) + when is_list(requests) and is_integer(chunk_size) and chunk_size > 0 do + {responses_lists, errors_lists} = + requests + |> Enum.chunk_every(chunk_size) + |> Enum.map(reader) + |> Enum.unzip() + + { + Enum.concat(responses_lists), + errors_lists |> Enum.concat() |> Enum.uniq() + } + end + @doc """ Retrieves decoded results of `eth_call` requests to contracts, with retry logic for handling errors. @@ -415,7 +462,8 @@ defmodule Indexer.Helper do [EthereumJSONRPC.Contract.call()], [map()], EthereumJSONRPC.json_rpc_named_arguments(), - integer() + integer(), + boolean() ) :: {[{:ok | :error, any()}], list()} def read_contracts_with_retries(requests, abi, json_rpc_named_arguments, retries_left, log_error? \\ true) when is_list(requests) and is_list(abi) and is_integer(retries_left) do diff --git a/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex b/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex new file mode 100644 index 000000000000..88c60a891d2e --- /dev/null +++ b/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex @@ -0,0 +1,50 @@ +defmodule Indexer.Transform.Celo.L1Epochs do + @moduledoc """ + Transformer for Celo L1 epoch data from blockchain blocks. + + This module processes blocks from the Celo blockchain to extract epoch + information for pre-migration blocks (L1 epochs). It identifies epoch blocks + based on the mathematical formula that governed Celo's epoch structure before + the L2 migration. + + This information is essential for tracking Celo's epoch structure during the + L1 era, which followed a deterministic mathematical formula. + """ + + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type] + + alias Explorer.Chain.Celo.Helper + + @spec parse([EthereumJSONRPC.Block.params()]) :: [map()] + def parse(blocks) do + if chain_type() == :celo do + do_parse(blocks) + else + [] + end + end + + defp do_parse(blocks) do + blocks + |> Enum.filter(fn %{number: number} -> + Helper.pre_migration_block_number?(number) and + Helper.epoch_block_number?(number) + end) + |> Enum.map(fn block -> + epoch_number = + Helper.block_number_to_epoch_number(block.number) + + {start_block_number, end_block_number} = + Helper.epoch_number_to_block_range(epoch_number) + + %{ + number: epoch_number, + start_block_number: start_block_number, + end_block_number: end_block_number, + start_processing_block_hash: block.hash, + end_processing_block_hash: block.hash + } + end) + end +end diff --git a/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex b/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex new file mode 100644 index 000000000000..0edc0c43ee22 --- /dev/null +++ b/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex @@ -0,0 +1,67 @@ +defmodule Indexer.Transform.Celo.L2Epochs do + @moduledoc """ + Transformer for Celo L2 epoch data from blockchain logs. + + This module processes logs from the Celo EpochManager contract to extract + epoch information for post-migration blocks (L2 epochs). It identifies epoch + processing start and end events by filtering logs based on their topics and + the contract address. + + This information is essential for tracking Celo's epoch structure after the L2 + migration, which no longer follows a simple deterministic formula. + """ + + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + epoch_manager_contract_address: [ + :explorer, + [:celo, :epoch_manager_contract_address] + ] + + import Explorer.Helper, only: [decode_data: 2] + + alias Explorer.Chain.Celo.Helper + + # Events from the EpochManager contract + @epoch_processing_started_topic "0xae58a33f8b8d696bcbaca9fa29d9fdc336c140e982196c2580db3d46f3e6d4b6" + @epoch_processing_ended_topic "0xc8e58d8e6979dd5e68bad79d4a4368a1091f6feb2323e612539b1b84e0663a8f" + + @spec parse([map()]) :: [map()] + def parse(logs) do + if chain_type() == :celo do + do_parse(logs) + else + [] + end + end + + defp do_parse(logs) do + logs + |> Enum.filter( + &(not Helper.pre_migration_block_number?(&1.block_number) and + &1.address_hash == epoch_manager_contract_address() |> String.downcase() and + &1.first_topic in [ + @epoch_processing_started_topic, + @epoch_processing_ended_topic + ]) + ) + |> Enum.reduce(%{}, fn log, epochs_acc -> + # Extract epoch number from the log + [epoch_number] = decode_data(log.second_topic, [{:uint, 256}]) + + current_epoch = Map.get(epochs_acc, epoch_number, %{number: epoch_number}) + + updated_epoch = + case log.first_topic do + @epoch_processing_started_topic -> + Map.put(current_epoch, :start_processing_block_hash, log.block_hash) + + @epoch_processing_ended_topic -> + Map.put(current_epoch, :end_processing_block_hash, log.block_hash) + end + + Map.put(epochs_acc, epoch_number, updated_epoch) + end) + |> Map.values() + end +end diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 49d0b0745268..62411e127ab9 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -812,6 +812,7 @@ defmodule Indexer.Block.FetcherTest do Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() + {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) UncleBlock.Supervisor.Case.start_supervised!( block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} diff --git a/apps/indexer/test/indexer/fetcher/celo/epoch_block_operations_test.exs b/apps/indexer/test/indexer/fetcher/celo/epoch_block_operations_test.exs deleted file mode 100644 index 02130aee01af..000000000000 --- a/apps/indexer/test/indexer/fetcher/celo/epoch_block_operations_test.exs +++ /dev/null @@ -1,64 +0,0 @@ -defmodule Indexer.Fetcher.Celo.EpochBlockOperationsTest do - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - use EthereumJSONRPC.Case - use Explorer.DataCase - - import Explorer.Chain.Celo.Helper, only: [blocks_per_epoch: 0] - import Mox - - alias Indexer.Fetcher.Celo.EpochBlockOperations - - # MUST use global mode because we aren't guaranteed to get - # `start_supervised`'s pid back fast enough to `allow` it to use - # expectations and stubs from test's pid. - setup :set_mox_global - - setup :verify_on_exit! - - if @chain_type == :celo do - describe "init/3" do - test "buffers blocks with pending epoch operation", %{ - json_rpc_named_arguments: json_rpc_named_arguments - } do - unfetched = insert(:block, number: 1 * blocks_per_epoch()) - insert(:celo_pending_epoch_block_operation, block: unfetched) - - assert [ - %{ - block_number: unfetched.number, - block_hash: unfetched.hash - } - ] == - EpochBlockOperations.init( - [], - fn block_number, acc -> [block_number | acc] end, - json_rpc_named_arguments - ) - end - end - - test "correctly buffers blocks when :l2_migration_block is set", %{ - json_rpc_named_arguments: json_rpc_named_arguments - } do - last_l1_epoch_block_number = 1 * blocks_per_epoch() - l1_block = insert(:block, number: last_l1_epoch_block_number) - l2_block = insert(:block, number: 2 * blocks_per_epoch()) - insert(:celo_pending_epoch_block_operation, block: l1_block) - insert(:celo_pending_epoch_block_operation, block: l2_block) - - Application.put_env(:explorer, :celo, l2_migration_block: last_l1_epoch_block_number) - - assert [ - %{ - block_number: l1_block.number, - block_hash: l1_block.hash - } - ] == - EpochBlockOperations.init( - [], - fn block_number, acc -> [block_number | acc] end, - json_rpc_named_arguments - ) - end - end -end diff --git a/config/runtime.exs b/config/runtime.exs index 31f180caa8e9..9996602c10ac 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -562,7 +562,14 @@ config :explorer, Explorer.Chain.Cache.Uncles, ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) -config :explorer, :celo, l2_migration_block: ConfigHelper.parse_integer_or_nil_env_var("CELO_L2_MIGRATION_BLOCK") +celo_l2_migration_block = ConfigHelper.parse_integer_or_nil_env_var("CELO_L2_MIGRATION_BLOCK") +celo_epoch_manager_contract_address = System.get_env("CELO_EPOCH_MANAGER_CONTRACT") + +config :explorer, :celo, + l2_migration_block: celo_l2_migration_block, + epoch_manager_contract_address: celo_epoch_manager_contract_address, + celo_unreleased_treasury_contract_address: System.get_env("CELO_UNRELEASED_TREASURY_CONTRACT"), + validators_contract_address: System.get_env("CELO_VALIDATORS_CONTRACT") config :explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: ConfigHelper.parse_json_env_var("CELO_CORE_CONTRACTS") @@ -764,6 +771,14 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_CONCURRENCY", 1) +config :explorer, Explorer.Migrator.CeloL2Epochs, + enabled: + ConfigHelper.chain_type() == :celo && + !is_nil(celo_l2_migration_block) && + !is_nil(celo_epoch_manager_contract_address) + +config :explorer, Explorer.Chain.Cache.CeloEpochs, enabled: ConfigHelper.chain_type() == :celo + config :explorer, Explorer.Migrator.ShrinkInternalTransactions, enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_SHRINK_INTERNAL_TRANSACTIONS_BATCH_SIZE", 100), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index ed2e55326d51..1ce0d183baf5 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -323,6 +323,9 @@ API_V1_WRITE_METHODS_DISABLED=false # SCROLL_L1_BLOB_BASE_FEE_INIT= # CELO_CORE_CONTRACTS= # CELO_L2_MIGRATION_BLOCK= +# CELO_EPOCH_MANAGER_CONTRACT= +# CELO_UNRELEASED_TREASURY_CONTRACT= +# CELO_VALIDATORS_CONTRACT= # INDEXER_CELO_VALIDATOR_GROUP_VOTES_BATCH_SIZE=200000 # INDEXER_DISABLE_CELO_EPOCH_FETCHER=false # INDEXER_DISABLE_CELO_VALIDATOR_GROUP_VOTES_FETCHER=false @@ -579,4 +582,4 @@ RELEASE_COOKIE=secret_cookie # RE_CAPTCHA_CLIENT_KEY= # RE_CAPTCHA_V3_CLIENT_KEY= # HACKNEY_DEFAULT_POOL_SIZE=1000 -# UNIVERSAL_PROXY_CONFIG_URL= \ No newline at end of file +# UNIVERSAL_PROXY_CONFIG_URL= From 7f461c292fe7521a4d20e075ca303d5f517e1003 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 9 Jun 2025 16:27:12 +0300 Subject: [PATCH 0088/1006] fix: resolve timeouts on Celo epoch reward contract reads (#12229) * feat: chunked rpc requests in celo epochs fetcher * chore: format * chore: remove artifact * fix: update timeout for fetch call in `ValidatorGroupVotes` fetcher --- .../delegated_payments_prior_l2_migration.ex | 1 + apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex index 3005e057988f..cb29fc1f9aa3 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex @@ -4,6 +4,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPaymentsPriorL2Migr """ import Ecto.Query, only: [from: 2] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + import Explorer.Helper, only: [abi_to_method_id: 1] import Indexer.Helper, diff --git a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex index eb59b0d726b5..2c461dc311b6 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex @@ -30,7 +30,7 @@ defmodule Indexer.Fetcher.Celo.ValidatorGroupVotes do @spec fetch(block_number :: EthereumJSONRPC.block_number()) :: :ok def fetch(block_number) do - GenServer.call(__MODULE__, {:fetch, block_number}, :infinity) + GenServer.call(__MODULE__, {:fetch, block_number}, 60_000) end def child_spec(start_link_arguments) do From 0875fc0a64b0517cdbd11bf1b5e765a8de9c2869 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:36:11 +0400 Subject: [PATCH 0089/1006] feat: Decode OP interop message payload, store cross-chain transfer data, display message page, send messages to Multichain (#12387) * Add payload parsing for interop messages * Ignore INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS for the first 3 iterations * Clear OP interop messages table * Limit memory consumption by Interop.MessageQueue fetcher * Add Optimism.Interop.MultichainExport fetcher * Add /api/v2/optimism/interop/messages/:unique_id API endpoint * Fix for sobelow * Refactoring * Use parameterized raw query * Refactoring * Improve post request * Fixes and refactoring * Add INDEXER_POLYGON_ZKEVM_BATCHES_IGNORE env variable * Refactoring --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../controllers/api/v2/optimism_controller.ex | 106 ++++++- .../lib/block_scout_web/routers/api_router.ex | 1 + .../views/api/v2/optimism_view.ex | 25 ++ .../views/api/v2/scroll_view.ex | 8 +- .../runner/optimism/interop_messages.ex | 15 +- .../chain/optimism/interop_message.ex | 176 +++++++++++- .../explorer/chain/polygon_zkevm/reader.ex | 2 + .../multichain_search.ex | 19 +- .../lib/explorer/utility/microservice.ex | 2 +- ...50501120524_op_interop_transfer_fields.exs | 15 + .../20250610100740_add_op_interop_indices.exs | 18 ++ .../fetcher/optimism/interop/message.ex | 36 ++- .../optimism/interop/message_failed.ex | 16 +- .../fetcher/optimism/interop/message_queue.ex | 130 ++++++--- .../optimism/interop/multichain_export.ex | 260 ++++++++++++++++++ .../indexer/fetcher/optimism/withdrawal.ex | 3 +- .../polygon_zkevm/transaction_batch.ex | 74 +++-- apps/indexer/lib/indexer/supervisor.ex | 4 + config/runtime.exs | 4 + cspell.json | 1 + docker-compose/envs/common-blockscout.env | 2 + 21 files changed, 817 insertions(+), 100 deletions(-) create mode 100644 apps/explorer/priv/optimism/migrations/20250501120524_op_interop_transfer_fields.exs create mode 100644 apps/explorer/priv/optimism/migrations/20250610100740_add_op_interop_indices.exs create mode 100644 apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index b965380de706..ede497734bde 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -20,7 +20,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do alias BlockScoutWeb.API.V2.ApiView alias Explorer.Chain alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.{Hash, Token, Transaction} alias Explorer.Chain.Optimism.{ Deposit, @@ -273,6 +273,79 @@ defmodule BlockScoutWeb.API.V2.OptimismController do items_count(conn, Deposit) end + @doc """ + Function to handle GET requests to `/api/v2/optimism/interop/messages/:unique_id` endpoint. + """ + @spec interop_message(Plug.Conn.t(), map()) :: Plug.Conn.t() + def interop_message(conn, params) do + unique_id = Map.get(params, "unique_id", "") + + with true <- String.length(unique_id) == 16, + {init_chain_id_string, nonce_string} = String.split_at(unique_id, 8), + {init_chain_id, ""} <- Integer.parse(init_chain_id_string, 16), + {nonce, ""} <- Integer.parse(nonce_string, 16), + msg = InteropMessage.get_message(init_chain_id, nonce), + false <- is_nil(msg) do + current_chain_id = + case ChainId.get_id() do + nil -> Application.get_env(:block_scout_web, :chain_id) + chain_id -> chain_id + end + + relay_chain_id = msg.relay_chain_id + + direction = + case current_chain_id do + ^init_chain_id -> :out + ^relay_chain_id -> :in + _ -> nil + end + + transfer_token = + if not is_nil(msg.transfer_token_address_hash) do + case Token.get_by_contract_address_hash(msg.transfer_token_address_hash, @api_true) do + nil -> %{contract_address_hash: msg.transfer_token_address_hash, symbol: nil, decimals: nil} + t -> %{contract_address_hash: t.contract_address_hash, symbol: t.symbol, decimals: t.decimals} + end + end + + message = + msg + |> InteropMessage.extend_with_status() + |> Map.put(:init_chain, interop_chain_id_to_instance_info(msg.init_chain_id)) + |> Map.put(:relay_chain, interop_chain_id_to_instance_info(msg.relay_chain_id)) + |> Map.put(:direction, direction) + |> Map.put(:transfer_token, transfer_token) + + conn + |> put_status(200) + |> render(:optimism_interop_message, %{message: message}) + else + _ -> + conn + |> put_view(ApiView) + |> put_status(:not_found) + |> render(:message, %{message: "Invalid message id or the message with such id is not found"}) + end + end + + # Calls `InteropMessage.interop_chain_id_to_instance_info` function and depending on the result + # returns a map with the instance info. + # + # ## Parameters + # - `chain_id`: ID of the chain the instance info is needed for. + # + # ## Returns + # - A map with the instance info. + # - If the info cannot be retrieved, anyway returns the map with a single `chain_id` item. + @spec interop_chain_id_to_instance_info(non_neg_integer()) :: map() + defp interop_chain_id_to_instance_info(chain_id) do + case InteropMessage.interop_chain_id_to_instance_info(chain_id) do + nil -> %{chain_id: chain_id} + chain -> chain + end + end + @doc """ Function to handle GET requests to `/api/v2/optimism/interop/messages` endpoint. """ @@ -298,18 +371,20 @@ defmodule BlockScoutWeb.API.V2.OptimismController do messages_extended = messages |> Enum.map(fn message -> - cond do - message.init_chain_id != current_chain_id and not is_nil(current_chain_id) -> - Map.put(message, :init_chain, InteropMessage.interop_chain_id_to_instance_info(message.init_chain_id)) + message_extended = + cond do + message.init_chain_id != current_chain_id and not is_nil(current_chain_id) -> + Map.put(message, :init_chain, InteropMessage.interop_chain_id_to_instance_info(message.init_chain_id)) - message.relay_chain_id != current_chain_id and not is_nil(current_chain_id) -> - Map.put(message, :relay_chain, InteropMessage.interop_chain_id_to_instance_info(message.relay_chain_id)) + message.relay_chain_id != current_chain_id and not is_nil(current_chain_id) -> + Map.put(message, :relay_chain, InteropMessage.interop_chain_id_to_instance_info(message.relay_chain_id)) - true -> - message - end + true -> + message + end + + InteropMessage.extend_with_status(message_extended) end) - |> Enum.map(&InteropMessage.extend_with_status(&1)) conn |> put_status(200) @@ -492,6 +567,11 @@ defmodule BlockScoutWeb.API.V2.OptimismController do # - Resulting map with the `op_interop_messages` table's fields. @spec interop_prepare_import(map()) :: map() defp interop_prepare_import(%{"init_transaction_hash" => init_transaction_hash} = params) do + payload = hash_to_binary(params["payload"]) + + [transfer_token_address_hash, transfer_from_address_hash, transfer_to_address_hash, transfer_amount] = + InteropMessage.decode_payload(payload) + %{ sender_address_hash: params["sender_address_hash"], target_address_hash: params["target_address_hash"], @@ -500,7 +580,11 @@ defmodule BlockScoutWeb.API.V2.OptimismController do init_transaction_hash: init_transaction_hash, timestamp: DateTime.from_unix!(params["timestamp"]), relay_chain_id: params["relay_chain_id"], - payload: hash_to_binary(params["payload"]) + payload: payload, + transfer_token_address_hash: transfer_token_address_hash, + transfer_from_address_hash: transfer_from_address_hash, + transfer_to_address_hash: transfer_to_address_hash, + transfer_amount: transfer_amount } end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index ded1bfd258ed..711916ddae63 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -310,6 +310,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/games/count", V2.OptimismController, :games_count) get("/interop/messages", V2.OptimismController, :interop_messages) get("/interop/messages/count", V2.OptimismController, :interop_messages_count) + get("/interop/messages/:unique_id", V2.OptimismController, :interop_message) get("/interop/public-key", V2.OptimismController, :interop_public_key) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 17bbf66540b0..8f81ba3eb1ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -251,6 +251,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do Enum.map(messages, fn message -> msg = %{ + "unique_id" => InteropMessage.message_unique_id(message), "nonce" => message.nonce, "timestamp" => message.timestamp, "status" => message.status, @@ -274,6 +275,30 @@ defmodule BlockScoutWeb.API.V2.OptimismView do } end + @doc """ + Function to render GET requests to `/api/v2/optimism/interop/messages/:unique_id` endpoint. + """ + def render("optimism_interop_message.json", %{message: message}) do + %{ + "init_chain" => message.init_chain, + "init_transaction_hash" => message.init_transaction_hash, + "timestamp" => message.timestamp, + "sender_address_hash" => message.sender_address_hash, + "relay_chain" => message.relay_chain, + "relay_transaction_hash" => message.relay_transaction_hash, + "relay_transaction_failed" => message.failed, + "target_address_hash" => message.target_address_hash, + "transfer_token" => message.transfer_token, + "transfer_amount" => message.transfer_amount, + "transfer_from_address_hash" => message.transfer_from_address_hash, + "transfer_to_address_hash" => message.transfer_to_address_hash, + "nonce" => message.nonce, + "direction" => message.direction, + "status" => message.status, + "payload" => ExplorerHelper.add_0x_prefix(message.payload) + } + end + @doc """ Function to render GET requests to `/api/v2/optimism/interop/public-key` endpoint. """ diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex index f0614e568dcc..bb11003bb15e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex @@ -138,6 +138,7 @@ defmodule BlockScoutWeb.API.V2.ScrollView do @doc """ Extends the json output with a sub-map containing information related Scroll. + For pending transactions the output is not extended. ## Parameters - `out_json`: A map defining output json which will be extended. @@ -148,11 +149,16 @@ defmodule BlockScoutWeb.API.V2.ScrollView do """ @spec extend_transaction_json_response(map(), %{ :__struct__ => Transaction, - :block_number => non_neg_integer(), + :block_number => non_neg_integer() | nil, :index => non_neg_integer(), :input => Data.t(), optional(any()) => any() }) :: map() + def extend_transaction_json_response(out_json, %Transaction{block_number: nil}) do + # this is a pending transaction + out_json + end + def extend_transaction_json_response(out_json, %Transaction{} = transaction) do config = Application.get_all_env(:explorer)[L1FeeParam] diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex index 165ebf9ece04..5451966d8682 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex @@ -96,13 +96,20 @@ defmodule Explorer.Chain.Import.Runner.Optimism.InteropMessages do fragment("COALESCE(EXCLUDED.relay_transaction_hash, ?)", message.relay_transaction_hash), payload: fragment("COALESCE(EXCLUDED.payload, ?)", message.payload), failed: fragment("COALESCE(EXCLUDED.failed, ?)", message.failed), + transfer_token_address_hash: + fragment("COALESCE(EXCLUDED.transfer_token_address_hash, ?)", message.transfer_token_address_hash), + transfer_from_address_hash: + fragment("COALESCE(EXCLUDED.transfer_from_address_hash, ?)", message.transfer_from_address_hash), + transfer_to_address_hash: + fragment("COALESCE(EXCLUDED.transfer_to_address_hash, ?)", message.transfer_to_address_hash), + transfer_amount: fragment("COALESCE(EXCLUDED.transfer_amount, ?)", message.transfer_amount), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", message.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", message.updated_at) ] ], where: fragment( - "(EXCLUDED.sender_address_hash, EXCLUDED.target_address_hash, EXCLUDED.init_transaction_hash, EXCLUDED.block_number, EXCLUDED.timestamp, EXCLUDED.relay_chain_id, EXCLUDED.relay_transaction_hash, EXCLUDED.payload, EXCLUDED.failed) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.sender_address_hash, EXCLUDED.target_address_hash, EXCLUDED.init_transaction_hash, EXCLUDED.block_number, EXCLUDED.timestamp, EXCLUDED.relay_chain_id, EXCLUDED.relay_transaction_hash, EXCLUDED.payload, EXCLUDED.failed, EXCLUDED.transfer_token_address_hash, EXCLUDED.transfer_from_address_hash, EXCLUDED.transfer_to_address_hash, EXCLUDED.transfer_amount) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", message.sender_address_hash, message.target_address_hash, message.init_transaction_hash, @@ -111,7 +118,11 @@ defmodule Explorer.Chain.Import.Runner.Optimism.InteropMessages do message.relay_chain_id, message.relay_transaction_hash, message.payload, - message.failed + message.failed, + message.transfer_token_address_hash, + message.transfer_from_address_hash, + message.transfer_to_address_hash, + message.transfer_amount ) ) end diff --git a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex index 4b6461579406..91b2714c9bfb 100644 --- a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex +++ b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex @@ -8,12 +8,13 @@ defmodule Explorer.Chain.Optimism.InteropMessage do import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] import Explorer.Helper, only: [add_0x_prefix: 1] + alias ABI.{FunctionSelector, TypeDecoder} alias Explorer.Chain.Hash alias Explorer.{PagingOptions, Repo} alias Indexer.Fetcher.Optimism.Interop.MessageQueue, as: InteropMessageQueue @required_attrs ~w(nonce init_chain_id relay_chain_id)a - @optional_attrs ~w(sender_address_hash target_address_hash init_transaction_hash block_number timestamp relay_transaction_hash payload failed)a + @optional_attrs ~w(sender_address_hash target_address_hash init_transaction_hash block_number timestamp relay_transaction_hash payload failed transfer_token_address_hash transfer_from_address_hash transfer_to_address_hash transfer_amount sent_to_multichain)a @interop_instance_api_url_to_public_key_cache :interop_instance_api_url_to_public_key_cache @interop_chain_id_to_instance_info_cache :interop_chain_id_to_instance_info_cache @@ -29,6 +30,11 @@ defmodule Explorer.Chain.Optimism.InteropMessage do * `relay_transaction_hash` - Transaction hash (on the target chain) associated with the message relay transaction. Can be `nil` (when relay transaction is not indexed yet). * `payload` - Message payload to call target with. Can be `nil` (when SentMessage event is not indexed yet). * `failed` - Fail status of the relay transaction. Can be `nil` (when relay transaction is not indexed yet). + * `transfer_token_address_hash` - Address of SuperchainERC20 token transferred within this message. Can be `nil` (if this is ETH transfer or not transfer operation at all). + * `transfer_from_address_hash` - The cross-chain transfer `from` address. Can be `nil` (if this is not transfer operation). + * `transfer_to_address_hash` - The cross-chain transfer `to` address. Can be `nil` (if this is not transfer operation). + * `transfer_amount` - The cross-chain transfer amount. Can be `nil` (if this is not transfer operation). + * `sent_to_multichain` - Equals to `true` if message details are sent to multichain service. Defaults to `false`. """ @primary_key false typed_schema "op_interop_messages" do @@ -43,6 +49,11 @@ defmodule Explorer.Chain.Optimism.InteropMessage do field(:relay_transaction_hash, Hash.Full) field(:payload, :binary) field(:failed, :boolean) + field(:transfer_token_address_hash, Hash.Address) + field(:transfer_from_address_hash, Hash.Address) + field(:transfer_to_address_hash, Hash.Address) + field(:transfer_amount, :decimal) + field(:sent_to_multichain, :boolean) timestamps() end @@ -124,30 +135,112 @@ defmodule Explorer.Chain.Optimism.InteropMessage do end end + @doc """ + Retrieves some statistics for the list of the last incomplete messages: min block number, max block number, and message count. + An incomplete message is the message for which an init transaction or relay transaction is unknown. + The selection is limited by a minimum block number (set to zero when the stats is needed for all messages). + + ## Parameters + - `current_chain_id`: The current chain ID to make correct query to the database. + - `start_block_number`: The block number starting from which the messages should be considered. + + ## Returns + - A map with `min`, `max`, and `count` fields. + - `%{min: nil, max: nil, count: 0}` map if there are no messages. + """ + @spec get_incomplete_messages_stats(non_neg_integer(), non_neg_integer()) :: + %{min: non_neg_integer() | nil, max: non_neg_integer() | nil, count: non_neg_integer()} + def get_incomplete_messages_stats(current_chain_id, start_block_number) + when is_integer(current_chain_id) and is_integer(start_block_number) do + Repo.one( + from( + m in __MODULE__, + select: %{min: min(m.block_number), max: max(m.block_number), count: fragment("COUNT(*)")}, + where: + ((is_nil(m.relay_transaction_hash) and m.init_chain_id == ^current_chain_id) or + (is_nil(m.init_transaction_hash) and m.relay_chain_id == ^current_chain_id)) and + m.block_number >= ^start_block_number + ) + ) + end + @doc """ Returns a list of incomplete messages from the `op_interop_messages` table. An incomplete message is the message for which an init transaction or relay transaction is unknown. - The selection is limited by a min block number. + The selection is limited by a block range. ## Parameters - `current_chain_id`: The current chain ID to make correct query to the database. - `min_block_number`: The block number starting from which the messages should be considered. + - `max_block_number`: The max block number before which (including) the messages should be considered. + - `limit`: Max number of retrieved items. + - `offset`: An offset within SQL query to retrieve items from. ## Returns - A list of the incomplete messages. Returns an empty list if they are not found. """ - @spec get_incomplete_messages(non_neg_integer(), non_neg_integer()) :: list() - def get_incomplete_messages(current_chain_id, min_block_number) do + @spec get_incomplete_messages( + non_neg_integer(), + non_neg_integer() | nil, + non_neg_integer() | nil, + non_neg_integer(), + non_neg_integer() + ) :: list() + def get_incomplete_messages(_current_chain_id, nil, nil, _limit, _offset), do: [] + + def get_incomplete_messages(current_chain_id, min_block_number, max_block_number, limit, offset) do Repo.all( from(m in __MODULE__, where: ((is_nil(m.relay_transaction_hash) and m.init_chain_id == ^current_chain_id) or (is_nil(m.init_transaction_hash) and m.relay_chain_id == ^current_chain_id)) and - m.block_number >= ^min_block_number + m.block_number >= ^min_block_number and m.block_number <= ^max_block_number, + order_by: [asc: m.nonce, asc: m.init_chain_id], + limit: ^limit, + offset: ^offset + ) + ) + end + + @doc """ + Retrieves messages to be exported to the multichain service. + + ## Parameters + - `current_chain_id`: The current chain ID to make correct query to the database. + - `limit`: The max number of retrieved items at once. + """ + @spec get_messages_for_multichain_export(non_neg_integer(), non_neg_integer()) :: list() + def get_messages_for_multichain_export(current_chain_id, limit) do + Repo.all( + from(m in __MODULE__, + where: + ((not is_nil(m.init_transaction_hash) and m.init_chain_id == ^current_chain_id) or + (not is_nil(m.relay_transaction_hash) and m.relay_chain_id == ^current_chain_id)) and + not m.sent_to_multichain, + order_by: [desc: m.block_number], + limit: ^limit ) ) end + @doc """ + Retrieves message fields by its primary key (`init_chain_id` and `nonce`). + + ## Parameters + - `init_chain_id`: The chain ID of the init transaction. + - `nonce`: The message nonce. + """ + @spec get_message(non_neg_integer(), non_neg_integer()) :: __MODULE__.t() | nil + def get_message(init_chain_id, nonce) do + query = + from(m in __MODULE__, + where: m.init_chain_id == ^init_chain_id and m.nonce == ^nonce + ) + + query + |> Repo.one() + end + @doc """ Returns relay transaction hash and failure status from the `op_interop_messages` table for the given `init_chain_id` and `nonce`. @@ -245,6 +338,53 @@ defmodule Explorer.Chain.Optimism.InteropMessage do end end + @doc """ + Decodes message payload to get cross-chain transfer details (such as token address, from, to addresses, and amount). + If the message doesn't encode cross-chain transfer, the function returns nils. + + ## Parameters + - `payload`: The payload to decode. + + ## Returns + - A list consisting of the following elements: `[token_address, from_address, to_address, amount]`. + - A list with nils if the message doesn't encode a cross-chain transfer: `[nil, nil, nil, nil]`. + """ + @spec decode_payload(binary() | nil) :: list() + def decode_payload(payload) do + case payload do + # relayERC20(address _token, address _from, address _to, uint256 _amount) + <<0x7C, 0xFD, 0x6D, 0xBC>> <> encoded_params -> + TypeDecoder.decode( + encoded_params, + %FunctionSelector{ + function: "relayERC20", + types: [ + :address, + :address, + :address, + {:uint, 256} + ] + } + ) + + # relayETH(address _from, address _to, uint256 _amount) + <<0x4F, 0x0E, 0xDC, 0xC9>> <> encoded_params -> + encoded_params + |> TypeDecoder.decode(%FunctionSelector{ + function: "relayETH", + types: [ + :address, + :address, + {:uint, 256} + ] + }) + |> List.insert_at(0, nil) + + _ -> + List.duplicate(nil, 4) + end + end + # Extends a query for listing interop messages with their filtering. # Filter conditions are applied with `and` relation. # @@ -597,6 +737,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do Map.merge( %{ + "unique_id" => message_unique_id(message), "nonce" => message.nonce, "status" => message.status, "sender_address_hash" => message.sender_address_hash, @@ -612,6 +753,31 @@ defmodule Explorer.Chain.Optimism.InteropMessage do end end + @doc """ + Constructs message id string for using in URLs on frontend. Concatenates hex representations of the `init_chain_id` + and `nonce` field (both consisting of 8 hex symbols and padded with leading zeroes). + + ## Parameters + - `message`: The message map containing `init_chain_id` and `nonce` keys. + + ## Returns + - The message id. Example for `init_chain_id` = 100 and `nonce` = 4000: "0000006400000FA0" + """ + @spec message_unique_id(map()) :: String.t() + def message_unique_id(%{init_chain_id: init_chain_id, nonce: nonce} = _message) do + init_chain_id_string = + init_chain_id + |> Integer.to_string(16) + |> String.pad_leading(8, "0") + + nonce_string = + nonce + |> Integer.to_string(16) + |> String.pad_leading(8, "0") + + init_chain_id_string <> nonce_string + end + @doc """ Sends HTTP request to Chainscout API to get instance info by its chain ID. diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex index fc4527cd522f..9e983a6d846f 100644 --- a/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex @@ -191,6 +191,8 @@ defmodule Explorer.Chain.PolygonZkevm.Reader do Reads a list of L1 transactions by their hashes from `polygon_zkevm_lifecycle_l1_transactions` table. """ @spec lifecycle_transactions(list()) :: list() + def lifecycle_transactions([]), do: [] + def lifecycle_transactions(l1_transaction_hashes) do query = from( diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index b69bc2d10f29..ee60a19b2566 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -379,7 +379,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do ] end - defp batch_import_url do + @doc """ + Returns a full URL to the Multichain service API import endpoint. + + ## Returns + - A string containing the URL. + """ + @spec batch_import_url() :: String.t() + def batch_import_url do "#{base_url()}/import:batch" end @@ -393,7 +400,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end end - defp api_key do + @doc """ + Returns an API key for the Multichain service. + + ## Returns + - A string containing the API key. + - `nil` if the key is not defined. + """ + @spec api_key() :: String.t() | nil + def api_key do Microservice.api_key(__MODULE__) end diff --git a/apps/explorer/lib/explorer/utility/microservice.ex b/apps/explorer/lib/explorer/utility/microservice.ex index 744bbec70a70..d839d1940912 100644 --- a/apps/explorer/lib/explorer/utility/microservice.ex +++ b/apps/explorer/lib/explorer/utility/microservice.ex @@ -46,7 +46,7 @@ defmodule Explorer.Utility.Microservice do "your_api_key_here" """ - @spec api_key(atom(), atom()) :: String.t() + @spec api_key(atom(), atom()) :: String.t() | nil def api_key(application \\ :explorer, module) do Application.get_env(application, module)[:api_key] end diff --git a/apps/explorer/priv/optimism/migrations/20250501120524_op_interop_transfer_fields.exs b/apps/explorer/priv/optimism/migrations/20250501120524_op_interop_transfer_fields.exs new file mode 100644 index 000000000000..78a0886f453b --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20250501120524_op_interop_transfer_fields.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Optimism.Migrations.OPInteropTransferFields do + use Ecto.Migration + + def change do + execute("TRUNCATE TABLE op_interop_messages;") + + alter table(:op_interop_messages) do + add(:transfer_token_address_hash, :bytea, null: true, default: nil) + add(:transfer_from_address_hash, :bytea, null: true, default: nil) + add(:transfer_to_address_hash, :bytea, null: true, default: nil) + add(:transfer_amount, :decimal, null: true, default: nil) + add(:sent_to_multichain, :boolean, null: false, default: false) + end + end +end diff --git a/apps/explorer/priv/optimism/migrations/20250610100740_add_op_interop_indices.exs b/apps/explorer/priv/optimism/migrations/20250610100740_add_op_interop_indices.exs new file mode 100644 index 000000000000..8e51e5e29ed9 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20250610100740_add_op_interop_indices.exs @@ -0,0 +1,18 @@ +defmodule Explorer.Repo.Optimism.Migrations.AddOPInteropIndices do + use Ecto.Migration + + def up do + create(index(:op_interop_messages, [:init_transaction_hash, :init_chain_id, :sent_to_multichain, :block_number])) + create(index(:op_interop_messages, [:relay_transaction_hash, :relay_chain_id, :sent_to_multichain, :block_number])) + end + + def down do + drop_if_exists( + index(:op_interop_messages, [:init_transaction_hash, :init_chain_id, :sent_to_multichain, :block_number]) + ) + + drop_if_exists( + index(:op_interop_messages, [:relay_transaction_hash, :relay_chain_id, :sent_to_multichain, :block_number]) + ) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex index 0f6fbce2408f..779d81e5ba8b 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex @@ -320,6 +320,9 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do if Enum.at(event["topics"], 0) == @sent_message_event do [sender_address_hash, payload] = decode_data(event["data"], [:address, :bytes]) + [transfer_token_address_hash, transfer_from_address_hash, transfer_to_address_hash, transfer_amount] = + InteropMessage.decode_payload(payload) + %{ sender_address_hash: sender_address_hash, target_address_hash: truncate_address_hash(Enum.at(event["topics"], 2)), @@ -329,7 +332,11 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do block_number: block_number, timestamp: Map.get(timestamps, block_number), relay_chain_id: quantity_to_integer(Enum.at(event["topics"], 1)), - payload: payload + payload: payload, + transfer_token_address_hash: transfer_token_address_hash, + transfer_from_address_hash: transfer_from_address_hash, + transfer_to_address_hash: transfer_to_address_hash, + transfer_amount: transfer_amount } else %{ @@ -432,4 +439,31 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do end def fetcher_name, do: @fetcher_name + + @doc """ + Returns a constant address of L2ToL2CrossDomainMessenger predeploy. + + ## Returns + - An address of L2ToL2CrossDomainMessenger predeploy. + """ + @spec l2tol2_cross_domain_messenger() :: String.t() + def l2tol2_cross_domain_messenger, do: @l2tol2_cross_domain_messenger + + @doc """ + Returns a max possible value for 32-bit signed integer. + + ## Returns + - A max possible value for 32-bit signed integer. + """ + @spec max_int32() :: non_neg_integer() + def max_int32, do: @max_int32 + + @doc """ + Returns a 32-byte signature of the `SentMessage` event: `SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message)`. + + ## Returns + - 32-byte signature of the `SentMessage` event. + """ + @spec sent_message_event_signature() :: String.t() + def sent_message_event_signature, do: @sent_message_event end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_failed.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_failed.ex index e101d324705e..d62c6ca39a0e 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_failed.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_failed.ex @@ -33,15 +33,10 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do alias Indexer.Helper @fetcher_name :optimism_interop_messages_failed - @l2tol2_cross_domain_messenger "0x4200000000000000000000000000000000000023" - @max_int32 2_147_483_647 # 4-byte signature of the method relayMessage((address origin, uint256 blockNumber, uint256 logIndex, uint256 timestamp, uint256 chainId), bytes _sentMessage) @relay_message_method "0x8d1d298f" - # 32-byte signature of the event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message) - @sent_message_event "0x382409ac69001e11931a28435afef442cbfd20d9891907e8fa373ba7d351f320" - def child_spec(start_link_arguments) do spec = %{ id: __MODULE__, @@ -59,8 +54,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do @impl GenServer def init(args) do - json_rpc_named_arguments = args[:json_rpc_named_arguments] - {:ok, %{}, {:continue, json_rpc_named_arguments}} + {:ok, %{}, {:continue, args[:json_rpc_named_arguments]}} end # Initialization function which is used instead of `init` to avoid Supervisor's stop in case of any critical issues @@ -312,6 +306,8 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do ] } + sent_message_event_signature = InteropMessageFetcher.sent_message_event_signature() + messages = transactions_params |> Enum.map(fn transaction -> @@ -319,7 +315,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do [ {_origin, _block_number, _log_index, _timestamp, init_chain_id}, - @sent_message_event <> sent_message_topics_and_data + ^sent_message_event_signature <> sent_message_topics_and_data ] = TypeDecoder.decode( Base.decode16!(encoded_params, case: :lower), @@ -339,7 +335,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do } end) |> Enum.filter(&(&1.relay_chain_id == current_chain_id)) - |> Enum.reject(&(&1.init_chain_id > @max_int32)) + |> Enum.reject(&(&1.init_chain_id > InteropMessageFetcher.max_int32())) {:ok, _} = Chain.import(%{ @@ -378,7 +374,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageFailed do |> String.downcase() |> String.starts_with?(@relay_message_method) - to_address == @l2tol2_cross_domain_messenger and is_relay_message_method + to_address == InteropMessageFetcher.l2tol2_cross_domain_messenger() and is_relay_message_method end) case fetch_transaction_receipts(relay_message_transactions, json_rpc_named_arguments) do diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex index 04e2e9d469d2..b9d1ea95bbe5 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex @@ -31,13 +31,16 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do import Indexer.Fetcher.Optimism.Interop.Helper, only: [log_cant_get_chain_id_from_rpc: 0] alias Explorer.Chain + alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Chain.Hash alias Explorer.Chain.Optimism.InteropMessage alias Indexer.Fetcher.Optimism alias Indexer.Helper + @counter_type "optimism_interop_messages_queue_iteration" @fetcher_name :optimism_interop_messages_queue @api_endpoint_import "/api/v2/import/optimism/interop/" + @chunk_size 1000 def child_spec(start_link_arguments) do spec = %{ @@ -56,8 +59,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do @impl GenServer def init(args) do - json_rpc_named_arguments = args[:json_rpc_named_arguments] - {:ok, %{}, {:continue, json_rpc_named_arguments}} + {:ok, %{}, {:continue, args[:json_rpc_named_arguments]}} end # Initialization function which is used instead of `init` to avoid Supervisor's stop in case of any critical issues @@ -111,6 +113,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do timeout: :timer.seconds(env[:connect_timeout]), recv_timeout: :timer.seconds(env[:recv_timeout]), export_expiration_blocks: div(env[:export_expiration] * 24 * 3600, block_duration), + iterations_done: Decimal.to_integer(LastFetchedCounter.get(@counter_type)), json_rpc_named_arguments: json_rpc_named_arguments }} else @@ -162,60 +165,90 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do timeout: timeout, recv_timeout: recv_timeout, export_expiration_blocks: export_expiration_blocks, + iterations_done: iterations_done, json_rpc_named_arguments: json_rpc_named_arguments } = state ) do - {:ok, latest_block_number} = - Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - private_key = hash_to_binary(Application.get_all_env(:indexer)[__MODULE__][:private_key]) - updated_chainscout_map = - current_chain_id - |> InteropMessage.get_incomplete_messages(max(latest_block_number - export_expiration_blocks, 0)) - |> Enum.reduce(chainscout_map, fn message, chainscout_map_acc -> - {instance_chain_id, post_data_signed} = prepare_post_data(message, private_key) - - url_from_map = Map.get(chainscout_map_acc, instance_chain_id) - - instance_url = - with {:url_from_map_is_nil, true, _} <- {:url_from_map_is_nil, is_nil(url_from_map), url_from_map}, - info = InteropMessage.get_instance_info_by_chain_id(instance_chain_id, chainscout_api_url), - {:url_from_chainscout_avail, true} <- {:url_from_chainscout_avail, not is_nil(info)} do - info.instance_url - else - {:url_from_map_is_nil, false, url_from_map} -> - url_from_map + LastFetchedCounter.upsert(%{ + counter_type: @counter_type, + value: iterations_done + }) - {:url_from_chainscout_avail, false} -> - nil - end + # the first three iterations scan all incomplete messages, + # but subsequent scans are limited by INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS env + start_block_number = + if iterations_done < 3 do + 0 + else + {:ok, latest_block_number} = + Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - with false <- is_nil(instance_url), - endpoint_url = instance_url <> @api_endpoint_import, - response = post_json_request(endpoint_url, post_data_signed, timeout, recv_timeout), - false <- is_nil(response) do - {:ok, _} = - Chain.import(%{ - optimism_interop_messages: %{params: [prepare_import(message, response)]}, - timeout: :infinity - }) - - Logger.info( - "Message details successfully sent to #{endpoint_url}. Request body: #{inspect(post_data_signed)}. Response body: #{inspect(response)}" - ) - end + max(latest_block_number - export_expiration_blocks, 0) + end - if is_nil(instance_url) do - chainscout_map_acc - else - Map.put(chainscout_map_acc, instance_chain_id, instance_url) - end + %{min: min_block_number, max: max_block_number, count: message_count} = + InteropMessage.get_incomplete_messages_stats(current_chain_id, start_block_number) + + chunks_number = ceil(message_count / @chunk_size) + chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) + + updated_chainscout_map = + chunk_range + |> Enum.reduce(chainscout_map, fn current_chunk, chainscout_map_acc -> + current_chain_id + |> InteropMessage.get_incomplete_messages( + min_block_number, + max_block_number, + @chunk_size, + current_chunk * @chunk_size + ) + |> Enum.reduce(chainscout_map_acc, fn message, chainscout_map_acc_internal -> + {instance_chain_id, post_data_signed} = prepare_post_data(message, private_key) + + url_from_map = Map.get(chainscout_map_acc_internal, instance_chain_id) + + instance_url = + with {:url_from_map_is_nil, true, _} <- {:url_from_map_is_nil, is_nil(url_from_map), url_from_map}, + info = InteropMessage.get_instance_info_by_chain_id(instance_chain_id, chainscout_api_url), + {:url_from_chainscout_avail, true} <- {:url_from_chainscout_avail, not is_nil(info)} do + info.instance_url + else + {:url_from_map_is_nil, false, url_from_map} -> + url_from_map + + {:url_from_chainscout_avail, false} -> + nil + end + + with false <- is_nil(instance_url), + endpoint_url = instance_url <> @api_endpoint_import, + response = post_json_request(endpoint_url, post_data_signed, timeout, recv_timeout), + false <- is_nil(response) do + {:ok, _} = + Chain.import(%{ + optimism_interop_messages: %{params: [prepare_import(message, response)]}, + timeout: :infinity + }) + + Logger.info( + "Message details successfully sent to #{endpoint_url}. Request body: #{inspect(post_data_signed)}. Response body: #{inspect(response)}" + ) + end + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if is_nil(instance_url) do + chainscout_map_acc_internal + else + Map.put(chainscout_map_acc_internal, instance_chain_id, instance_url) + end + end) end) Process.send_after(self(), :continue, :timer.seconds(3)) - {:noreply, %{state | chainscout_map: updated_chainscout_map}} + {:noreply, %{state | chainscout_map: updated_chainscout_map, iterations_done: iterations_done + 1}} end @impl GenServer @@ -343,6 +376,9 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do pl -> hash_to_binary(pl) end + [transfer_token_address_hash, transfer_from_address_hash, transfer_to_address_hash, transfer_amount] = + InteropMessage.decode_payload(payload) + %{ sender_address_hash: Map.get(response, "sender_address_hash"), target_address_hash: Map.get(response, "target_address_hash"), @@ -351,7 +387,11 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do init_transaction_hash: Map.get(response, "init_transaction_hash"), relay_chain_id: message.relay_chain_id, timestamp: timestamp, - payload: payload + payload: payload, + transfer_token_address_hash: transfer_token_address_hash, + transfer_from_address_hash: transfer_from_address_hash, + transfer_to_address_hash: transfer_to_address_hash, + transfer_amount: transfer_amount } end end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex new file mode 100644 index 000000000000..ad2432e02fd8 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex @@ -0,0 +1,260 @@ +defmodule Indexer.Fetcher.Optimism.Interop.MultichainExport do + @moduledoc """ + Finds messages without `sent_to_multichain` flag in the `op_interop_messages` database table and + sends them to the Multichain service using its API. + + The found messages are combined into a batch and the batch is sent to the remote API. The batch max + size is defined with the optional INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE env variable having + a default value. Once the found messages are sent, the module starts the next iteration for another + batch of messages, and so on. + + The Multichain API endpoint URL is defined with MICROSERVICE_MULTICHAIN_SEARCH_URL env variable. + API key for the remote service is defined with MICROSERVICE_MULTICHAIN_SEARCH_API_KEY env variable. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + import Explorer.Helper, only: [add_0x_prefix: 1, valid_url?: 1] + import Indexer.Fetcher.Optimism.Interop.Helper, only: [log_cant_get_chain_id_from_rpc: 0] + + alias Ecto.Multi + alias Explorer.Chain.Hash + alias Explorer.Chain.Optimism.InteropMessage + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Repo + alias Indexer.Fetcher.Optimism + + @fetcher_name :optimism_interop_multichain_export + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, nil}} + end + + # Initialization function which is used instead of `init` to avoid Supervisor's stop in case of any critical issues + # during initialization. It checks the value of MICROSERVICE_MULTICHAIN_SEARCH_URL and MICROSERVICE_MULTICHAIN_SEARCH_API_KEY + # env variables and starts the handling loop. + # + # Also, the function fetches the current chain id to use it in the handler. + # + # When the initialization succeeds, the `:continue` message is sent to GenServer to start the handler loop. + # + # ## Parameters + # - `_state`: Initial state of the fetcher (empty map when starting). + # + # ## Returns + # - `{:noreply, state}` when the initialization is successful and the handling can start. The `state` contains + # necessary parameters needed for the handling. + # - `{:stop, :normal, %{}}` in case of error or when one of env variables is not defined. + @impl GenServer + @spec handle_continue(nil, map()) :: {:noreply, map()} | {:stop, :normal, map()} + def handle_continue(nil, _state) do + Logger.metadata(fetcher: @fetcher_name) + + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + :timer.sleep(2000) + + env = Application.get_all_env(:indexer)[__MODULE__] + + multichain_api_url = MultichainSearch.batch_import_url() + multichain_api_key = MultichainSearch.api_key() + + with {:api_url_is_valid, true} <- {:api_url_is_valid, valid_url?(multichain_api_url)}, + {:api_key_is_nil, false} <- {:api_key_is_nil, is_nil(multichain_api_key)}, + chain_id = Optimism.fetch_chain_id(), + {:chain_id_is_nil, false} <- {:chain_id_is_nil, is_nil(chain_id)} do + Process.send(self(), :continue, []) + + {:noreply, + %{ + chain_id: chain_id, + multichain_api_url: multichain_api_url, + multichain_api_key: multichain_api_key, + batch_size: env[:batch_size] + }} + else + {:api_url_is_valid, false} -> + # Multichain service API URL is not defined, so we don't start this module + Logger.warning( + "MICROSERVICE_MULTICHAIN_SEARCH_URL env variable is invalid or not defined. The module #{__MODULE__} will not start." + ) + + {:stop, :normal, %{}} + + {:api_key_is_nil, true} -> + Logger.error( + "MICROSERVICE_MULTICHAIN_SEARCH_API_KEY env variable is not defined. The module #{__MODULE__} will not start." + ) + + {:stop, :normal, %{}} + + {:chain_id_is_nil, true} -> + log_cant_get_chain_id_from_rpc() + {:stop, :normal, %{}} + end + end + + # Performs the main handling loop scanning for unsent part of messages. + # + # Details of each unsent message part are prepared and sent to the remote multichain instance through its API. + # + # ## Parameters + # - `:continue`: The GenServer message. + # - `state`: The current state of the fetcher containing the current chain ID, API URL and key, and batch size. + # + # ## Returns + # - `{:noreply, state}` tuple. + @impl GenServer + def handle_info( + :continue, + %{ + chain_id: current_chain_id, + multichain_api_url: multichain_api_url, + multichain_api_key: multichain_api_key, + batch_size: batch_size + } = state + ) do + messages = InteropMessage.get_messages_for_multichain_export(current_chain_id, batch_size) + + data = prepare_post_data(current_chain_id, messages, multichain_api_key) + + if post_json_request(multichain_api_url, data) do + {:ok, _} = + messages + |> Enum.reduce(Multi.new(), fn message, multi_acc -> + Multi.update_all( + multi_acc, + {:message, message.nonce, message.init_chain_id}, + from(m in InteropMessage, where: m.nonce == ^message.nonce and m.init_chain_id == ^message.init_chain_id), + set: [sent_to_multichain: true] + ) + end) + |> Repo.transaction() + + Logger.info("#{length(messages)} item(s) were successfully sent to the Multichain service.") + end + + if messages == [] do + Logger.info("There are no items to send to the Multichain service. Retrying in a few moments...") + end + + Process.send_after(self(), :continue, :timer.seconds(3)) + + {:noreply, state} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + # Prepares data to send as POST request to the remote multichain API endpoint. + # + # ## Parameters + # - `current_chain_id`: The current chain ID. + # - `messages`: A list of maps containing the messages data. + # - `api_key`: API key to access the remote import endpoint. + # + # ## Returns + # - The prepared data map if the `messages` list is not empty. + # - `nil` if the `messages` list is empty. + @spec prepare_post_data(non_neg_integer(), [map()], String.t()) :: map() | nil + defp prepare_post_data(_current_chain_id, [], _api_key), do: nil + + defp prepare_post_data(current_chain_id, messages, api_key) do + interop_messages = + messages + |> Enum.map(fn message -> + if message.init_chain_id == current_chain_id do + # this is `init` part of an outgoing message + %{ + init: %{ + sender_address_hash: Hash.to_string(message.sender_address_hash), + target_address_hash: Hash.to_string(message.target_address_hash), + nonce: Integer.to_string(message.nonce), + init_chain_id: Integer.to_string(message.init_chain_id), + init_transaction_hash: Hash.to_string(message.init_transaction_hash), + timestamp: Integer.to_string(DateTime.to_unix(message.timestamp)), + relay_chain_id: Integer.to_string(message.relay_chain_id), + payload: add_0x_prefix(message.payload), + transfer_token_address_hash: add_0x_prefix(message.transfer_token_address_hash), + transfer_from_address_hash: add_0x_prefix(message.transfer_from_address_hash), + transfer_to_address_hash: add_0x_prefix(message.transfer_to_address_hash), + transfer_amount: + if(not is_nil(message.transfer_amount), do: Decimal.to_string(message.transfer_amount, :normal)) + } + } + else + # this is `relay` part of an incoming message + %{ + relay: %{ + nonce: Integer.to_string(message.nonce), + init_chain_id: Integer.to_string(message.init_chain_id), + relay_chain_id: Integer.to_string(message.relay_chain_id), + relay_transaction_hash: Hash.to_string(message.relay_transaction_hash), + failed: message.failed + } + } + end + end) + + %{ + chain_id: Integer.to_string(current_chain_id), + interop_messages: interop_messages, + api_key: api_key + } + end + + # Sends message's data to the given remote multichain instance using HTTP POST request. + # + # ## Parameters + # - `url`: URL of the remote API endpoint to send the data to. + # - `body`: A map with message's data. + # + # ## Returns + # - `true` in case of success. + # - `false` in case of failure. + @spec post_json_request(String.t(), map() | nil) :: boolean() + defp post_json_request(_url, nil), do: false + + defp post_json_request(url, body) do + timeout = 8_000 + recv_timeout = 5_000 + + client = Tesla.client([{Tesla.Middleware.Timeout, timeout: recv_timeout}], Tesla.Adapter.Mint) + json_body = Jason.encode!(body) + headers = [{"Content-Type", "application/json"}] + + # Mint adapter doesn't support sending more than 65535 bytes when using HTTP/2, so we use HTTP/1 + opts = [adapter: [timeout: recv_timeout, transport_opts: [timeout: timeout], protocols: [:http1]]] + + case Tesla.post(client, url, json_body, headers: headers, opts: opts) do + {:ok, %{status: 200}} -> + true + + reason -> + Logger.error("Cannot post HTTP request to #{url}. Reason: #{inspect(reason)}. Body: #{inspect(json_body)}") + false + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex index 25b19eb8b4a6..c995be37bced 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex @@ -42,8 +42,7 @@ defmodule Indexer.Fetcher.Optimism.Withdrawal do @impl GenServer def init(args) do - json_rpc_named_arguments = args[:json_rpc_named_arguments] - {:ok, %{}, {:continue, json_rpc_named_arguments}} + {:ok, %{}, {:continue, args[:json_rpc_named_arguments]}} end @impl GenServer diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex index 6efeff072e7c..c1ae8972c541 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/transaction_batch.ex @@ -41,12 +41,23 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do chunk_size = config[:chunk_size] recheck_interval = config[:recheck_interval] + ignore_numbers = + config[:ignore_numbers] + |> String.trim() + |> String.split(",") + |> Enum.map(fn ignore_number -> + ignore_number + |> String.trim() + |> String.to_integer() + end) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues Process.send_after(self(), :continue, 2000) {:ok, %{ chunk_size: chunk_size, + ignore_numbers: ignore_numbers, json_rpc_named_arguments: args[:json_rpc_named_arguments], prev_latest_batch_number: 0, prev_virtual_batch_number: 0, @@ -60,6 +71,7 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do :continue, %{ chunk_size: chunk_size, + ignore_numbers: ignore_numbers, json_rpc_named_arguments: json_rpc_named_arguments, prev_latest_batch_number: prev_latest_batch_number, prev_virtual_batch_number: prev_virtual_batch_number, @@ -86,7 +98,13 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do {handle_duration, _} = :timer.tc(fn -> - handle_batch_range(start_batch_number, end_batch_number, json_rpc_named_arguments, chunk_size) + handle_batch_range( + start_batch_number, + end_batch_number, + json_rpc_named_arguments, + chunk_size, + ignore_numbers + ) end) { @@ -113,7 +131,7 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do {:noreply, state} end - defp handle_batch_range(start_batch_number, end_batch_number, json_rpc_named_arguments, chunk_size) do + defp handle_batch_range(start_batch_number, end_batch_number, json_rpc_named_arguments, chunk_size, ignore_numbers) do start_batch_number..end_batch_number |> Enum.chunk_every(chunk_size) |> Enum.each(fn chunk -> @@ -121,7 +139,7 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do chunk_end = List.last(chunk) log_batches_chunk_handling(chunk_start, chunk_end, start_batch_number, end_batch_number) - fetch_and_save_batches(chunk_start, chunk_end, json_rpc_named_arguments) + fetch_and_save_batches(chunk_start, chunk_end, json_rpc_named_arguments, ignore_numbers) end) end @@ -156,23 +174,8 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do end end - defp fetch_and_save_batches(batch_start, batch_end, json_rpc_named_arguments) do - # For every batch from batch_start to batch_end request the batch info - requests = - batch_start - |> Range.new(batch_end, 1) - |> Enum.map(fn batch_number -> - EthereumJSONRPC.request(%{ - id: batch_number, - method: "zkevm_getBatchByNumber", - params: [integer_to_quantity(batch_number), false] - }) - end) - - error_message = - &"Cannot call zkevm_getBatchByNumber for the batch range #{batch_start}..#{batch_end}. Error: #{inspect(&1)}" - - {:ok, responses} = Helper.repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) + defp fetch_and_save_batches(batch_start, batch_end, json_rpc_named_arguments, ignore_numbers) do + {:ok, responses} = perform_jsonrpc_requests(batch_start, batch_end, json_rpc_named_arguments, ignore_numbers) # For every batch info extract batches' L1 sequence transaction and L1 verify transaction {sequence_hashes, verify_hashes} = @@ -292,6 +295,37 @@ defmodule Indexer.Fetcher.PolygonZkevm.TransactionBatch do end end + defp perform_jsonrpc_requests(batch_start, batch_end, json_rpc_named_arguments, ignore_numbers) do + # For every batch from batch_start to batch_end request the batch info + requests = + batch_start + |> Range.new(batch_end, 1) + |> Enum.reject(fn batch_number -> + if Enum.member?(ignore_numbers, batch_number) do + Logger.warning("The batch #{batch_number} will be ignored.") + true + else + false + end + end) + |> Enum.map(fn batch_number -> + EthereumJSONRPC.request(%{ + id: batch_number, + method: "zkevm_getBatchByNumber", + params: [integer_to_quantity(batch_number), false] + }) + end) + + if requests == [] do + {:ok, []} + else + error_message = + &"Cannot call zkevm_getBatchByNumber for the batch range #{batch_start}..#{batch_end}. Error: #{inspect(&1)}" + + Helper.repeated_call(&json_rpc/2, [requests, json_rpc_named_arguments], error_message, 3) + end + end + defp fetch_latest_batch_numbers(json_rpc_named_arguments) do requests = [ EthereumJSONRPC.request(%{id: 0, method: "zkevm_batchNumber", params: []}), diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 4b849e75bb4d..09b3b2b43a22 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -177,6 +177,10 @@ defmodule Indexer.Supervisor do Indexer.Fetcher.Optimism.Interop.MessageQueue.Supervisor, [[memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments]] }, + { + Indexer.Fetcher.Optimism.Interop.MultichainExport.Supervisor, + [[memory_monitor: memory_monitor]] + }, configure(Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, [[memory_monitor: memory_monitor]]), configure(Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, [ [memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments] diff --git a/config/runtime.exs b/config/runtime.exs index 9996602c10ac..2af07ea3a79c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1155,6 +1155,9 @@ config :indexer, Indexer.Fetcher.Optimism.Interop.MessageQueue, recv_timeout: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_INTEROP_RECV_TIMEOUT", 10), export_expiration: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS", 10) +config :indexer, Indexer.Fetcher.Optimism.Interop.MultichainExport, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE", 100) + config :indexer, Indexer.Fetcher.Withdrawal.Supervisor, disabled?: System.get_env("INDEXER_DISABLE_WITHDRAWALS_FETCHER", "true") == "true" @@ -1333,6 +1336,7 @@ config :indexer, Indexer.Fetcher.PolygonZkevm.BridgeL2.Supervisor, enabled: Conf config :indexer, Indexer.Fetcher.PolygonZkevm.TransactionBatch, chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_ZKEVM_BATCHES_CHUNK_SIZE", 20), + ignore_numbers: System.get_env("INDEXER_POLYGON_ZKEVM_BATCHES_IGNORE", "0"), recheck_interval: ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_ZKEVM_BATCHES_RECHECK_INTERVAL", 60) config :indexer, Indexer.Fetcher.PolygonZkevm.TransactionBatch.Supervisor, diff --git a/cspell.json b/cspell.json index a998afe515d7..ea7e859995ff 100644 --- a/cspell.json +++ b/cspell.json @@ -607,6 +607,7 @@ "subtraces", "successa", "successb", + "Superchain", "supermajority", "supernet", "sushiswap", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 1ce0d183baf5..09e6a7e6b59d 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -236,6 +236,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_POLYGON_ZKEVM_BATCHES_ENABLED= # INDEXER_POLYGON_ZKEVM_BATCHES_CHUNK_SIZE= # INDEXER_POLYGON_ZKEVM_BATCHES_RECHECK_INTERVAL= +# INDEXER_POLYGON_ZKEVM_BATCHES_IGNORE=0 # INDEXER_POLYGON_ZKEVM_L1_RPC= # INDEXER_POLYGON_ZKEVM_L1_BRIDGE_START_BLOCK= # INDEXER_POLYGON_ZKEVM_L1_BRIDGE_CONTRACT= @@ -303,6 +304,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_OPTIMISM_INTEROP_CONNECT_TIMEOUT= # INDEXER_OPTIMISM_INTEROP_RECV_TIMEOUT= # INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS= +# INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE= # INDEXER_SCROLL_L1_RPC= # INDEXER_SCROLL_L1_MESSENGER_CONTRACT= # INDEXER_SCROLL_L1_MESSENGER_START_BLOCK= From 13f258dc2a06c6f4ec7f7b1dce7db10b13e72eb1 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:40:59 +0400 Subject: [PATCH 0090/1006] chore: Support multiple interop messages view on transaction page (#12455) * chore: Support multiple messages view on transaction page * Refactoring --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../views/api/v2/optimism_view.ex | 11 +++++--- .../chain/optimism/interop_message.ex | 25 ++++++++----------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 8f81ba3eb1ac..75a1e3b73158 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -471,16 +471,19 @@ defmodule BlockScoutWeb.API.V2.OptimismView do } end) - interop_message = + interop_messages = transaction_hash - |> InteropMessage.message_by_transaction() + |> InteropMessage.messages_by_transaction() out_json = Map.put(out_json, "op_withdrawals", withdrawals) - if is_nil(interop_message) do + if interop_messages == [] do out_json else - Map.put(out_json, "op_interop", interop_message) + out_json + |> Map.put("op_interop_messages", interop_messages) + # TODO: remove the deprecated `op_interop` map after frontend switches to the new `op_interop_messages` + |> Map.put("op_interop", Enum.at(interop_messages, 0)) end end diff --git a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex index 91b2714c9bfb..5986ebfc0968 100644 --- a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex +++ b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex @@ -697,31 +697,28 @@ defmodule Explorer.Chain.Optimism.InteropMessage do end @doc """ - Finds a message by transaction hash and prepares to display the message details on transaction page. + Finds messages by transaction hash and prepares to display the message details on transaction page. Used by `BlockScoutWeb.API.V2.OptimismView.add_optimism_fields` function. ## Parameters - - `transaction_hash`: The transaction hash we need to find the corresponding message for. + - `transaction_hash`: The transaction hash we need to find the corresponding messages for. ## Returns - - A map with message details ready to be displayed on transaction page. - - `nil` if the message not found. + - A list with maps containing message details ready to be displayed on transaction page. The list can be empty. """ - @spec message_by_transaction(Hash.t()) :: map() | nil - def message_by_transaction(transaction_hash) do + @spec messages_by_transaction(Hash.t()) :: [map()] + def messages_by_transaction(transaction_hash) do query = from( m in __MODULE__, - where: m.init_transaction_hash == ^transaction_hash or m.relay_transaction_hash == ^transaction_hash, - limit: 1 + where: m.init_transaction_hash == ^transaction_hash or m.relay_transaction_hash == ^transaction_hash ) - message = - query - |> Repo.replica().one() - |> extend_with_status() + query + |> Repo.replica().all() + |> Enum.map(fn msg -> + message = extend_with_status(msg) - if not is_nil(message) do chain_info = if message.init_transaction_hash == transaction_hash do %{ @@ -750,7 +747,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do }, chain_info ) - end + end) end @doc """ From 1cc3325a80827711b061e3d03818c0690dcb4347 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Jun 2025 14:44:52 +0300 Subject: [PATCH 0091/1006] chore(deps): bump absinthe from 1.7.9 to 1.7.10 (#12576) Bumps [absinthe](https://github.com/absinthe-graphql/absinthe) from 1.7.9 to 1.7.10. - [Release notes](https://github.com/absinthe-graphql/absinthe/releases) - [Changelog](https://github.com/absinthe-graphql/absinthe/blob/main/CHANGELOG.md) - [Commits](https://github.com/absinthe-graphql/absinthe/compare/v1.7.9...v1.7.10) --- updated-dependencies: - dependency-name: absinthe dependency-version: 1.7.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 26c15f6ff3c0..30846bab68bb 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "absinthe": {:hex, :absinthe, "1.7.9", "85c5bc9dfcb984fadf632d6e9a9f6daa7770c68d4f751f20cd5f9e77c57be4a1", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "08ae6339b05252b98e859ccd76614c94325c991f85aa325edde30027b7b044fc"}, + "absinthe": {:hex, :absinthe, "1.7.10", "b33471b593260f148d05e4d771d1857e07b70a680f89cfa75184098bef4ec893", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffda95735364c041a65a4b0e02ffb04eabb1e52ab664fa7eeecefb341449e8c2"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, From 389b544318a4a83f920cb314be68562ea3693256 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 13 Jun 2025 12:44:29 +0300 Subject: [PATCH 0092/1006] Temp CI for OP interop --- ...lish-docker-image-for-optimism-interop.yml | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 .github/workflows/publish-docker-image-for-optimism-interop.yml diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/publish-docker-image-for-optimism-interop.yml new file mode 100644 index 000000000000..a0bb756b6608 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-optimism-interop.yml @@ -0,0 +1,58 @@ +name: Optimism Interop Publish Docker image (temporary) + +on: + workflow_dispatch: + push: + branches: + - production-optimism-interop +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 8.1.1 + DOCKER_CHAIN_NAME: optimism + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-interop:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + + - name: Build and push Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-interop:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + From baac235ae056e620111f6f9fd060e22e731eacf3 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 18 Jun 2025 12:09:38 +0300 Subject: [PATCH 0093/1006] fix: ignore unknown type txs in gas price oracle (#12613) --- .../explorer/chain/cache/gas_price_oracle.ex | 18 ++++- .../chain/cache/gas_price_oracle_test.exs | 65 +++++++++++++++---- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex index 11f28389e1a5..28a1f3105d4c 100644 --- a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex +++ b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex @@ -7,7 +7,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do import Ecto.Query, only: [ - from: 2 + from: 2, + where: 3 ] alias Explorer.Chain.{Block, Wei} @@ -98,6 +99,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do from( block in Block, left_join: transaction in assoc(block, :transactions), + as: :transaction, where: block.consensus == true, where: is_nil(transaction.gas_price) or transaction.gas_price > ^0, where: transaction.block_number > ^from_block_query, @@ -168,7 +170,11 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do } ) - new_acc = fee_query |> Repo.all(timeout: :infinity) |> merge_gas_prices(acc, from_block_actual) + new_acc = + fee_query + |> filter_unknown_transaction_types(Application.get_env(:explorer, :chain_type)) + |> Repo.all(timeout: :infinity) + |> merge_gas_prices(acc, from_block_actual) gas_prices = new_acc |> process_fee_data_from_db() @@ -179,6 +185,14 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {{:error, error}, get_gas_prices_acc()} end + defp filter_unknown_transaction_types(query, :celo) do + query |> where([transaction: t], is_nil(t.type) or t.type != 123) + end + + defp filter_unknown_transaction_types(query, _chain_type) do + query + end + defp merge_gas_prices(new, acc, from_block), do: Enum.take_while(new ++ acc, &(&1.block_number > from_block)) defp process_fee_data_from_db([]) do diff --git a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs index a42910336666..ed5b745e8ad2 100644 --- a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs +++ b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs @@ -353,7 +353,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, - hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269", + type: 2 ) :transaction @@ -367,7 +368,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, - hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03", + type: 2 ) :transaction @@ -381,7 +383,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 3_000_000_000, max_priority_fee_per_gas: 3_000_000_000, max_fee_per_gas: 3_000_000_000, - hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016", + type: 2 ) assert {{:ok, @@ -423,7 +426,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269", - earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z], + type: 2 ) :transaction @@ -439,7 +443,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03", - earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z], + type: 2 ) :transaction @@ -455,7 +460,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do max_priority_fee_per_gas: 3_000_000_000, max_fee_per_gas: 3_000_000_000, hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016", - earliest_processing_start: ~U[2023-12-12 12:12:55.000000Z] + earliest_processing_start: ~U[2023-12-12 12:12:55.000000Z], + type: 2 ) assert {{ @@ -527,7 +533,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, - hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269", + type: 2 ) :transaction @@ -541,7 +548,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, max_fee_per_gas: 1_000_000_000, - hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03", + type: 2 ) :transaction @@ -555,7 +563,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do gas_price: 3_000_000_000, max_priority_fee_per_gas: 3_000_000_000, max_fee_per_gas: 3_000_000_000, - hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016", + type: 2 ) AverageBlockTime.refresh() @@ -595,7 +604,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do index: 0, gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, - max_fee_per_gas: 1_000_000_000 + max_fee_per_gas: 1_000_000_000, + type: 2 ) :transaction @@ -608,7 +618,8 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do index: 0, gas_price: 1_000_000_000, max_priority_fee_per_gas: 1_000_000_000, - max_fee_per_gas: 1_000_000_000 + max_fee_per_gas: 1_000_000_000, + type: 2 ) :transaction @@ -660,5 +671,37 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do assert {{:ok, %{average: %{price: 1.0}, fast: %{base_fee: 1.0}, slow: %{base_fee: 1.0}}}, _} = GasPriceOracle.get_average_gas_price(1, 35, 60, 90) end + + if Application.compile_env(:explorer, :chain_type) == :celo do + test "ignores transactions with unsupported types" do + block = + insert(:block, + number: 200, + hash: "0xfeedface00000000000000000000000000000000000000000000000000000000", + base_fee_per_gas: nil + ) + + :transaction + |> insert( + status: :ok, + block_hash: block.hash, + block_number: block.number, + index: 0, + # 1 Gwei + gas_price: 1_000_000_000, + type: 123, + hash: "0xcafe010000000000000000000000000000000000000000000000000000000000", + cumulative_gas_used: 884_322, + gas_used: 106_025 + ) + + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(1, 35, 60, 90) + end + end end end From 79fa763b6c9cd989d88b8baea53286d7be041bdd Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 24 Jun 2025 13:57:26 +0300 Subject: [PATCH 0094/1006] feat: add `creation_status` field to address response (#12660) * feat: add `creation_status` field to address response * feat: rename `status` to `creation_status` in smart contracts response * feat: add test when `creation_status` is `failed` --- .../views/api/v2/address_view.ex | 17 ++++++- .../views/api/v2/smart_contract_view.ex | 6 +-- .../api/v2/address_controller_test.exs | 32 +++++++++++- .../api/v2/smart_contract_controller_test.exs | 51 ++++++++++++++----- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 40cd38ce49bb..6fdffefb63e7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - alias BlockScoutWeb.AddressView + alias BlockScoutWeb.{AddressContractView, AddressView} alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} alias Explorer.{Chain, Market} alias Explorer.Chain.Address @@ -113,6 +113,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do Map.merge(base_info, %{ "creator_address_hash" => creator_hash && Address.checksum(creator_hash), "creation_transaction_hash" => creation_transaction_hash, + "creation_status" => creation_status(address), "token" => token, "coin_balance" => balance, "exchange_rate" => exchange_rate, @@ -255,6 +256,20 @@ defmodule BlockScoutWeb.API.V2.AddressView do }) end + @spec creation_status(Address.t()) :: :success | :failed | :selfdestructed | nil + defp creation_status(address) do + with true <- Address.smart_contract?(address), + {status, _bytecode} <- AddressContractView.contract_creation_code(address) do + case status do + :ok -> :success + :failed -> :failed + :selfdestructed -> :selfdestructed + end + else + _ -> nil + end + end + @spec chain_type_fields( map(), %{address: Address.t(), creation_transaction_from_address: Address.t()} diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index d487946df075..cc105e897599 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -267,21 +267,21 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do %{ "deployed_bytecode" => nil, "creation_bytecode" => init, - "status" => "selfdestructed" + "creation_status" => "selfdestructed" } {:failed, creation_code} -> %{ "deployed_bytecode" => "0x", "creation_bytecode" => creation_code, - "status" => "failed" + "creation_status" => "failed" } {:ok, contract_code} -> %{ "deployed_bytecode" => contract_code, "creation_bytecode" => AddressContractView.creation_code(address), - "status" => "success" + "creation_status" => "success" } end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 25b2a9220332..e1aa2fd31032 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -75,7 +75,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "watchlist_address_id" => nil, "has_beacon_chain_withdrawals" => false, "ens_domain_name" => nil, - "metadata" => nil + "metadata" => nil, + "creation_status" => nil } stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> @@ -117,7 +118,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "watchlist_address_id" => nil, "has_beacon_chain_withdrawals" => false, "ens_domain_name" => nil, - "metadata" => nil + "metadata" => nil, + "creation_status" => nil } stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> @@ -157,6 +159,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert response["is_contract"] assert response["creation_transaction_hash"] == to_string(succeeded_transaction.hash) + assert response["creation_status"] == "success" + end + + test "returns failed creation transaction for a contract", + %{conn: conn} do + contract_address = insert(:address, contract_code: "0x") + + failed_transaction = + insert(:transaction, + created_contract_address_hash: contract_address.hash + ) + |> with_block(status: :error) + + stub(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> + {:ok, []} + end) + + request = get(conn, "/api/v2/addresses/#{Address.checksum(contract_address.hash)}") + response = json_response(request, 200) + assert response["is_contract"] + assert response["creation_transaction_hash"] == to_string(failed_transaction.hash) + assert response["creation_status"] == "failed" end defp check_response(pattern_response, response) do @@ -183,6 +207,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert pattern_response["has_beacon_chain_withdrawals"] == response["has_beacon_chain_withdrawals"] assert pattern_response["ens_domain_name"] == response["ens_domain_name"] assert pattern_response["metadata"] == response["metadata"] + assert pattern_response["creation_status"] == response["creation_status"] end test "get EIP-1167 proxy contract info", %{conn: conn} do @@ -273,6 +298,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "watchlist_names" => [], "creator_address_hash" => ^from, "creation_transaction_hash" => ^transaction_hash, + "creation_status" => "success", "proxy_type" => "eip1167", "implementations" => [ %{ @@ -323,6 +349,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "watchlist_names" => [], "creator_address_hash" => ^from, "creation_transaction_hash" => ^transaction_hash, + "creation_status" => "success", "proxy_type" => "eip1967", "implementations" => [ %{ @@ -396,6 +423,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "watchlist_names" => [], "creator_address_hash" => ^from, "creation_transaction_hash" => ^transaction_hash, + "creation_status" => "success", "proxy_type" => "resolved_delegate_proxy", "implementations" => [ %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 8cd003ac14f8..363f5fd63203 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -62,7 +62,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [], "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => nil, - "status" => "success" + "creation_status" => "success" } insert(:transaction, @@ -84,7 +84,34 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" + } + end + + test "get unverified smart-contract with failed creation status", %{conn: conn} do + address = insert(:address, contract_code: "0x") + + creation_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029" + + insert(:transaction, + created_contract_address_hash: address.hash, + input: creation_bytecode + ) + |> with_block(status: :error) + + TestHelper.get_eip1967_implementation_error_response() + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") + response = json_response(request, 200) + + assert response == + %{ + "proxy_type" => nil, + "implementations" => [], + "deployed_bytecode" => "0x", + "creation_bytecode" => creation_bytecode, + "creation_status" => "failed" } end @@ -109,7 +136,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert implementation.proxy_type == :eip1967 request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") - response = json_response(request, 200) + json_response(request, 200) end test "get smart-contract", %{conn: conn} do @@ -179,7 +206,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none", "certified" => false, "is_blueprint" => false, - "status" => "success" + "creation_status" => "success" } TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) @@ -547,7 +574,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none", "certified" => false, "is_blueprint" => true, - "status" => "success" + "creation_status" => "success" } TestHelper.get_all_proxies_implementation_zero_addresses() @@ -728,7 +755,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } TestHelper.get_all_proxies_implementation_zero_addresses() @@ -813,7 +840,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } TestHelper.get_all_proxies_implementation_zero_addresses() @@ -945,7 +972,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1032,7 +1059,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } TestHelper.get_all_proxies_implementation_zero_addresses() @@ -1152,7 +1179,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1289,7 +1316,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1426,7 +1453,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "status" => "success" + "creation_status" => "success" } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") From b3fd5ca7928d2262c3acccfc259351feeb58dd86 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:16:52 +0300 Subject: [PATCH 0095/1006] chore(deps): bump timex from 3.7.11 to 3.7.13 (#12606) Bumps [timex](https://github.com/bitwalker/timex) from 3.7.11 to 3.7.13. - [Release notes](https://github.com/bitwalker/timex/releases) - [Changelog](https://github.com/bitwalker/timex/blob/main/CHANGELOG.md) - [Commits](https://github.com/bitwalker/timex/commits) --- updated-dependencies: - dependency-name: timex dependency-version: 3.7.13 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 30846bab68bb..89afcc086d07 100644 --- a/mix.lock +++ b/mix.lock @@ -160,9 +160,9 @@ "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, - "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, + "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, - "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, + "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.1.0", "0632d5844049fa2f26823f15e1120aa32f27df6f27ce515a4b04641736594bf4", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "8d3b30fa27c95c9e82c30c4afb016251405706d2e9627e603c3c9787fd1314fc"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, From 5cf41615596273e09a978dd8c904eed9c9250210 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:18:42 +0300 Subject: [PATCH 0096/1006] chore(deps): bump ex_aws from 2.5.9 to 2.5.10 (#12604) Bumps [ex_aws](https://github.com/ex-aws/ex_aws) from 2.5.9 to 2.5.10. - [Release notes](https://github.com/ex-aws/ex_aws/releases) - [Changelog](https://github.com/ex-aws/ex_aws/blob/main/CHANGELOG.md) - [Commits](https://github.com/ex-aws/ex_aws/compare/v2.5.9...v2.5.10) --- updated-dependencies: - dependency-name: ex_aws dependency-version: 2.5.10 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 89afcc086d07..28023ddc19f9 100644 --- a/mix.lock +++ b/mix.lock @@ -49,7 +49,7 @@ "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "evision": {:hex, :evision, "0.2.13", "15ce6e1ba70c2485ff6ea1fba67fa9d32abb7442c9189fa65f8cda20357e079e", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "49ff677cbbeaa1870841b5ac9dc4211e62d32d9585253c02a95dc98e30981e7a"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, - "ex_aws": {:hex, :ex_aws, "2.5.9", "8e2455172f0e5cbe2f56dd68de514f0dae6bb26d6b6e2f435a06434cf9dbb412", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cbdb6ffb0e6c6368de05ed8641fe1376298ba23354674428e5b153a541f23359"}, + "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"}, From d3bcaa37b12d36e99bba2f042ebac644347435f1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 10:25:04 +0300 Subject: [PATCH 0097/1006] chore(deps): bump pbkdf2 in /apps/block_scout_web/assets (#12659) Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.2 to 3.1.3. - [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.2...v3.1.3) --- updated-dependencies: - dependency-name: pbkdf2 dependency-version: 3.1.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 511 ++++++++++++++---- 1 file changed, 393 insertions(+), 118 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 86559606e5f5..423572a317c0 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -5383,15 +5383,44 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" }, "engines": { "node": ">= 0.4" @@ -6740,6 +6769,20 @@ "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz", "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -6952,12 +6995,10 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -6977,10 +7018,10 @@ "dev": true }, "node_modules/es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -8323,11 +8364,18 @@ "license": "ISC" }, "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/forever-agent": { @@ -8449,15 +8497,21 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8475,6 +8529,19 @@ "node": ">=8.0.0" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -8596,11 +8663,12 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8665,6 +8733,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -8673,9 +8742,10 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -9319,11 +9389,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -11829,6 +11900,15 @@ "jquery-mousewheel": ">=3.0.6" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -12593,20 +12673,73 @@ } }, "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", + "license": "MIT", "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" }, "engines": { "node": ">=0.12" } }, + "node_modules/pbkdf2/node_modules/create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "node_modules/pbkdf2/node_modules/hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "license": "MIT", + "dependencies": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/pbkdf2/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -14944,6 +15077,46 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/to-buffer/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -15084,14 +15257,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -16115,14 +16288,17 @@ "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -20177,15 +20353,32 @@ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" }, "call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "requires": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + } + }, + "call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "requires": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" } }, "callsites": { @@ -21140,6 +21333,16 @@ "resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.9.3.tgz", "integrity": "sha512-Azk8kD/2/nJIuVPK+zQ9sjKMRIpRvNyqn9XwbBHNq+iNuSccbJS6hwm1Woy0pMST0erSo0u4j+KJaodndDk4vA==" }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -21315,12 +21518,9 @@ } }, "es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "requires": { - "get-intrinsic": "^1.2.4" - } + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==" }, "es-errors": { "version": "1.3.0", @@ -21334,10 +21534,9 @@ "dev": true }, "es-object-atoms": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", - "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "requires": { "es-errors": "^1.3.0" } @@ -22422,11 +22621,11 @@ "dev": true }, "for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "requires": { - "is-callable": "^1.1.3" + "is-callable": "^1.2.7" } }, "forever-agent": { @@ -22507,15 +22706,20 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==" }, "get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "requires": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" } }, "get-package-type": { @@ -22524,6 +22728,15 @@ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -22611,12 +22824,9 @@ } }, "gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "requires": { - "get-intrinsic": "^1.1.3" - } + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==" }, "graceful-fs": { "version": "4.2.11", @@ -22660,12 +22870,13 @@ "has-proto": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==" + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true }, "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, "has-tostringtag": { "version": "1.0.2", @@ -23095,11 +23306,11 @@ } }, "is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "requires": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" } }, "is-typedarray": { @@ -25029,6 +25240,11 @@ "jquery-mousewheel": ">=3.0.6" } }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==" + }, "md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -25615,15 +25831,51 @@ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" }, "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.3.tgz", + "integrity": "sha512-wfRLBZ0feWRhCIkoMB6ete7czJcnNnqRpcoWQBLqatqXXmelSRqfdDK4F3u9T2s2cXas/hQJcryI/4lAL+XTlA==", "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "create-hash": "~1.1.3", + "create-hmac": "^1.1.7", + "ripemd160": "=2.0.1", + "safe-buffer": "^5.2.1", + "sha.js": "^2.4.11", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "create-hash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.1.3.tgz", + "integrity": "sha512-snRpch/kwQhcdlnZKYanNF1m0RDlrCdSKQaH87w1FCFPVPNCQ/Il9QJKAX2jVBZddRdaHBMC+zXa9Gw9tmkNUA==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "sha.js": "^2.4.0" + } + }, + "hash-base": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-2.0.2.tgz", + "integrity": "sha512-0TROgQ1/SxE6KmxWSvXHvRj90/Xo1JvZShofnYF+f6ZsGtR4eES7WfrQzPalmyagfKZCXpVnitiRebZulWsbiw==", + "requires": { + "inherits": "^2.0.1" + } + }, + "ripemd160": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.1.tgz", + "integrity": "sha512-J7f4wutN8mdbV08MJnXibYpCOPHR+yzy+iQ/AsjMv2j8cLavQ8VGagDFUwwTAdF8FmRKVeNpbTTEwNHCW1g94w==", + "requires": { + "hash-base": "^2.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "performance-now": { @@ -27245,6 +27497,28 @@ "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, + "to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -27356,14 +27630,13 @@ "dev": true }, "typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", - "dev": true, + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" } }, "typed-array-byte-length": { @@ -28161,14 +28434,16 @@ "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==" }, "which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "requires": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "for-each": "^0.3.3", - "gopd": "^1.0.1", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, From a38251159b0c4dc577aac2b1d72546f54fec6dfe Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 30 Jun 2025 12:34:20 +0400 Subject: [PATCH 0098/1006] chore: Move background migrations under indexer mode (#12480) --- apps/explorer/lib/explorer/application.ex | 28 +++++++++++------------ 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index b25fe85d24f9..c87facc702ac 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -148,20 +148,20 @@ defmodule Explorer.Application do configure(Explorer.Chain.Cache.Counters.Rootstock.LockedBTCCount), configure(Explorer.Chain.Cache.OptimismFinalizationPeriod), configure(Explorer.Chain.Cache.CeloEpochs), - configure(Explorer.Migrator.TransactionsDenormalization), - configure(Explorer.Migrator.AddressCurrentTokenBalanceTokenType), - configure(Explorer.Migrator.AddressTokenBalanceTokenType), - configure(Explorer.Migrator.SanitizeMissingBlockRanges), - configure(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers), - configure(Explorer.Migrator.TokenTransferTokenType), - configure(Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers), - configure(Explorer.Migrator.TransactionBlockConsensus), - configure(Explorer.Migrator.TokenTransferBlockConsensus), - configure(Explorer.Migrator.RestoreOmittedWETHTransfers), - configure(Explorer.Migrator.FilecoinPendingAddressOperations), - configure(Explorer.Migrator.SmartContractLanguage), - configure(Explorer.Migrator.CeloL2Epochs), - configure(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds), + configure_mode_dependent_process(Explorer.Migrator.TransactionsDenormalization, :indexer), + configure_mode_dependent_process(Explorer.Migrator.AddressCurrentTokenBalanceTokenType, :indexer), + configure_mode_dependent_process(Explorer.Migrator.AddressTokenBalanceTokenType, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SanitizeMissingBlockRanges, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers, :indexer), + configure_mode_dependent_process(Explorer.Migrator.TokenTransferTokenType, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SanitizeIncorrectWETHTokenTransfers, :indexer), + configure_mode_dependent_process(Explorer.Migrator.TransactionBlockConsensus, :indexer), + configure_mode_dependent_process(Explorer.Migrator.TokenTransferBlockConsensus, :indexer), + configure_mode_dependent_process(Explorer.Migrator.RestoreOmittedWETHTransfers, :indexer), + configure_mode_dependent_process(Explorer.Migrator.FilecoinPendingAddressOperations, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SmartContractLanguage, :indexer), + configure_mode_dependent_process(Explorer.Migrator.CeloL2Epochs, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, :indexer), Explorer.Migrator.BackfillMultichainSearchDB |> configure_mode_dependent_process(:indexer) |> configure_multichain_search_microservice(), From 14c9b5a5b264583385ae030ab55915e5ff3b65fd Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 30 Jun 2025 11:37:54 +0300 Subject: [PATCH 0099/1006] fix: Correctly use Geth importer for Besu genesis file. (#12466) (#12686) * fix(explorer/chain_spec): use Geth importer for Besu genesis data * test: verify that chainspec uses GethImporter for Besu variant * improve test and fix credo --------- Co-authored-by: Cristiano Aguzzi Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- .../lib/explorer/chain_spec/genesis_data.ex | 8 +-- .../explorer/chain_spec/genesis_data_test.exs | 53 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 apps/explorer/test/explorer/chain_spec/genesis_data_test.exs diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index 7fa2a5ddcb60..62323e2f45f5 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -205,7 +205,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer defp extend_chain_spec(chain_spec, precompiles_config, variant) - when is_list(chain_spec) and variant == EthereumJSONRPC.Geth do + when is_list(chain_spec) and variant in [EthereumJSONRPC.Geth, EthereumJSONRPC.Besu] do precompiles_as_map = precompiles_config |> Enum.reduce(%{}, fn contract, acc -> @@ -227,7 +227,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer defp extend_chain_spec(%{"genesis" => sub_entity} = chain_spec, precompiles_config, variant) - when variant == EthereumJSONRPC.Geth do + when variant in [EthereumJSONRPC.Geth, EthereumJSONRPC.Besu] do updated_sub_entity = extend_chain_spec(sub_entity, precompiles_config, variant) Map.put(chain_spec, "genesis", updated_sub_entity) @@ -235,7 +235,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Resulting spec will be handled by Explorer.ChainSpec.Geth.Importer defp extend_chain_spec(chain_spec, precompiles_config, variant) - when is_map(chain_spec) and variant == EthereumJSONRPC.Geth do + when is_map(chain_spec) and variant in [EthereumJSONRPC.Geth, EthereumJSONRPC.Besu] do accounts = case chain_spec["alloc"] do nil -> %{} @@ -275,7 +275,7 @@ defmodule Explorer.ChainSpec.GenesisData do defp import_genesis_accounts(chain_spec, variant) do if not Enum.empty?(chain_spec) do case variant do - EthereumJSONRPC.Geth -> + variant when variant in [EthereumJSONRPC.Geth, EthereumJSONRPC.Besu] -> {:ok, _} = GethImporter.import_genesis_accounts(chain_spec) _ -> diff --git a/apps/explorer/test/explorer/chain_spec/genesis_data_test.exs b/apps/explorer/test/explorer/chain_spec/genesis_data_test.exs new file mode 100644 index 000000000000..db408148f587 --- /dev/null +++ b/apps/explorer/test/explorer/chain_spec/genesis_data_test.exs @@ -0,0 +1,53 @@ +# apps/explorer/test/explorer/chain_spec/geth/genesis_data.exs +defmodule Explorer.ChainSpec.GenesisDataTest do + use ExUnit.Case, async: false + + @besu_genesis "#{File.cwd!()}/test/support/fixture/chain_spec/qdai_genesis.json" + |> File.read!() + |> Jason.decode!() + setup do + # Patch application env + old_genesis_data = Application.get_env(:explorer, Explorer.ChainSpec.GenesisData) + + old_json_rpc_named_arguments = + Application.get_env(:indexer, :json_rpc_named_arguments) + + Application.put_env( + :explorer, + Explorer.ChainSpec.GenesisData, + Keyword.merge(old_genesis_data, + chain_spec_path: "#{File.cwd!()}/test/support/fixture/chain_spec/qdai_genesis.json", + precompiled_config_path: nil + ) + ) + + Application.put_env( + :indexer, + :json_rpc_named_arguments, + Keyword.merge(old_json_rpc_named_arguments, variant: EthereumJSONRPC.Besu) + ) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.ChainSpec.GenesisData, old_genesis_data) + Application.put_env(:indexer, :json_rpc_named_arguments, old_json_rpc_named_arguments) + :meck.unload() + end) + + :ok + end + + test "remaps Besu variant to Geth and calls GethImporter.import_genesis_accounts/1" do + test_pid = self() + + :meck.new(Explorer.ChainSpec.Geth.Importer, [:passthrough]) + + :meck.expect(Explorer.ChainSpec.Geth.Importer, :import_genesis_accounts, fn args -> + send(test_pid, {:import_called, args}) + {:ok, []} + end) + + Explorer.ChainSpec.GenesisData.fetch_genesis_data() + + assert_receive {:import_called, @besu_genesis}, 1000 + end +end From 1fc0f74bf29746d6e96950a7b8d86c5a5eabea77 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 30 Jun 2025 11:56:33 +0300 Subject: [PATCH 0100/1006] Remove Berachain workflows from the master branch --- .github/workflows/pre-release-berachain.yml | 62 ------------------- .../publish-docker-image-for-berachain.yml | 57 ----------------- .github/workflows/release-berachain.yml | 60 ------------------ 3 files changed, 179 deletions(-) delete mode 100644 .github/workflows/pre-release-berachain.yml delete mode 100644 .github/workflows/publish-docker-image-for-berachain.yml delete mode 100644 .github/workflows/release-berachain.yml diff --git a/.github/workflows/pre-release-berachain.yml b/.github/workflows/pre-release-berachain.yml deleted file mode 100644 index a35dad7120ca..000000000000 --- a/.github/workflows/pre-release-berachain.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Pre-release for Berachain - -on: - workflow_dispatch: - inputs: - number: - type: number - required: true - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 8.1.1 - steps: - - uses: actions/checkout@v4 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Ethereum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-berachain:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain - - - name: Build and push Docker image for Ethereum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-berachain:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain diff --git a/.github/workflows/publish-docker-image-for-berachain.yml b/.github/workflows/publish-docker-image-for-berachain.yml deleted file mode 100644 index e8672c3b41c8..000000000000 --- a/.github/workflows/publish-docker-image-for-berachain.yml +++ /dev/null @@ -1,57 +0,0 @@ -name: Berachain Publish Docker image - -on: - workflow_dispatch: - push: - branches: - - production-berachain -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 8.1.1 - DOCKER_CHAIN_NAME: berachain - steps: - - uses: actions/checkout@v4 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain - - - name: Build and push Docker image (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain diff --git a/.github/workflows/release-berachain.yml b/.github/workflows/release-berachain.yml deleted file mode 100644 index 689b9d5eea80..000000000000 --- a/.github/workflows/release-berachain.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Release for Berachain - -on: - workflow_dispatch: - release: - types: [published] - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 8.1.1 - steps: - - uses: actions/checkout@v4 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Ethereum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-berachain:latest, ghcr.io/blockscout/blockscout-berachain:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain - - - name: Build and push Docker image for Ethereum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-berachain:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=berachain From a21e6f98b9bb123b4b0a9da604f5ddc7cb8d34d0 Mon Sep 17 00:00:00 2001 From: Rui JingAn <166888152+racequite@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:24:39 +0800 Subject: [PATCH 0101/1006] chore: remove redundant word in comment (#12603) Signed-off-by: racequite --- .../indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex | 2 +- .../lib/indexer/fetcher/arbitrum/workers/confirmations/tasks.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex index 811bbca3de03..e0bca6a885d4 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/settlement.ex @@ -349,7 +349,7 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Db.Settlement do {{:error, _}, _} -> # Error case: DB is inconsistent: although there should not be any unconfirmed blocks - # above the highest unconfirmed block, we cannot find the the confirmations transaction + # above the highest unconfirmed block, we cannot find the confirmations transaction # for one of the block higher than the highest unconfirmed block. raise "DB is inconsistent: could not get the L1 block of the closest confirmed block above the highest unconfirmed block" end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/tasks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/tasks.ex index 0a61e39b4f4a..5c06a333224b 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/tasks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/tasks.ex @@ -393,7 +393,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.Tasks do # The situation when the interim start block is not equal to the start block # means that the confirmations gap has not been inspected fully yet. It is # necessary to continue the confirmations discovery from the interim start - # block to the the block predecessor of the current start block. + # block to the block predecessor of the current start block. {retcode, state_for_next_iteration_historical(state, start_block - 1, interim_start_block)} {:confirmation_missed, _} -> From 6d6ccb5fc84dc0b0d0423e8e64a8cec6a77e759f Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 30 Jun 2025 13:08:57 +0300 Subject: [PATCH 0102/1006] feat: support ethereum pre-deploy contracts (#12579) * feat: add `geas` smart contract language * feat: add `geas` smart contract publisher --- ...ook_up_smart_contract_sources_on_demand.ex | 5 + .../lib/explorer/chain/smart_contract.ex | 23 ++- .../explorer/smart_contract/geas/publisher.ex | 172 ++++++++++++++++++ .../smart_contract/solidity/publisher.ex | 6 +- .../smart_contract/geas/publisher_test.exs | 86 +++++++++ cspell.json | 3 +- 6 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 apps/explorer/lib/explorer/smart_contract/geas/publisher.ex create mode 100644 apps/explorer/test/explorer/smart_contract/geas/publisher_test.exs diff --git a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex index b16731ae86f9..eb18e5f01318 100644 --- a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex +++ b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex @@ -26,6 +26,7 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do alias Explorer.Chain.{Address, Data, SmartContract} alias Explorer.Chain.Events.Publisher alias Explorer.SmartContract.EthBytecodeDBInterface + alias Explorer.SmartContract.Geas.Publisher, as: GeasPublisher alias Explorer.SmartContract.Solidity.Publisher, as: SolidityPublisher alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher alias Explorer.Utility.RateLimiter @@ -196,6 +197,10 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do SolidityPublisher.process_rust_verifier_response(source, address_hash_string, %{}, true, true, true) end + def process_contract_source("GEAS", source, address_hash_string) do + GeasPublisher.process_rust_verifier_response(source, address_hash_string, %{}, true, true, true) + end + def process_contract_source(_, _source, _address_hash), do: false defp check_match_type("PARTIAL", true), do: :full_match_required diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index ffd1ee65a1af..2aa753914f13 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -189,23 +189,30 @@ defmodule Explorer.Chain.SmartContract do } ] - @default_languages ~w(solidity vyper yul)a + @default_languages [ + solidity: 1, + vyper: 2, + yul: 3, + geas: 5 + ] + @chain_type_languages (case @chain_type do :arbitrum -> - ~w(stylus_rust)a + [stylus_rust: 4] :zilliqa -> - ~w(scilla)a + [scilla: 4] _ -> - ~w()a + [] end) - @languages @default_languages ++ @chain_type_languages - @languages_enum @languages |> Enum.with_index(1) - @language_string_to_atom @languages |> Map.new(&{to_string(&1), &1}) + @languages_enum @default_languages ++ @chain_type_languages + @language_string_to_atom @languages_enum + |> Enum.map(&elem(&1, 0)) + |> Map.new(&{to_string(&1), &1}) - @type base_language :: :solidity | :vyper | :yul + @type base_language :: :solidity | :vyper | :yul | :geas case @chain_type do :arbitrum -> diff --git a/apps/explorer/lib/explorer/smart_contract/geas/publisher.ex b/apps/explorer/lib/explorer/smart_contract/geas/publisher.ex new file mode 100644 index 000000000000..c65c7f32c2ed --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/geas/publisher.ex @@ -0,0 +1,172 @@ +defmodule Explorer.SmartContract.Geas.Publisher do + @moduledoc """ + Module responsible for verifying and publishing GEAS smart contracts. + + The verification process includes: + 1. Processing verification response from the Ethereum Bytecode DB + 2. Extracting contract source files and ABI + 3. Creating or updating the smart contract record in the database + 4. Handling verification failures by creating invalid changesets with error messages + """ + + require Logger + + alias Explorer.Chain.SmartContract + alias Explorer.SmartContract.Helper + alias Explorer.SmartContract.Solidity.Publisher, as: SolidityPublisher + + @doc """ + Processes the verification response from the Ethereum Bytecode DB for GEAS contracts. + + ## Parameters + - `source`: Map containing verification response with GEAS-specific fields: + - `"abi"`: Contract ABI as JSON string + - `"compilerVersion"`: Version of the GEAS compiler used + - `"constructorArguments"`: Constructor arguments (can be null) + - `"contractName"`: Name of the contract + - `"fileName"`: Main source file name (typically .eas extension) + - `"sourceFiles"`: Map of file paths to source code contents + - `"compilerSettings"`: Compiler settings as JSON string + - `"matchType"`: Type of bytecode match ("FULL" or "PARTIAL") + - `address_hash`: The contract's address hash as binary or `t:Explorer.Chain.Hash.t/0` + - `initial_params`: Initial parameters from the verification request + - `save_file_path?`: Boolean indicating whether to save the file path + - `is_standard_json?`: Boolean indicating if this was a standard JSON verification + - `automatically_verified?`: Boolean indicating if this was automatically verified + + ## Returns + - `{:ok, smart_contract}` if verification and database storage succeed + - `{:error, changeset}` if verification fails or there are validation errors + """ + @spec process_rust_verifier_response( + map(), + binary() | Explorer.Chain.Hash.t(), + map(), + boolean(), + boolean(), + boolean() + ) :: + {:ok, Explorer.Chain.SmartContract.t()} | {:error, Ecto.Changeset.t()} + def process_rust_verifier_response( + %{ + "abi" => abi_string, + "compilerVersion" => compiler_version, + "constructorArguments" => constructor_arguments, + "contractName" => contract_name, + "fileName" => file_name, + "sourceFiles" => sources, + "compilerSettings" => compiler_settings_string, + "matchType" => match_type + } = source, + address_hash, + initial_params, + save_file_path?, + _is_standard_json?, + automatically_verified? \\ false + ) do + secondary_sources = + for {file, source_code} <- sources, + file != file_name, + do: %{ + "file_name" => file, + "contract_source_code" => source_code, + "address_hash" => address_hash + } + + %{^file_name => contract_source_code} = sources + + compiler_settings = Jason.decode!(compiler_settings_string) + + prepared_params = + %{} + |> Map.put("compiler_version", compiler_version) + |> Map.put("constructor_arguments", constructor_arguments) + |> Map.put("contract_source_code", contract_source_code) + |> Map.put("name", contract_name) + |> Map.put("file_path", if(save_file_path?, do: file_name)) + |> Map.put("secondary_sources", secondary_sources) + |> Map.put("partially_verified", match_type == "PARTIAL") + |> Map.put("verified_via_eth_bytecode_db", automatically_verified?) + |> Map.put("verified_via_verifier_alliance", source["verifier_alliance?"] || false) + |> Map.put("compiler_settings", compiler_settings) + |> Map.put("license_type", initial_params["license_type"]) + |> Map.put("is_blueprint", source["isBlueprint"] || false) + + publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null"), save_file_path?) + end + + @doc """ + Stores information about a verified GEAS smart contract in the database. + + ## Parameters + - `address_hash`: The contract's address hash as binary or `t:Explorer.Chain.Hash.t/0` + - `params`: Map containing contract details + - `abi`: Contract's ABI (Application Binary Interface) + - `verification_with_files?`: Boolean indicating if verification used source files + + ## Returns + - `{:ok, smart_contract}` if publishing succeeds + - `{:error, changeset}` if there are validation errors + - `{:error, message}` if the database operation fails + """ + @spec publish_smart_contract(binary() | Explorer.Chain.Hash.t(), map(), map(), boolean()) :: + {:ok, Explorer.Chain.SmartContract.t()} | {:error, Ecto.Changeset.t() | String.t()} + def publish_smart_contract(address_hash, params, abi, verification_with_files?) do + attrs = address_hash |> attributes(params, abi) + + ok_or_error = + SmartContract.create_or_update_smart_contract( + address_hash, + attrs, + verification_with_files? + ) + + case ok_or_error do + {:ok, _smart_contract} -> + Logger.info("GEAS smart-contract #{address_hash} successfully published") + + {:error, error} -> + Logger.error("GEAS smart-contract #{address_hash} failed to publish: #{inspect(error)}") + end + + ok_or_error + end + + # Private function to build contract attributes for database storage + defp attributes(address_hash, params, abi) do + constructor_arguments = params["constructor_arguments"] + compiler_settings = params["compiler_settings"] + + clean_constructor_arguments = SolidityPublisher.clear_constructor_arguments(constructor_arguments) + + clean_compiler_settings = + if compiler_settings in ["", nil, %{}] do + nil + else + compiler_settings + end + + %{ + address_hash: address_hash, + name: params["name"], + compiler_version: params["compiler_version"], + evm_version: nil, + optimization_runs: nil, + optimization: false, + contract_source_code: params["contract_source_code"], + constructor_arguments: clean_constructor_arguments, + external_libraries: [], + secondary_sources: params["secondary_sources"], + abi: abi, + verified_via_sourcify: false, + verified_via_eth_bytecode_db: params["verified_via_eth_bytecode_db"] || false, + verified_via_verifier_alliance: params["verified_via_verifier_alliance"] || false, + partially_verified: params["partially_verified"] || false, + file_path: params["file_path"], + compiler_settings: clean_compiler_settings, + license_type: Helper.prepare_license_type(params["license_type"]) || :none, + is_blueprint: params["is_blueprint"] || false, + language: :geas + } + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex index 8c97bd77e59a..9ebf593081a0 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex @@ -406,7 +406,11 @@ defmodule Explorer.SmartContract.Solidity.Publisher do )).() end - defp clear_constructor_arguments(constructor_arguments) do + @doc """ + Helper function to clean constructor arguments + """ + @spec clear_constructor_arguments(String.t() | nil) :: String.t() | nil + def clear_constructor_arguments(constructor_arguments) do if constructor_arguments != nil && constructor_arguments != "" do constructor_arguments else diff --git a/apps/explorer/test/explorer/smart_contract/geas/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/geas/publisher_test.exs new file mode 100644 index 000000000000..1f2672b6783d --- /dev/null +++ b/apps/explorer/test/explorer/smart_contract/geas/publisher_test.exs @@ -0,0 +1,86 @@ +defmodule Explorer.SmartContract.Geas.PublisherTest do + use ExUnit.Case, async: true + + use Explorer.DataCase + + doctest Explorer.SmartContract.Geas.Publisher + + @moduletag timeout: :infinity + + alias Explorer.Chain.{SmartContract} + alias Explorer.SmartContract.Geas.Publisher + + setup do + configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + end) + end + + describe "process_rust_verifier_response/6" do + test "successfully processes a GEAS verification response" do + contract_address = insert(:contract_address) + + geas_response = %{ + "fileName" => "src/consolidations/main.eas", + "contractName" => "ConsolidationRequestPredeploy", + "compilerVersion" => "v0.2.2", + "compilerSettings" => "{}", + "sourceType" => "GEAS", + "sourceFiles" => %{ + "src/common/fake_expo.eas" => "// Test GEAS contract source code", + "src/consolidations/ctor.eas" => "// Test GEAS contract source code", + "src/consolidations/main.eas" => "// Test GEAS contract source code" + }, + "abi" => + Jason.encode!([ + %{ + "type" => "function", + "name" => "test_function", + "inputs" => [], + "outputs" => [%{"type" => "bool"}], + "stateMutability" => "view" + } + ]), + "constructorArguments" => nil, + "matchType" => "PARTIAL", + "compilationArtifacts" => "{}", + "creationInputArtifacts" => "{}", + "deployedBytecodeArtifacts" => "{}", + "isBlueprint" => false, + "libraries" => %{} + } + + response = + Publisher.process_rust_verifier_response( + geas_response, + contract_address.hash, + %{}, + # save_file_path? + true, + # is_standard_json? + true, + # automatically_verified? + true + ) + + assert {:ok, %SmartContract{} = smart_contract} = response + + assert smart_contract.address_hash == contract_address.hash + assert smart_contract.name == "ConsolidationRequestPredeploy" + assert smart_contract.compiler_version == "v0.2.2" + assert smart_contract.file_path == "src/consolidations/main.eas" + + assert smart_contract.contract_source_code == + "// Test GEAS contract source code" + + assert smart_contract.language == :geas + assert smart_contract.verified_via_eth_bytecode_db == true + assert smart_contract.partially_verified == true + assert is_list(smart_contract.abi) + assert length(smart_contract.abi) == 1 + end + end +end diff --git a/cspell.json b/cspell.json index ea7e859995ff..97dda466143d 100644 --- a/cspell.json +++ b/cspell.json @@ -263,6 +263,7 @@ "fvdskvjglav", "fwrite", "fwupv", + "geas", "getabi", "getbalance", "getblockbyhash", @@ -510,6 +511,7 @@ "rarr", "ratiolector", "rbtc", + "rdparty", "rebranded", "recaptcha", "rechunk", @@ -537,7 +539,6 @@ "returnaddress", "retval", "reuseaddr", - "rdparty", "rollup", "rollups", "RPC's", From 04a1875fca79633da18458fbe5bea51164b0d5be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:22:40 +0300 Subject: [PATCH 0103/1006] chore(deps): bump ecto from 3.12.5 to 3.13.1 (#12657) Bumps [ecto](https://github.com/elixir-ecto/ecto) from 3.12.5 to 3.13.1. - [Release notes](https://github.com/elixir-ecto/ecto/releases) - [Changelog](https://github.com/elixir-ecto/ecto/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-ecto/ecto/compare/v3.12.5...v3.13.1) --- updated-dependencies: - dependency-name: ecto dependency-version: 3.13.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 28023ddc19f9..cf8d40d63a6a 100644 --- a/mix.lock +++ b/mix.lock @@ -42,7 +42,7 @@ "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, From f4453e7c92bb98030d733d69acc93bfe59354e45 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:27:49 +0300 Subject: [PATCH 0104/1006] chore: Update deprecated address to address_hash in tx summary response (#12617) --- .../microservice_interfaces/transaction_interpretation.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 069bb4ca6ba3..2685f5ec523c 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -310,12 +310,19 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do defp preload_template_variables(error), do: error + # TODO: remove this once we have updated the transaction interpretation service defp preload_template_variable(%{"type" => "token", "value" => %{"address" => address_hash_string} = value}), do: %{ "type" => "token", "value" => address_hash_string |> Chain.token_from_address_hash(@api_true) |> token_from_db() |> Map.merge(value) } + defp preload_template_variable(%{"type" => "token", "value" => %{"address_hash" => address_hash_string} = value}), + do: %{ + "type" => "token", + "value" => address_hash_string |> Chain.token_from_address_hash(@api_true) |> token_from_db() |> Map.merge(value) + } + defp preload_template_variable(%{"type" => "address", "value" => %{"hash" => address_hash_string} = value}), do: %{ "type" => "address", From 08442417018008b26892294138dc16f876596a3e Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 30 Jun 2025 15:54:43 +0300 Subject: [PATCH 0105/1006] feat: support merged tenants (#12109) * feat: support merged tenants * Process review comments * Update doc --- .../explorer/lib/explorer/account/identity.ex | 4 +- .../third_party_integrations/auth0.ex | 534 +------------- .../auth0/internal.ex | 663 ++++++++++++++++++ .../third_party_integrations/auth0/legacy.ex | 246 +++++++ .../auth0/migrated.ex | 190 +++++ config/runtime.exs | 3 +- docker-compose/envs/common-blockscout.env | 1 + 7 files changed, 1136 insertions(+), 505 deletions(-) create mode 100644 apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex index c5ab81b9c339..79231f4dc9f6 100644 --- a/apps/explorer/lib/explorer/account/identity.ex +++ b/apps/explorer/lib/explorer/account/identity.ex @@ -50,8 +50,8 @@ defmodule Explorer.Account.Identity do @doc false def changeset(identity, attrs) do identity - |> cast(attrs, [:uid, :email, :name, :nickname, :avatar, :verification_email_sent_at]) - |> validate_required([:uid, :email, :name]) + |> cast(attrs, [:uid, :email, :name, :nickname, :avatar, :verification_email_sent_at, :otp_sent_at]) + |> validate_required([:uid]) |> put_hashed_fields() end diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex index 12d2189e5391..e8629288824f 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex @@ -5,20 +5,14 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do require Logger alias Explorer.Account.Identity - alias Explorer.{Account, Helper, Repo} - alias Explorer.Chain.Address - alias OAuth2.{AccessToken, Client} + alias Explorer.{Account, Helper} + alias Explorer.ThirdPartyIntegrations.Auth0.Internal alias Ueberauth.Auth - alias Ueberauth.Strategy.Auth0 alias Ueberauth.Strategy.Auth0.OAuth - @redis_key "auth0" - @request_siwe_message "Request Sign in with Ethereum message via /api/account/v2/siwe_message" @wrong_nonce "Wrong nonce in message" @misconfiguration_detected "Misconfiguration detected, please contact support." - @disabled_otp_error_description "Grant type 'http://auth0.com/oauth/grant-type/passwordless/otp' not allowed for the client." - @users_path "/api/v2/users" @json_content_type [{"Content-type", "application/json"}] @doc """ @@ -33,7 +27,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ @spec get_m2m_jwt() :: nil | String.t() def get_m2m_jwt do - get_m2m_jwt_inner(Redix.command(:redix, ["GET", cookie_key(@redis_key)])) + get_m2m_jwt_inner(Redix.command(:redix, ["GET", Internal.redis_key()])) end def get_m2m_jwt_inner({:ok, token}) when not is_nil(token), do: token @@ -87,7 +81,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do end defp cache_token(token, ttl) do - Redix.command(:redix, ["SET", cookie_key(@redis_key), token, "EX", ttl]) + Redix.command(:redix, ["SET", Internal.redis_key(), token, "EX", ttl]) token end @@ -108,9 +102,9 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ @spec send_otp_for_linking(String.t(), String.t()) :: :error | :ok | {:error, String.t()} def send_otp_for_linking(email, ip) do - case find_users_by_email(email) do + case Internal.find_users_by_email(email) do {:ok, []} -> - do_send_otp(email, ip) + Internal.send_otp(email, ip) {:ok, users} when is_list(users) and length(users) > 0 -> {:error, "Account with this email already exists"} @@ -138,12 +132,12 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ @spec send_otp(String.t(), String.t()) :: :error | :ok | {:interval, integer()} def send_otp(email, ip) do - case find_users_by_email(email) do + case Internal.find_users_by_email(email) do {:ok, []} -> - do_send_otp(email, ip) + Internal.send_otp(email, ip) {:ok, [user | _]} -> - handle_existing_user(user, email, ip) + Internal.handle_existing_user(user, email, ip) error -> error @@ -169,14 +163,13 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ @spec link_email(Identity.session(), String.t(), String.t(), String.t()) :: :error | {:ok, Auth.t()} | {:error, String.t()} - def link_email(%{uid: primary_user_id, email: nil}, email, otp, ip) do - case find_users_by_email(email) do + def link_email(%{uid: user_id_without_email, email: nil}, email, otp, ip) do + case Internal.find_users_by_email(email) do {:ok, []} -> - with {:ok, token} <- confirm_otp(email, otp, ip), - {:ok, %{"sub" => "email|" <> identity_id}} <- get_user_from_token(token), - :ok <- link_users(primary_user_id, identity_id, "email"), - {:ok, user} <- update_user_email(primary_user_id, email) do - {:ok, create_auth(user)} + with {:ok, token} <- Internal.confirm_otp(email, otp, ip), + {:ok, %{"sub" => user_id_with_email}} <- Internal.get_user_from_token(token), + {:ok, user} <- Internal.link_email(user_id_without_email, user_id_with_email, email) do + {:ok, Internal.create_auth(user)} end {:ok, users} when is_list(users) -> @@ -207,14 +200,15 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ @spec confirm_otp_and_get_auth(String.t(), String.t(), String.t()) :: :error | {:error, String.t()} | {:ok, Auth.t()} def confirm_otp_and_get_auth(email, otp, ip) do - with {:ok, token} <- confirm_otp(email, otp, ip), - {:ok, %{"sub" => user_id} = user} <- get_user_from_token(token), - {:search, _user_from_token, {:ok, user}} <- {:search, user, get_user_by_id(user_id)} do - maybe_link_email_and_get_auth(user) + with {:ok, token} <- Internal.confirm_otp(email, otp, ip), + {:ok, %{"sub" => user_id} = user} <- Internal.get_user_from_token(token), + {:search, _user_from_token, {:ok, user}} <- {:search, user, Internal.get_user_by_id(user_id)}, + {:ok, user} <- Internal.process_email_user(user) do + {:ok, Internal.create_auth(user)} else # newly created user, sometimes just created user with otp does not appear in the search - {:search, %{"sub" => user_id} = user_from_token, {:error, "User not found"}} -> - {:ok, user_from_token |> Map.put("user_id", user_id) |> create_auth()} + {:search, %{"sub" => _user_id} = user_from_token, {:error, "User not found"}} -> + Internal.handle_not_found_just_created_email_user(user_from_token) err -> err @@ -235,18 +229,8 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do - `{:new, Identity.session()}` if the address hash was added to the session """ @spec update_session_with_address_hash(Identity.session()) :: {:old, Identity.session()} | {:new, Identity.session()} - def update_session_with_address_hash(%{address_hash: _} = session), do: {:old, session} - - def update_session_with_address_hash(%{uid: user_id} = session) do - case get_user_by_id(user_id) do - {:ok, user} -> - {:new, Map.put(session, :address_hash, user |> create_auth() |> Identity.address_hash_from_auth())} - - error -> - Logger.error("Error when updating session with address hash: #{inspect(error)}") - {:old, session} - end - end + def update_session_with_address_hash(session), + do: Internal.update_session_with_address_hash(session) @doc """ Generates a Sign-In with Ethereum (SIWE) message for the given address. @@ -280,7 +264,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do expiration_time: DateTime.utc_now() |> DateTime.add(300, :second) |> DateTime.to_iso8601() } - with {:cache, {:ok, _nonce}} <- {:cache, cache_nonce_for_address(nonce, address)}, + with {:cache, {:ok, _nonce}} <- {:cache, Internal.cache_nonce_for_address(nonce, address)}, {:message, {:ok, message}} <- {:message, Siwe.to_str(message)} do {:ok, message} else @@ -315,10 +299,10 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do def link_address(user_id, message, signature) do with {:signature, {:ok, %{nonce: nonce, address: address}}} <- {:signature, message |> String.trim() |> Siwe.parse_if_valid(signature)}, - {:nonce, {:ok, ^nonce}} <- {:nonce, get_nonce_for_address(address)}, - {:user, {:ok, []}} <- {:user, find_users_by_web3_address(address)}, - {:ok, user} <- update_user_with_web3_address(user_id, address) do - {:ok, create_auth(user)} + {:nonce, {:ok, ^nonce}} <- {:nonce, Internal.get_nonce_for_address(address)}, + {:user, {:ok, []}} <- {:user, Internal.find_users_by_web3_address(address)}, + {:ok, user} <- Internal.update_user_with_web3_address(user_id, address) do + {:ok, Internal.create_auth(user)} else {:nonce, {:ok, _}} -> {:error, @wrong_nonce} @@ -359,9 +343,9 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do def get_auth_with_web3(message, signature) do with {:signature, {:ok, %{nonce: nonce, address: address}}} <- {:signature, message |> String.trim() |> Siwe.parse_if_valid(signature)}, - {:nonce, {:ok, ^nonce}} <- {:nonce, get_nonce_for_address(address)}, - {:user, {:ok, user}} <- {:user, find_or_create_web3_user(address, signature)} do - {:ok, create_auth(user)} + {:nonce, {:ok, ^nonce}} <- {:nonce, Internal.get_nonce_for_address(address)}, + {:user, {:ok, user}} <- {:user, Internal.process_web3_user(address, signature)} do + {:ok, Internal.create_auth(user)} else {:nonce, {:ok, nil}} -> {:error, @request_siwe_message} @@ -373,458 +357,4 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do error end end - - defp handle_existing_user(user, email, ip) do - user - |> create_auth() - |> Identity.find_identity() - |> handle_identity(email, ip) - end - - defp handle_identity(nil, email, ip), do: do_send_otp(email, ip) - - defp handle_identity(%Identity{otp_sent_at: otp_sent_at} = identity, email, ip) do - otp_resend_interval = Application.get_env(:explorer, Account, :otp_resend_interval) - - case Helper.check_time_interval(otp_sent_at, otp_resend_interval) do - true -> - identity - |> Identity.changeset(%{otp_sent_at: DateTime.utc_now()}) - |> Repo.account_repo().update() - - do_send_otp(email, ip) - - interval -> - {:interval, interval} - end - end - - defp do_send_otp(email, ip) do - client = OAuth.client() - - body = - %{ - email: email, - connection: :email, - send: :code - } - |> put_client_id_and_secret() - - headers = [{"auth0-forwarded-for", ip} | @json_content_type] - - case Client.post(client, "/passwordless/start", body, headers) do - {:ok, %OAuth2.Response{status_code: 200}} -> - :ok - - other -> - Logger.error("Error while sending otp: ", inspect(other)) - - :error - end - end - - defp get_user_from_token(%AccessToken{other_params: %{"id_token" => token}}) do - case Joken.peek_claims(token) do - {:ok, %{"sub" => _} = user} -> - {:ok, user} - - error -> - Logger.error("Error while peeking claims from token: #{inspect(error)}") - :error - end - end - - defp get_user_from_token(token) do - Logger.error("No id_token in token: #{inspect(Map.update(token, :access_token, "xxx", fn _ -> "xxx" end))}") - - {:error, @misconfiguration_detected} - end - - defp confirm_otp(email, otp, ip) do - client = OAuth.client() - - body = - %{ - username: email, - otp: otp, - realm: :email, - grant_type: :"http://auth0.com/oauth/grant-type/passwordless/otp" - } - |> put_client_id_and_secret() - - headers = [{"auth0-forwarded-for", ip} | @json_content_type] - - case Client.post(client, "/oauth/token", body, headers) do - {:ok, %OAuth2.Response{status_code: 200, body: body}} -> - {:ok, AccessToken.new(body)} - - {:error, - %OAuth2.Response{ - status_code: 403, - body: - %{ - "error" => "unauthorized_client", - "error_description" => @disabled_otp_error_description, - "error_uri" => "https://auth0.com/docs/clients/client-grant-types" - } = body - }} -> - Logger.error("Need to enable OTP: #{inspect(body)}") - {:error, @misconfiguration_detected} - - {:error, - %OAuth2.Response{ - status_code: 403, - body: %{"error" => "invalid_grant", "error_description" => "Wrong email or verification code."} - }} -> - {:error, "Wrong verification code."} - - {:error, - %OAuth2.Response{ - status_code: 403, - body: %{ - "error" => "invalid_grant", - "error_description" => "You've reached the maximum number of attempts. Please try to login again." - } - }} -> - {:error, "Max attempts reached. Please resend code."} - - other -> - Logger.error("Error while confirming otp: #{inspect(other)}") - - :error - end - end - - defp get_user_by_id(id) do - case get_m2m_jwt() do - token when is_binary(token) -> - client = OAuth.client(token: token) - - case Client.get(client, "#{@users_path}/#{URI.encode(id)}") do - {:ok, %OAuth2.Response{status_code: 200, body: %{"user_id" => ^id} = user}} -> - {:ok, user} - - {:error, %OAuth2.Response{status_code: 404}} -> - {:error, "User not found"} - - other -> - Logger.error(["Error while getting user by id: ", inspect(other)]) - :error - end - end - end - - defp find_users_by_email(email) do - case get_m2m_jwt() do - token when is_binary(token) -> - client = OAuth.client(token: token) - email = URI.encode(email) - - case Client.get(client, @users_path, [], - params: %{"q" => ~s(email:"#{email}" OR user_metadata.email:"#{email}")} - ) do - {:ok, %OAuth2.Response{status_code: 200, body: users}} when is_list(users) -> - {:ok, users} - - {:error, %OAuth2.Response{status_code: 403, body: %{"errorCode" => "insufficient_scope"} = body}} -> - Logger.error(["Failed to get web3 user. Insufficient scope: ", inspect(body)]) - {:error, @misconfiguration_detected} - - other -> - Logger.error(["Error while getting web3 user: ", inspect(other)]) - :error - end - - nil -> - Logger.error("Failed to get M2M JWT") - {:error, @misconfiguration_detected} - end - end - - defp maybe_link_email_and_get_auth(%{"email" => email, "user_id" => "email|" <> identity_id = user_id} = user) do - case get_m2m_jwt() do - token when is_binary(token) -> - client = OAuth.client(token: token) - - case Client.get(client, @users_path, [], - params: %{"q" => ~s(email:"#{URI.encode(email)}" AND NOT user_id:"#{URI.encode(user_id)}")} - ) do - {:ok, %OAuth2.Response{status_code: 200, body: []}} -> - {:ok, create_auth(user)} - - {:ok, %OAuth2.Response{status_code: 200, body: [%{"user_id" => primary_user_id} = user]}} -> - link_users(primary_user_id, identity_id, "email") - maybe_verify_email(user) - {:ok, create_auth(user)} - - {:ok, %OAuth2.Response{status_code: 200, body: users}} when is_list(users) and length(users) > 1 -> - merge_email_users(users, identity_id, "email") - - {:error, %OAuth2.Response{status_code: 403, body: %{"errorCode" => "insufficient_scope"} = body}} -> - Logger.error(["Failed to get web3 user. Insufficient scope: ", inspect(body)]) - {:error, @misconfiguration_detected} - - other -> - Logger.error(["Error while getting web3 user: ", inspect(other)]) - :error - end - - nil -> - Logger.error("Failed to get M2M JWT") - {:error, @misconfiguration_detected} - end - end - - defp maybe_link_email_and_get_auth(user) do - {:ok, create_auth(user)} - end - - defp merge_web3_users([primary_user | _] = users) do - identity_map = - users - |> Enum.map(& &1["user_id"]) - |> Identity.find_identities() - |> Map.new(&{&1.uid, &1}) - - users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() - - case users |> Enum.map(&identity_map[&1["user_id"]]) |> Enum.reject(&is_nil(&1)) |> Account.merge() do - {{:ok, 0}, nil} -> - unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user) do - update_user_with_web3_address( - primary_user["user_id"], - primary_user |> create_auth() |> Identity.address_hash_from_auth() - ) - end - - {:ok, primary_user} - - {{:ok, _}, primary_identity} -> - primary_user_from_db = users_map[primary_identity.uid] - - unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user_from_db) do - update_user_with_web3_address( - primary_user_from_db["user_id"], - primary_user_from_db |> create_auth() |> Identity.address_hash_from_auth() - ) - end - - {:ok, primary_user_from_db} - - error -> - Logger.error(["Error while merging users with the same address: ", inspect(error)]) - :error - end - end - - defp merge_email_users([primary_user | _] = users, identity_id_to_link, provider_for_linking) do - identity_map = - users - |> Enum.map(& &1["user_id"]) - |> Identity.find_identities() - |> Map.new(&{&1.uid, &1}) - - users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() - - case users |> Enum.map(&identity_map[&1["user_id"]]) |> Enum.reject(&is_nil(&1)) |> Account.merge() do - {{:ok, 0}, nil} -> - link_users(primary_user["user_id"], identity_id_to_link, provider_for_linking) - maybe_verify_email(primary_user) - {:ok, create_auth(primary_user)} - - {{:ok, _}, primary_identity} -> - link_users(primary_identity.uid, identity_id_to_link, provider_for_linking) - maybe_verify_email(users_map[primary_identity.uid]) - {:ok, create_auth(users_map[primary_identity.uid])} - - error -> - Logger.error(["Error while merging users with the same email: ", inspect(error)]) - :error - end - end - - defp maybe_verify_email(%{"email_verified" => false, "user_id" => user_id}) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - body = %{"email_verified" => true}, - {:ok, %OAuth2.Response{status_code: 200, body: _user}} <- - Client.patch(client, "#{@users_path}/#{URI.encode(user_id)}", body, @json_content_type) do - :ok - else - error -> handle_common_errors(error, "Failed to patch email_verified to true") - end - end - - defp maybe_verify_email(_), do: :ok - - defp cache_nonce_for_address(nonce, address) do - case Redix.command(:redix, ["SET", cookie_key(address <> "siwe_nonce"), nonce, "EX", 300]) do - {:ok, _} -> {:ok, nonce} - err -> err - end - end - - defp get_nonce_for_address(address_hash) do - cookie_key = cookie_key(Address.checksum(address_hash) <> "siwe_nonce") - - with {:get, {:ok, nonce}} <- {:get, Redix.command(:redix, ["GET", cookie_key])}, - {:del, {:ok, _}} <- {:del, Redix.command(:redix, ["DEL", cookie_key])} do - {:ok, nonce} - else - _ -> {:error, "Redis configuration problem, please contact support."} - end - end - - defp find_or_create_web3_user(address, signature) do - case find_users_by_web3_address(address) do - {:ok, [%{"user_metadata" => %{"web3_address_hash" => ^address}} = user]} -> - {:ok, user} - - {:ok, [%{"user_id" => user_id}]} -> - update_user_with_web3_address(user_id, address) - - {:ok, []} -> - create_web3_user(address, signature) - - {:ok, users} when is_list(users) and length(users) > 1 -> - merge_web3_users(users) - - other -> - other - end - end - - defp find_users_by_web3_address(address) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - {:ok, %OAuth2.Response{status_code: 200, body: users}} when is_list(users) <- - Client.get( - client, - @users_path, - [], - params: %{ - "q" => - ~s|user_id:*siwe*#{address} OR user_id:*Passkey*#{address} OR user_metadata.web3_address_hash:"#{address}" OR (user_id:*Passkey* AND nickname:"#{address}")| - } - ) do - {:ok, users} - else - error -> handle_common_errors(error, "Failed to search user by address") - end - end - - defp update_user_email(user_id, email) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - body = %{"user_metadata" => %{"email" => email}}, - {:ok, %OAuth2.Response{status_code: 200, body: user}} <- - Client.patch(client, "#{@users_path}/#{URI.encode(user_id)}", body, @json_content_type) do - {:ok, user} - else - error -> handle_common_errors(error, "Failed to update user email") - end - end - - defp update_user_with_web3_address(user_id, address) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - body = %{"user_metadata" => %{"web3_address_hash" => address}}, - {:ok, %OAuth2.Response{status_code: 200, body: user}} <- - Client.patch(client, "#{@users_path}/#{URI.encode(user_id)}", body, @json_content_type) do - {:ok, user} - else - error -> handle_common_errors(error, "Failed to update user address") - end - end - - defp create_web3_user(address, signature) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - body = %{ - username: address, - password: signature, - email_verified: true, - connection: "Username-Password-Authentication", - user_metadata: %{web3_address_hash: address} - }, - {:ok, %OAuth2.Response{status_code: 201, body: user}} <- - Client.post(client, @users_path, body, @json_content_type) do - {:ok, user} - else - {:error, - %OAuth2.Response{ - status_code: 400, - body: - %{ - "errorCode" => "invalid_body", - "message" => "Payload validation error: 'Missing required property: email'." - } = body - }} -> - Logger.error([ - "Failed to create web3 user. Need to allow users without email in Username-Password-Authentication connection: ", - inspect(body) - ]) - - {:error, @misconfiguration_detected} - - error -> - handle_common_errors(error, "Failed to create web3 user") - end - end - - defp link_users(primary_user_id, secondary_identity_id, provider) do - with token when is_binary(token) <- get_m2m_jwt(), - client = OAuth.client(token: token), - body = %{ - provider: provider, - user_id: secondary_identity_id - }, - {:ok, %OAuth2.Response{status_code: 201}} <- - Client.post(client, "#{@users_path}/#{URI.encode(primary_user_id)}/identities", body, @json_content_type) do - :ok - else - error -> handle_common_errors(error, "Failed to link accounts") - end - end - - defp create_auth(user) do - conn_stub = %{private: %{auth0_user: user, auth0_token: nil}} - - %Auth{ - uid: user["user_id"], - provider: :auth0, - strategy: Auth0, - info: Auth0.info(conn_stub), - credentials: %Auth.Credentials{}, - extra: Auth0.extra(conn_stub) - } - end - - defp put_client_id_and_secret(map) do - auth0_config = Application.get_env(:ueberauth, OAuth) - - Map.merge( - map, - %{ - client_id: auth0_config[:client_id], - client_secret: auth0_config[:client_secret] - } - ) - end - - defp handle_common_errors(error, error_msg) do - case error do - nil -> - Logger.error("Failed to get M2M JWT") - {:error, @misconfiguration_detected} - - {:error, %OAuth2.Response{status_code: 403, body: %{"errorCode" => "insufficient_scope"} = body}} -> - Logger.error(["#{error_msg}. Insufficient scope: ", inspect(body)]) - {:error, @misconfiguration_detected} - - other -> - Logger.error(["#{error_msg}: ", inspect(other)]) - :error - end - end end diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex new file mode 100644 index 000000000000..bdf153be55b4 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex @@ -0,0 +1,663 @@ +defmodule Explorer.ThirdPartyIntegrations.Auth0.Internal do + @moduledoc """ + Module for internal usage, not supposed to be used directly, if + you want to interact with Auth0, use `Explorer.ThirdPartyIntegrations.Auth0`. + + Provides internal implementation for Auth0 authentication functionality. + + This module handles core Auth0 operations including user management, OTP verification, + web3 authentication, and user identity handling. It supports both legacy and migrated + Auth0 configurations, and implements various authentication flows such as email-based + OTP authentication and web3 wallet-based authentication. + + The module serves as the internal implementation layer for the Explorer application's + Auth0 integration, managing API interactions with Auth0 services while providing error + handling and logging capabilities. + """ + + require Logger + + alias Explorer.Account.Identity + alias Explorer.{Account, Helper, Repo} + alias Explorer.Chain.Address + alias Explorer.ThirdPartyIntegrations.Auth0 + alias Explorer.ThirdPartyIntegrations.Auth0.{Legacy, Migrated} + alias OAuth2.{AccessToken, Client} + alias Ueberauth.Strategy.Auth0.OAuth + + @misconfiguration_detected "Misconfiguration detected, please contact support." + @disabled_otp_error_description "Grant type 'http://auth0.com/oauth/grant-type/passwordless/otp' not allowed for the client." + @users_path "/api/v2/users" + @json_content_type [{"Content-type", "application/json"}] + + def users_path, do: @users_path + def json_content_type, do: @json_content_type + + @doc """ + Returns the Redis key prefix for Auth0-related cached data. + + Delegates to the appropriate Auth0 module (Legacy or Migrated) to provide the + correct Redis key prefix based on the current Auth0 configuration: + - Legacy: Returns "auth0" + - Migrated: Returns "auth0_migrated" + + ## Returns + - `String.t()`: The Redis key prefix to use for Auth0-related data + """ + @spec redis_key() :: String.t() + def redis_key do + auth0_module().redis_key() + end + + @doc """ + Searches for Auth0 users by email address. + + Encodes the email, creates a query using the appropriate Auth0 module + (Legacy or Migrated), and searches for users matching the query. + + ## Parameters + - `email`: The email address to search for + + ## Returns + - `{:ok, [map()]}`: List of user objects if successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec find_users_by_email(String.t()) :: {:ok, [map()]} | :error | {:error, String.t()} + def find_users_by_email(email) do + email + |> URI.encode() + |> auth0_module().find_users_by_email_query() + |> find_users("Failed to search user by email") + end + + @doc """ + Searches for Auth0 users based on a provided query string. + + Obtains an M2M JWT token, creates an OAuth client, and sends a request to the Auth0 + Users API with the provided query. Handles any errors through the common error + handling mechanism. + + ## Parameters + - `q`: The query string for searching users in Auth0 + - `error_message`: Custom error message for logging (defaults to "Failed to find users") + + ## Returns + - `{:ok, [map()]}`: List of user objects if successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec find_users(String.t(), String.t()) :: {:ok, [map()]} | :error | {:error, String.t()} + def find_users(q, error_message \\ "Failed to find users") do + with token when is_binary(token) <- Auth0.get_m2m_jwt(), + client = OAuth.client(token: token), + {:ok, %OAuth2.Response{status_code: 200, body: users}} when is_list(users) <- + Client.get(client, @users_path, [], params: %{"q" => q}) do + {:ok, users} + else + error -> handle_common_errors(error, error_message) + end + end + + @doc """ + Sends a one-time password (OTP) to the specified email address. + + Initiates the Auth0 passwordless authentication flow by requesting an OTP code be + sent to the user's email. Includes the client's IP address in the request for audit + and security purposes. + + ## Parameters + - `email`: The email address to which the OTP will be sent + - `ip`: The IP address of the client requesting the OTP + + ## Returns + - `:ok`: If the OTP was successfully sent + - `:error`: If there was an error sending the OTP + """ + @spec send_otp(String.t(), String.t()) :: :ok | :error + def send_otp(email, ip) do + client = OAuth.client() + + body = + %{ + email: email, + connection: :email, + send: :code + } + |> put_client_id_and_secret() + + headers = [{"auth0-forwarded-for", ip} | @json_content_type] + + case Client.post(client, "/passwordless/start", body, headers) do + {:ok, %OAuth2.Response{status_code: 200}} -> + :ok + + other -> + Logger.error("Error while sending otp: ", inspect(other)) + + :error + end + end + + @doc """ + Processes an existing Auth0 user and manages OTP authentication flow. + + Creates an auth structure from the user, finds the corresponding identity record, + and handles the identity by either sending an OTP or checking the resend interval. + + ## Parameters + - `user`: The Auth0 user map containing user information + - `email`: The email address of the user + - `ip`: The IP address of the client making the request + + ## Returns + - `:ok`: If OTP was successfully sent + - `:error`: If there was an error in the process + - `{:interval, integer()}`: If OTP was recently sent and the resend interval hasn't elapsed + """ + @spec handle_existing_user(map(), String.t(), String.t()) :: :ok | :error | {:interval, integer()} + def handle_existing_user(user, email, ip) do + user + |> create_auth() + |> Identity.find_identity() + |> handle_identity(email, ip) + end + + @doc """ + Validates a one-time password (OTP) for the specified email. + + Attempts to authenticate with Auth0 using the provided email and OTP code. Includes + the client's IP address for audit and security purposes. Handles various error + scenarios with specific error messages. + + ## Parameters + - `email`: The email address associated with the OTP + - `otp`: The one-time password code to validate + - `ip`: The IP address of the client submitting the OTP + + ## Returns + - `{:ok, AccessToken.t()}`: Access token if OTP validation succeeds + - `{:error, "Wrong verification code."}`: If the OTP is incorrect + - `{:error, "Max attempts reached. Please resend code."}`: If maximum attempt limit exceeded + - `{:error, String.t()}`: For other known errors with descriptive messages + - `:error`: For unspecified errors + """ + @spec confirm_otp(String.t(), String.t(), String.t()) :: {:ok, AccessToken.t()} | :error | {:error, String.t()} + def confirm_otp(email, otp, ip) do + client = OAuth.client() + + body = + email + |> auth0_module().confirm_otp_body(otp) + |> put_client_id_and_secret() + + headers = [{"auth0-forwarded-for", ip} | @json_content_type] + + case Client.post(client, "/oauth/token", body, headers) do + {:ok, %OAuth2.Response{status_code: 200, body: body}} -> + {:ok, AccessToken.new(body)} + + {:error, + %OAuth2.Response{ + status_code: 403, + body: + %{ + "error" => "unauthorized_client", + "error_description" => @disabled_otp_error_description, + "error_uri" => "https://auth0.com/docs/clients/client-grant-types" + } = body + }} -> + Logger.error("Need to enable OTP: #{inspect(body)}") + {:error, @misconfiguration_detected} + + {:error, + %OAuth2.Response{ + status_code: 403, + body: %{"error" => "invalid_grant", "error_description" => "Wrong email or verification code."} + }} -> + {:error, "Wrong verification code."} + + {:error, + %OAuth2.Response{ + status_code: 403, + body: %{ + "error" => "invalid_grant", + "error_description" => "You've reached the maximum number of attempts. Please try to login again." + } + }} -> + {:error, "Max attempts reached. Please resend code."} + + other -> + Logger.error("Error while confirming otp: #{inspect(other)}") + + :error + end + end + + @doc """ + Extracts user information from an Auth0 access token. + + Retrieves the user claims from the ID token within the access token. Handles two + scenarios: tokens with valid ID tokens, and tokens missing ID tokens. + + ## Parameters + - `token`: The OAuth2 AccessToken struct containing Auth0 authentication information + + ## Returns + - `{:ok, map()}`: User claims if successfully extracted from the token + - `:error`: If there was an error decoding the claims + - `{:error, String.t()}`: If the token doesn't contain an ID token + """ + @spec get_user_from_token(AccessToken.t()) :: {:ok, map()} | :error | {:error, String.t()} + def get_user_from_token(%AccessToken{other_params: %{"id_token" => token}}) do + case Joken.peek_claims(token) do + {:ok, %{"sub" => _} = user} -> + {:ok, user} + + error -> + Logger.error("Error while peeking claims from token: #{inspect(error)}") + :error + end + end + + def get_user_from_token(token) do + Logger.error("No id_token in token: #{inspect(Map.update(token, :access_token, "xxx", fn _ -> "xxx" end))}") + + {:error, @misconfiguration_detected} + end + + @doc """ + Links an email-based identity to a user who doesn't have an email. + + Delegates to the appropriate Auth0 module implementation (Legacy or Migrated) to + associate an email address with a user. The implementation varies depending on + the Auth0 configuration: + - Legacy: Links the users and updates the email for the user without email + - Migrated: Transfers metadata between users and updates both user records + + ## Parameters + - `user_id_without_email`: The ID of the user who doesn't have an email + - `user_id_with_email`: The ID of the user who has the email to be linked + - `email`: The email address to link + + ## Returns + - `{:ok, map()}`: User information if linking was successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec link_email(String.t(), String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def link_email(user_id_without_email, user_id_with_email, email) do + auth0_module().link_email(user_id_without_email, user_id_with_email, email) + end + + @doc """ + Creates a Ueberauth authentication structure from an Auth0 user. + + Delegates to the appropriate Auth0 module (Legacy or Migrated) to construct a + standardized Ueberauth.Auth struct from an Auth0 user object. The implementation + varies based on the Auth0 configuration: + - Legacy: Uses the user_id directly from the user object + - Migrated: Extracts the user_id from nested metadata within the user object + + ## Parameters + - `user`: Map containing Auth0 user information + + ## Returns + - `Ueberauth.Auth.t()`: A structured authentication object containing user information + """ + @spec create_auth(map()) :: Ueberauth.Auth.t() + def create_auth(user) do + auth0_module().create_auth(user) + end + + @doc """ + Retrieves Auth0 user information by user ID. + + Obtains an M2M JWT token, creates an OAuth client, and sends a request to the Auth0 + Users API with the specified user ID. Returns the user information or handles errors + appropriately. + + ## Parameters + - `id`: The Auth0 user ID to retrieve + + ## Returns + - `{:ok, map()}`: User information if found + - `{:error, "User not found"}`: If the user ID doesn't exist + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec get_user_by_id(String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def get_user_by_id(id) do + with token when is_binary(token) <- Auth0.get_m2m_jwt(), + client = OAuth.client(token: token), + {:ok, %OAuth2.Response{status_code: 200, body: %{"user_id" => ^id} = user}} <- + Client.get(client, "#{@users_path}/#{URI.encode(id)}") do + {:ok, user} + else + {:error, %OAuth2.Response{status_code: 404}} -> {:error, "User not found"} + error -> handle_common_errors(error, "Failed to get user by id") + end + end + + @doc """ + Processes an email-based Auth0 user account. + + Delegates to the appropriate Auth0 module based on configuration. The behavior + differs between implementations: + - Legacy: Attempts to link the email user with existing accounts having the same + email address, potentially merging multiple accounts + - Migrated: Updates the user with application-specific metadata if not already + present + + ## Parameters + - `user`: Map containing Auth0 user information + + ## Returns + - `{:ok, map()}`: User information after processing + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec process_email_user(map()) :: {:ok, map()} | :error | {:error, String.t()} + def process_email_user(user) do + auth0_module().process_email_user(user) + end + + @spec handle_not_found_just_created_email_user(map()) :: {:ok, Ueberauth.Auth.t()} + def handle_not_found_just_created_email_user(user_from_token) do + auth0_module().handle_not_found_just_created_email_user(user_from_token) + end + + @doc """ + Ensures a session contains an address hash value. + + For sessions that already have an address hash, returns the unchanged session. + For sessions without an address hash but with a user ID, retrieves the user from + Auth0 and adds the address hash to the session. Uses different retrieval methods + based on Auth0 module configuration: + - Legacy: Retrieves the user directly by ID + - Migrated: Finds the user by querying metadata for the application-specific user ID + + ## Parameters + - `session`: Map containing session information + + ## Returns + - `{:old, map()}`: The original session if it already contains an address hash or if + retrieval fails + - `{:new, map()}`: Updated session with the address hash if retrieval succeeds + """ + @spec update_session_with_address_hash(map()) :: + {:new, %{:address_hash => nil | binary(), optional(any()) => any()}} + | {:old, map()} + def update_session_with_address_hash(%{address_hash: _} = session), do: {:old, session} + + def update_session_with_address_hash(%{uid: user_id} = session) do + case auth0_module().get_user_by_id_from_session(user_id) do + {:ok, user} -> + {:new, Map.put(session, :address_hash, user |> create_auth() |> Identity.address_hash_from_auth())} + + error -> + Logger.error("Error when updating session with address hash: #{inspect(error)}") + {:old, session} + end + end + + @doc """ + Caches a nonce value for Sign-In with Ethereum (SIWE) authentication. + + Stores the provided nonce in Redis with an expiration time of 300 seconds (5 minutes), + using a key derived from the wallet address. This cached nonce is used for the SIWE + authentication flow to prevent replay attacks. + + ## Parameters + - `nonce`: The random nonce value to cache + - `address`: The Ethereum wallet address associated with the nonce + + ## Returns + - `{:ok, nonce}`: If the nonce was successfully cached + - `{:error, reason}`: If there was an error caching the nonce + """ + @spec cache_nonce_for_address(nonce, String.t()) :: + {:ok, nonce} | {:error, atom() | Redix.Error.t() | Redix.ConnectionError.t()} + when nonce: String.t() + def cache_nonce_for_address(nonce, address) do + case Redix.command(:redix, ["SET", Auth0.cookie_key(address <> "siwe_nonce"), nonce, "EX", 300]) do + {:ok, _} -> {:ok, nonce} + error -> error + end + end + + def get_nonce_for_address(address_hash) do + cookie_key = Auth0.cookie_key(Address.checksum(address_hash) <> "siwe_nonce") + + with {:get, {:ok, nonce}} <- {:get, Redix.command(:redix, ["GET", cookie_key])}, + {:del, {:ok, _}} <- {:del, Redix.command(:redix, ["DEL", cookie_key])} do + {:ok, nonce} + else + _ -> {:error, "Redis configuration problem, please contact support."} + end + end + + @doc """ + Searches for Auth0 users associated with a specific web3 wallet address. + + Constructs a query using the appropriate Auth0 module implementation (Legacy or + Migrated) and searches for users matching the query. The search criteria differ + between implementations: + - Legacy: Searches across multiple fields including user_id and metadata + - Migrated: Searches specifically in the application-scoped metadata + + ## Parameters + - `address`: The web3 wallet address to search for + + ## Returns + - `{:ok, [map()]}`: List of user objects if successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec find_users_by_web3_address(String.t()) :: {:ok, [map()]} | :error | {:error, String.t()} + def find_users_by_web3_address(address) do + address + |> auth0_module().find_users_by_web3_address_query() + |> find_users("Failed to find users by address") + end + + @doc """ + Associates a web3 wallet address with an Auth0 user. + + Delegates to the appropriate Auth0 module implementation (Legacy or Migrated) to + update the user's metadata with their web3 wallet address. The implementation + differs based on Auth0 configuration: + - Legacy: Directly updates the user's metadata with the address + - Migrated: First retrieves the user by session ID, then updates the application-scoped + metadata with the address + + ## Parameters + - `user_id`: The Auth0 user ID to update + - `address`: The web3 wallet address to associate with the user + + ## Returns + - `{:ok, map()}`: Updated user information if successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec update_user_with_web3_address(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def update_user_with_web3_address(user_id, address) do + auth0_module().update_user_with_web3_address(user_id, address) + end + + @doc """ + Updates an Auth0 user's information. + + Obtains an M2M JWT token, creates an OAuth client, and sends a PATCH request to the + Auth0 Users API to update the specified user with the provided data. Handles any + errors through the common error handling mechanism. + + ## Parameters + - `user_id`: The Auth0 user ID to update + - `body`: The data to update (usually a map that will be JSON-encoded) + - `error_message`: Custom error message for logging (defaults to "Failed to update user") + + ## Returns + - `{:ok, map()}`: Updated user information if successful + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec update_user(String.t(), map(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def update_user(user_id, body, error_message \\ "Failed to update user") do + with token when is_binary(token) <- Auth0.get_m2m_jwt(), + client = OAuth.client(token: token), + {:ok, %OAuth2.Response{status_code: 200, body: user}} <- + Client.patch(client, "#{@users_path}/#{URI.encode(user_id)}", body, @json_content_type) do + {:ok, user} + else + error -> handle_common_errors(error, error_message) + end + end + + @doc """ + Processes authentication for a web3 wallet address. + + Delegates to the appropriate Auth0 module (Legacy or Migrated) to find an existing + user with the provided address or create a new one. The implementation differs based + on Auth0 configuration: + - Legacy: Finds existing users by address, updates if found without address metadata, + creates a new user if none found, or merges multiple matching users + - Migrated: Finds one user, creates a new user with application-scoped metadata if none + found, or returns an error if multiple users are found + + ## Parameters + - `address`: The web3 wallet address for authentication + - `signature`: The cryptographic signature for authentication + + ## Returns + - `{:ok, map()}`: User information if processing succeeds + - `:error`: If an unspecified error occurs + - `{:error, String.t()}`: If a known error occurs with error message + """ + @spec process_web3_user(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def process_web3_user(address, signature) do + auth0_module().find_or_create_web3_user(address, signature) + end + + @doc """ + Creates a new Auth0 user with web3 wallet credentials. + + Obtains an M2M JWT token, creates an OAuth client, and sends a request to the Auth0 + Users API to create a new user with the web3 wallet address as username and the + signature as password. The user is created without an email address, which requires + specific Auth0 connection configuration. + + ## Parameters + - `address`: The web3 wallet address to use as username + - `signature`: The cryptographic signature to use as password + - `metadata`: Additional metadata to store with the user + + ## Returns + - `{:ok, map()}`: Created user information if successful + - `{:error, String.t()}`: If a configuration issue is detected + - `:error`: If an unspecified error occurs + """ + @spec create_web3_user(String.t(), String.t(), map()) :: {:ok, map()} | :error | {:error, String.t()} + def create_web3_user(address, signature, metadata) do + with token when is_binary(token) <- Auth0.get_m2m_jwt(), + client = OAuth.client(token: token), + body = %{ + username: address, + password: signature, + email_verified: true, + connection: "Username-Password-Authentication", + user_metadata: metadata + }, + {:ok, %OAuth2.Response{status_code: 201, body: user}} <- + Client.post(client, @users_path, body, @json_content_type) do + {:ok, user} + else + {:error, + %OAuth2.Response{ + status_code: 400, + body: + %{ + "errorCode" => "invalid_body", + "message" => "Payload validation error: 'Missing required property: email'." + } = body + }} -> + Logger.error([ + "Failed to create web3 user. Need to allow users without email in Username-Password-Authentication connection: ", + inspect(body) + ]) + + {:error, @misconfiguration_detected} + + error -> + handle_common_errors(error, "Failed to create web3 user") + end + end + + @doc """ + Standardizes error handling for Auth0 API requests. + + Processes common error patterns from Auth0 API responses, logs appropriate error + messages, and returns standardized error formats. Specifically handles missing JWT + tokens and insufficient scope errors with user-friendly messages. + + ## Parameters + - `error`: The error value or structure to handle + - `error_msg`: A descriptive message for logging that explains the context of the error + + ## Returns + - `{:error, String.t()}`: For known error types with a user-friendly message + - `:error`: For unspecified errors + """ + @spec handle_common_errors(any(), String.t()) :: :error | {:error, String.t()} + def handle_common_errors(error, error_msg) do + case error do + nil -> + Logger.error("Failed to get M2M JWT") + {:error, @misconfiguration_detected} + + {:error, %OAuth2.Response{status_code: 403, body: %{"errorCode" => "insufficient_scope"} = body}} -> + Logger.error(["#{error_msg}. Insufficient scope: ", inspect(body)]) + {:error, @misconfiguration_detected} + + other -> + Logger.error(["#{error_msg}: ", inspect(other)]) + :error + end + end + + defp auth0_module do + if Application.get_env(:ueberauth, OAuth)[:auth0_application_id] === "" do + Legacy + else + Migrated + end + end + + defp put_client_id_and_secret(map) do + auth0_config = Application.get_env(:ueberauth, OAuth) + + Map.merge( + map, + %{ + client_id: auth0_config[:client_id], + client_secret: auth0_config[:client_secret] + } + ) + end + + defp handle_identity(nil, email, ip), do: send_otp(email, ip) + + defp handle_identity(%Identity{otp_sent_at: otp_sent_at} = identity, email, ip) do + otp_resend_interval = Application.get_env(:explorer, Account)[:otp_resend_interval] + + case Helper.check_time_interval(otp_sent_at, otp_resend_interval) do + true -> + identity |> Identity.changeset(%{otp_sent_at: DateTime.utc_now()}) |> Repo.account_repo().update() + + send_otp(email, ip) + + interval -> + {:interval, interval} + end + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex new file mode 100644 index 000000000000..b01e9abdde82 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex @@ -0,0 +1,246 @@ +defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do + @moduledoc """ + Module for internal usage, not supposed to be used directly, if + you want to interact with Auth0, use `Explorer.ThirdPartyIntegrations.Auth0`. + + Provides Auth0 authentication for legacy Auth0 configuration. + + This module implements Auth0 authentication functionality for the legacy Auth0 + configuration where the application identifier is not set. It handles user + management operations including email and web3 authentication flows, user + linking, and identity merging. + + The module specializes in managing Auth0 users with different identity + providers, linking multiple identities to a single user, and handling user + metadata for both email and web3-based authentication. + """ + + require Logger + + alias Explorer.Account + alias Explorer.Account.Identity + alias Explorer.ThirdPartyIntegrations.Auth0 + alias Explorer.ThirdPartyIntegrations.Auth0.Internal + alias OAuth2.Client + alias Ueberauth.Auth + alias Ueberauth.Strategy.Auth0, as: UeberauthAuth0 + alias Ueberauth.Strategy.Auth0.OAuth + + @spec redis_key() :: String.t() + def redis_key do + client_id = Application.get_env(:ueberauth, OAuth)[:client_id] + + client_id <> "auth0:legacy" + end + + @spec find_users_by_email_query(String.t()) :: String.t() + def find_users_by_email_query(encoded_email) do + ~s(email:"#{encoded_email}" OR user_metadata.email:"#{encoded_email}") + end + + @spec link_email(String.t(), String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def link_email(user_id_without_email, user_id_with_email, email) do + with :ok <- link_users(user_id_without_email, user_id_with_email, "email") do + update_user_email(user_id_without_email, email) + end + end + + @spec create_auth(map()) :: Auth.t() + def create_auth(user) do + conn_stub = %{private: %{auth0_user: user, auth0_token: nil}} + + %Auth{ + uid: user["user_id"], + provider: :auth0, + strategy: UeberauthAuth0, + info: UeberauthAuth0.info(conn_stub), + credentials: %Auth.Credentials{}, + extra: UeberauthAuth0.extra(conn_stub) + } + end + + @spec confirm_otp_body(String.t(), String.t()) :: map() + def confirm_otp_body(email, otp) do + %{ + username: email, + otp: otp, + realm: :email, + grant_type: :"http://auth0.com/oauth/grant-type/passwordless/otp" + } + end + + @spec process_email_user(map()) :: {:ok, map()} | :error | {:error, String.t()} + def process_email_user(user) do + maybe_link_email(user) + end + + @spec handle_not_found_just_created_email_user(map()) :: {:ok, Auth.t()} + def handle_not_found_just_created_email_user(%{"sub" => user_id} = user_from_token) do + {:ok, user_from_token |> Map.put("user_id", user_id) |> Internal.create_auth()} + end + + @spec get_user_by_id_from_session(String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def get_user_by_id_from_session(user_id) do + Internal.get_user_by_id(user_id) + end + + @spec find_users_by_web3_address_query(String.t()) :: String.t() + def find_users_by_web3_address_query(address) do + ~s|user_id:*siwe*#{address} OR user_id:*Passkey*#{address} OR user_metadata.web3_address_hash:"#{address}" OR (user_id:*Passkey* AND nickname:"#{address}")| + end + + @spec update_user_with_web3_address(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def update_user_with_web3_address(user_id, address) do + Internal.update_user( + user_id, + %{"user_metadata" => %{"web3_address_hash" => address}}, + "Failed to update user address" + ) + end + + @spec find_or_create_web3_user(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def find_or_create_web3_user(address, signature) do + case Internal.find_users_by_web3_address(address) do + {:ok, [%{"user_metadata" => %{"web3_address_hash" => ^address}} = user]} -> + {:ok, user} + + {:ok, [%{"user_id" => user_id}]} -> + update_user_with_web3_address(user_id, address) + + {:ok, []} -> + Internal.create_web3_user(address, signature, %{web3_address_hash: address}) + + {:ok, users} when is_list(users) and length(users) > 1 -> + merge_web3_users(users) + + other -> + other + end + end + + defp link_users(primary_user_id, secondary_identity_id, provider) do + with token when is_binary(token) <- Auth0.get_m2m_jwt(), + client = OAuth.client(token: token), + body = %{ + provider: provider, + user_id: secondary_identity_id + }, + {:ok, %OAuth2.Response{status_code: 201}} <- + Client.post( + client, + "#{Internal.users_path()}/#{URI.encode(primary_user_id)}/identities", + body, + Internal.json_content_type() + ) do + :ok + else + error -> Internal.handle_common_errors(error, "Failed to link accounts") + end + end + + defp update_user_email(user_id, email) do + Internal.update_user(user_id, %{"user_metadata" => %{"email" => email}}, "Failed to update user email") + end + + defp maybe_link_email(%{"email" => email, "user_id" => "email|" <> identity_id = user_id} = user) do + case Internal.find_users( + ~s(email:"#{URI.encode(email)}" AND NOT user_id:"#{URI.encode(user_id)}"), + "Failed to find legacy users by email" + ) do + {:ok, []} -> + {:ok, user} + + {:ok, [%{"user_id" => primary_user_id} = user]} -> + link_users(primary_user_id, identity_id, "email") + maybe_verify_email(user) + {:ok, user} + + {:ok, users} -> + merge_email_users(users, identity_id, "email") + + error -> + error + end + end + + defp maybe_link_email(user) do + {:ok, user} + end + + defp maybe_verify_email(%{"email_verified" => false, "user_id" => user_id}) do + with {:ok, _} <- + Internal.update_user(user_id, %{"email_verified" => true}, "Failed to patch email_verified to true") do + :ok + end + end + + defp maybe_verify_email(_), do: :ok + + defp merge_email_users([primary_user | _] = users, identity_id_to_link, provider_for_linking) do + identity_map = + users + |> Enum.map(& &1["user_id"]) + |> Identity.find_identities() + |> Map.new(&{&1.uid, &1}) + + users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() + + case users |> Enum.map(&identity_map[&1["user_id"]]) |> Enum.reject(&is_nil(&1)) |> Account.merge() do + {{:ok, 0}, nil} -> + link_users(primary_user["user_id"], identity_id_to_link, provider_for_linking) + maybe_verify_email(primary_user) + {:ok, primary_user} + + {{:ok, _}, primary_identity} -> + link_users(primary_identity.uid, identity_id_to_link, provider_for_linking) + maybe_verify_email(users_map[primary_identity.uid]) + {:ok, users_map[primary_identity.uid]} + + error -> + Logger.error(["Error while merging users with the same email: ", inspect(error)]) + :error + end + end + + defp merge_web3_users([primary_user | _] = users) do + identity_map = + users + |> Enum.map(& &1["user_id"]) + |> Identity.find_identities() + |> Map.new(&{&1.uid, &1}) + + users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() + + users + |> Enum.map(&identity_map[&1["user_id"]]) + |> Enum.reject(&is_nil(&1)) + |> Account.merge() + |> case do + {{:ok, 0}, nil} -> + unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user) do + update_user_with_web3_address( + primary_user["user_id"], + primary_user |> Internal.create_auth() |> Identity.address_hash_from_auth() + ) + end + + {:ok, primary_user} + + {{:ok, _}, primary_identity} -> + primary_user_from_db = users_map[primary_identity.uid] + + unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user_from_db) do + update_user_with_web3_address( + primary_user_from_db["user_id"], + primary_user_from_db |> Internal.create_auth() |> Identity.address_hash_from_auth() + ) + end + + {:ok, primary_user_from_db} + + error -> + Logger.error(["Error while merging users with the same address: ", inspect(error)]) + :error + end + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex new file mode 100644 index 000000000000..a54fb61a4880 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex @@ -0,0 +1,190 @@ +defmodule Explorer.ThirdPartyIntegrations.Auth0.Migrated do + @moduledoc """ + Module for internal usage, not supposed to be used directly, if + you want to interact with Auth0, use `Explorer.ThirdPartyIntegrations.Auth0`. + + Provides Auth0 authentication for migrated Auth0 configuration. + + This module implements Auth0 authentication functionality for the migrated Auth0 + configuration where the application identifier is set. It handles user management + within application-scoped metadata, providing namespaced storage of user + attributes. + + The module supports both email and web3 authentication flows, focusing on + managing user identity within an application-specific context. It uses nested + metadata structures to prevent conflicts between different applications using + the same Auth0 tenant. + """ + + use Utils.RuntimeEnvHelper, + auth0_application_identifier: [:ueberauth, Ueberauth.Strategy.Auth0.OAuth, :auth0_application_id] + + alias Explorer.ThirdPartyIntegrations.Auth0.Internal + alias Ueberauth.Auth + alias Ueberauth.Strategy.Auth0, as: UeberauthAuth0 + + @spec redis_key() :: String.t() + def redis_key do + client_id = Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id] + + client_id <> "auth0:migrated" + end + + @spec find_users_by_email_query(String.t()) :: String.t() + def find_users_by_email_query(encoded_email) do + ~s(email:"#{encoded_email}") + end + + @spec link_email(String.t(), String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def link_email(session_user_id_without_email, user_id_with_email, _email) do + auth0_application_id = auth0_application_identifier() + + with {:ok, + %{ + "user_id" => user_id_without_email, + "user_metadata" => %{^auth0_application_id => user_without_email_metadata} + }} <- + get_user_by_id_from_session(session_user_id_without_email), + {:ok, user_with_email} <- + Internal.update_user( + user_id_with_email, + user_without_email_metadata, + "Failed to link email on updating user with email metadata step" + ), + {:ok, _} <- + Internal.update_user( + user_id_without_email, + %{"user_metadata" => %{auth0_application_id => %{}}}, + "Failed to link email on updating user without email metadata" + ) do + {:ok, user_with_email} + end + end + + @spec create_auth(map()) :: Auth.t() + def create_auth(user) do + uid = user["user_metadata"][auth0_application_identifier()]["user_id"] + user = Map.update(user, "user_metadata", nil, fn user_metadata -> user_metadata[auth0_application_identifier()] end) + conn_stub = %{private: %{auth0_user: user, auth0_token: nil}} + + %Auth{ + uid: uid, + provider: :auth0, + strategy: UeberauthAuth0, + info: UeberauthAuth0.info(conn_stub), + credentials: %Auth.Credentials{}, + extra: UeberauthAuth0.extra(conn_stub) + } + end + + @spec confirm_otp_body(String.t(), String.t()) :: map() + def confirm_otp_body(email, otp) do + %{ + username: email, + otp: otp, + realm: :email, + grant_type: :"http://auth0.com/oauth/grant-type/passwordless/otp", + chain_slug: auth0_application_identifier() + } + end + + @spec process_email_user(map()) :: {:ok, map()} | :error | {:error, String.t()} + def process_email_user(user) do + if user["user_metadata"][auth0_application_identifier()] do + {:ok, user} + else + Internal.update_user( + user["user_id"], + %{ + "user_metadata" => %{ + auth0_application_identifier() => Map.take(user, ["user_id", "name", "nickname", "picture"]) + } + }, + "Failed to update email user metadata" + ) + end + end + + @spec handle_not_found_just_created_email_user(map()) :: {:ok, Auth.t()} | :error | {:error, String.t()} + def handle_not_found_just_created_email_user(%{"sub" => user_id} = user_from_token) do + with {:ok, user} <- + Internal.update_user( + user_id, + %{ + "user_metadata" => %{ + auth0_application_identifier() => Map.take(user_from_token, ["sub", "name", "nickname", "picture"]) + } + }, + "Failed to update just created email user metadata" + ) do + {:ok, create_auth(user)} + end + end + + @spec get_user_by_id_from_session(String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def get_user_by_id_from_session(user_id) do + case Internal.find_users( + ~s(user_metadata.#{auth0_application_identifier()}.user_id:"#{user_id}"), + "Failed to find user by id from session" + ) do + {:ok, [user]} -> {:ok, user} + {:ok, []} -> {:error, "User with id from session not found"} + {:ok, _} -> {:error, "Multiple users with the same id in metadata found"} + error -> error + end + end + + @spec find_users_by_web3_address_query(String.t()) :: String.t() + def find_users_by_web3_address_query(address) do + ~s(user_metadata.#{auth0_application_identifier()}.web3_address_hash:"#{address}") + end + + @spec update_user_with_web3_address(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def update_user_with_web3_address(session_user_id, address) do + auth0_application_id = auth0_application_identifier() + + with {:ok, %{"user_id" => user_id, "user_metadata" => %{^auth0_application_id => user_metadata}}} <- + get_user_by_id_from_session(session_user_id) do + Internal.update_user( + user_id, + %{"user_metadata" => %{auth0_application_id => Map.put(user_metadata, "web3_address_hash", address)}}, + "Failed to update user with web3 address" + ) + end + end + + @spec find_or_create_web3_user(String.t(), String.t()) :: {:ok, map()} | :error | {:error, String.t()} + def find_or_create_web3_user(address, signature) do + case Internal.find_users_by_web3_address(address) do + {:ok, [user]} -> + {:ok, user} + + {:ok, []} -> + create_web3_user(address, signature) + + {:ok, _} -> + {:error, "Multiple users with the same web3 address found"} + + error -> + error + end + end + + defp create_web3_user(address, signature) do + with {:ok, %{"user_id" => user_id} = user} <- Internal.create_web3_user(address, signature, %{}) do + Internal.update_user( + user_id, + %{ + "user_metadata" => %{ + auth0_application_identifier() => + Map.merge( + %{"web3_address_hash" => address}, + Map.take(user, ["user_id", "name", "nickname", "picture"]) + ) + } + }, + "Failed to update user with web3 address" + ) + end + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 2af07ea3a79c..94bf9eb05650 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -182,7 +182,8 @@ config :block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpr config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"), client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"), - client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET") + client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET"), + auth0_application_id: ConfigHelper.safe_get_env("ACCOUNT_AUTH0_APPLICATION_ID", nil) |> String.replace(".", "") # Configures Ueberauth local settings config :ueberauth, Ueberauth, logout_url: "https://#{System.get_env("ACCOUNT_AUTH0_DOMAIN")}/v2/logout" diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 09e6a7e6b59d..f33ee2f91dd7 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -429,6 +429,7 @@ DECODE_NOT_A_CONTRACT_CALLS=true # ACCOUNT_AUTH0_DOMAIN= # ACCOUNT_AUTH0_CLIENT_ID= # ACCOUNT_AUTH0_CLIENT_SECRET= +# ACCOUNT_AUTH0_APPLICATION_ID= # ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL= # ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY= # ACCOUNT_SENDGRID_API_KEY= From cf884c6c86e6595cae7c84602d659a3660f74e44 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:58:44 +0400 Subject: [PATCH 0106/1006] chore: Drop address_coin_balances value_fetched_at index (#12598) --- ...drop_address_coin_balances_value_fetched_at_index.exs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 apps/explorer/priv/repo/migrations/20250613144606_drop_address_coin_balances_value_fetched_at_index.exs diff --git a/apps/explorer/priv/repo/migrations/20250613144606_drop_address_coin_balances_value_fetched_at_index.exs b/apps/explorer/priv/repo/migrations/20250613144606_drop_address_coin_balances_value_fetched_at_index.exs new file mode 100644 index 000000000000..615cf3ffcb9c --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250613144606_drop_address_coin_balances_value_fetched_at_index.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.DropAddressCoinBalancesValueFetchedAtIndex do + use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + + def change do + drop_if_exists(index(:address_coin_balances, [:value_fetched_at], concurrently: true)) + end +end From d39b9aeb6a7cb70881f2a812b99783d8bccb164a Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 30 Jun 2025 16:15:07 +0300 Subject: [PATCH 0107/1006] fix: add missing fields to Celo Epochs-related endpoints (#12589) * fix: add missing fields to Celo Epochs-related endpoints * fix: handle nil values in transfers total * fix: overflow for epoch number * refactor: return all epochs (including unfetched ones) in `/api/v2/celo/epochs` * fix: error when community transfer is absent * fix: update type specification for `apply_sorting/3` to fix dialyzer * fix: total transfers rendering * fix: credo --- .../controllers/api/v2/celo_controller.ex | 4 +- .../block_scout_web/views/api/v2/celo_view.ex | 201 ++++++++++-------- .../explorer/lib/explorer/chain/celo/epoch.ex | 5 +- .../lib/explorer/chain/celo/helper.ex | 7 +- apps/explorer/lib/explorer/sorting_helper.ex | 2 +- 5 files changed, 126 insertions(+), 93 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index aa567feb12d4..cbf7e152de16 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -42,7 +42,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do {epochs, next_page} = options - |> Epoch.fetched_epochs() + |> Epoch.all() |> split_list_by_page() filtered_params = @@ -181,7 +181,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do {:ok, non_neg_integer()} | {:error, {:invalid, :number}} defp parse_epoch_number(number) do case safe_parse_non_negative_integer(number) do - {:ok, epoch_number} -> {:ok, epoch_number} + {:ok, epoch_number} when epoch_number < 32_768 -> {:ok, epoch_number} _ -> {:error, {:invalid, :number}} end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index 3dcde14044ff..ee4841e18747 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do alias Explorer.Chain.Cache.{CeloCoreContracts, CeloEpochs} alias Explorer.Chain.Celo.Helper, as: CeloHelper alias Explorer.Chain.Celo.{Epoch, EpochReward} - alias Explorer.Chain.{Block, Token, Transaction, Wei} + alias Explorer.Chain.{Block, Token, TokenTransfer, Transaction, Wei} @address_params [ necessity_by_association: %{ @@ -43,40 +43,22 @@ defmodule BlockScoutWeb.API.V2.CeloView do |> prepare_distribution() aggregated_election_rewards_json = - aggregated_election_rewards - |> Map.new(fn {type, %{total: total, count: count, token: token}} -> - {type, - %{ - total: total, - count: count, - token: - TokenView.render("token.json", %{ - token: token, - contract_address_hash: token && token.contract_address_hash - }) - }} - end) - # For L2, delegated payments are implemented differently. They're - # distributed on-demand via direct payments rather than through epoch - # processing, so we need to handle them separately. - |> (&(if CeloHelper.premigration_epoch_number?(epoch.number) do - &1 - else - &1 - |> Map.put(:delegated_payment, nil) - end)).() + epoch + |> prepare_aggregated_election_rewards(aggregated_election_rewards) %{ number: epoch.number, + type: epoch_type(epoch), + is_finalized: epoch.fetched?, start_block_number: epoch.start_block_number, end_block_number: epoch.end_block_number, - timestamp: epoch.end_processing_block.timestamp, - start_processing_block_hash: epoch.start_processing_block.hash, - start_processing_block_number: epoch.start_processing_block.number, - end_processing_block_hash: epoch.end_processing_block.hash, - end_processing_block_number: epoch.end_processing_block.number, distribution: distribution_json, - aggregated_election_rewards: aggregated_election_rewards_json + aggregated_election_rewards: aggregated_election_rewards_json, + timestamp: epoch.end_processing_block && epoch.end_processing_block.timestamp, + start_processing_block_hash: epoch.start_processing_block && epoch.start_processing_block.hash, + start_processing_block_number: epoch.start_processing_block && epoch.start_processing_block.number, + end_processing_block_hash: epoch.end_processing_block && epoch.end_processing_block.hash, + end_processing_block_number: epoch.end_processing_block && epoch.end_processing_block.number } end @@ -167,6 +149,41 @@ defmodule BlockScoutWeb.API.V2.CeloView do } end + defp prepare_aggregated_election_rewards(%Epoch{fetched?: false}, _), do: nil + + defp prepare_aggregated_election_rewards(%Epoch{} = epoch, aggregated_election_rewards) do + aggregated_election_rewards + |> Map.new(fn {type, %{total: total, count: count, token: token}} -> + {type, + %{ + total: total, + count: count, + token: + TokenView.render("token.json", %{ + token: token, + contract_address_hash: token && token.contract_address_hash + }) + }} + end) + # For L2, delegated payments are implemented differently. They're + # distributed on-demand via direct payments rather than through epoch + # processing, so we need to handle them separately. + |> then(fn rewards -> + if CeloHelper.pre_migration_epoch_number?(epoch.number) do + rewards + else + rewards + |> Map.put(:delegated_payment, nil) + end + end) + end + + defp epoch_type(epoch) do + epoch.number + |> CeloHelper.pre_migration_epoch_number?() + |> if(do: "L1", else: "L2") + end + @doc """ Extends the JSON output with a sub-map containing information related to Celo, such as the epoch number, whether the block is an epoch block, and the routing @@ -249,29 +266,41 @@ defmodule BlockScoutWeb.API.V2.CeloView do @spec prepare_epoch(Epoch.t()) :: map() defp prepare_epoch(epoch) do - community_transfer = - epoch.distribution.community_transfer |> TokenTransferView.prepare_token_transfer_total() + distribution_json = + if epoch.distribution do + community_transfer = + epoch.distribution.community_transfer && + epoch.distribution.community_transfer + |> TokenTransferView.prepare_token_transfer_total() + + carbon_offsetting_transfer = + epoch.distribution.carbon_offsetting_transfer && + epoch.distribution.carbon_offsetting_transfer + |> TokenTransferView.prepare_token_transfer_total() + + reserve_bolster_transfer = + epoch.distribution.reserve_bolster_transfer && + epoch.distribution.reserve_bolster_transfer + |> TokenTransferView.prepare_token_transfer_total() - carbon_offsetting_transfer = - epoch.distribution.carbon_offsetting_transfer |> TokenTransferView.prepare_token_transfer_total() + result = calculate_total_epoch_rewards(epoch.distribution) + + %{ + community_transfer: community_transfer, + carbon_offsetting_transfer: carbon_offsetting_transfer, + reserve_bolster_transfer: reserve_bolster_transfer, + transfers_total: result && result.total + } + end %{ number: epoch.number, + type: epoch_type(epoch), start_block_number: epoch.start_block_number, end_block_number: epoch.end_block_number, - timestamp: epoch.end_processing_block.timestamp, - distribution: %{ - community_transfer: community_transfer, - carbon_offsetting_transfer: carbon_offsetting_transfer, - transfers_total: %{ - decimals: "18", - value: - Decimal.add( - community_transfer["value"], - carbon_offsetting_transfer["value"] - ) - } - } + timestamp: epoch.end_processing_block && epoch.end_processing_block.timestamp, + is_finalized: epoch.fetched?, + distribution: distribution_json } end @@ -302,7 +331,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do end ) - total = calculate_total_epoch_rewards(transfers_json) + total = calculate_total_epoch_rewards(distribution) transfers_json |> Map.put(:transfers_total, total) @@ -339,10 +368,13 @@ defmodule BlockScoutWeb.API.V2.CeloView do } """ @spec calculate_total_epoch_rewards(map()) :: map() | nil - def calculate_total_epoch_rewards(transfers) do + def calculate_total_epoch_rewards(distribution) do transfers = - transfers - |> Map.values() + [ + distribution.reserve_bolster_transfer, + distribution.community_transfer, + distribution.carbon_offsetting_transfer + ] |> Enum.reject(&is_nil/1) case transfers do @@ -350,46 +382,47 @@ defmodule BlockScoutWeb.API.V2.CeloView do nil [first_transfer | rest_transfers] -> - token_info = validate_and_extract_token(first_transfer, rest_transfers) - - total_value = - transfers - |> Enum.map(fn transfer -> - get_in(transfer, ["total", "value"]) || Decimal.new(0) - end) - |> Enum.reduce(Decimal.new(0), &Decimal.add/2) + case validate_and_extract_token(first_transfer, rest_transfers) do + {:ok, token} -> + total_value = + transfers + |> Enum.map(&(&1 |> TokenTransferView.prepare_token_transfer_total() |> Map.get("value"))) + |> Enum.reduce(Decimal.new(0), &Decimal.add/2) + + token_json = + TokenView.render("token.json", %{ + token: token, + contract_address_hash: token.contract_address_hash + }) + + %{ + token: token_json, + total: %{ + decimals: token.decimals, + value: total_value + } + } - %{ - token: token_info, - total: %{ - decimals: token_info["decimals"], - value: total_value - } - } + :error -> + raise ArgumentError, + "All transfers must use the same token, but found different tokens: #{inspect(transfers)}" + end end end - @spec validate_and_extract_token(map(), [map()]) :: map() + @spec validate_and_extract_token(TokenTransfer.t(), [TokenTransfer.t()]) :: + {:ok, Token.t()} | :error defp validate_and_extract_token(first_transfer, rest_transfers) do - first_token = get_in(first_transfer, ["token"]) - first_token_address = get_in(first_token, ["address"]) - - if is_nil(first_token_address) do - raise ArgumentError, "Token transfer missing token address information" + with token when not is_nil(token) <- first_transfer.token, + true <- + Enum.all?( + rest_transfers, + &(&1.token && &1.token.contract_address_hash == token.contract_address_hash) + ) do + {:ok, token} + else + _ -> :error end - - # Validate all transfers use the same token - Enum.each(rest_transfers, fn transfer -> - token_address = get_in(transfer, ["token", "address"]) - - if token_address != first_token_address do - raise ArgumentError, - "Inconsistent token addresses found in epoch rewards. " <> - "Expected: #{first_token_address}, Found: #{token_address}" - end - end) - - first_token end # Get the breakdown of the base fee for the case when FeeHandler is a contract diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex index 43542b2cd789..3089581a30b0 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -133,8 +133,8 @@ defmodule Explorer.Chain.Celo.Epoch do iex> Explorer.Chain.Celo.Epoch.fetched_epochs(sorting: [asc: :number], paging_options: %{page_size: 10}) [%Explorer.Chain.Celo.Epoch{number: 1, fetched?: true, ...}, ...] """ - @spec fetched_epochs(Keyword.t()) :: [__MODULE__.t()] - def fetched_epochs(options) do + @spec all(Keyword.t()) :: [__MODULE__.t()] + def all(options) do default_sorting = [desc: :number] paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -142,7 +142,6 @@ defmodule Explorer.Chain.Celo.Epoch do sorting_options = Keyword.get(options, :sorting, []) __MODULE__ - |> where(fetched?: true) |> SortingHelper.apply_sorting(sorting_options, default_sorting) |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) |> Chain.join_associations(necessity_by_association) diff --git a/apps/explorer/lib/explorer/chain/celo/helper.ex b/apps/explorer/lib/explorer/chain/celo/helper.ex index b2b92f69aa7a..67327ddff147 100644 --- a/apps/explorer/lib/explorer/chain/celo/helper.ex +++ b/apps/explorer/lib/explorer/chain/celo/helper.ex @@ -126,12 +126,13 @@ defmodule Explorer.Chain.Celo.Helper do @doc """ Checks if an epoch number is prior to Celo L2 migration. """ - @spec premigration_epoch_number?(non_neg_integer()) :: boolean() - def premigration_epoch_number?(epoch_number) do + @spec pre_migration_epoch_number?(non_neg_integer()) :: boolean() + def pre_migration_epoch_number?(epoch_number) do l2_migration_block_number = Application.get_env(:explorer, :celo)[:l2_migration_block] if l2_migration_block_number do - epoch_number < block_number_to_epoch_number(l2_migration_block_number) + l2_migration_epoch_number = l2_migration_block_number |> block_number_to_epoch_number() + epoch_number < l2_migration_epoch_number else true end diff --git a/apps/explorer/lib/explorer/sorting_helper.ex b/apps/explorer/lib/explorer/sorting_helper.ex index 1c0e064b2e13..637c78bb1934 100644 --- a/apps/explorer/lib/explorer/sorting_helper.ex +++ b/apps/explorer/lib/explorer/sorting_helper.ex @@ -24,7 +24,7 @@ defmodule Explorer.SortingHelper do Applies sorting to query based on default sorting params and sorting params from the client, these params merged keeping provided one over default one. """ - @spec apply_sorting(Ecto.Query.t(), sorting_params, sorting_params) :: Ecto.Query.t() + @spec apply_sorting(Ecto.Query.t() | module(), sorting_params, sorting_params) :: Ecto.Query.t() def apply_sorting(query, sorting, default_sorting) when is_list(sorting) and is_list(default_sorting) do sorting |> merge_sorting_params_with_defaults(default_sorting) |> sorting_params_to_order_by(query) end From 959512602fe3b411b7d01efea66bed3aa9fbeb25 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:21:53 +0300 Subject: [PATCH 0108/1006] fix: Don't send logs without topic to sig provider (#12620) * fix: Don't send logs without topic to sig provider * Ignore cspell warn --- .../views/api/v2/transaction_view.ex | 15 +++-- .../api/v2/address_controller_test.exs | 59 +++++++++++++++++++ apps/explorer/lib/explorer/chain/log.ex | 1 + .../smart_contract/sig_provider_interface.ex | 4 +- 4 files changed, 73 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index bdac36950334..98629c38d30b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -260,7 +260,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do @doc """ Decodes list of logs """ - @spec decode_logs([Log.t()], boolean) :: [tuple] + @spec decode_logs([Log.t()], boolean()) :: [tuple() | nil] def decode_logs(logs, skip_sig_provider?) do full_abi_per_address_hash = Enum.reduce(logs, %{}, fn log, acc -> @@ -294,17 +294,22 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> Enum.with_index(fn element, index -> {index, element} end) %{ - :already_decoded_logs => already_decoded_logs, + :already_decoded_or_ignored_logs => already_decoded_or_ignored_logs, :input_for_sig_provider_batched_request => input_for_sig_provider_batched_request } = all_logs_with_index |> Enum.reduce( %{ - :already_decoded_logs => [], + :already_decoded_or_ignored_logs => [], :input_for_sig_provider_batched_request => [] }, fn {index, result}, acc -> case result do + {:error, :try_with_sig_provider, {log, _transaction_hash}} when is_nil(log.first_topic) -> + Map.put(acc, :already_decoded_or_ignored_logs, [ + {index, {:error, :could_not_decode}} | acc.already_decoded_or_ignored_logs + ]) + {:error, :try_with_sig_provider, {log, transaction_hash}} -> Map.put(acc, :input_for_sig_provider_batched_request, [ {index, @@ -316,7 +321,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do ]) _ -> - Map.put(acc, :already_decoded_logs, [{index, result} | acc.already_decoded_logs]) + Map.put(acc, :already_decoded_or_ignored_logs, [{index, result} | acc.already_decoded_or_ignored_logs]) end end ) @@ -324,7 +329,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do decoded_with_sig_provider_logs = Log.decode_events_batch_via_sig_provider(input_for_sig_provider_batched_request, skip_sig_provider?) - full_logs = already_decoded_logs ++ decoded_with_sig_provider_logs + full_logs = already_decoded_or_ignored_logs ++ decoded_with_sig_provider_logs full_logs |> Enum.sort_by(fn {index, _log} -> index end, :asc) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index e1aa2fd31032..37ec33e8f617 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2543,6 +2543,65 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do log_from_api = Enum.at(response["items"], 0) compare_item(log, log_from_api) end + + test "ignore logs without topics when trying to decode with sig provider", %{conn: conn} do + bypass = Bypass.open() + + old_env_sig_provider = Application.get_env(:explorer, Explorer.SmartContract.SigProviderInterface) + + Application.put_env(:explorer, Explorer.SmartContract.SigProviderInterface, + enabled: true, + service_url: "http://localhost:#{bypass.port}" + ) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.SmartContract.SigProviderInterface, old_env_sig_provider) + end) + + address = insert(:contract_address) + + log_data = + "0x000000000000000000000000aeb81cbe6b19ceeb0dbe0d230cffe35bb40a13a700000000000000000000000000000000000000000000045d964b80006597b700fffffffffffffffffffffffffffffffffffffffffffffffffe55aca2c2f40000ffffffffffffffffffffffffffffffffffffffffffffffe3a8289da3d7a13ef2" + + transaction = :transaction |> insert() |> with_block() + + insert(:log, + transaction: transaction, + first_topic: nil, + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address + ) + + insert(:log, + transaction: transaction, + first_topic: TestHelper.topic("0x0000000000000000000000000000000000000000000000000000000000005d19"), + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address + ) + + # cspell:disable + Bypass.expect_once(bypass, "POST", "/api/v1/abi/events%3Abatch-get", fn conn -> + # cspell:enable + {:ok, body, conn} = Plug.Conn.read_body(conn) + body = Jason.decode!(body) + assert Enum.count(body["requests"]) == 1 + + Conn.resp(conn, 200, Jason.encode!([])) + end) + + request = get(conn, "/api/v2/addresses/#{address.hash}/logs") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + + Bypass.down(bypass) + end end describe "/addresses/{address_hash}/tokens" do diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 2741f7201f59..0858ea8875bf 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -391,6 +391,7 @@ defmodule Explorer.Chain.Log do ) do with true <- SigProviderInterface.enabled?(), false <- skip_sig_provider?, + false <- is_nil(log.first_topic), {:ok, result} <- SigProviderInterface.decode_event( [ diff --git a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex index 606c5bb5a6fe..50dffccb61f6 100644 --- a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex @@ -142,7 +142,9 @@ defmodule Explorer.SmartContract.SigProviderInterface do defp decode_event_url, do: "#{base_api_url()}" <> "/event" - defp decode_events_batch_url, do: "#{base_api_url()}" <> "/events:batch-get" + # cspell:disable + defp decode_events_batch_url, do: "#{base_api_url()}" <> "/events%3Abatch-get" + # cspell:enable def base_api_url, do: "#{base_url()}" <> "/api/v1/abi" From e266ee41c30ab538f835ac959ec4a28cb1f414b2 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 30 Jun 2025 19:07:51 +0400 Subject: [PATCH 0109/1006] fix: Fix pending transactions sanitizer (#12559) * fix: Fix pending transactions sanitizer * Optimize ReindexBlocksWithMissingTransactions migration * Move fetch_transactions_count under EthereumJSONRPC --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 42 +++++++++ apps/explorer/lib/explorer/application.ex | 1 + apps/explorer/lib/explorer/chain.ex | 1 + ...eindex_blocks_with_missing_transactions.ex | 85 +++++++++++++++++++ ..._blocks_with_missing_transactions_test.exs | 79 +++++++++++++++++ .../indexer/pending_transactions_sanitizer.ex | 23 +++-- config/runtime.exs | 6 ++ docker-compose/envs/common-blockscout.env | 4 + 8 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex create mode 100644 apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index f945c6a566ad..89ccef0e08ec 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -237,6 +237,48 @@ defmodule EthereumJSONRPC do end end + @doc """ + Fetches transactions count for every `block_number` + """ + @spec fetch_transactions_count([integer()], json_rpc_named_arguments) :: + {:ok, %{transactions_count_map: %{integer() => integer()}, errors: [{:error, map()}]}} + | {:error, reason :: term()} + def fetch_transactions_count(block_numbers, json_rpc_named_arguments) do + id_to_params = EthereumJSONRPC.id_to_params(block_numbers) + + id_to_params + |> Enum.map(fn {id, number} -> + EthereumJSONRPC.request(%{ + id: id, + method: "eth_getBlockTransactionCountByNumber", + params: [EthereumJSONRPC.integer_to_quantity(number)] + }) + end) + |> EthereumJSONRPC.json_rpc(json_rpc_named_arguments) + |> case do + {:ok, responses} -> + %{errors: errors, counts: counts} = + responses + |> EthereumJSONRPC.sanitize_responses(id_to_params) + |> Enum.reduce(%{errors: [], counts: %{}}, fn + %{id: id, result: nil}, %{errors: errors} = acc -> + error = {:error, %{code: 404, message: "Not Found", data: Map.fetch!(id_to_params, id)}} + %{acc | errors: [error | errors]} + + %{id: id, result: count}, %{counts: counts} = acc -> + %{acc | counts: Map.put(counts, Map.fetch!(id_to_params, id), EthereumJSONRPC.quantity_to_integer(count))} + + %{id: id, error: error}, %{errors: errors} = acc -> + %{acc | errors: [{:error, Map.put(error, :data, Map.fetch!(id_to_params, id))} | errors]} + end) + + {:ok, %{transactions_count_map: Map.new(counts), errors: errors}} + + error -> + error + end + end + @doc """ Fetches contract code for multiple addresses at specified block numbers. diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index c87facc702ac..e220c6c28022 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -190,6 +190,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.SanitizeEmptyContractCodeAddresses, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexDuplicatedInternalTransactions, :indexer), + configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 71f17dec0db3..bc4d25c4d36f 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -2245,6 +2245,7 @@ defmodule Explorer.Chain do def pending_transactions_list do Transaction |> pending_transactions_query() + |> where([t], t.inserted_at < ago(1, "day")) |> Repo.all(timeout: :infinity) end diff --git a/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex new file mode 100644 index 000000000000..31c3ec94deec --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex @@ -0,0 +1,85 @@ +defmodule Explorer.Migrator.ReindexBlocksWithMissingTransactions do + @moduledoc """ + Searches for all blocks where the number of transactions differs from the number of transactions on the node, + and sets refetch_needed=true for them. + """ + + use Explorer.Migrator.FillingMigration + + require Logger + + import Ecto.Query + + alias Explorer.Repo + alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Migrator.{FillingMigration, MigrationStatus} + + @migration_name "reindex_blocks_with_missing_transactions" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(%{"max_block_number" => -1} = state), do: {[], state} + + def last_unprocessed_identifiers(%{"max_block_number" => from_block_number} = state) do + limit = batch_size() * concurrency() + to_block_number = max(from_block_number - limit + 1, 0) + + {Enum.to_list(from_block_number..to_block_number), %{state | "max_block_number" => to_block_number - 1}} + end + + def last_unprocessed_identifiers(state) do + state + |> Map.put("max_block_number", BlockNumber.get_max()) + |> last_unprocessed_identifiers() + end + + @impl FillingMigration + def unprocessed_data_query, do: nil + + @impl FillingMigration + def update_batch(block_numbers) do + Block + |> where([b], b.number in ^block_numbers) + |> where([b], b.consensus == true) + |> where([b], b.refetch_needed == false) + |> select([b], b.number) + |> Repo.all() + |> do_update() + end + + @impl FillingMigration + def update_cache, do: :ok + + defp do_update([]), do: :ok + + defp do_update(consensus_block_numbers) do + db_transactions_count_map = + Transaction + |> where([t], t.block_number in ^consensus_block_numbers) + |> group_by([t], t.block_number) + |> select([t], {t.block_number, count("*")}) + |> Repo.all() + |> Map.new() + + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + case EthereumJSONRPC.fetch_transactions_count(consensus_block_numbers, json_rpc_named_arguments) do + {:ok, %{transactions_count_map: node_transactions_count_map, errors: errors}} -> + unless Enum.empty?(errors) do + Logger.warning("Migration #{@migration_name} encountered errors fetching blocks: #{inspect(errors)}") + end + + consensus_block_numbers + |> Enum.filter(&Map.has_key?(node_transactions_count_map, &1)) + |> Enum.reject(fn number -> db_transactions_count_map[number] == node_transactions_count_map[number] end) + |> Block.set_refetch_needed() + + error -> + Logger.error("Migration #{@migration_name} failed: #{inspect(error)}") + {:error, error} + end + end +end diff --git a/apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs b/apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs new file mode 100644 index 000000000000..549d7535d118 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/reindex_blocks_with_missing_transactions_test.exs @@ -0,0 +1,79 @@ +defmodule Explorer.Migrator.ReindexBlocksWithMissingTransactionsTest do + use Explorer.DataCase, async: false + + import Mox + + alias Explorer.Chain.Block + alias Explorer.Migrator.{MigrationStatus, ReindexBlocksWithMissingTransactions} + alias Explorer.Repo + + setup :set_mox_global + setup :verify_on_exit! + + setup do + configuration = Application.get_env(:explorer, ReindexBlocksWithMissingTransactions) + + Application.put_env( + :explorer, + ReindexBlocksWithMissingTransactions, + Keyword.merge(configuration, batch_size: 1, concurrency: 1) + ) + + on_exit(fn -> + Application.put_env(:explorer, ReindexBlocksWithMissingTransactions, configuration) + end) + end + + test "Reindex blocks with missing transactions" do + %{block: %{number: block_number_correct}} = + :transaction + |> insert() + |> with_block() + + correct_block_number_quantity = EthereumJSONRPC.integer_to_quantity(block_number_correct) + + %{block: %{number: block_number_incorrect}} = + :transaction + |> insert() + |> with_block() + + incorrect_block_number_quantity = EthereumJSONRPC.integer_to_quantity(block_number_incorrect) + + expect(EthereumJSONRPC.Mox, :json_rpc, 2, fn + [ + %{ + id: id, + method: "eth_getBlockTransactionCountByNumber", + params: [^correct_block_number_quantity] + } + ], + _ -> + {:ok, [%{id: id, result: "0x1", jsonrpc: "2.0"}]} + + [ + %{ + id: id, + method: "eth_getBlockTransactionCountByNumber", + params: [^incorrect_block_number_quantity] + } + ], + _ -> + {:ok, [%{id: id, result: "0x2", jsonrpc: "2.0"}]} + end) + + assert MigrationStatus.get_status("reindex_blocks_with_missing_transactions") == nil + + ReindexBlocksWithMissingTransactions.start_link([]) + + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: ms.migration_name == ^"reindex_blocks_with_missing_transactions" and ms.status == "completed" + ) + ) + end) + + assert %{consensus: true, refetch_needed: false} = Repo.get_by(Block, number: block_number_correct) + assert %{consensus: true, refetch_needed: true} = Repo.get_by(Block, number: block_number_incorrect) + end +end diff --git a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex index 28f64794f371..71706b32a023 100644 --- a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex +++ b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex @@ -124,15 +124,14 @@ defmodule Indexer.PendingTransactionsSanitizer do defp fetch_pending_transaction_and_delete(transaction) do pending_transaction_hash_string = ExplorerHelper.add_0x_prefix(transaction.hash) - case transaction - |> Changeset.change() - |> Repo.delete(timeout: :infinity) do - {:ok, _transaction} -> - Logger.debug( - "Transaction with hash #{pending_transaction_hash_string} successfully deleted from Blockscout DB because it doesn't exist in the archive node anymore", - fetcher: :pending_transactions_to_refetch - ) - + with %{block_hash: nil} <- Repo.reload(transaction), + changeset = Changeset.change(transaction), + {:ok, _transaction} <- Repo.delete(changeset, timeout: :infinity) do + Logger.debug( + "Transaction with hash #{pending_transaction_hash_string} successfully deleted from Blockscout DB because it doesn't exist in the archive node anymore", + fetcher: :pending_transactions_to_refetch + ) + else {:error, changeset} -> Logger.debug( [ @@ -141,6 +140,12 @@ defmodule Indexer.PendingTransactionsSanitizer do ], fetcher: :pending_transactions_to_refetch ) + + _transaction -> + Logger.debug( + "Transaction with hash #{pending_transaction_hash_string} is already included in block, cancel deletion", + fetcher: :pending_transactions_to_refetch + ) end end diff --git a/config/runtime.exs b/config/runtime.exs index 94bf9eb05650..428605795eb7 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -729,6 +729,12 @@ config :explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_CONCURRENCY", 1), timeout: ConfigHelper.parse_time_env_var("MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_TIMEOUT", "0s") +config :explorer, Explorer.Migrator.ReindexBlocksWithMissingTransactions, + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_BATCH_SIZE", 10), + concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_CONCURRENCY", 1), + timeout: ConfigHelper.parse_time_env_var("MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_TIMEOUT", "0s"), + enabled: ConfigHelper.parse_bool_env_var("MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_ENABLED", "false") + config :explorer, Explorer.Migrator.RestoreOmittedWETHTransfers, concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_CONCURRENCY", 5), batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_BATCH_SIZE", 50), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index f33ee2f91dd7..41c4f5899159 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -482,6 +482,10 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_BATCH_SIZE= # MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_CONCURRENCY= # MIGRATION_REINDEX_DUPLICATED_INTERNAL_TRANSACTIONS_TIMEOUT= +# MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_BATCH_SIZE= +# MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_CONCURRENCY= +# MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_TIMEOUT= +# MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_ENABLED= # MIGRATION_SHRINK_INTERNAL_TRANSACTIONS_BATCH_SIZE= # MIGRATION_SHRINK_INTERNAL_TRANSACTIONS_CONCURRENCY= # MIGRATION_ARBITRUM_DA_RECORDS_NORMALIZATION_BATCH_SIZE= From 3ca4372351b8c55aa784dae747d1010e257e0ae5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:11:47 +0300 Subject: [PATCH 0110/1006] chore(deps): bump plug_cowboy from 2.7.3 to 2.7.4 (#12656) Bumps [plug_cowboy](https://github.com/elixir-plug/plug_cowboy) from 2.7.3 to 2.7.4. - [Changelog](https://github.com/elixir-plug/plug_cowboy/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-plug/plug_cowboy/compare/v2.7.3...v2.7.4) --- updated-dependencies: - dependency-name: plug_cowboy dependency-version: 2.7.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index cf8d40d63a6a..8f4e605034b1 100644 --- a/mix.lock +++ b/mix.lock @@ -30,7 +30,7 @@ "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, - "cowlib": {:hex, :cowlib, "2.14.0", "623791c56c1cc9df54a71a9c55147a401549917f00a2e48a6ae12b812c586ced", [:make, :rebar3], [], "hexpm", "0af652d1550c8411c3b58eed7a035a7fb088c0b86aff6bc504b0bc3b7f791aa2"}, + "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, @@ -128,7 +128,7 @@ "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.3", "1304d36752e8bdde213cea59ef424ca932910a91a07ef9f3874be709c4ddb94b", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "77c95524b2aa5364b247fa17089029e73b951ebc1adeef429361eab0bb55819d"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, From 0eeb03f341fc69d6c213cb7e3dcf3515a9487556 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:03:07 +0300 Subject: [PATCH 0111/1006] chore(deps): bump ecto_sql from 3.12.1 to 3.13.1 (#12658) Bumps [ecto_sql](https://github.com/elixir-ecto/ecto_sql) from 3.12.1 to 3.13.1. - [Changelog](https://github.com/elixir-ecto/ecto_sql/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-ecto/ecto_sql/compare/v3.12.1...v3.13.1) --- updated-dependencies: - dependency-name: ecto_sql dependency-version: 3.13.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 8f4e605034b1..a163c93f7e17 100644 --- a/mix.lock +++ b/mix.lock @@ -43,7 +43,7 @@ "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, - "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.1", "d3d76d78afd2757644b5c4f7ca37f90bcf1e05d05a06cca8526e30cefb0034a1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fba0174284fd339f69376b0405942036ce5f0ff7d59402a6ccf3b7ce2903198"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, From c57905a19400cb831a50cf59ce63277e8d0c9b7d Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:04:36 +0300 Subject: [PATCH 0112/1006] fix: Update all block fields on conflict (#12418) --- .../explorer/chain/import/runner/blocks.ex | 170 ++++++++++++++---- 1 file changed, 139 insertions(+), 31 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index d6f862ef246d..fd2a0a2f9b2a 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -5,7 +5,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do require Ecto.Query - import Ecto.Query, only: [from: 2, where: 3, subquery: 1] + import Ecto.Query, only: [dynamic: 1, dynamic: 2, from: 2, where: 3, subquery: 1] import Explorer.Chain.Import.Runner.Helper, only: [chain_type_dependent_import: 3] alias Ecto.{Changeset, Multi, Repo} @@ -361,40 +361,148 @@ defmodule Explorer.Chain.Import.Runner.Blocks do ) end - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defp default_on_conflict do - from( - block in Block, - update: [ - set: [ - consensus: fragment("EXCLUDED.consensus"), - difficulty: fragment("EXCLUDED.difficulty"), - gas_limit: fragment("EXCLUDED.gas_limit"), - gas_used: fragment("EXCLUDED.gas_used"), - miner_hash: fragment("EXCLUDED.miner_hash"), - nonce: fragment("EXCLUDED.nonce"), - number: fragment("EXCLUDED.number"), - parent_hash: fragment("EXCLUDED.parent_hash"), - size: fragment("EXCLUDED.size"), - timestamp: fragment("EXCLUDED.timestamp"), - total_difficulty: fragment("EXCLUDED.total_difficulty"), - refetch_needed: fragment("EXCLUDED.refetch_needed"), - # Don't update `hash` as it is used for the conflict target - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", block.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", block.updated_at) - ] + chain_type = Application.get_env(:explorer, :chain_type) + + base_fields = [ + consensus: dynamic(fragment("EXCLUDED.consensus")), + difficulty: dynamic(fragment("EXCLUDED.difficulty")), + gas_limit: dynamic(fragment("EXCLUDED.gas_limit")), + gas_used: dynamic(fragment("EXCLUDED.gas_used")), + miner_hash: dynamic(fragment("EXCLUDED.miner_hash")), + nonce: dynamic(fragment("EXCLUDED.nonce")), + number: dynamic(fragment("EXCLUDED.number")), + parent_hash: dynamic(fragment("EXCLUDED.parent_hash")), + size: dynamic(fragment("EXCLUDED.size")), + timestamp: dynamic(fragment("EXCLUDED.timestamp")), + total_difficulty: dynamic(fragment("EXCLUDED.total_difficulty")), + refetch_needed: dynamic(fragment("EXCLUDED.refetch_needed")), + base_fee_per_gas: dynamic(fragment("EXCLUDED.base_fee_per_gas")), + is_empty: dynamic(fragment("EXCLUDED.is_empty")), + # Don't update `hash` as it is used for the conflict target + inserted_at: dynamic([block], fragment("LEAST(?, EXCLUDED.inserted_at)", block.inserted_at)), + updated_at: dynamic([block], fragment("GREATEST(?, EXCLUDED.updated_at)", block.updated_at)) + ] + + base_condition = + dynamic( + [block], + fragment( + "(EXCLUDED.consensus, EXCLUDED.difficulty, EXCLUDED.gas_limit, EXCLUDED.gas_used, EXCLUDED.miner_hash, EXCLUDED.nonce, EXCLUDED.number, EXCLUDED.parent_hash, EXCLUDED.size, EXCLUDED.timestamp, EXCLUDED.total_difficulty, EXCLUDED.refetch_needed, EXCLUDED.base_fee_per_gas, EXCLUDED.is_empty) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + block.consensus, + block.difficulty, + block.gas_limit, + block.gas_used, + block.miner_hash, + block.nonce, + block.number, + block.parent_hash, + block.size, + block.timestamp, + block.total_difficulty, + block.refetch_needed, + block.base_fee_per_gas, + block.is_empty + ) + ) + + case on_conflict_chain_type_extension(chain_type) do + {chain_type_fields, chain_type_condition} -> + base_with_chain_type_fields = Keyword.merge(base_fields, chain_type_fields) + base_with_chain_type_condition = dynamic(^base_condition or ^chain_type_condition) + + from( + block in Block, + update: ^[set: base_with_chain_type_fields], + where: ^base_with_chain_type_condition + ) + + _ -> + from( + block in Block, + update: ^[set: base_fields], + where: ^base_condition + ) + end + end + + defp on_conflict_chain_type_extension(:ethereum) do + { + [ + blob_gas_used: dynamic(fragment("EXCLUDED.blob_gas_used")), + excess_blob_gas: dynamic(fragment("EXCLUDED.excess_blob_gas")) ], - where: - fragment("EXCLUDED.consensus <> ?", block.consensus) or fragment("EXCLUDED.difficulty <> ?", block.difficulty) or - fragment("EXCLUDED.gas_limit <> ?", block.gas_limit) or fragment("EXCLUDED.gas_used <> ?", block.gas_used) or - fragment("EXCLUDED.miner_hash <> ?", block.miner_hash) or fragment("EXCLUDED.nonce <> ?", block.nonce) or - fragment("EXCLUDED.number <> ?", block.number) or fragment("EXCLUDED.parent_hash <> ?", block.parent_hash) or - fragment("EXCLUDED.size <> ?", block.size) or fragment("EXCLUDED.timestamp <> ?", block.timestamp) or - fragment("EXCLUDED.total_difficulty <> ?", block.total_difficulty) or - fragment("EXCLUDED.refetch_needed <> ?", block.refetch_needed) - ) + dynamic( + [block], + fragment( + "(EXCLUDED.blob_gas_used, EXCLUDED.excess_blob_gas) IS DISTINCT FROM (?, ?)", + block.blob_gas_used, + block.excess_blob_gas + ) + ) + } + end + + defp on_conflict_chain_type_extension(:rsk) do + { + [ + bitcoin_merged_mining_header: dynamic(fragment("EXCLUDED.bitcoin_merged_mining_header")), + bitcoin_merged_mining_coinbase_transaction: + dynamic(fragment("EXCLUDED.bitcoin_merged_mining_coinbase_transaction")), + bitcoin_merged_mining_merkle_proof: dynamic(fragment("EXCLUDED.bitcoin_merged_mining_merkle_proof")), + hash_for_merged_mining: dynamic(fragment("EXCLUDED.hash_for_merged_mining")), + minimum_gas_price: dynamic(fragment("EXCLUDED.minimum_gas_price")) + ], + dynamic( + [block], + fragment( + "(EXCLUDED.bitcoin_merged_mining_header, EXCLUDED.bitcoin_merged_mining_coinbase_transaction, EXCLUDED.bitcoin_merged_mining_merkle_proof, EXCLUDED.hash_for_merged_mining, EXCLUDED.minimum_gas_price) IS DISTINCT FROM (?, ?, ?, ?, ?)", + block.bitcoin_merged_mining_header, + block.bitcoin_merged_mining_coinbase_transaction, + block.bitcoin_merged_mining_merkle_proof, + block.hash_for_merged_mining, + block.minimum_gas_price + ) + ) + } + end + + defp on_conflict_chain_type_extension(:arbitrum) do + { + [ + send_count: dynamic(fragment("EXCLUDED.send_count")), + send_root: dynamic(fragment("EXCLUDED.send_root")), + l1_block_number: dynamic(fragment("EXCLUDED.l1_block_number")) + ], + dynamic( + [block], + fragment( + "(EXCLUDED.send_count, EXCLUDED.send_root, EXCLUDED.l1_block_number) IS DISTINCT FROM (?, ?, ?)", + block.send_count, + block.send_root, + block.l1_block_number + ) + ) + } + end + + defp on_conflict_chain_type_extension(:zilliqa) do + { + [ + zilliqa_view: dynamic(fragment("EXCLUDED.zilliqa_view")) + ], + dynamic( + [block], + fragment( + "EXCLUDED.zilliqa_view IS DISTINCT FROM ?", + block.zilliqa_view + ) + ) + } end + defp on_conflict_chain_type_extension(_), do: nil + defp consensus_block_numbers(blocks_changes) when is_list(blocks_changes) do blocks_changes |> Enum.filter(& &1.consensus) From 94649db313db33c464241baa281935ee09702151 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 30 Jun 2025 22:25:47 +0300 Subject: [PATCH 0113/1006] fix: coin balance history with internal tx changes (#12631) --- .../address_coin_balance_controller.ex | 4 +- .../controllers/api/v2/address_controller.ex | 4 +- .../models/transaction_state_helper.ex | 4 +- .../lib/block_scout_web/notifier.ex | 3 +- .../api/v2/address_controller_test.exs | 49 +++ apps/explorer/lib/explorer/chain.ex | 220 ---------- .../explorer/chain/address/coin_balance.ex | 387 +++++++++++++++-- .../chain/address/coin_balance_test.exs | 406 +++++++++++++++++- apps/explorer/test/explorer/chain_test.exs | 404 ----------------- .../indexer/fetcher/coin_balance/catchup.ex | 4 +- .../fetcher/on_demand/coin_balance_test.exs | 5 +- 11 files changed, 810 insertions(+), 680 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex index 2466dfce5b4d..8c44303247a2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do alias BlockScoutWeb.{AccessHelper, AddressCoinBalanceView, Controller} alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.{Address, Address.CoinBalance, Wei} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Phoenix.View @@ -21,7 +21,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do full_options = paging_options(params) - coin_balances_plus_one = Chain.address_to_coin_balances(address, full_options) + coin_balances_plus_one = CoinBalance.address_to_coin_balances(address, full_options) {coin_balances, next_page} = split_list_by_page(coin_balances_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index b1092a2f5700..12680adb3203 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -33,7 +33,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} alias Explorer.{Chain, Market, PagingOptions} alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction} - alias Explorer.Chain.Address.Counters + alias Explorer.Chain.Address.{CoinBalance, Counters} alias Explorer.Chain.Token.Instance alias Explorer.SmartContract.Helper, as: SmartContractHelper @@ -574,7 +574,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {:ok, address} -> full_options = params |> paging_options() |> Keyword.merge(@api_true) - results_plus_one = Chain.address_to_coin_balances(address, full_options) + results_plus_one = CoinBalance.address_to_coin_balances(address, full_options) {coin_balances, next_page} = split_list_by_page(results_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 6987912cbf6d..7b7d05777fe0 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do alias Explorer.Chain.Transaction.StateChange alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.{BlockNumberHelper, InternalTransaction, Transaction, Wei} + alias Explorer.Chain.{Address.CoinBalance, BlockNumberHelper, InternalTransaction, Transaction, Wei} alias Explorer.Chain.Cache.StateChanges alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Indexer.Fetcher.OnDemand.TokenBalance, as: TokenBalanceOnDemand @@ -139,7 +139,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do end defp coin_balance(address_hash, block_number, options) do - case Chain.get_coin_balance(address_hash, block_number, options) do + case CoinBalance.get_coin_balance(address_hash, block_number, options) do %{value: val} when not is_nil(val) -> val diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index fdd541d56dfc..831cf86599a7 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -31,6 +31,7 @@ defmodule BlockScoutWeb.Notifier do alias Explorer.Chain.{ Address, + Address.CoinBalance, BlockNumberHelper, DenormalizationHelper, InternalTransaction, @@ -550,7 +551,7 @@ defmodule BlockScoutWeb.Notifier do end defp broadcast_address_coin_balance(%{address_hash: address_hash, block_number: block_number}) do - coin_balance = Chain.get_coin_balance(address_hash, block_number) + coin_balance = CoinBalance.get_coin_balance(address_hash, block_number) # TODO: delete duplicated event when old UI becomes deprecated Endpoint.broadcast("addresses_old:#{address_hash}", "coin_balance", %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 37ec33e8f617..47cba82887cf 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2156,6 +2156,55 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(acb, Enum.at(response["items"], 0)) end + test "get coin balance with transaction", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert(to_address_hash: address.hash, to_address: address, value: 123) + |> with_block() + + acb = insert(:address_coin_balance, address: address, block_number: transaction.block_number) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert %{"items" => [acb_json], "next_page_params" => nil} = json_response(request, 200) + assert acb_json["transaction_hash"] == to_string(transaction.hash) + + compare_item(acb, acb_json) + end + + test "get coin balance with internal transaction", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block() + + address = insert(:address) + + insert(:internal_transaction, + type: "call", + call_type: "call", + transaction: transaction, + block: transaction.block, + to_address: address, + value: 123, + block_number: transaction.block_number, + index: 1, + block_index: 1 + ) + + insert(:address_coin_balance) + acb = insert(:address_coin_balance, address: address, block_number: transaction.block_number) + + request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + + assert %{"items" => [acb_json], "next_page_params" => nil} = json_response(request, 200) + assert acb_json["transaction_hash"] == to_string(transaction.hash) + + compare_item(acb, acb_json) + end + test "coin balance history can paginate", %{conn: conn} do address = insert(:address) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index bc4d25c4d36f..416f576cc825 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1557,50 +1557,6 @@ defmodule Explorer.Chain do Decimal.mult(tokens, fiat_value) end - @doc """ - Returns a stream of unfetched `t:Explorer.Chain.Address.CoinBalance.t/0`. - - When there are addresses, the `reducer` is called for each `t:Explorer.Chain.Address.t/0` `hash` and all - `t:Explorer.Chain.Block.t/0` `block_number` that address is mentioned. - - | Address Hash Schema | Address Hash Field | Block Number Schema | Block Number Field | - |--------------------------------------------|---------------------------------|------------------------------------|--------------------| - | `t:Explorer.Chain.Block.t/0` | `miner_hash` | `t:Explorer.Chain.Block.t/0` | `number` | - | `t:Explorer.Chain.Transaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - | `t:Explorer.Chain.Transaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - | `t:Explorer.Chain.Log.t/0` | `address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - | `t:Explorer.Chain.InternalTransaction.t/0` | `created_contract_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - | `t:Explorer.Chain.InternalTransaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - | `t:Explorer.Chain.InternalTransaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | - - Pending `t:Explorer.Chain.Transaction.t/0` `from_address_hash` and `to_address_hash` aren't returned because they - don't have an associated block number. - - When there are no addresses, the `reducer` is never called and the `initial` is returned in an `:ok` tuple. - - When an `t:Explorer.Chain.Address.t/0` `hash` is used multiple times, all unique `t:Explorer.Chain.Block.t/0` `number` - will be returned. - """ - @spec stream_unfetched_balances( - initial :: accumulator, - reducer :: - (entry :: %{address_hash: Hash.Address.t(), block_number: Block.block_number()}, accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_unfetched_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do - query = - from( - balance in CoinBalance, - where: is_nil(balance.value_fetched_at), - select: %{address_hash: balance.address_hash, block_number: balance.block_number} - ) - - query - |> add_coin_balances_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - @doc """ Returns a stream of all token balances that weren't fetched values. """ @@ -2867,12 +2823,6 @@ defmodule Explorer.Chain do where(query, [block], block.number < ^block_number) end - defp page_coin_balances(query, %PagingOptions{key: nil}), do: query - - defp page_coin_balances(query, %PagingOptions{key: {block_number}}) do - where(query, [coin_balance], coin_balance.block_number < ^block_number) - end - defp page_logs(query, %PagingOptions{key: nil}), do: query defp page_logs(query, %PagingOptions{key: {index}}) do @@ -3206,11 +3156,6 @@ defmodule Explorer.Chain do end end - defp fetch_coin_balances(address, paging_options) do - address.hash - |> CoinBalance.fetch_coin_balances(paging_options) - end - @spec fetch_last_token_balance(Hash.Address.t(), Hash.Address.t()) :: Decimal.t() def fetch_last_token_balance(address_hash, token_contract_address_hash) do if address_hash !== %{} do @@ -3231,161 +3176,12 @@ defmodule Explorer.Chain do end end - @spec address_to_coin_balances(Address.t(), [paging_options | api?]) :: [] - def address_to_coin_balances(address, options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - case paging_options do - %PagingOptions{key: {0}} -> - [] - - _ -> - address_to_coin_balances_internal(address, options, paging_options) - end - end - - defp address_to_coin_balances_internal(address, options, paging_options) do - balances_raw = - address - |> fetch_coin_balances(paging_options) - |> page_coin_balances(paging_options) - |> select_repo(options).all() - |> preload_transactions(options) - - if Enum.empty?(balances_raw) do - balances_raw - else - balances_raw_filtered = - balances_raw - |> Enum.filter(fn balance -> balance.value end) - - min_block_number = - balances_raw_filtered - |> Enum.min_by(fn balance -> balance.block_number end, fn -> %{} end) - |> Map.get(:block_number) - - max_block_number = - balances_raw_filtered - |> Enum.max_by(fn balance -> balance.block_number end, fn -> %{} end) - |> Map.get(:block_number) - - min_block_timestamp = find_block_timestamp(min_block_number, options) - max_block_timestamp = find_block_timestamp(max_block_number, options) - - min_block_unix_timestamp = - min_block_timestamp - |> Timex.to_unix() - - max_block_unix_timestamp = - max_block_timestamp - |> Timex.to_unix() - - blocks_delta = max_block_number - min_block_number - - balances_with_dates = - if blocks_delta > 0 do - add_block_timestamp_to_balances( - balances_raw_filtered, - min_block_number, - min_block_unix_timestamp, - max_block_unix_timestamp, - blocks_delta - ) - else - add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp) - end - - balances_with_dates - |> Enum.sort(fn balance1, balance2 -> balance1.block_number >= balance2.block_number end) - end - end - - # Here we fetch from DB one transaction per one coin balance. It's much more faster than LEFT OUTER JOIN which was before. - defp preload_transactions(balances, options) do - tasks = - Enum.map(balances, fn balance -> - Task.async(fn -> - Transaction - |> where( - [transaction], - transaction.block_number == ^balance.block_number and - (transaction.value > ^0 or (transaction.gas_price > ^0 and transaction.gas_used > ^0)) and - (transaction.to_address_hash == ^balance.address_hash or - transaction.from_address_hash == ^balance.address_hash) - ) - |> select([transaction], transaction.hash) - |> limit(1) - |> select_repo(options).one() - end) - end) - - tasks - |> Task.yield_many(120_000) - |> Enum.zip(balances) - |> Enum.map(fn {{task, res}, balance} -> - case res do - {:ok, hash} -> - put_transaction_hash(hash, balance) - - {:exit, _reason} -> - balance - - nil -> - Task.shutdown(task, :brutal_kill) - balance - end - end) - end - - defp put_transaction_hash(hash, coin_balance), - do: if(hash, do: %CoinBalance{coin_balance | transaction_hash: hash}, else: coin_balance) - - defp add_block_timestamp_to_balances( - balances_raw_filtered, - min_block_number, - min_block_unix_timestamp, - max_block_unix_timestamp, - blocks_delta - ) do - balances_raw_filtered - |> Enum.map(fn balance -> - date = - trunc( - min_block_unix_timestamp + - (balance.block_number - min_block_number) * (max_block_unix_timestamp - min_block_unix_timestamp) / - blocks_delta - ) - - add_date_to_balance(balance, date) - end) - end - - defp add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp) do - balances_raw_filtered - |> Enum.map(fn balance -> - date = min_block_unix_timestamp - - add_date_to_balance(balance, date) - end) - end - - defp add_date_to_balance(balance, date) do - formatted_date = Timex.from_unix(date) - %{balance | block_timestamp: formatted_date} - end - def get_token_balance(address_hash, token_contract_address_hash, block_number, token_id \\ nil, options \\ []) do query = TokenBalance.fetch_token_balance(address_hash, token_contract_address_hash, block_number, token_id) select_repo(options).one(query) end - def get_coin_balance(address_hash, block_number, options \\ []) do - query = CoinBalance.fetch_coin_balance(address_hash, block_number) - - select_repo(options).one(query) - end - @spec address_to_balances_by_day(Hash.Address.t(), [api?]) :: [balance_by_day] def address_to_balances_by_day(address_hash, options \\ []) do latest_block_timestamp = @@ -3917,14 +3713,6 @@ defmodule Explorer.Chain do end end - defp find_block_timestamp(number, options) do - Block - |> where([block], block.number == ^number) - |> select([block], block.timestamp) - |> limit(1) - |> select_repo(options).one() - end - @spec get_token_transfer_type(TokenTransfer.t()) :: :token_burning | :token_minting | :token_spawning | :token_transfer def get_token_transfer_type(transfer) do @@ -4405,14 +4193,6 @@ defmodule Explorer.Chain do limit(query, ^token_balances_fetcher_limit) end - defp add_coin_balances_fetcher_limit(query, false), do: query - - defp add_coin_balances_fetcher_limit(query, true) do - coin_balances_fetcher_limit = Application.get_env(:indexer, :coin_balances_fetcher_init_limit) - - limit(query, ^coin_balances_fetcher_limit) - end - @spec default_paging_options() :: map() def default_paging_options do @default_paging_options diff --git a/apps/explorer/lib/explorer/chain/address/coin_balance.ex b/apps/explorer/lib/explorer/chain/address/coin_balance.ex index 3f0f9d7f5277..ce88b1a00b87 100644 --- a/apps/explorer/lib/explorer/chain/address/coin_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/coin_balance.ex @@ -6,8 +6,8 @@ defmodule Explorer.Chain.Address.CoinBalance do use Explorer.Schema - alias Explorer.PagingOptions - alias Explorer.Chain.{Address, Block, Hash, Wei} + alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, Transaction, Wei} alias Explorer.Chain.Address.CoinBalance @optional_fields ~w(value value_fetched_at)a @@ -42,9 +42,340 @@ defmodule Explorer.Chain.Address.CoinBalance do end @doc """ - Builds an `Ecto.Query` to fetch the coin balance of the given address in the given block. + Builds a query to fetch the timestamp and value of the most recent coin balance for a given address. + + This function constructs a query that retrieves the latest coin balance record + for the specified address and joins it with block information to get the + timestamp when that balance was recorded. The query ensures that only + consensus blocks (not uncle blocks) are considered for the balance data. + + ## Parameters + - `address_hash`: The address hash to look up the most recent coin balance for + + ## Returns + - An Ecto query that when executed returns a map with: + - `timestamp`: The timestamp when the block containing the latest balance was mined + - `value`: The coin balance value in Wei at that block + """ + @spec last_coin_balance_timestamp(Hash.Address.t()) :: Ecto.Query.t() + def last_coin_balance_timestamp(address_hash) do + coin_balance_query = + CoinBalance + |> where([cb], cb.address_hash == ^address_hash) + |> last(:block_number) + |> select([cb, b], %{block_number: cb.block_number, value: cb.value}) + + from( + cb in subquery(coin_balance_query), + inner_join: block in Block, + on: cb.block_number == block.number, + where: block.consensus == true, + select: %{timestamp: block.timestamp, value: cb.value} + ) + end + + def changeset(%__MODULE__{} = balance, params) do + balance + |> cast(params, @allowed_fields) + |> validate_required(@required_fields) + |> unique_constraint(:block_number, name: :address_coin_balances_address_hash_block_number_index) + end + + @doc """ + Query to fetch latest coin balance for the given address + """ + @spec latest_coin_balance_query(Hash.Address.t(), non_neg_integer()) :: Ecto.Query.t() + def latest_coin_balance_query(address_hash, stale_balance_window) do + from( + cb in __MODULE__, + where: cb.address_hash == ^address_hash, + where: cb.block_number >= ^stale_balance_window, + where: is_nil(cb.value_fetched_at), + order_by: [desc: :block_number], + limit: 1 + ) + end + + @doc """ + Returns a stream of unfetched `t:Explorer.Chain.Address.CoinBalance.t/0`. + + When there are addresses, the `reducer` is called for each `t:Explorer.Chain.Address.t/0` `hash` and all + `t:Explorer.Chain.Block.t/0` `block_number` that address is mentioned. + + | Address Hash Schema | Address Hash Field | Block Number Schema | Block Number Field | + |--------------------------------------------|---------------------------------|------------------------------------|--------------------| + | `t:Explorer.Chain.Block.t/0` | `miner_hash` | `t:Explorer.Chain.Block.t/0` | `number` | + | `t:Explorer.Chain.Transaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + | `t:Explorer.Chain.Transaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + | `t:Explorer.Chain.Log.t/0` | `address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + | `t:Explorer.Chain.InternalTransaction.t/0` | `created_contract_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + | `t:Explorer.Chain.InternalTransaction.t/0` | `from_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + | `t:Explorer.Chain.InternalTransaction.t/0` | `to_address_hash` | `t:Explorer.Chain.Transaction.t/0` | `block_number` | + + Pending `t:Explorer.Chain.Transaction.t/0` `from_address_hash` and `to_address_hash` aren't returned because they + don't have an associated block number. + + When there are no addresses, the `reducer` is never called and the `initial` is returned in an `:ok` tuple. + + When an `t:Explorer.Chain.Address.t/0` `hash` is used multiple times, all unique `t:Explorer.Chain.Block.t/0` `number` + will be returned. """ - def fetch_coin_balance(address_hash, block_number) do + @spec stream_unfetched_balances( + initial :: accumulator, + reducer :: + (entry :: %{address_hash: Hash.Address.t(), block_number: Block.block_number()}, accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_unfetched_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + query = + from( + balance in CoinBalance, + where: is_nil(balance.value_fetched_at), + select: %{address_hash: balance.address_hash, block_number: balance.block_number} + ) + + query + |> add_coin_balances_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Retrieves the most recent coin balance for an address at or before a specified block number. + + This function queries the coin balance records for the given address hash, + filtering for balances at or before the specified block number. It returns + the most recent balance record, including calculated delta values and block + timestamps. The query uses a subquery approach to efficiently find the + latest balance while computing the difference from the previous balance. + + ## Parameters + - `address_hash`: Hash.Address.t() - The address hash to query balances for + - `block_number`: Block.block_number() - The maximum block number to consider + - `options`: keyword() - Query options including `:api?` for replica selection + + ## Returns + - `t() | nil` - The coin balance record with delta and timestamp fields, or + `nil` if no balance exists for the address at or before the block number + """ + @spec get_coin_balance(Hash.Address.t(), Block.block_number(), keyword()) :: t() | nil + def get_coin_balance(address_hash, block_number, options \\ []) do + query = fetch_coin_balance(address_hash, block_number) + + Chain.select_repo(options).one(query) + end + + @doc """ + Retrieves paginated coin balance records for a given address with timestamp interpolation. + + This function fetches coin balance history for an address, applying pagination + and performing timestamp calculations for blocks. It includes an optimization + that returns an empty list immediately when the paging key is `{0}`, avoiding + unnecessary database queries. For other cases, it processes balances by + filtering records with values, calculating block timestamp ranges, and + interpolating timestamps for intermediate blocks when multiple blocks are + present. + + ## Parameters + - `address`: Address.t() - The address record to fetch coin balances for + - `options`: [Chain.paging_options() | Chain.api?()] - Query options including + paging configuration and API mode selection + + ## Returns + - `[t()]` - List of coin balance records sorted by block number in descending + order, with interpolated timestamps, or empty list if paging key is `{0}` or + no balances exist + """ + @spec address_to_coin_balances(Address.t(), [Chain.paging_options() | Chain.api?()]) :: [t()] + def address_to_coin_balances(address, options) do + paging_options = Keyword.get(options, :paging_options, PagingOptions.default_paging_options()) + + case paging_options do + %PagingOptions{key: {0}} -> + [] + + _ -> + address_to_coin_balances_internal(address, options, paging_options) + end + end + + defp address_to_coin_balances_internal(address, options, paging_options) do + balances_raw = + address.hash + |> fetch_coin_balances(paging_options) + |> page_coin_balances(paging_options) + |> Chain.select_repo(options).all() + |> preload_transactions(options) + + if Enum.empty?(balances_raw) do + balances_raw + else + balances_raw_filtered = + balances_raw + |> Enum.filter(fn balance -> balance.value end) + + min_block_number = + balances_raw_filtered + |> Enum.min_by(fn balance -> balance.block_number end, fn -> %{} end) + |> Map.get(:block_number) + + max_block_number = + balances_raw_filtered + |> Enum.max_by(fn balance -> balance.block_number end, fn -> %{} end) + |> Map.get(:block_number) + + min_block_timestamp = find_block_timestamp(min_block_number, options) + max_block_timestamp = find_block_timestamp(max_block_number, options) + + min_block_unix_timestamp = + min_block_timestamp + |> Timex.to_unix() + + max_block_unix_timestamp = + max_block_timestamp + |> Timex.to_unix() + + blocks_delta = max_block_number - min_block_number + + balances_with_dates = + if blocks_delta > 0 do + add_block_timestamp_to_balances( + balances_raw_filtered, + min_block_number, + min_block_unix_timestamp, + max_block_unix_timestamp, + blocks_delta + ) + else + add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp) + end + + balances_with_dates + |> Enum.sort(fn balance1, balance2 -> balance1.block_number >= balance2.block_number end) + end + end + + # Here we fetch from DB one transaction per one coin balance. It's much more faster than LEFT OUTER JOIN which was before. + defp preload_transactions(balances, options) do + tasks = + Enum.map(balances, fn balance -> + Task.async(fn -> preload_transactions_task(balance, options) end) + end) + + tasks + |> Task.yield_many(120_000) + |> Enum.zip(balances) + |> Enum.map(fn {{task, res}, balance} -> + case res do + {:ok, hash} -> + put_transaction_hash(hash, balance) + + {:exit, _reason} -> + balance + + nil -> + Task.shutdown(task, :brutal_kill) + balance + end + end) + end + + defp preload_transactions_task(balance, options) do + transaction_hash = + balance + |> preload_transaction_query() + |> Chain.select_repo(options).one() + + if is_nil(transaction_hash) do + balance + |> preload_internal_transaction_query() + |> Chain.select_repo(options).one() + else + transaction_hash + end + end + + defp preload_transaction_query(balance) do + Transaction + |> where( + [transaction], + transaction.block_number == ^balance.block_number and + (transaction.value > ^0 or (transaction.gas_price > ^0 and transaction.gas_used > ^0)) and + (transaction.to_address_hash == ^balance.address_hash or + transaction.from_address_hash == ^balance.address_hash) + ) + |> select([transaction], transaction.hash) + |> limit(1) + end + + defp preload_internal_transaction_query(balance) do + InternalTransaction + |> where( + [internal_transaction], + internal_transaction.block_number == ^balance.block_number and + internal_transaction.type in ~w(call create create2 selfdestruct)a and + (is_nil(internal_transaction.call_type) or internal_transaction.call_type == :call) and + internal_transaction.value > ^0 and is_nil(internal_transaction.error) and + (internal_transaction.to_address_hash == ^balance.address_hash or + internal_transaction.from_address_hash == ^balance.address_hash or + internal_transaction.created_contract_address_hash == ^balance.address_hash) + ) + |> select([internal_transaction], internal_transaction.transaction_hash) + |> limit(1) + end + + defp put_transaction_hash(hash, coin_balance), + do: if(hash, do: %CoinBalance{coin_balance | transaction_hash: hash}, else: coin_balance) + + defp add_block_timestamp_to_balances( + balances_raw_filtered, + min_block_number, + min_block_unix_timestamp, + max_block_unix_timestamp, + blocks_delta + ) do + balances_raw_filtered + |> Enum.map(fn balance -> + date = + trunc( + min_block_unix_timestamp + + (balance.block_number - min_block_number) * (max_block_unix_timestamp - min_block_unix_timestamp) / + blocks_delta + ) + + add_date_to_balance(balance, date) + end) + end + + defp add_min_block_timestamp_to_balances(balances_raw_filtered, min_block_unix_timestamp) do + balances_raw_filtered + |> Enum.map(fn balance -> + date = min_block_unix_timestamp + + add_date_to_balance(balance, date) + end) + end + + defp add_date_to_balance(balance, date) do + formatted_date = Timex.from_unix(date) + %{balance | block_timestamp: formatted_date} + end + + defp page_coin_balances(query, %PagingOptions{key: nil}), do: query + + defp page_coin_balances(query, %PagingOptions{key: {block_number}}) do + where(query, [coin_balance], coin_balance.block_number < ^block_number) + end + + defp find_block_timestamp(number, options) do + Block + |> where([block], block.number == ^number) + |> select([block], block.timestamp) + |> limit(1) + |> Chain.select_repo(options).one() + end + + defp fetch_coin_balance(address_hash, block_number) do coin_balance_subquery = from( cb in CoinBalance, @@ -65,6 +396,7 @@ defmodule Explorer.Chain.Address.CoinBalance do ) end + @doc false def fetch_coin_balances(address_hash, %PagingOptions{page_size: page_size}) do query = from( @@ -86,10 +418,7 @@ defmodule Explorer.Chain.Address.CoinBalance do ) end - @doc """ - Builds an `Ecto.Query` to fetch a series of balances by day for the given account. Each element in the series - corresponds to the maximum balance in that day. Only the last 90 days of data are used. - """ + @doc false def balances_by_day(address_hash, block_timestamp \\ nil) do days_to_consider = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance)[:coin_balance_history_days] @@ -103,7 +432,7 @@ defmodule Explorer.Chain.Address.CoinBalance do |> select([cb, block], %{date: type(fragment("date_trunc('day', ?)", block.timestamp), :date), value: max(cb.value)}) end - def limit_time_interval(query, days_to_consider, nil) do + defp limit_time_interval(query, days_to_consider, nil) do query |> where( [cb, block], @@ -112,7 +441,7 @@ defmodule Explorer.Chain.Address.CoinBalance do ) end - def limit_time_interval(query, days_to_consider, %{timestamp: timestamp}) do + defp limit_time_interval(query, days_to_consider, %{timestamp: timestamp}) do query |> where( [cb, block], @@ -126,41 +455,11 @@ defmodule Explorer.Chain.Address.CoinBalance do ) end - def last_coin_balance_timestamp(address_hash) do - coin_balance_query = - CoinBalance - |> where([cb], cb.address_hash == ^address_hash) - |> last(:block_number) - |> select([cb, b], %{block_number: cb.block_number, value: cb.value}) + defp add_coin_balances_fetcher_limit(query, false), do: query - from( - cb in subquery(coin_balance_query), - inner_join: block in Block, - on: cb.block_number == block.number, - where: block.consensus == true, - select: %{timestamp: block.timestamp, value: cb.value} - ) - end + defp add_coin_balances_fetcher_limit(query, true) do + coin_balances_fetcher_limit = Application.get_env(:indexer, :coin_balances_fetcher_init_limit) - def changeset(%__MODULE__{} = balance, params) do - balance - |> cast(params, @allowed_fields) - |> validate_required(@required_fields) - |> unique_constraint(:block_number, name: :address_coin_balances_address_hash_block_number_index) - end - - @doc """ - Query to fetch latest coin balance for the given address - """ - @spec latest_coin_balance_query(Hash.Address.t(), non_neg_integer() | {:error, :empty_database}) :: Ecto.Query.t() - def latest_coin_balance_query(address_hash, stale_balance_window) do - from( - cb in __MODULE__, - where: cb.address_hash == ^address_hash, - where: cb.block_number >= ^stale_balance_window, - where: is_nil(cb.value_fetched_at), - order_by: [desc: :block_number], - limit: 1 - ) + limit(query, ^coin_balances_fetcher_limit) end end diff --git a/apps/explorer/test/explorer/chain/address/coin_balance_test.exs b/apps/explorer/test/explorer/chain/address/coin_balance_test.exs index 171a2764c033..b87e25750b46 100644 --- a/apps/explorer/test/explorer/chain/address/coin_balance_test.exs +++ b/apps/explorer/test/explorer/chain/address/coin_balance_test.exs @@ -3,7 +3,7 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do alias Ecto.Changeset alias Explorer.Chain.Address.CoinBalance - alias Explorer.Chain.{Block, Wei} + alias Explorer.Chain.{Address, Block, Wei} alias Explorer.PagingOptions describe "changeset/2" do @@ -298,4 +298,408 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do assert(value == Wei.from(Decimal.new(2000), :wei)) end end + + describe "stream_unfetched_balances/2" do + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.Block.t/0` `miner_hash`" do + %Address{hash: miner_hash} = miner = insert(:address) + %Block{number: block_number} = insert(:block, miner: miner) + balance = insert(:unfetched_balance, address_hash: miner_hash, block_number: block_number) + + assert {:ok, [%{address_hash: ^miner_hash, block_number: ^block_number}]} = + CoinBalance.stream_unfetched_balances([], &[&1 | &2]) + + update_balance_value(balance, 1) + + assert {:ok, []} = CoinBalance.stream_unfetched_balances([], &[&1 | &2]) + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.Transaction.t/0` `from_address_hash`" do + %Address{hash: from_address_hash} = from_address = insert(:address) + %Block{number: block_number} = block = insert(:block) + + :transaction + |> insert(from_address: from_address) + |> with_block(block) + + balance = insert(:unfetched_balance, address_hash: from_address_hash, block_number: block_number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{address_hash: from_address_hash, block_number: block_number} in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{address_hash: from_address_hash, block_number: block_number} in balance_fields_list + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.Transaction.t/0` `to_address_hash`" do + %Address{hash: to_address_hash} = to_address = insert(:address) + %Block{number: block_number} = block = insert(:block) + + :transaction + |> insert(to_address: to_address) + |> with_block(block) + + balance = insert(:unfetched_balance, address_hash: to_address_hash, block_number: block_number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{address_hash: to_address_hash, block_number: block_number} in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{address_hash: to_address_hash, block_number: block_number} in balance_fields_list + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.Log.t/0` `address_hash`" do + address = insert(:address) + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:log, address: address, transaction: transaction, block: block, block_number: block.number) + + balance = insert(:unfetched_balance, address_hash: address.hash, block_number: block.number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{ + address_hash: address.hash, + block_number: block.number + } in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{ + address_hash: address.hash, + block_number: block.number + } in balance_fields_list + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.InternalTransaction.t/0` `created_contract_address_hash`" do + created_contract_address = insert(:address) + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :internal_transaction_create, + created_contract_address: created_contract_address, + index: 0, + transaction: transaction, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + balance = insert(:unfetched_balance, address_hash: created_contract_address.hash, block_number: block.number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{ + address_hash: created_contract_address.hash, + block_number: block.number + } in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{ + address_hash: created_contract_address.hash, + block_number: block.number + } in balance_fields_list + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.InternalTransaction.t/0` `from_address_hash`" do + from_address = insert(:address) + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :internal_transaction_create, + from_address: from_address, + index: 0, + transaction: transaction, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + balance = insert(:unfetched_balance, address_hash: from_address.hash, block_number: block.number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{address_hash: from_address.hash, block_number: block.number} in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{address_hash: from_address.hash, block_number: block.number} in balance_fields_list + end + + test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> + "does not return `t:Explorer.Chain.InternalTransaction.t/0` `to_address_hash`" do + to_address = insert(:address) + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :internal_transaction_create, + to_address: to_address, + index: 0, + transaction: transaction, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: 0, + transaction_index: transaction.index + ) + + balance = insert(:unfetched_balance, address_hash: to_address.hash, block_number: block.number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + assert %{address_hash: to_address.hash, block_number: block.number} in balance_fields_list + + update_balance_value(balance, 1) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + refute %{address_hash: to_address.hash, block_number: block.number} in balance_fields_list + end + + test "an address_hash used for multiple block_numbers returns all block_numbers" do + miner = insert(:address) + mined_block = insert(:block, miner: miner) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: mined_block.number) + + from_transaction_block = insert(:block) + + :transaction + |> insert(from_address: miner) + |> with_block(from_transaction_block) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: from_transaction_block.number) + + to_transaction_block = insert(:block) + + :transaction + |> insert(to_address: miner) + |> with_block(to_transaction_block) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: to_transaction_block.number) + + log_block = insert(:block) + + log_transaction = + :transaction + |> insert() + |> with_block(log_block) + + insert(:log, address: miner, transaction: log_transaction, block: log_block, block_number: log_block.number) + insert(:unfetched_balance, address_hash: miner.hash, block_number: log_block.number) + + from_internal_transaction_block = insert(:block) + + from_internal_transaction_transaction = + :transaction + |> insert() + |> with_block(from_internal_transaction_block) + + insert( + :internal_transaction_create, + from_address: miner, + index: 0, + transaction: from_internal_transaction_transaction, + block_number: from_internal_transaction_transaction.block_number, + block_hash: from_internal_transaction_transaction.block_hash, + block_index: 0, + transaction_index: from_internal_transaction_transaction.index + ) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: from_internal_transaction_block.number) + + to_internal_transaction_block = insert(:block) + + to_internal_transaction_transaction = + :transaction + |> insert() + |> with_block(to_internal_transaction_block) + + insert( + :internal_transaction_create, + index: 0, + to_address: miner, + transaction: to_internal_transaction_transaction, + block_number: to_internal_transaction_transaction.block_number, + block_hash: to_internal_transaction_transaction.block_hash, + block_index: 0, + transaction_index: to_internal_transaction_transaction.index + ) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: to_internal_transaction_block.number) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + balance_fields_list_by_address_hash = Enum.group_by(balance_fields_list, & &1.address_hash) + + assert balance_fields_list_by_address_hash[miner.hash] |> Enum.map(& &1.block_number) |> Enum.sort() == + Enum.sort([ + to_internal_transaction_block.number, + from_internal_transaction_block.number, + log_block.number, + to_transaction_block.number, + from_transaction_block.number, + mined_block.number + ]) + end + + test "an address_hash used for the same block_number is only returned once" do + miner = insert(:address) + block = insert(:block, miner: miner) + + insert(:unfetched_balance, address_hash: miner.hash, block_number: block.number) + + :transaction + |> insert(from_address: miner) + |> with_block(block) + + :transaction + |> insert(to_address: miner) + |> with_block(block) + + log_transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:log, address: miner, transaction: log_transaction, block: block, block_number: block.number) + + from_internal_transaction_transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :internal_transaction_create, + from_address: miner, + index: 0, + transaction: from_internal_transaction_transaction, + block_number: from_internal_transaction_transaction.block_number, + block_hash: from_internal_transaction_transaction.block_hash, + block_index: 0, + transaction_index: from_internal_transaction_transaction.index + ) + + to_internal_transaction_transaction = + :transaction + |> insert() + |> with_block(block) + + insert( + :internal_transaction_create, + to_address: miner, + index: 0, + transaction: to_internal_transaction_transaction, + block_number: to_internal_transaction_transaction.block_number, + block_hash: to_internal_transaction_transaction.block_hash, + block_index: 1, + transaction_index: to_internal_transaction_transaction.index + ) + + {:ok, balance_fields_list} = + CoinBalance.stream_unfetched_balances( + [], + fn balance_fields, acc -> [balance_fields | acc] end + ) + + balance_fields_list_by_address_hash = Enum.group_by(balance_fields_list, & &1.address_hash) + + assert balance_fields_list_by_address_hash[miner.hash] |> Enum.map(& &1.block_number) |> Enum.sort() == [ + block.number + ] + end + end end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 95f020982770..1e2463740285 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -2226,410 +2226,6 @@ defmodule Explorer.ChainTest do end end - describe "stream_unfetched_balances/2" do - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.Block.t/0` `miner_hash`" do - %Address{hash: miner_hash} = miner = insert(:address) - %Block{number: block_number} = insert(:block, miner: miner) - balance = insert(:unfetched_balance, address_hash: miner_hash, block_number: block_number) - - assert {:ok, [%{address_hash: ^miner_hash, block_number: ^block_number}]} = - Chain.stream_unfetched_balances([], &[&1 | &2]) - - update_balance_value(balance, 1) - - assert {:ok, []} = Chain.stream_unfetched_balances([], &[&1 | &2]) - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.Transaction.t/0` `from_address_hash`" do - %Address{hash: from_address_hash} = from_address = insert(:address) - %Block{number: block_number} = block = insert(:block) - - :transaction - |> insert(from_address: from_address) - |> with_block(block) - - balance = insert(:unfetched_balance, address_hash: from_address_hash, block_number: block_number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{address_hash: from_address_hash, block_number: block_number} in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{address_hash: from_address_hash, block_number: block_number} in balance_fields_list - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.Transaction.t/0` `to_address_hash`" do - %Address{hash: to_address_hash} = to_address = insert(:address) - %Block{number: block_number} = block = insert(:block) - - :transaction - |> insert(to_address: to_address) - |> with_block(block) - - balance = insert(:unfetched_balance, address_hash: to_address_hash, block_number: block_number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{address_hash: to_address_hash, block_number: block_number} in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{address_hash: to_address_hash, block_number: block_number} in balance_fields_list - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.Log.t/0` `address_hash`" do - address = insert(:address) - block = insert(:block) - - transaction = - :transaction - |> insert() - |> with_block(block) - - insert(:log, address: address, transaction: transaction, block: block, block_number: block.number) - - balance = insert(:unfetched_balance, address_hash: address.hash, block_number: block.number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{ - address_hash: address.hash, - block_number: block.number - } in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{ - address_hash: address.hash, - block_number: block.number - } in balance_fields_list - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.InternalTransaction.t/0` `created_contract_address_hash`" do - created_contract_address = insert(:address) - block = insert(:block) - - transaction = - :transaction - |> insert() - |> with_block(block) - - insert( - :internal_transaction_create, - created_contract_address: created_contract_address, - index: 0, - transaction: transaction, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - balance = insert(:unfetched_balance, address_hash: created_contract_address.hash, block_number: block.number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{ - address_hash: created_contract_address.hash, - block_number: block.number - } in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{ - address_hash: created_contract_address.hash, - block_number: block.number - } in balance_fields_list - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.InternalTransaction.t/0` `from_address_hash`" do - from_address = insert(:address) - block = insert(:block) - - transaction = - :transaction - |> insert() - |> with_block(block) - - insert( - :internal_transaction_create, - from_address: from_address, - index: 0, - transaction: transaction, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - balance = insert(:unfetched_balance, address_hash: from_address.hash, block_number: block.number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{address_hash: from_address.hash, block_number: block.number} in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{address_hash: from_address.hash, block_number: block.number} in balance_fields_list - end - - test "with `t:Explorer.Chain.Address.CoinBalance.t/0` with value_fetched_at with same `address_hash` and `block_number` " <> - "does not return `t:Explorer.Chain.InternalTransaction.t/0` `to_address_hash`" do - to_address = insert(:address) - block = insert(:block) - - transaction = - :transaction - |> insert() - |> with_block(block) - - insert( - :internal_transaction_create, - to_address: to_address, - index: 0, - transaction: transaction, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index - ) - - balance = insert(:unfetched_balance, address_hash: to_address.hash, block_number: block.number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - assert %{address_hash: to_address.hash, block_number: block.number} in balance_fields_list - - update_balance_value(balance, 1) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - refute %{address_hash: to_address.hash, block_number: block.number} in balance_fields_list - end - - test "an address_hash used for multiple block_numbers returns all block_numbers" do - miner = insert(:address) - mined_block = insert(:block, miner: miner) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: mined_block.number) - - from_transaction_block = insert(:block) - - :transaction - |> insert(from_address: miner) - |> with_block(from_transaction_block) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: from_transaction_block.number) - - to_transaction_block = insert(:block) - - :transaction - |> insert(to_address: miner) - |> with_block(to_transaction_block) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: to_transaction_block.number) - - log_block = insert(:block) - - log_transaction = - :transaction - |> insert() - |> with_block(log_block) - - insert(:log, address: miner, transaction: log_transaction, block: log_block, block_number: log_block.number) - insert(:unfetched_balance, address_hash: miner.hash, block_number: log_block.number) - - from_internal_transaction_block = insert(:block) - - from_internal_transaction_transaction = - :transaction - |> insert() - |> with_block(from_internal_transaction_block) - - insert( - :internal_transaction_create, - from_address: miner, - index: 0, - transaction: from_internal_transaction_transaction, - block_number: from_internal_transaction_transaction.block_number, - block_hash: from_internal_transaction_transaction.block_hash, - block_index: 0, - transaction_index: from_internal_transaction_transaction.index - ) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: from_internal_transaction_block.number) - - to_internal_transaction_block = insert(:block) - - to_internal_transaction_transaction = - :transaction - |> insert() - |> with_block(to_internal_transaction_block) - - insert( - :internal_transaction_create, - index: 0, - to_address: miner, - transaction: to_internal_transaction_transaction, - block_number: to_internal_transaction_transaction.block_number, - block_hash: to_internal_transaction_transaction.block_hash, - block_index: 0, - transaction_index: to_internal_transaction_transaction.index - ) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: to_internal_transaction_block.number) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - balance_fields_list_by_address_hash = Enum.group_by(balance_fields_list, & &1.address_hash) - - assert balance_fields_list_by_address_hash[miner.hash] |> Enum.map(& &1.block_number) |> Enum.sort() == - Enum.sort([ - to_internal_transaction_block.number, - from_internal_transaction_block.number, - log_block.number, - to_transaction_block.number, - from_transaction_block.number, - mined_block.number - ]) - end - - test "an address_hash used for the same block_number is only returned once" do - miner = insert(:address) - block = insert(:block, miner: miner) - - insert(:unfetched_balance, address_hash: miner.hash, block_number: block.number) - - :transaction - |> insert(from_address: miner) - |> with_block(block) - - :transaction - |> insert(to_address: miner) - |> with_block(block) - - log_transaction = - :transaction - |> insert() - |> with_block(block) - - insert(:log, address: miner, transaction: log_transaction, block: block, block_number: block.number) - - from_internal_transaction_transaction = - :transaction - |> insert() - |> with_block(block) - - insert( - :internal_transaction_create, - from_address: miner, - index: 0, - transaction: from_internal_transaction_transaction, - block_number: from_internal_transaction_transaction.block_number, - block_hash: from_internal_transaction_transaction.block_hash, - block_index: 0, - transaction_index: from_internal_transaction_transaction.index - ) - - to_internal_transaction_transaction = - :transaction - |> insert() - |> with_block(block) - - insert( - :internal_transaction_create, - to_address: miner, - index: 0, - transaction: to_internal_transaction_transaction, - block_number: to_internal_transaction_transaction.block_number, - block_hash: to_internal_transaction_transaction.block_hash, - block_index: 1, - transaction_index: to_internal_transaction_transaction.index - ) - - {:ok, balance_fields_list} = - Explorer.Chain.stream_unfetched_balances( - [], - fn balance_fields, acc -> [balance_fields | acc] end - ) - - balance_fields_list_by_address_hash = Enum.group_by(balance_fields_list, & &1.address_hash) - - assert balance_fields_list_by_address_hash[miner.hash] |> Enum.map(& &1.block_number) |> Enum.sort() == [ - block.number - ] - end - end - describe "update_replaced_transactions/2" do test "update replaced transactions" do replaced_transaction_hash = "0x2a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex index 8487c7b0d187..f063137d586c 100644 --- a/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex +++ b/apps/indexer/lib/indexer/fetcher/coin_balance/catchup.ex @@ -7,7 +7,7 @@ defmodule Indexer.Fetcher.CoinBalance.Catchup do use Indexer.Fetcher, restart: :permanent use Spandex.Decorators - alias Explorer.Chain + alias Explorer.Chain.Address.CoinBalance alias Explorer.Chain.{Block, Hash} alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.CoinBalance.Catchup.Supervisor, as: CoinBalanceSupervisor @@ -41,7 +41,7 @@ defmodule Indexer.Fetcher.CoinBalance.Catchup do @impl BufferedTask def init(initial, reducer, _) do {:ok, final} = - Chain.stream_unfetched_balances( + CoinBalance.stream_unfetched_balances( initial, fn address_fields, acc -> address_fields diff --git a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs index 378d19bb312b..d11fc4910b01 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs @@ -8,6 +8,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain + alias Explorer.Chain.Address.CoinBalance alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.Wei alias Explorer.Chain.Cache.Counters.AverageBlockTime @@ -116,7 +117,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do block_number = block.number string_block_number = integer_to_quantity(block_number) balance = 42 - assert nil == Chain.get_coin_balance(address.hash, block_number) + assert nil == CoinBalance.get_coin_balance(address.hash, block_number) EthereumJSONRPC.Mox |> expect(:json_rpc, fn [ @@ -149,7 +150,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do :timer.sleep(1000) - assert %{value: ^expected_wei} = Chain.get_coin_balance(address.hash, block_number) + assert %{value: ^expected_wei} = CoinBalance.get_coin_balance(address.hash, block_number) end end From 578c9a4f906b56cd23fecffde66e94cf2bd78a0d Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 1 Jul 2025 12:32:34 +0400 Subject: [PATCH 0114/1006] chore: Optimize realtime events notifier (#12494) --- .../lib/block_scout_web/notifier.ex | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 831cf86599a7..021bd5713691 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -704,14 +704,15 @@ defmodule BlockScoutWeb.Notifier do }) end - v2_params_function = fn transactions -> + prepared_transactions = TransactionView.render("transactions.json", %{ transactions: Repo.preload(transactions, @transaction_associations), conn: nil }) - end - group_by_address_hashes_and_broadcast(transactions, event, :transactions, v2_params_function) + transactions + |> Enum.zip(prepared_transactions) + |> group_by_address_hashes_and_broadcast(event, :transactions, & &1["hash"]) end defp broadcast_transaction(%Transaction{block_number: nil} = pending) do @@ -749,14 +750,19 @@ defmodule BlockScoutWeb.Notifier do }) end - v2_params_function = fn transfers -> + prepared_token_transfers = TransactionView.render("token_transfers.json", %{ - token_transfers: transfers, + token_transfers: tokens_transfers, conn: nil }) - end - group_by_address_hashes_and_broadcast(tokens_transfers, "token_transfer", :token_transfers, v2_params_function) + tokens_transfers + |> Enum.zip(prepared_token_transfers) + |> group_by_address_hashes_and_broadcast( + "token_transfer", + :token_transfers, + &{&1["transaction_hash"], &1["block_hash"], &1["log_index"]} + ) end defp broadcast_token_transfer(token_transfer) do @@ -782,19 +788,19 @@ defmodule BlockScoutWeb.Notifier do end end - defp group_by_address_hashes_and_broadcast(elements, event, map_key, params_function) do + defp group_by_address_hashes_and_broadcast(elements, event, map_key, uniq_function) do grouped_by_from = elements - |> Enum.group_by(fn el -> el.from_address_hash end) + |> Enum.group_by(fn {el, _} -> el.from_address_hash end, fn {_, prepared_el} -> prepared_el end) grouped_by_to = elements - |> Enum.group_by(fn el -> el.to_address_hash end) + |> Enum.group_by(fn {el, _} -> el.to_address_hash end, fn {_, prepared_el} -> prepared_el end) - grouped = Map.merge(grouped_by_to, grouped_by_from, fn _k, v1, v2 -> Enum.uniq(v1 ++ v2) end) + grouped = Map.merge(grouped_by_to, grouped_by_from, fn _k, v1, v2 -> Enum.uniq_by(v1 ++ v2, uniq_function) end) for {address_hash, elements} <- grouped do - Endpoint.broadcast("addresses:#{address_hash}", event, %{map_key => params_function.(elements)}) + Endpoint.broadcast("addresses:#{address_hash}", event, %{map_key => elements}) end end From a6984a0b8478505189692d630534dff9dc71bdfe Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:37:55 +0300 Subject: [PATCH 0115/1006] chore: Add filter for value > 0 to txlistinternal (#12679) --- .../controllers/api/rpc/address_controller.ex | 71 ++- .../api/rpc/address_controller_test.exs | 538 +++++++++++++++++- .../explorer/chain/internal_transaction.ex | 22 + apps/explorer/lib/explorer/etherscan.ex | 35 +- .../explorer/test/explorer/etherscan_test.exs | 257 ++++++++- 5 files changed, 865 insertions(+), 58 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index b15f8acf453b..bcaf7b543b26 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -131,35 +131,25 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:error, :error} -> txlistinternal(conn, params, :no_param) - {{:ok, transaction_param}, :error} -> - txlistinternal(conn, transaction_param, :transaction) - - {:error, {:ok, address_param}} -> + {_, {:ok, address_param}} -> txlistinternal(conn, params, address_param, :address) - end - end - - def txlistinternal(conn, transaction_param, :transaction) do - with {:format, {:ok, transaction_hash}} <- to_transaction_hash(transaction_param), - {:ok, internal_transactions} <- list_internal_transactions(transaction_hash) do - render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) - else - {:format, :error} -> - render(conn, :error, error: "Invalid txhash format") - {:error, :not_found} -> - render(conn, :error, error: "No internal transactions found", data: []) + {{:ok, transaction_param}, _} -> + txlistinternal(conn, params, transaction_param, :transaction) end end - def txlistinternal(conn, params, :no_param) do + def txlistinternal(conn, params, transaction_param, :transaction) do options = params |> optional_params() - case list_internal_transactions(:all, options) do - {:ok, internal_transactions} -> - render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + with {:format, {:ok, transaction_hash}} <- to_transaction_hash(transaction_param), + {:ok, internal_transactions} <- list_internal_transactions(transaction_hash, options) do + render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + else + {:format, :error} -> + render(conn, :error, error: "Invalid txhash format") {:error, :not_found} -> render(conn, :error, error: "No internal transactions found", data: []) @@ -182,6 +172,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end + def txlistinternal(conn, params, :no_param) do + options = + params + |> optional_params() + + case list_internal_transactions(:all, options) do + {:ok, internal_transactions} -> + render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + + {:error, :not_found} -> + render(conn, :error, error: "No internal transactions found", data: []) + end + end + def tokentx(conn, params) do options = optional_params(params) @@ -294,6 +298,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do @spec optional_params(map()) :: map() def optional_params(params) do %{} + |> put_boolean(params, "include_zero_value", :include_zero_value) |> put_order_by_direction(params) |> Helper.put_pagination_options(params) |> put_block(params, "startblock") @@ -452,6 +457,14 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:format, Chain.string_to_full_hash(transaction_hash_string)} end + defp put_boolean(options, params, params_key, options_key) do + case params |> Map.get(params_key, "") |> String.downcase() do + "true" -> Map.put(options, options_key, true) + "false" -> Map.put(options, options_key, false) + _ -> options + end + end + defp put_order_by_direction(options, params) do case params do %{"sort" => sort} when sort in ["asc", "desc"] -> @@ -516,22 +529,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end - defp list_internal_transactions(transaction_hash) do - case Etherscan.list_internal_transactions(transaction_hash) do - [] -> {:error, :not_found} - internal_transactions -> {:ok, internal_transactions} - end - end - - defp list_internal_transactions(:all, options) do - case Etherscan.list_internal_transactions(:all, options) do - [] -> {:error, :not_found} - internal_transactions -> {:ok, internal_transactions} - end - end - - defp list_internal_transactions(address_hash, options) do - case Etherscan.list_internal_transactions(address_hash, options) do + defp list_internal_transactions(transaction_or_address_hash_param_or_no_param, options) do + case Etherscan.list_internal_transactions(transaction_or_address_hash_param_or_no_param, options) do [] -> {:error, :not_found} internal_transactions -> {:ok, internal_transactions} end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index f04f026fa84d..7fe69832b077 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -1835,6 +1835,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> insert( transaction: transaction, index: 0, + value: 1, from_address: address, to_address: address_2, block_hash: transaction.block_hash, @@ -1847,6 +1848,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> insert( transaction: transaction, index: 1, + value: 2, from_address: address, to_address: address_2, block_hash: transaction.block_hash, @@ -1884,6 +1886,185 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + + test "returns only non zero value internal transactions by default", %{conn: conn, params: params} do + address = insert(:address) + address_2 = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address, to_address: address_2) + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + internal_transaction = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction.input}", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with zero value internal transactions when `include_zero_value=true` param is provided", %{ + conn: conn, + params: params + } do + address = insert(:address) + address_2 = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert(from_address: address, to_address: address_2) + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + internal_transaction_a = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + internal_transaction_b = + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_b.from_address_hash}", + "to" => "#{internal_transaction_b.to_address_hash}", + "value" => "#{internal_transaction_b.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_b.input}", + "type" => "#{internal_transaction_b.type}", + "callType" => "#{internal_transaction_b.call_type}", + "gas" => "#{internal_transaction_b.gas}", + "gasUsed" => "#{internal_transaction_b.gas_used}", + "index" => "#{internal_transaction_b.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_b.error}" + }, + %{ + "blockNumber" => "#{internal_transaction_a.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_a.from_address_hash}", + "to" => "#{internal_transaction_a.to_address_hash}", + "value" => "#{internal_transaction_a.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_a.input}", + "type" => "#{internal_transaction_a.type}", + "callType" => "#{internal_transaction_a.call_type}", + "gas" => "#{internal_transaction_a.gas}", + "gasUsed" => "#{internal_transaction_a.gas_used}", + "index" => "#{internal_transaction_a.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_a.error}" + } + ] + + assert response = + conn + |> get("/api/v1", Map.merge(params, %{"include_zero_value" => "true"})) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end end describe "txlistinternal with txhash" do @@ -1941,6 +2122,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> insert( transaction: transaction, index: 0, + value: 1, from_address: address, block_hash: transaction.block_hash, block_index: 0 @@ -1993,6 +2175,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do internal_transaction_details = [ transaction: transaction, index: 0, + value: 1, type: :reward, error: "some error", block_hash: transaction.block_hash, @@ -2030,7 +2213,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do transaction: transaction, index: index, block_hash: transaction.block_hash, - block_index: index + block_index: index, + value: 1 ) end @@ -2051,6 +2235,177 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + + test "returns only non zero value internal transactions by default", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + internal_transaction = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction.input}", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}" + } + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with zero value internal transactions when `include_zero_value=true` param is provided", %{conn: conn} do + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + internal_transaction_a = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + internal_transaction_b = + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_a.from_address_hash}", + "to" => "#{internal_transaction_a.to_address_hash}", + "value" => "#{internal_transaction_a.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_a.input}", + "type" => "#{internal_transaction_a.type}", + "callType" => "#{internal_transaction_a.call_type}", + "gas" => "#{internal_transaction_a.gas}", + "gasUsed" => "#{internal_transaction_a.gas_used}", + "index" => "#{internal_transaction_a.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_a.error}" + }, + %{ + "blockNumber" => "#{internal_transaction_b.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_b.from_address_hash}", + "to" => "#{internal_transaction_b.to_address_hash}", + "value" => "#{internal_transaction_b.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_b.input}", + "type" => "#{internal_transaction_b.type}", + "callType" => "#{internal_transaction_b.call_type}", + "gas" => "#{internal_transaction_b.gas}", + "gasUsed" => "#{internal_transaction_b.gas_used}", + "index" => "#{internal_transaction_b.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_b.error}" + } + ] + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}", + "include_zero_value" => "true" + } + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end end describe "txlistinternal with address" do @@ -2229,6 +2584,187 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + + test "returns only non zero value internal transactions by default", %{conn: conn} do + address = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + from_address: address, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + internal_transaction = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + from_address: address, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction.input}", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}" + } + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + + test "with zero value internal transactions when `include_zero_value=true` param is provided", %{conn: conn} do + address = insert(:address) + + block = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + internal_transaction_a = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + from_address: address, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + internal_transaction_b = + :internal_transaction + |> insert( + transaction: transaction, + index: 2, + value: 0, + from_address: address, + block_hash: transaction.block_hash, + block_index: 2, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_b.from_address_hash}", + "to" => "#{internal_transaction_b.to_address_hash}", + "value" => "#{internal_transaction_b.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_b.input}", + "type" => "#{internal_transaction_b.type}", + "callType" => "#{internal_transaction_b.call_type}", + "gas" => "#{internal_transaction_b.gas}", + "gasUsed" => "#{internal_transaction_b.gas_used}", + "index" => "#{internal_transaction_b.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_b.error}" + }, + %{ + "blockNumber" => "#{internal_transaction_a.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction_a.from_address_hash}", + "to" => "#{internal_transaction_a.to_address_hash}", + "value" => "#{internal_transaction_a.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction_a.input}", + "type" => "#{internal_transaction_a.type}", + "callType" => "#{internal_transaction_a.call_type}", + "gas" => "#{internal_transaction_a.gas}", + "gasUsed" => "#{internal_transaction_a.gas_used}", + "index" => "#{internal_transaction_a.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction_a.error}" + } + ] + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}", + "include_zero_value" => "true" + } + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end end describe "tokentx" do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 74918e94c874..df04f8ee8a84 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -977,4 +977,26 @@ defmodule Explorer.Chain.InternalTransaction do query |> where([internal_transaction], internal_transaction.transaction_hash == ^transaction_hash) end + + @doc """ + Conditionally filters internal transactions to include or exclude zero-value transfers. + + When `include_zero` is `true`, the query remains unchanged and will return all + internal transactions regardless of their value. When `include_zero` is `false`, + the query is modified to exclude internal transactions where the transferred + value is zero, returning only transactions with positive Wei values. + + ## Parameters + - `query`: An Ecto query for internal transactions + - `include_zero`: Whether to include zero-value internal transactions + + ## Returns + - Modified Ecto query that either includes or excludes zero-value transfers + """ + @spec include_zero_value(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + def include_zero_value(query, true), do: query + + def include_zero_value(query, false) do + where(query, [internal_transaction], internal_transaction.value > ^0) + end end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 0cb6aea24dd6..6ca55ae3f3da 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -27,6 +27,7 @@ defmodule Explorer.Etherscan do @default_options %{ order_by_direction: :desc, + include_zero_value: false, page_number: 1, page_size: 10_000, startblock: nil, @@ -99,7 +100,8 @@ defmodule Explorer.Etherscan do )a @doc """ - Gets a list of internal transactions for a given transaction hash + Gets a list of all internal transactions (with :all option) or for a given address hash + (`t:Explorer.Chain.Hash.Address.t/0`) or transaction hash (`t:Explorer.Chain.Hash.Full.t/0`). Note that this function relies on `Explorer.Chain` to exclude/include @@ -109,10 +111,13 @@ defmodule Explorer.Etherscan do transaction * include internal transactions of type create, reward, or selfdestruct even when they are alone in the parent transaction - """ - @spec list_internal_transactions(Hash.Full.t()) :: [map()] - def list_internal_transactions(%Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash) do + @spec list_internal_transactions(Hash.Full.t() | Hash.Address.t() | :all, map()) :: [map()] + def list_internal_transactions(transaction_or_address_hash_param_or_no_param, raw_options \\ %{}) + + def list_internal_transactions(%Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash, raw_options) do + options = Map.merge(@default_options, raw_options) + query = if DenormalizationHelper.transactions_denormalization_finished?() do from( @@ -146,25 +151,10 @@ defmodule Explorer.Etherscan do |> InternalTransaction.where_transaction_has_multiple_internal_transactions() |> InternalTransaction.where_is_different_from_parent_transaction() |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> Repo.replica().all() end - @doc """ - Gets a list of all internal transactions (with :all option) or for a given address hash - (`t:Explorer.Chain.Hash.Address.t/0`). - - Note that this function relies on `Explorer.Chain` to exclude/include - internal transactions as follows: - - * exclude internal transactions of type call with no siblings in the - transaction - * include internal transactions of type create, reward, or selfdestruct - even when they are alone in the parent transaction - - """ - @spec list_internal_transactions(Hash.Address.t() | :all, map()) :: [map()] - def list_internal_transactions(address_hash_param_or_no_param, raw_options \\ %{}) - def list_internal_transactions( %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, raw_options @@ -184,6 +174,7 @@ defmodule Explorer.Etherscan do |> InternalTransaction.where_transaction_has_multiple_internal_transactions() |> InternalTransaction.where_address_fields_match(address_hash, direction) |> InternalTransaction.where_is_different_from_parent_transaction() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match(options) |> where_end_block_match(options) |> InternalTransaction.where_nonpending_block() @@ -196,6 +187,7 @@ defmodule Explorer.Etherscan do query |> InternalTransaction.where_address_fields_match(address_hash, :to_address_hash) |> InternalTransaction.where_is_different_from_parent_transaction() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match(options) |> where_end_block_match(options) |> Chain.wrapped_union_subquery() @@ -204,6 +196,7 @@ defmodule Explorer.Etherscan do query |> InternalTransaction.where_address_fields_match(address_hash, :from_address_hash) |> InternalTransaction.where_is_different_from_parent_transaction() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match(options) |> where_end_block_match(options) |> Chain.wrapped_union_subquery() @@ -212,6 +205,7 @@ defmodule Explorer.Etherscan do query |> InternalTransaction.where_address_fields_match(address_hash, :created_contract_address_hash) |> InternalTransaction.where_is_different_from_parent_transaction() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match(options) |> where_end_block_match(options) |> Chain.wrapped_union_subquery() @@ -242,6 +236,7 @@ defmodule Explorer.Etherscan do options |> internal_transactions_query(consensus_blocks) |> InternalTransaction.where_is_different_from_parent_transaction() + |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match(options) |> where_end_block_match(options) |> Repo.replica().all() diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index 37b6e685d618..4cbee4ff49c7 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -615,7 +615,7 @@ defmodule Explorer.EtherscanTest do end end - describe "list_internal_transactions/1 with transaction hash" do + describe "list_internal_transactions/2 with transaction hash" do test "with empty db" do transaction = build(:transaction) @@ -639,6 +639,7 @@ defmodule Explorer.EtherscanTest do |> insert( transaction: transaction, index: 0, + value: 1, from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, @@ -684,6 +685,7 @@ defmodule Explorer.EtherscanTest do insert(:internal_transaction, transaction: transaction, index: index, + value: index + 1, block_number: transaction.block_number, block_hash: transaction.block_hash, block_index: index, @@ -711,6 +713,7 @@ defmodule Explorer.EtherscanTest do insert(:internal_transaction, transaction: transaction1, index: 0, + value: 1, block_number: transaction1.block_number, block_hash: transaction1.block_hash, block_index: 0, @@ -720,6 +723,7 @@ defmodule Explorer.EtherscanTest do insert(:internal_transaction, transaction: transaction1, index: 1, + value: 2, block_number: transaction1.block_number, block_hash: transaction1.block_hash, block_index: 1, @@ -729,6 +733,7 @@ defmodule Explorer.EtherscanTest do insert(:internal_transaction, transaction: transaction2, index: 0, + value: 3, type: :reward, block_number: transaction2.block_number, block_hash: transaction2.block_hash, @@ -746,6 +751,78 @@ defmodule Explorer.EtherscanTest do assert length(internal_transactions2) == 1 end + test "only non zero value internal transactions by default" do + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..2 do + insert(:internal_transaction, + transaction: transaction, + index: index, + value: index + 1, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + ) + end + + for index <- 3..5 do + insert(:internal_transaction, + transaction: transaction, + index: index, + value: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + ) + end + + found_internal_transactions = Etherscan.list_internal_transactions(transaction.hash) + + assert length(found_internal_transactions) == 2 + end + + test "with zero value internal transactions when `include_zero_value: true` option is set" do + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..2 do + insert(:internal_transaction, + transaction: transaction, + index: index, + value: index + 1, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + ) + end + + for index <- 3..5 do + insert(:internal_transaction, + transaction: transaction, + index: index, + value: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + ) + end + + options = %{include_zero_value: true} + + found_internal_transactions = Etherscan.list_internal_transactions(transaction.hash, options) + + assert length(found_internal_transactions) == 5 + end + # Note that `list_internal_transactions/1` relies on # `Chain.where_transaction_has_multiple_transactions/1` to ensure the # following behavior: @@ -989,6 +1066,94 @@ defmodule Explorer.EtherscanTest do end end + test "only non zero value internal transactions by default" do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..3 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: 0, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + for index <- 4..5 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: index, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + found_internal_transactions = Etherscan.list_internal_transactions(address.hash) + + assert length(found_internal_transactions) == 2 + end + + test "with zero value internal transactions when `include_zero_value: true` option is set" do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..3 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: 0, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + for index <- 4..5 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: index, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + options = %{include_zero_value: true} + + found_internal_transactions = Etherscan.list_internal_transactions(address.hash, options) + + assert length(found_internal_transactions) == 5 + end + # Note that `list_internal_transactions/2` relies on # `Chain.where_transaction_has_multiple_transactions/1` to ensure the # following behavior: @@ -1002,6 +1167,96 @@ defmodule Explorer.EtherscanTest do # These two requirements are tested in `Explorer.ChainTest`. end + describe "list_internal_transactions/2 without param" do + test "only non zero value internal transactions by default" do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..3 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: 0, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + for index <- 4..5 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: index, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + found_internal_transactions = Etherscan.list_internal_transactions(:all) + + assert length(found_internal_transactions) == 2 + end + + test "with zero value internal transactions when `include_zero_value: true` option is set" do + address = insert(:address) + + transaction = + :transaction + |> insert() + |> with_block() + + for index <- 0..3 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: 0, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + for index <- 4..5 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + value: index, + from_address: address, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + block_index: index, + transaction_index: transaction.index + } + + insert(:internal_transaction, internal_transaction_details) + end + + options = %{include_zero_value: true} + + found_internal_transactions = Etherscan.list_internal_transactions(:all, options) + + assert length(found_internal_transactions) == 5 + end + end + describe "list_token_transfers/2" do test "with empty db" do address = build(:address) From 969f31026482f7b49e250f246940d98e18a9c259 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:40:54 +0300 Subject: [PATCH 0116/1006] chore(deps): bump nimble_csv from 1.2.0 to 1.3.0 (#12690) Bumps [nimble_csv](https://github.com/dashbitco/nimble_csv) from 1.2.0 to 1.3.0. - [Changelog](https://github.com/dashbitco/nimble_csv/blob/master/CHANGELOG.md) - [Commits](https://github.com/dashbitco/nimble_csv/compare/v1.2.0...v1.3.0) --- updated-dependencies: - dependency-name: nimble_csv dependency-version: 1.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index a163c93f7e17..f67c151b8f90 100644 --- a/mix.lock +++ b/mix.lock @@ -109,7 +109,7 @@ "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, - "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, + "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_ownership": {:hex, :nimble_ownership, "1.0.1", "f69fae0cdd451b1614364013544e66e4f5d25f36a2056a9698b793305c5aa3a6", [:mix], [], "hexpm", "3825e461025464f519f3f3e4a1f9b68c47dc151369611629ad08b636b73bb22d"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, From 748c8b989a2cdef304adee1f77dcc2d18992d42e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 11:55:15 +0300 Subject: [PATCH 0117/1006] chore(deps): bump cachex from 4.1.0 to 4.1.1 (#12693) Bumps [cachex](https://github.com/whitfin/cachex) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/whitfin/cachex/releases) - [Commits](https://github.com/whitfin/cachex/commits) --- updated-dependencies: - dependency-name: cachex dependency-version: 4.1.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index f67c151b8f90..37ba50cd6240 100644 --- a/mix.lock +++ b/mix.lock @@ -14,7 +14,7 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "cachex": {:hex, :cachex, "4.1.0", "f9a9123feac30df2b75260c9a74903c0bd7ec54f345f5f6f60c7830b81ead43a", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "77f9417a5549e424bdbfeea3f8060841384c39c0b5f563a8f62f1f2c2b6d2345"}, + "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, From 69a7533c28bc7d8365d8ca1e36a906572d6d3d91 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:09:42 +0300 Subject: [PATCH 0118/1006] chore(deps): bump typed_ecto_schema from 0.4.1 to 0.4.3 (#12692) Bumps [typed_ecto_schema](https://github.com/bamorim/typed_ecto_schema) from 0.4.1 to 0.4.3. - [Release notes](https://github.com/bamorim/typed_ecto_schema/releases) - [Commits](https://github.com/bamorim/typed_ecto_schema/compare/0.4.1...0.4.3) --- updated-dependencies: - dependency-name: typed_ecto_schema dependency-version: 0.4.3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 37ba50cd6240..00edbea82a3b 100644 --- a/mix.lock +++ b/mix.lock @@ -161,7 +161,7 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, - "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.1", "a373ca6f693f4de84cde474a67467a9cb9051a8a7f3f615f1e23dc74b75237fa", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "85c6962f79d35bf543dd5659c6adc340fd2480cacc6f25d2cc2933ea6e8fcb3b"}, + "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"}, "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.1.0", "0632d5844049fa2f26823f15e1120aa32f27df6f27ce515a4b04641736594bf4", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "8d3b30fa27c95c9e82c30c4afb016251405706d2e9627e603c3c9787fd1314fc"}, From bfc8c25a6033469bc967d83518550f7b9d2a2692 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:10:06 +0300 Subject: [PATCH 0119/1006] chore(deps): bump phoenix_ecto from 4.6.4 to 4.6.5 (#12695) Bumps [phoenix_ecto](https://github.com/phoenixframework/phoenix_ecto) from 4.6.4 to 4.6.5. - [Changelog](https://github.com/phoenixframework/phoenix_ecto/blob/main/CHANGELOG.md) - [Commits](https://github.com/phoenixframework/phoenix_ecto/commits/v4.6.5) --- updated-dependencies: - dependency-name: phoenix_ecto dependency-version: 4.6.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 00edbea82a3b..9396f4a4b1a0 100644 --- a/mix.lock +++ b/mix.lock @@ -34,7 +34,7 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, - "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, + "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, @@ -122,7 +122,7 @@ "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.5.14", "2d5db884be496eefa5157505ec0134e66187cb416c072272420c5509d67bf808", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "207f1aa5520320cbb7940d7ff2dde2342162cf513875848f88249ea0ba02fef7"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, From 4b171ed2f7d8683d6b0ab21791903c5aa8407a1c Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:54:32 +0300 Subject: [PATCH 0120/1006] fix: Fix timeout on fetching address internal transactions (#12570) * fix: Fix timeout on fetching address internal transactions * Improve test --- .../api/v2/address_controller_test.exs | 2 +- .../lib/explorer/chain/address/counters.ex | 26 +++++++++++----- .../explorer/chain/internal_transaction.ex | 31 ++++++++++++++++--- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 47cba82887cf..3f02caed040e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -3256,7 +3256,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do transaction_index: transaction.index, block_hash: transaction.block_hash, block_index: x, - from_address: address + to_address: address ) end diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index 7a534d5a51be..fabd7f91e438 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -2,12 +2,12 @@ defmodule Explorer.Chain.Address.Counters do @moduledoc """ Functions related to Explorer.Chain.Address counters """ - import Ecto.Query, only: [from: 2, limit: 2, select: 3, union: 2, where: 3] + import Ecto.Query, only: [from: 2, limit: 2, select: 3, union_all: 2, where: 3] import Explorer.Chain, only: [select_repo: 1, wrapped_union_subquery: 1] - alias Explorer.{Chain, Repo} + alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.Cache.Counters.{ AddressTabsElementsCount, @@ -237,11 +237,9 @@ defmodule Explorer.Chain.Address.Counters do |> wrapped_union_subquery() query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) + |> union_all(^query_from_address_hash_wrapped) + |> union_all(^query_created_contract_address_hash_wrapped) |> wrapped_union_subquery() - |> InternalTransaction.where_is_different_from_parent_transaction() - |> limit(@counters_limit) end def address_counters(address, options \\ []) do @@ -521,8 +519,7 @@ defmodule Explorer.Chain.Address.Counters do run_or_ignore(cache[counter_type], counter_type, address_hash, fn -> result = query - |> limit(@counters_limit) - |> select_repo(options).aggregate(:count) + |> count(options, counter_type) stop = System.monotonic_time() diff = System.convert_time_unit(stop - start, :native, :millisecond) @@ -536,6 +533,19 @@ defmodule Explorer.Chain.Address.Counters do end) end + defp count(query, options, :internal_transactions) do + query + |> select_repo(options).all() + |> InternalTransaction.deduplicate_and_trim_internal_transactions(%PagingOptions{page_size: @counters_limit}) + |> Enum.count() + end + + defp count(query, options, _counter_type) do + query + |> limit(@counters_limit) + |> select_repo(options).aggregate(:count) + end + defp process_transactions_counter( %{transactions_types: [_ | _] = transactions_types, transactions_hashes: hashes} = map ) do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index df04f8ee8a84..ebaa3e7e6b81 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -711,6 +711,7 @@ defmodule Explorer.Chain.InternalTransaction do |> where_nonpending_block() |> where_address_fields_match(hash, :to_address_hash) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() |> common_where_limit_order(paging_options) |> Chain.wrapped_union_subquery() @@ -719,6 +720,7 @@ defmodule Explorer.Chain.InternalTransaction do |> where_nonpending_block() |> where_address_fields_match(hash, :from_address_hash) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() |> common_where_limit_order(paging_options) |> Chain.wrapped_union_subquery() @@ -727,22 +729,25 @@ defmodule Explorer.Chain.InternalTransaction do |> where_nonpending_block() |> where_address_fields_match(hash, :created_contract_address_hash) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() |> common_where_limit_order(paging_options) |> Chain.wrapped_union_subquery() query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) + |> union_all(^query_from_address_hash_wrapped) + |> union_all(^query_created_contract_address_hash_wrapped) |> Chain.wrapped_union_subquery() - |> common_where_limit_order(paging_options) + |> common_where_and_order(paging_options) |> preload(:block) |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() + |> deduplicate_and_trim_internal_transactions(paging_options) else __MODULE__ |> where_nonpending_block() |> where_address_fields_match(hash, direction) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() |> common_where_limit_order(paging_options) |> preload(:block) |> Chain.join_associations(necessity_by_association) @@ -751,11 +756,27 @@ defmodule Explorer.Chain.InternalTransaction do end end + @doc """ + Deduplicates and trims internal transactions based on the page_size specified in paging options. + """ + @spec deduplicate_and_trim_internal_transactions([__MODULE__.t()], PagingOptions.t()) :: [__MODULE__.t()] + def deduplicate_and_trim_internal_transactions(internal_transactions, paging_options) do + internal_transactions + |> Enum.uniq_by(fn internal_transaction -> + {internal_transaction.transaction_hash, internal_transaction.index} + end) + |> Enum.take(paging_options.page_size) + end + defp common_where_limit_order(query, paging_options) do query - |> where_is_different_from_parent_transaction() - |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) + |> common_where_and_order(paging_options) |> limit(^paging_options.page_size) + end + + defp common_where_and_order(query, paging_options) do + query + |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) |> order_by( [it], desc: it.block_number, From 03f3d41b953573d47b16ed7cfed65f638ddffb90 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:59:13 +0300 Subject: [PATCH 0121/1006] fix: CryptoRank integration (#12523) --- .../lib/explorer/market/source/crypto_rank.ex | 33 +- .../market/source/crypto_rank_test.exs | 419 ++++++++++++++++++ config/runtime.exs | 3 +- 3 files changed, 439 insertions(+), 16 deletions(-) create mode 100644 apps/explorer/test/explorer/market/source/crypto_rank_test.exs diff --git a/apps/explorer/lib/explorer/market/source/crypto_rank.ex b/apps/explorer/lib/explorer/market/source/crypto_rank.ex index 37607db348bb..100ea511afc9 100644 --- a/apps/explorer/lib/explorer/market/source/crypto_rank.ex +++ b/apps/explorer/lib/explorer/market/source/crypto_rank.ex @@ -38,8 +38,12 @@ defmodule Explorer.Market.Source.CryptoRank do |> URI.to_string(), headers() ) do - {tokens_to_import, initial_tokens_len} = tokens |> Enum.reduce({[], 0}, &reduce_token(platform_id, &1, &2)) - {:ok, skip + batch_size, initial_tokens_len < batch_size, tokens_to_import} + {tokens_to_import, initial_tokens_len} = + tokens |> Enum.reduce({[], 0}, &reduce_token(platform_id, &1, &2)) + + fetch_finished? = initial_tokens_len < batch_size + new_state = if fetch_finished?, do: nil, else: skip + batch_size + {:ok, new_state, fetch_finished?, tokens_to_import} else nil -> {:error, "Platform ID not specified"} {:ok, unexpected_response} -> {:error, Source.unexpected_response_error("CryptoRank", unexpected_response)} @@ -47,19 +51,16 @@ defmodule Explorer.Market.Source.CryptoRank do end end - defp reduce_token(platform_id, %{"contracts" => [_ | _] = tokens} = token, acc) do - Enum.reduce(tokens, acc, fn - %{ - "address" => token_contract_address_hash_string, - "chainId" => ^platform_id - }, - {tokens, count} -> + defp reduce_token(platform_id, %{"contracts" => [_ | _] = tokens} = token, {tokens_to_import, count}) do + tokens + |> Enum.find_value(fn + %{"chainId" => ^platform_id, "address" => token_contract_address_hash_string} -> case Hash.Address.cast(token_contract_address_hash_string) do {:ok, token_contract_address_hash} -> fiat_value = Source.to_decimal(token["priceUSD"]) circulating_supply = Source.to_decimal(token["circulatingSupply"]) - token = %{ + %{ symbol: token["symbol"], name: token["name"], fiat_value: fiat_value, @@ -69,15 +70,17 @@ defmodule Explorer.Market.Source.CryptoRank do type: "ERC-20" } - {[token | tokens], count + 1} - _ -> - {tokens, count + 1} + false end - _, {tokens, count} -> - {tokens, count + 1} + _ -> + false end) + |> case do + nil -> {tokens_to_import, count + 1} + token -> {[token | tokens_to_import], count + 1} + end end defp reduce_token(_, _, {tokens, count}), do: {tokens, count + 1} diff --git a/apps/explorer/test/explorer/market/source/crypto_rank_test.exs b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs new file mode 100644 index 000000000000..5640eca28bf4 --- /dev/null +++ b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs @@ -0,0 +1,419 @@ +defmodule Explorer.Market.Source.CryptoRankTest do + use ExUnit.Case + + alias Explorer.Market.Source.CryptoRank + alias Plug.Conn + + setup do + bypass = Bypass.open() + old_env = Application.get_env(:explorer, CryptoRank, []) + + Application.put_env( + :explorer, + CryptoRank, + Keyword.merge( + old_env, + platform: 96, + base_url: "http://localhost:#{bypass.port}", + api_key: "api_key", + coin_id: "3", + secondary_coin_id: "4" + ) + ) + + on_exit(fn -> + Application.put_env(:explorer, CryptoRank, old_env) + end) + + {:ok, old_env: old_env, bypass: bypass} + end + + describe "native_coin_fetching_enabled?" do + test "returns true if coin_id is configured" do + assert CryptoRank.native_coin_fetching_enabled?() + end + + test "returns false if coin_id is not configured", %{old_env: old_env} do + Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, coin_id: nil)) + + refute CryptoRank.native_coin_fetching_enabled?() + end + end + + describe "fetch_native_coin" do + test "fetches native coin", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/currencies/3", fn conn -> + assert conn.query_string == "api_key=api_key" + Conn.resp(conn, 200, json_coin(3, 2514.75325703684)) + end) + + assert {:ok, + %Explorer.Market.Token{ + available_supply: Decimal.new("120723694"), + btc_value: Decimal.new("0.0241643324256002"), + fiat_value: Decimal.new("2514.75325703684"), + image_url: "https://img.cryptorank.io/coins/60x60.ethereum1524754015525.png", + last_updated: ~U[2025-06-02 14:22:32.759Z], + market_cap: Decimal.new("303823300492.5147"), + name: "Ethereum", + symbol: "ETH", + total_supply: Decimal.new("120723694"), + tvl: nil, + volume_24h: Decimal.new("5826359746") + }} == + CryptoRank.fetch_native_coin() + end + end + + describe "secondary_coin_fetching_enabled?" do + test "returns true if secondary_coin_id is configured" do + assert CryptoRank.secondary_coin_fetching_enabled?() + end + + test "returns false if secondary_coin_id is not configured", %{old_env: old_env} do + Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, secondary_coin_id: nil)) + + refute CryptoRank.secondary_coin_fetching_enabled?() + end + end + + describe "fetch_secondary_coin" do + test "fetches secondary coin", %{bypass: bypass} do + Bypass.expect_once(bypass, "GET", "/currencies/4", fn conn -> + assert conn.query_string == "api_key=api_key" + Conn.resp(conn, 200, json_coin(4, 1000.123456789)) + end) + + assert {:ok, + %Explorer.Market.Token{ + available_supply: Decimal.new("120723694"), + btc_value: Decimal.new("0.0241643324256002"), + fiat_value: Decimal.new("1000.123456789"), + image_url: "https://img.cryptorank.io/coins/60x60.ethereum1524754015525.png", + last_updated: ~U[2025-06-02 14:22:32.759Z], + market_cap: Decimal.new("303823300492.5147"), + name: "Ethereum", + symbol: "ETH", + total_supply: Decimal.new("120723694"), + tvl: nil, + volume_24h: Decimal.new("5826359746") + }} == + CryptoRank.fetch_secondary_coin() + end + end + + describe "tokens_fetching_enabled?" do + test "returns true if coin_id is configured" do + assert CryptoRank.tokens_fetching_enabled?() + end + + test "returns false if coin_id is not configured", %{old_env: old_env} do + Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, platform: nil)) + + refute CryptoRank.tokens_fetching_enabled?() + end + end + + describe "fetch_tokens" do + test "fetches tokens", %{bypass: bypass} do + Bypass.expect(bypass, "GET", "/dedicated/blockscout/currencies/contracts/96", fn conn -> + case conn.query_string do + "api_key=api_key&limit=50&skip=0" -> + Conn.resp(conn, 200, json_tokens()) + + "api_key=api_key&limit=50&skip=50" -> + Conn.resp(conn, 200, json_tokens_2nd_page()) + + _ -> + raise "Unexpected query string: #{conn.query_string}" + end + end) + + assert {:ok, 50, false, first_batch} = CryptoRank.fetch_tokens(nil, 50) + assert length(first_batch) == 50 + + # cspell:disable + + assert { + :ok, + nil, + true, + [ + %{ + name: "Heurist", + type: "ERC-20", + symbol: "HEU", + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<171, 236, 94, 203, 224, 139, 108, 2, 245, 201, 162, 255, 130, 105, 110, 30, 125, 182, 249, + 191>> + }, + fiat_value: Decimal.new("0.019624517115"), + circulating_market_cap: Decimal.new("3046537.982244971760"), + volume_24h: Decimal.new("214644.7428230154531300") + }, + %{ + name: "Zyfi", + type: "ERC-20", + symbol: "ZFI", + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<93, 13, 123, 202, 5, 14, 46, 152, 253, 74, 94, 141, 59, 168, 35, 180, 159, 57, 134, 141>> + }, + fiat_value: Decimal.new("0.004467522847"), + circulating_market_cap: Decimal.new("1019602.903569369320"), + volume_24h: Decimal.new("527.5318580293320804") + } + ] + } == CryptoRank.fetch_tokens(50, 50) + + # cspell:enable + end + end + + describe "native_coin_price_history_fetching_enabled?" do + test "returns true if coin_id is configured" do + assert CryptoRank.native_coin_price_history_fetching_enabled?() + end + + test "returns false if coin_id is not configured", %{old_env: old_env} do + Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, coin_id: nil)) + + refute CryptoRank.native_coin_price_history_fetching_enabled?() + end + end + + describe "fetch_native_coin_price_history" do + test "fetches native coin price history", %{bypass: bypass} do + previous_days = 5 + from = Date.utc_today() |> Date.add(-previous_days) |> Date.to_iso8601() + to = Date.utc_today() |> Date.to_iso8601() + + Bypass.expect_once(bypass, "GET", "/currencies/3/sparkline", fn conn -> + assert conn.query_string == "api_key=api_key&interval=1d&from=#{from}&to=#{to}" + + Conn.resp(conn, 200, json_native_coin_history()) + end) + + assert {:ok, + [ + %{ + closing_price: Decimal.new("2681.794582393094"), + date: ~D[2025-05-28], + opening_price: Decimal.new("2662.609005038795"), + secondary_coin: false + }, + %{ + closing_price: Decimal.new("2631.742984069461"), + date: ~D[2025-05-29], + opening_price: Decimal.new("2681.794582393094"), + secondary_coin: false + }, + %{ + date: ~D[2025-05-30], + closing_price: Decimal.new("2532.024768716731"), + opening_price: Decimal.new("2631.742984069461"), + secondary_coin: false + }, + %{ + date: ~D[2025-05-31], + closing_price: Decimal.new("2528.507007933771"), + opening_price: Decimal.new("2532.024768716731"), + secondary_coin: false + }, + %{ + date: ~D[2025-06-01], + closing_price: Decimal.new("2528.507007933771"), + opening_price: Decimal.new("2528.507007933771"), + secondary_coin: false + } + ]} == + CryptoRank.fetch_native_coin_price_history(previous_days) + end + end + + describe "secondary_coin_price_history_fetching_enabled?" do + test "returns true if secondary_coin_id is configured" do + assert CryptoRank.secondary_coin_price_history_fetching_enabled?() + end + + test "returns false if secondary_coin_id is not configured", %{old_env: old_env} do + Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, secondary_coin_id: nil)) + + refute CryptoRank.secondary_coin_price_history_fetching_enabled?() + end + end + + describe "fetch_secondary_coin_price_history" do + test "fetches secondary coin price history", %{bypass: bypass} do + previous_days = 5 + from = Date.utc_today() |> Date.add(-previous_days) |> Date.to_iso8601() + to = Date.utc_today() |> Date.to_iso8601() + + Bypass.expect_once(bypass, "GET", "/currencies/4/sparkline", fn conn -> + assert conn.query_string == "api_key=api_key&interval=1d&from=#{from}&to=#{to}" + + Conn.resp(conn, 200, json_secondary_coin_history()) + end) + + assert {:ok, + [ + %{ + closing_price: Decimal.new("6.558096674134"), + date: ~D[2025-05-28], + opening_price: Decimal.new("6.669271727877"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("6.443746862121"), + date: ~D[2025-05-29], + opening_price: Decimal.new("6.558096674134"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("5.80657534093"), + date: ~D[2025-05-30], + opening_price: Decimal.new("6.443746862121"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("5.903371964672"), + date: ~D[2025-05-31], + opening_price: Decimal.new("5.80657534093"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("5.903371964672"), + date: ~D[2025-06-01], + opening_price: Decimal.new("5.903371964672"), + secondary_coin: true + } + ]} == + CryptoRank.fetch_secondary_coin_price_history(previous_days) + end + end + + describe "market_cap_history_fetching_enabled?" do + test "ignored" do + assert CryptoRank.market_cap_history_fetching_enabled?() == :ignore + end + end + + describe "fetch_market_cap_history" do + test "ignored" do + assert CryptoRank.fetch_market_cap_history(0) == :ignore + end + end + + describe "tvl_history_fetching_enabled?" do + test "ignored" do + assert CryptoRank.tvl_history_fetching_enabled?() == :ignore + end + end + + describe "fetch_tvl_history" do + test "ignored" do + assert CryptoRank.fetch_tvl_history(0) == :ignore + end + end + + defp json_coin(coin_id, fiat_value) do + """ + { + "data": { + "id": #{coin_id}, + "rank": 2, + "slug": "ethereum", + "name": "Ethereum", + "symbol": "ETH", + "category": "Chain", + "type": "coin", + "volume24hBase": 2315094.5634, + "circulatingSupply": 120723694, + "totalSupply": 120723694, + "images": { + "16x16": "https://img.cryptorank.io/coins/icon.ethereum1524754015525.png", + "200x200": "https://img.cryptorank.io/coins/ethereum1524754015525.png", + "60x60": "https://img.cryptorank.io/coins/60x60.ethereum1524754015525.png" + }, + "values": { + "USD": { + "price": #{fiat_value}, + "volume24h": 5826359746, + "high24h": 2546.560850604021, + "low24h": 2480.175296733773, + "marketCap": 303823300492.5147, + "percentChange24h": 0.4248, + "percentChange7d": -1.3799, + "percentChange30d": 37.1343, + "percentChange3m": 16.9385, + "percentChange6m": -30.9087 + }, + "BTC": { + "price": 0.0241643324256002, + "volume24h": 55942, + "high24h": 0.0241883039284151, + "low24h": 0.0236807597840912, + "marketCap": 2917207.4734624363, + "percentChange24h": 0.8852, + "percentChange7d": 3.8147, + "percentChange30d": 26.8718, + "percentChange3m": -3.0863, + "percentChange6m": -36.4735 + }, + "ETH": { + "price": 1, + "volume24h": 2315094, + "high24h": 1, + "low24h": 1, + "marketCap": 120723694, + "percentChange24h": 0, + "percentChange7d": 0, + "percentChange30d": 0, + "percentChange3m": 0, + "percentChange6m": 0 + } + }, + "lastUpdated": "2025-06-02T14:22:32.759Z", + "tokens": [] + }, + "status": { + "time": "2025-06-02T14:23:23.902Z", + "success": true, + "code": 200, + "message": "OK", + "responseTime": 1, + "creditsCost": 1 + } + } + """ + end + + # cspell:disable + defp json_tokens do + """ + { "data": [ { "id": 16, "slug": "tether", "symbol": "USDT", "name": "Tether", "priceUSD": "1.000312118824", "volume24hUSD": "34598333608.3056447616193232", "circulatingSupply": "153148231147", "contracts": [ { "address": "0xdac17f958d2ee523a2206206994597c13d831ec7", "chainId": 3 }, { "address": "0x551a5dcac57c66aa010940c2dcff5da9c53aa53b", "chainId": 18 }, { "address": "0x493257fD37EDB34451f62EDf8D2a0C418852bA4C", "chainId": 96 }, { "address": "0x9636d3294e45823ec924c8d89dd1f1dffcf044e6", "chainId": 21 }, { "address": "0xfadbbf8ce7d5b7041be672561bba99f79c532e10", "chainId": 47 }, { "address": "0xe3f5a90f9cb311505cd691a46596599aa1a0ad7d", "chainId": 100 }, { "address": "0x5f0155d08ef4aae2b500aefb64a3419da8bb611a", "chainId": 121 }, { "address": "0x5eDCCFcAC24A89F3b87f2bfAB618a509FA6e3105", "chainId": 122 }, { "address": "0xd378634119d2f7b3cf3d60e0b0f5e048e74ce3cf", "chainId": 124 }, { "address": "0x398dcA951cD4fc18264d995DCD171aa5dEbDa129", "chainId": 134 }, { "address": "0x02f9bebf5e54968d8cc2562356c91ecde135801b", "chainId": 166 }, { "address": "0x900101d06a7426441ae63e9ab3b9b0f63be145f1", "chainId": 197 }, { "address": "0x3a337a6ada9d885b6ad95ec48f9b75f197b5ae35", "chainId": 487 }, { "address": "0x0709F39376dEEe2A2dfC94A58EdEb2Eb9DF012bD", "chainId": 489 }, { "address": "KT1XnTn74bUtxHfDtBmm2bGZAQfhPbvKWR8o", "chainId": 41 }, { "address": "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", "chainId": 95 }, { "address": "0xf0F161fDA2712DB8b566946122a5af183995e2eD", "chainId": 150 }, { "address": "0x28c9c7Fb3fE3104d2116Af26cC8eF7905547349c", "chainId": 210 }, { "address": "0x6fbcdc1169b5130c59e72e51ed68a84841c98cd1", "chainId": 43 }, { "address": "0x3795c36e7d12a8c252a20c5a7b455f7c57b60283", "chainId": 83 }, { "address": "0x919c1c267bc06a7039e03fcc2ef738525769109c", "chainId": 81 }, { "address": "0x201eba5cc46d216ce6dc03f6a759e8e766e956ae", "chainId": 103 }, { "address": "terra1ce06wkrdm4vl6t0hvc0g86rsy27pu8yadg3dva", "chainId": 2 }, { "address": "0x1e4a5963abfd975d8c9021ce480b42188849d41d", "chainId": 102 }, { "address": "0x6047828dc181963ba44974801FF68e538dA5eaF9", "chainId": 465 }, { "address": "0x357b0b74bc833e95a115ad22604854d6b0fca151cecd94111770e5d6ffc9dc2b", "chainId": 91 }, { "address": "0x0039f574ee5cc39bdd162e9a88e3eb1f111baf48", "chainId": 71 }, { "address": "0xa71edc38d189767582c38a3145b5873052c3e47a", "chainId": 14 }, { "address": "0x3c2b8be99c50593081eaa2a724f0b8285f5aba8f", "chainId": 12 }, { "address": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", "chainId": 5 }, { "address": "0:a519f99bb5d6d51ef958ed24d337ad75a1c770885dcd42d51d6663f9fcdacfb2", "chainId": 93 }, { "address": "312769", "chainId": 57 }, { "address": null, "chainId": 90 }, { "address": "0x46dDa6a5a559d861c06EC9a95Fb395f5C3Db0742", "chainId": 450 }, { "address": "0x375f70cf2ae4c00bf37117d0c85a2c71545e6ee05c4a5c7d282cd66a4504b068::usdt::USDT", "chainId": 99 }, { "address": "0x01445c31581c354b7338ac35693ab2001b50b9ae", "chainId": 33 }, { "address": "0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e", "chainId": 13 }, { "address": "0xe936caa7f6d9f5c9e907111fcaf7c351c184cda7", "chainId": 11 }, { "address": "0xfe9f969faf8ad72a83b761138bf25de87eff9dd2", "chainId": 426 }, { "address": "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", "chainId": 61 }, { "address": "0x66e428c3f67a68878562e79a0234c1f83c208770", "chainId": 75 }, { "address": "0xEf213441a85DF4d7acBdAe0Cf78004E1e486BB96", "chainId": 82 }, { "address": "0x382bb369d343125bfb2117af9c149795c6c65c50", "chainId": 45 }, { "address": "0xbb06dca3ae6887fabf931640f67cab3e3a16f4dc", "chainId": 17 }, { "address": "0x4988a896b1227218e4a686fde5eabdcabd91571f", "chainId": 31 }, { "address": "0xdc19a122e268128b5ee20366299fc7b5b199c8e3", "chainId": 85 }, { "address": "secret18wpjn83dayu4meu6wnn29khfkwdxs7kyrz9c8f", "chainId": 63 }, { "address": "zil1sxx29cshups269ahh5qjffyr58mxjv9ft78jqy", "chainId": 76 }, { "address": "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", "chainId": 1 }, { "address": "0xB75D0B03c06A926e488e2659DF1A861F860bD3d1", "chainId": 157 }, { "address": "0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7", "chainId": 7 }, { "address": "0xC0F4b3a52B0532Bf48784d2202C812B1841d8812", "chainId": 411 }, { "address": "0x17270E5364f226Cd5D77a16957c1b2663dC9699B", "chainId": 382 }, { "address": "0xf417F5A458eC102B90352F697D6e2Ac3A3d2851f", "chainId": 143 }, { "address": "0x94b008aa00579c1307b0ef2c499ad98a8ce58e58", "chainId": 84 }, { "address": "0xefaeee334f0fd1712f9a8cc375f427d9cdd40d73", "chainId": 52 }, { "address": "0x55d398326f99059ff775485246999027b3197955", "chainId": 4 }, { "address": "0xfd36c336eb67a092dc80a063ff0644e13142d454", "chainId": 152 }, { "address": "e1d869a83212628ec82a4e039a95371a9ae4598c8459193ccfd0f2f2c689831f", "chainId": 138 }, { "address": "4Q89182juiadeFgGw3fupnrwnnDmBhf7e7fHWxnUP3S3", "chainId": 137 }, { "address": "0xfa9343c3897324496a05fc75abed6bac29f8a40f", "chainId": 125 }, { "address": "0x0cb6f5a34ad42ec934882a05265a7d5f59b51a2f", "chainId": 97 }, { "address": "usdt.tether-token.near", "chainId": 29 }, { "address": "32TLn1WLcu8LtfvweLzYUYU6ubc2YV9eZs", "chainId": 38 }, { "address": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "chainId": 6 }, { "address": "0xA510432E4aa60B4acd476fb850EC84B7EE226b2d", "chainId": 469 }, { "address": "0xc2c527c0cacf457746bd31b2a698fe89de2b6d49", "chainId": 232 }, { "address": "0x6a2d262D56735DbA19Dd70682B39F6bE9a931D98", "chainId": 218 }, { "address": "0xfA9343C3897324496A05fC75abeD6bAC29f8A40f", "chainId": 217 }, { "address": "0x9350502a3af6c617e9a42fa9e306a385::BX_USDT::BX_USDT", "chainId": 215 }, { "address": "ibc/4ABBEF4C8926DDDB320AE5188CFD63267ABBCEFC0583E4AE05D6E5AA2401DDAB", "chainId": 141 }, { "address": "0x381B31409e4D220919B2cFF012ED94d70135A59e", "chainId": 48 }, { "address": "0x4ECaBa5870353805a9F068101A40E0f32ed605C6", "chainId": 78 }, { "address": "0x0Cf7c2A584988871b654Bd79f96899e4cd6C41C0", "chainId": 117 }, { "address": "0x68f5c6a61780768455de69077e07e89787839bf8166decfbf92b645209c0fb8", "chainId": 106 }, { "address": "0x80a16016cc4a2e6a2caca8a4a498b1699ff0f844", "chainId": 127 }, { "address": "0xfe97E85d13ABD9c1c33384E796F10B73905637cE", "chainId": 39 }, { "address": "0xA219439258ca9da29E9Cc4cE5596924745e12B93", "chainId": 101 }, { "address": "0xf55BEC9cafDbE8730f096Aa55dad6D22d44099Df", "chainId": 108 }, { "address": "0x9e5aac1ba1a2e6aed6b32689dfcf62a509ca96f3", "chainId": 110 }, { "address": "0x988a631caf24e14bb77ee0f5ca881e8b5dcfcec7", "chainId": 151 }, { "address": "0xa3200696761a0cf122de2a679f745b3f8cfa2623", "chainId": 155 }, { "address": "peggy0xdAC17F958D2ee523a2206206994597C13D831ec7", "chainId": 147 }, { "address": "A95EBDF88AC1E3FDECAA1D6E250B07C0C86475A3E2BFA2E7DC94A6CDE23DF6D6", "chainId": 255 }, { "address": "0x008D7923Fe2941Ceb549bf5646B1ddb1aC93C8a6", "chainId": 408 }, { "address": "0xefcAA73145B5e29eEfc47bcbaeFF9e870Fa6a610", "chainId": 130 }, { "address": "0x91Aa258324072dFf6F82408c2beB2F82D353b300", "chainId": 131 }, { "address": "0x3eee5d2ed0205f93969a59f7c8597fb614264436", "chainId": 114 } ] }, { "id": 72, "slug": "kyber-network", "symbol": "KNC", "name": "Kyber Network Crystal ", "priceUSD": "0.325609862550", "volume24hUSD": "10125280.0251828828477000", "circulatingSupply": "170152851", "contracts": [ { "address": "0x6ee46Cb7cD2f15Ee1ec9534cf29a5b51C83283e6", "chainId": 96 }, { "address": "0x6A80A465409ce8D36C513129C0FEEa61BEd579ba", "chainId": 102 }, { "address": "0x3b2F62d42DB19B30588648bf1c184865D4C3B1D6", "chainId": 101 }, { "address": "0x1e1085eFaA63EDFE74aaD7C05a28EAE4ef917C3F", "chainId": 10 }, { "address": "0x39fC9e94Caeacb435842FADeDeCB783589F50f5f", "chainId": 7 }, { "address": "0x316772cFEc9A3E976FDE42C3Ba21F5A13aAaFf12", "chainId": 61 }, { "address": "0xdeFA4e8a7bcBA345F687a2f1456F5Edd9CE97202", "chainId": 3 }, { "address": "0xfe56d5892bdffc7bf58f2e84be1b2c32d21c308b", "chainId": 4 }, { "address": "0x1c954e8fe737f99f68fa1ccda3e51ebdb291948c", "chainId": 5 } ] }, { "id": 1254, "slug": "weth", "symbol": "WETH", "name": "WETH", "priceUSD": "2523.982311036187", "volume24hUSD": "1061921578.9958917235204746", "circulatingSupply": "2652758", "contracts": [ { "address": "0:59b6b64ac6798aacf385ae9910008a525a84fc6dcf9f942ae81f8e8485fe160d", "chainId": 93 }, { "address": "0x2EAA73Bd0db20c64f53fEbeA7b5F5E5Bccc7fb8b", "chainId": 48 }, { "address": "0xf22bede237a07e121b56d91a491eb7bcdfd1f5907926a9e58338f964a01b17fa::asset::WETH", "chainId": 91 }, { "address": "0x722e8bdd2ce80a4422e880164f2079488e115365", "chainId": 89 }, { "address": "7vfCXTUXx5WJV5JADk17DUJ4ksgau7utNKj4b963voxs", "chainId": 1 }, { "address": "0x2170Ed0880ac9A755fd29B2688956BD959F933F8", "chainId": 4 }, { "address": "0xE7798f023fC62146e8Aa1b36Da45fb70855a77Ea", "chainId": 247 }, { "address": "0xe44Fd7fCb2b1581822D0c862B68222998a0c299a", "chainId": 75 }, { "address": "0xab3f0245B83feB11d15AAffeFD7AD465a59817eD", "chainId": 52 }, { "address": "0xdEAddEaDdeadDEadDEADDEAddEADDEAddead1111", "chainId": 103 }, { "address": "zil19j33tapjje2xzng7svslnsjjjgge930jx0w09v", "chainId": 76 }, { "address": "terra14tl83xcwqjy0ken9peu4pjjuu755lrry2uy25r", "chainId": 2 }, { "address": "0xc99a6a985ed2cac1ef41640596c5a5f9f4e19ef5", "chainId": 34 }, { "address": "0x02DcdD04e3F455D838cd1249292C58f3B79e3C3C", "chainId": 97 }, { "address": "0xC9BdeEd33CD01541e1eeD10f90519d2C06Fe3feB", "chainId": 31 }, { "address": "0x420000000000000000000000000000000000000A", "chainId": 17 }, { "address": "0x0258866edaf84d6081df17660357ab20a07d0c80", "chainId": 43 }, { "address": "0x135cb19acde9ffb4654cace4189a0e0fb4b6954e", "chainId": 181 }, { "address": "0x1540020a94aA8bc189aA97639Da213a4ca49d9a7", "chainId": 18 }, { "address": "0x4300000000000000000000000000000000000004", "chainId": 221 }, { "address": "0x7339e5586280dfa2b1f315827392ce414a44b1c0", "chainId": 174 }, { "address": "0x4200000000000000000000000000000000000006", "chainId": 150 }, { "address": "0x4200000000000000000000000000000000000006", "chainId": 98 }, { "address": "0xCD3f9D5Cbf51016e0d2340FE909E99C006422A48", "chainId": 159 }, { "address": "0xe5d7c2a44ffddf6b295a15c148167daaaf5cf34f", "chainId": 101 }, { "address": "0xe7798f023fc62146e8aa1b36da45fb70855a77ea", "chainId": 110 }, { "address": "0x0Dc808adcE2099A9F62AA87D9670745AbA741746", "chainId": 143 }, { "address": "0x5300000000000000000000000000000000000004", "chainId": 108 }, { "address": "0x7ceb23fd6bc0add59e62ac25578270cff1b9f619", "chainId": 5 }, { "address": "0x8ed7d143Ef452316Ab1123d28Ab302dC3b80d3ce", "chainId": 105 }, { "address": "0x0ce35b0d42608ca54eb7bcc8044f7087c18e7717", "chainId": 202 }, { "address": "0x4200000000000000000000000000000000000006", "chainId": 487 }, { "address": "0x50c42dEAcD8Fc9773493ED674b675bE577f2634b", "chainId": 465 }, { "address": "0x3439153EB7AF838Ad19d56E1571FBD09333C2809", "chainId": 489 }, { "address": "0x2F6F07CDcf3588944Bf4C42aC74ff24bF56e7590", "chainId": 491 }, { "address": "0xa722c13135930332Eb3d749B2F0906559D2C5b99", "chainId": 47 }, { "address": "0xf55af137a98607f7ed2efefa4cd2dfe70e4253b1", "chainId": 71 }, { "address": "0x5aea5775959fbc2557cc8789bc1bf90a239d9a91", "chainId": 96 }, { "address": "0xa47f43DE2f9623aCb395CA4905746496D2014d57", "chainId": 39 }, { "address": "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", "chainId": 106 }, { "address": "0x7EbeF2A4b1B09381Ec5B9dF8C5c6f2dBECA59c73", "chainId": 128 }, { "address": "85219708c49aa701871ad330a94ea0f41dff24ca", "chainId": 33 }, { "address": "0xe3f5a90f9cb311505cd691a46596599aa1a0ad7d", "chainId": 127 }, { "address": "0x57eea49ec1087695274a9c4f341e414eb64328c2", "chainId": 119 }, { "address": "ibc/EA1D43981D5C9A1C4AAEA9C23BB1D4FA126BA9BC7020A25E0AE4AA841EA25DC5", "chainId": null }, { "address": "0x695921034f0387eAc4e11620EE91b1b15A6A09fE", "chainId": 10 }, { "address": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", "chainId": 61 }, { "address": "THb4CqiFdwNHsWsQCs4JhzwjMWys4aqCbF", "chainId": 6 }, { "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", "chainId": 3 }, { "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "chainId": 3 }, { "address": "0x122013fd7dF1C6F636a5bb8f03108E876548b455", "chainId": 13 }, { "address": "0x160345fC359604fC6e70E3c5fAcbdE5F7A9342d8", "chainId": 157 }, { "address": "0xfa9343c3897324496a05fc75abed6bac29f8a40f", "chainId": 55 }, { "address": "0x3223f17957Ba502cbe71401D55A0DB26E5F7c68F", "chainId": 85 }, { "address": "0x81ECac0D6Be0550A00FF064a4f9dd2400585FE9c", "chainId": 83 }, { "address": "0x6983d1e6def3690c4d616b13597a09e6193ea013", "chainId": 12 }, { "address": "0x6a023ccd1ff6f2045c3309768ead9e68f978f6e1", "chainId": 78 }, { "address": "0x49D5c2BdFfac6CE2BFdB6640F4F80f226bc10bAB", "chainId": 7 }, { "address": "0x4200000000000000000000000000000000000006", "chainId": 84 } ] }, { "id": 1883, "slug": "wrapped-bitcoin", "symbol": "WBTC", "name": "Wrapped Bitcoin", "priceUSD": "104247.030463929140", "volume24hUSD": "282269194.4261377519110420", "circulatingSupply": "128815", "contracts": [ { "address": "EQDcBkGHmC4pTf34x3Gm05XvepO5w60DNxZ-XT4I6-UGG5L5", "chainId": 95 }, { "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", "chainId": 157 }, { "address": "0x19df5689Cfce64bC2A55F7220B0Cd522659955EF", "chainId": 450 }, { "address": "0x3095c7557bcb296ccc6e363de01b760ba031f2d9", "chainId": 12 }, { "address": "0x8e5bbbb09ed1ebde8674cda39a0c169401db4252", "chainId": 78 }, { "address": "0:2ba32b75870d572e255809b7b423f30f36dd5dea075bd5f026863fceb81f2bcf", "chainId": 93 }, { "address": "0x68f180fcCe6836688e9084f035309E29Bf0A2095", "chainId": 84 }, { "address": "3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh", "chainId": 1 }, { "address": "io1c7unwg8h8vph89xwqru4f7zfa4yy5002wxvlrm", "chainId": 43 }, { "address": "0xF4eB217Ba2454613b15dBdea6e5f22276410e89e", "chainId": 31 }, { "address": "0xa5B55ab1dAF0F8e1EFc0eB1931a957fd89B918f4", "chainId": 17 }, { "address": "0x408d4cd0adb7cebd1f1a1c33a0ba2098e1295bab", "chainId": 7 }, { "address": "0x062E66477Faf219F25D27dCED647BF57C3107d52", "chainId": 75 }, { "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "chainId": 3 }, { "address": "0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f", "chainId": 61 }, { "address": "0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6", "chainId": 5 }, { "address": "0x313dbD8e65C6499dE939e5317cA97Ccc6eD4c621", "chainId": 33 }, { "address": "0xE57eBd2d67B462E9926e04a8e33f01cD0D64346D", "chainId": 52 }, { "address": "0xfa93c12cd345c658bc4644d1d4e1b9615952258c", "chainId": 71 }, { "address": "2260fac5e5542a773aa44fbcfedf7c193bc2c599.factory.bridge.near", "chainId": 29 }, { "address": "0x1f545487c62e5acfea45dcadd9c627361d1616d8", "chainId": 39 }, { "address": "8LQW8f7P5d5PZM7GtZEBgaqRPGSzS3DfPuiXrURJ4AJS", "chainId": 69 }, { "address": "0x927B51f251480a681271180DA4de28D44EC4AfB8", "chainId": 492 }, { "address": "0xcabae6f6ea1ecab08ad02fe02ce9a44f09aebfa2", "chainId": 103 }, { "address": "0xb17d901469b9208b17d916112988a3fed19b5ca1", "chainId": 97 }, { "address": "0xEA034fb02eB1808C2cc3adbC15f447B93CbE08e1", "chainId": 102 }, { "address": "zil1wha8mzaxhm22dpm5cav2tepuldnr8kwkvmqtjq", "chainId": 76 }, { "address": "0x3C1BCa5a656e69edCD0D4E36BEbb3FcDAcA60Cf1", "chainId": 108 }, { "address": "0x3fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac", "chainId": 106 }, { "address": "0xF6D226f9Dc15d9bB51182815b320D3fBE324e1bA", "chainId": 220 }, { "address": "0xbbeb516fb02a01611cbbe0453fe3c580d7281011", "chainId": 96 }, { "address": "0x027792d9fed7f9844eb4839566001bb6f6cb4804f66aa2da6fe1ee242d896881::coin::COIN", "chainId": 99 }, { "address": "0xcDd475325D6F564d27247D1DddBb0DAc6fA0a5CF", "chainId": 150 }, { "address": "0x3aAB2285ddcDdaD8edf438C1bAB47e1a9D05a9b4", "chainId": 101 }, { "address": "0x503b2ddc059b81788fd1239561596614b27faade", "chainId": 48 }, { "address": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c", "chainId": 491 }, { "address": "0xcb011e86df014a46f4e3ac3f3cbb114a4eb80870", "chainId": 233 }, { "address": "0xf390830df829cf22c53c8840554b98eafc5dcbc2", "chainId": 55 }, { "address": "0x78F811A431D248c1EDcF6d95ec8551879B2897C3", "chainId": 11 } ] }, { "id": 2854, "slug": "venus", "symbol": "XVS", "name": "Venus", "priceUSD": "5.882048680590", "volume24hUSD": "2054331.9320935988631060", "circulatingSupply": "16655176", "contracts": [ { "address": "0xd3CC9d8f3689B83c91b7B59cAB4946B063EB894A", "chainId": 3 }, { "address": "0xc1Eb7689147C81aC840d4FF0D298489fc7986d52", "chainId": 61 }, { "address": "0xcf6bb5389c92bdda8a3747ddb454cb7a64626c63", "chainId": 4 }, { "address": "0x3E2e61F1c075881F3fB8dd568043d8c221fd5c61", "chainId": 110 }, { "address": "0xD78ABD81a3D57712a3af080dc4185b698Fe9ac5A", "chainId": 96 } ] }, { "id": 5487, "slug": "usdcoin", "symbol": "USDC", "name": "USDC", "priceUSD": "0.999819756251", "volume24hUSD": "5357299343.0449997080455914", "circulatingSupply": "62369678914", "contracts": [ { "address": "0x81ECac0D6Be0550A00FF064a4f9dd2400585FE9c", "chainId": 85 }, { "address": "0x818ec0a7fe18ff94269904fced6ae3dae6d6dc0b", "chainId": 52 }, { "address": "0xe2c120f188ebd5389f71cf4d9c16d05b62a58993", "chainId": 33 }, { "address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", "chainId": 5 }, { "address": "0x28a92dde19D9989F39A49905d7C9C2FAc7799bDf", "chainId": 10 }, { "address": "0xaf88d065e77c8cc2239327c5edb3a432268e5831", "chainId": 61 }, { "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", "chainId": 3 }, { "address": "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC", "chainId": 99 }, { "address": "0xc21223249ca28397b4b6541dffaecc539bff0c59", "chainId": 75 }, { "address": "0xceba9300f2b948710d2653dd7b07f33a8b32118c", "chainId": 13 }, { "address": "io18v4l9dfr74xyu320pz4zsmgrz9d07vnvy20yrh", "chainId": 43 }, { "address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "chainId": 1 }, { "address": "secret1h6z05y90gwm4sqxzhz4pkyp36cna9xtp7q0urv", "chainId": 63 }, { "address": "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", "chainId": 4 }, { "address": "0xbae207659db88bea0cbead6da0ed00aac12edcdda169e591cd41c94180b46f3b", "chainId": 91 }, { "address": "0x980a5afef3d17ad98635f6c5aebcbaeded3c3430", "chainId": 71 }, { "address": "0x985458e523db3d53125813ed68c274899e9dfab4", "chainId": 12 }, { "address": "0x0b7007c13325c48911f73a2dad5fa5dcbf808adc", "chainId": 34 }, { "address": "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E", "chainId": 7 }, { "address": "0x52a9cea01c4cbdd669883e41758b8eb8e8e2b34b", "chainId": 44 }, { "address": "0xc946daf81b08146b1c7a8da2a851ddf2b3eaaf85", "chainId": 45 }, { "address": "0x620fd5fa44be6af63715ef4e65ddfa0387ad13f5", "chainId": 47 }, { "address": "0xea32a96608495e54156ae48931a7c20f0dcc1a21", "chainId": 17 }, { "address": "31566704", "chainId": 57 }, { "address": "0x6d1e7cde53ba9467b783cb7c530ce054", "chainId": 303 }, { "address": "0x6a2d262d56735dba19dd70682b39f6be9a931d98", "chainId": 90 }, { "address": "0xB12BFcA5A55806AaF64E99521918A4bf0fC40802", "chainId": 31 }, { "address": "0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83", "chainId": 78 }, { "address": "0xcca4e6302510d555b654b3eab9c0fcb223bcfdf0", "chainId": 48 }, { "address": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", "chainId": 6 }, { "address": "0x078D782b760474a361dDA0AF3839290b0EF57AD6", "chainId": 492 }, { "address": "USDC-c76f1f", "chainId": 16 }, { "address": "0x0b2c639c533813f4aa9d7837caf62653d097ff85", "chainId": 84 }, { "address": "0x275e916Ab1E93A6862a7b380751DdD87D6F66267", "chainId": 371 }, { "address": "0xd988097fb8612cc24eeC14542bC03424c656005f", "chainId": 150 }, { "address": "ibc/4A1C18CA7F50544760CF306189B810CE4C1CB156C7FC870143D401FE7280E591", "chainId": 223 }, { "address": "0xe3f5a90f9cb311505cd691a46596599aa1a0ad7d", "chainId": 168 }, { "address": "0x80b5a32e4f032b2a058b4f29ec95eefeeb87adcd", "chainId": 148 }, { "address": "0xb73603C5d87fA094B7314C74ACE2e64D165016fb", "chainId": 143 }, { "address": "0x06eFdBFf2a14a7c8E15944D1F4A48F9F95F663A4", "chainId": 108 }, { "address": "ibc/D189335C6E4A68B513C10AB227BF1C1D38C746766278BA3EEB4FB14124F1D858", "chainId": null }, { "address": "0x818ec0a7fe18ff94269904fced6ae3dae6d6dc0b", "chainId": 55 }, { "address": "0x00f0d8595797943c12605cd59bc0d9f63d750ccf", "chainId": 119 }, { "address": "0x540d90635a0C10CdD4D27fc8edCbf88c18DfB1eD", "chainId": 116 }, { "address": "0xE1aB220E37AC55A4E2dD5Ba148298A9c09fBD716", "chainId": 183 }, { "address": "0x640952e7984f2ecedead8fd97aa618ab1210a21c", "chainId": 171 }, { "address": "ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5", "chainId": 250 }, { "address": "17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1", "chainId": 29 }, { "address": "ibc/498A0751C798A0D9A389AA3691123DADA57DAA4FE165D5C75894505B876BA6E4", "chainId": 141 }, { "address": "A.b19436aae4d94622.FiatToken", "chainId": 68 }, { "address": "0xfa9343c3897324496a05fc75abed6bac29f8a40f", "chainId": 230 }, { "address": "0xFFD7510ca0a3279c7a5F50018A26c21d5bc1DBcF", "chainId": 107 }, { "address": "0.0.456858", "chainId": 94 }, { "address": "0x09bc4e0d864854c6afb6eb9a9cdf58ac190d0df9", "chainId": 103 }, { "address": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", "chainId": 98 }, { "address": "0x53c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8", "chainId": 106 }, { "address": "1337", "chainId": 54 }, { "address": "USDC-GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", "chainId": 66 }, { "address": "e4b8164dccf3489f66124cace1570dc35b58fc90", "chainId": 209 }, { "address": "0xe2aa35C2039Bd0Ff196A6Ef99523CC0D3972ae3e", "chainId": 198 }, { "address": "0x3355df6d4c9c3035724fd0e3914de96a5a83aaf4", "chainId": 96 }, { "address": "0x6963EfED0aB40F6C3d7BdA44A05dcf1437C44372", "chainId": 39 } ] }, { "id": 7511, "slug": "binance-usd", "symbol": "BUSD", "name": "BUSD", "priceUSD": "0.999635670343", "volume24hUSD": "2398317.6519931188988041", "circulatingSupply": "57792684", "contracts": [ { "address": "0x4Fabb145d64652a948d72533023f6E7A623C7C53", "chainId": 3 }, { "address": "0x6ab6d61428fde76768d7b45d8bfeec19c6ef91a8", "chainId": 75 }, { "address": "0xe9e7cea3dedca5984780bafc599bd69add087d56", "chainId": 4 }, { "address": "0x19860ccb0a68fd4213ab9d8266f7bbf05a8dde98", "chainId": 7 }, { "address": "0x9c9e5fd8bbc25984b178fdce6117defa39d2db39", "chainId": 84 }, { "address": "0xa649325aa7c5093d12d6f98eb4378deae68ce23f", "chainId": 52 }, { "address": "0xc111c29a988ae0c0087d97b33c6e6766808a3bd3", "chainId": 33 }, { "address": "0x2039bb4116B4EFc145Ec4f0e2eA75012D6C0f181", "chainId": 96 }, { "address": "0x7d43AABC515C356145049227CeE54B608342c0ad", "chainId": 101 }, { "address": "0x5d9ab5522c64e1f6ef5e3627eccc093f56167818", "chainId": 11 }, { "address": "0x4bf769b05e832fcdc9053fffbc78ca889acb5e1e", "chainId": 90 }, { "address": "0xe176ebe47d621b984a73036b9da5d834411ef734", "chainId": 12 }, { "address": "0x332730a4f6e03d9c55829435f10360e13cfa41ff", "chainId": 45 }, { "address": "0x3444273afdf9e00fd0491c8a97738aca3ebb2a93", "chainId": 18 }, { "address": "0x4bf769b05e832fcdc9053fffbc78ca889acb5e1e", "chainId": 83 }, { "address": "0x9C9e5fD8bbc25984B178FdCE6117Defa39d2db39", "chainId": 5 }, { "address": "5RpUwQ8wtdPCZHhu6MERp2RGrpobsbZ6MH5dDHkUjs2", "chainId": 1 }, { "address": "0x84abcb2832be606341a50128aeb1db43aa017449", "chainId": 43 }, { "address": "0x7b37d0787a3424a0810e02b24743a45ebd5530b2", "chainId": 74 } ] }, { "id": 19793, "slug": "multicollateraldai", "symbol": "DAI", "name": "Dai", "priceUSD": "0.999911538842", "volume24hUSD": "43665636.5443962007704738", "circulatingSupply": "4015833934", "contracts": [ { "address": "0xef977d2f931c1978db5f6747666fa1eacb0d0339", "chainId": 12 }, { "address": "0x80a16016cc4a2e6a2caca8a4a498b1699ff0f844", "chainId": 11 }, { "address": "0x44fA8E6f47987339850636F88629646662444217", "chainId": 78 }, { "address": "terra1zmclyfepfmqvfqflu8r3lv6f75trmg05z7xq95", "chainId": 2 }, { "address": "0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3", "chainId": 4 }, { "address": "0xf2001b145b43032aaf5ee2884e456ccd805f677d", "chainId": 75 }, { "address": "0x6b175474e89094c44da98b954eedeac495271d0f", "chainId": 3 }, { "address": "0xd586e7f844cea2f87f50152665bcbc2c279d8d70", "chainId": 7 }, { "address": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063", "chainId": 5 }, { "address": "0x50c5725949A6F0c72E6C4a641F24049A917DB0Cb", "chainId": 98 }, { "address": "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", "chainId": 89 }, { "address": "0xcA77eB3fEFe3725Dc33bccB54eDEFc3D9f764f97", "chainId": 108 }, { "address": "0x0ee5893f434017d8881750101Ea2F7c49c0eb503", "chainId": 107 }, { "address": "0xda114221cb83fa859dbdb4c44beeaa0bb37c7537ad5ae66fe5e0efd20e6eb3", "chainId": 106 }, { "address": "0x4C1f6fCBd233241bF2f4D02811E3bF8429BC27B8", "chainId": 149 }, { "address": "0x6cc8f0b5607e1f947e83667368881a1bccc3f1c4", "chainId": 212 }, { "address": "0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4", "chainId": 102 }, { "address": "0x765277eebeca2e31912c9946eae1021199b39c61", "chainId": 52 }, { "address": "0x4b9eb6c0b6ea15176bbf62841c6b2a8a398cb656", "chainId": 96 }, { "address": "0xda10009cbd5d07dd0cecc66161fc93d7c9000da1", "chainId": 61 }, { "address": "0x0200060000000000000000000000000000000000000000000000000000000000", "chainId": 24 }, { "address": "0xe3520349f477a5f6eb06107066048508498a291b", "chainId": 31 }, { "address": "0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1", "chainId": 84 }, { "address": "0x6de33698e9e9b787e09d3bd7771ef63557e148bb", "chainId": 90 }, { "address": "0x4651b38e7ec14bb3db731369bfe5b08f2466bd0a", "chainId": 17 }, { "address": "EjmyN6qEC1Tf1JxiG1ae7UTJhUxSwk1TCWNWqxWV4J6o", "chainId": 1 } ] }, { "id": 21076, "slug": "avalanche", "symbol": "AVAX", "name": "Avalanche", "priceUSD": "20.463198322952", "volume24hUSD": "234079380.7814911234932608", "circulatingSupply": "421534916", "contracts": [ { "address": "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b", "chainId": 5 }, { "address": "0x1CE0c2827e2eF14D5C4f29a091d735A204794041", "chainId": 4 }, { "address": "0x4792c1ecb969b036eb51330c63bd27899a13d84e", "chainId": 52 }, { "address": "0xcd8fe44a29db9159db36f96570d7a4d91986f528", "chainId": 19 }, { "address": "0x14a0243C333A5b238143068dC3A7323Ba4C30ECB", "chainId": 11 }, { "address": "0x65e66a61D0a8F1e686C2D6083ad611a10D84D97A", "chainId": 83 }, { "address": "0x6a5279e99ca7786fb13f827fc1fb4f61684933d6", "chainId": 96 } ] }, { "id": 23000, "slug": "pancakeswap", "symbol": "CAKE", "name": "PancakeSwap", "priceUSD": "2.371277962566", "volume24hUSD": "82354494.6215425524394026", "circulatingSupply": "321554264", "contracts": [ { "address": "0x0D1E753a25eBda689453309112904807625bEFBe", "chainId": 101 }, { "address": "0x3A287a06c66f9E95a56327185cA2BDF5f031cEcD", "chainId": 96 }, { "address": "0x1b896893dfc86bb67Cf57767298b9073D2c1bA2c", "chainId": 61 }, { "address": "0x2779106e4F4A8A28d77A24c18283651a2AE22D1C", "chainId": 110 }, { "address": "0x0d1e753a25ebda689453309112904807625befbe", "chainId": 102 }, { "address": "0x3055913c90Fcc1A6CE9a358911721eEb942013A1", "chainId": 98 }, { "address": "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", "chainId": 3 }, { "address": "0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82", "chainId": 4 }, { "address": "0x159df6b7689437016108a019fd5bef736bac692b6d4a1f10c941f6fbb9a74ca6::oft::CakeOFT", "chainId": 91 } ] }, { "id": 25225, "slug": "dextf-protocol", "symbol": "DEXTF", "name": "Domani Protocol", "priceUSD": "0.144790927447", "volume24hUSD": "134748.9592184632356044", "circulatingSupply": "65807235", "contracts": [ { "address": "0x9929bcac4417a21d7e6fc86f6dae1cc7f27a2e41", "chainId": 96 }, { "address": "0x03E8D118A1864c7Dc53bf91e007ab7D91f5A06fA", "chainId": 7 }, { "address": "0x5F64Ab1544D28732F0A24F4713c2C8ec0dA089f0", "chainId": 3 } ] }, { "id": 25781, "slug": "wbnb", "symbol": "WBNB", "name": "Wrapped BNB", "priceUSD": "656.079994507931", "volume24hUSD": "2292132064.2050728573560094", "circulatingSupply": "1523844", "contracts": [ { "address": "0xfa9343c3897324496a05fc75abed6bac29f8a40f", "chainId": 75 }, { "address": "0x442F7f22b1EE2c842bEAFf52880d4573E9201158", "chainId": 7 }, { "address": "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c", "chainId": 4 }, { "address": "0xc9baa8cfdde8e328787e29b4b078abf2dadc2055", "chainId": 52 }, { "address": "0x7f27352D5F83Db87a5A3E00f4B07Cc2138D8ee52", "chainId": 83 }, { "address": "9gP2kCy3wA1ctvYWQk75guqXuHfrEomqydHLtcTCqiLa", "chainId": 1 }, { "address": "0x673d2ec54e0a6580fc7e098295b70e3ce0350d03", "chainId": 12 }, { "address": "0x7400793aad94c8ca801aa036357d10f5fd0ce08f", "chainId": 96 }, { "address": "0xeCDCB5B88F8e3C15f95c720C51c71c9E2080525d", "chainId": 5 }, { "address": "0xd7D045BFBa6Ea93b480F409DB3dd1729337C1d13", "chainId": 18 }, { "address": "0x97e6c48867fdc391a8dfe9d169ecd005d1d90283", "chainId": 43 }, { "address": "0x2bF9b864cdc97b08B6D79ad4663e71B8aB65c45c", "chainId": 31 }, { "address": "0x2c78f1b70ccf63cdee49f9233e9faa99d43aa07e", "chainId": 55 }, { "address": "terra1cetg5wruw2wsdjp7j46rj44xdel00z006e9yg8", "chainId": 2 }, { "address": "ibc/F4A070A6D78496D53127EA85C094A9EC87DFC1F36071B8CCDDBD020F933D213D", "chainId": null }, { "address": "0xf5c6825015280cdfd0b56903f9f8b5a2233476f5", "chainId": 101 }, { "address": "0x94bd7A37d2cE24cC597E158fACaa8d601083ffeC", "chainId": 39 }, { "address": "0xb848cce11ef3a8f62eccea6eb5b35a12c4c2b1ee1af7755d02d7bd6218e8226f::coin::COIN", "chainId": 99 }, { "address": "0xD67de0e0a0Fd7b15dC8348Bb9BE742F3c5850454", "chainId": 10 }, { "address": "0x418D75f65a02b3D53B2418FB8E1fe493759c7605", "chainId": 3 } ] }, { "id": 26062, "slug": "wootrade", "symbol": "WOO", "name": "WOO", "priceUSD": "0.075585907922", "volume24hUSD": "3381083.3045858390631228", "circulatingSupply": "1918868005", "contracts": [ { "address": "0xabc9547b534519ff73921b1fba6e672b5f58d083", "chainId": 7 }, { "address": "0x6626c47c00f1d87902fc13eecfac3ed06d5e8d8a", "chainId": 10 }, { "address": "0x4691937a7508860f876c9c0a2a617e7d9e945d4b", "chainId": 4 }, { "address": "0x3befb2308bce92da97264077faf37dcd6c8a75e6", "chainId": 14 }, { "address": "0x1B815d120B3eF02039Ee11dC2d33DE7aA4a8C603", "chainId": 5 }, { "address": "0x4691937a7508860f876c9c0a2a617e7d9e945d4b", "chainId": 3 }, { "address": "0xcafcd85d8ca7ad1e1c6f82f651fa15e33aefd07b", "chainId": 61 }, { "address": "E5rk3nmgLUuKUiS94gg4bpWwWwyjCMtddsAXkTFLtHEy", "chainId": 1 }, { "address": "4691937a7508860f876c9c0a2a617e7d9e945d4b.factory.bridge.near", "chainId": 29 }, { "address": "0x9E22D758629761FC5708c171d06c2faBB60B5159", "chainId": 96 } ] }, { "id": 28986, "slug": "govi", "symbol": "GOVI", "name": "CVI", "priceUSD": "0.014434503875", "volume24hUSD": "84854.0413822350425000", "circulatingSupply": "15439655", "contracts": [ { "address": "0xeeaa40b28a2d1b0b08f6f97bb1dd4b75316c6107", "chainId": 3 }, { "address": "0x43Df9c0a1156c96cEa98737b511ac89D0e2A1F46", "chainId": 5 }, { "address": "0x07e49d5de43dda6162fa28d24d5935c151875283", "chainId": 61 }, { "address": "0xD63eF5e9C628c8a0E8984CDfb7444AEE44B09044", "chainId": 96 } ] }, { "id": 29494, "slug": "mute", "symbol": "MUTE", "name": "Mute", "priceUSD": "0.016605279624", "volume24hUSD": "4.6335687763122456", "circulatingSupply": "40000000", "contracts": [ { "address": "0xa49d7499271ae71cd8ab9ac515e6694c755d400c", "chainId": 3 }, { "address": "0x0e97c7a0f8b2c9885c8ac9fc6136e829cbc21d42", "chainId": 96 } ] }, { "id": 78303, "slug": "wpol", "symbol": "WPOL", "name": "Wrapped POL", "priceUSD": "0.214828338580", "volume24hUSD": "7167694.6332244566141460", "circulatingSupply": "298839976", "contracts": [ { "address": "0xf2f13f0B7008ab2FA4A2418F4ccC3684E49D20Eb", "chainId": 7 }, { "address": "0xc836d8dC361E44DbE64c4862D55BA041F88Ddd39", "chainId": 4 }, { "address": "0x28a487240e4d45cff4a2980d334cc933b7483842", "chainId": 96 }, { "address": "0xdbe380b13a6d0f5cdedd58de8f04625263f113b3f9db32b3e1983f49e2841676::coin::COIN", "chainId": 99 }, { "address": "Gz7VkD4MacbEB6yC5XD3HcumEiYx2EtDYYrfikGsvopG", "chainId": 1 }, { "address": "0x6aB6d61428fde76768D7b45D8BFeec19c6eF91A8", "chainId": 31 }, { "address": "0x8e66c0d6b70c0b23d39f4b21a1eac52bba8ed89a", "chainId": 43 }, { "address": "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270", "chainId": 5 } ] }, { "id": 169625, "slug": "idexo-token", "symbol": "IDO", "name": "Idexo Token", "priceUSD": "0.019806179953", "volume24hUSD": "28219.3104301756690000", "circulatingSupply": "81027500", "contracts": [ { "address": "0xF9c53268e9de692AE1b2ea5216E24e1c3ad7CB1E", "chainId": 3 }, { "address": "0xDea6d5161978d36b5C0FA6a491faA754f4BC809C", "chainId": 96 } ] }, { "id": 169760, "slug": "lido-finance-wsteth", "symbol": "wstETH", "name": "Wrapped stETH", "priceUSD": "3038.168722518680", "volume24hUSD": "6334397.0657931186642560", "circulatingSupply": "3545482", "contracts": [ { "address": "0x1F32b1c2345538c0c6f582fCB022739c4A194Ebb", "chainId": 84 }, { "address": "0xf610A9dfB7C89644979b4A0f27063E9e7d7Cda32", "chainId": 108 }, { "address": "0x6C76971f98945AE98dD7d4DFcA8711ebea946eA6", "chainId": 78 }, { "address": "0x703b52f2b28febcb60e1372858af5b18849fe867", "chainId": 96 }, { "address": "0xB5beDd42000b71FddE22D3eE8a79Bd49A568fC8F", "chainId": 101 }, { "address": "0x458ed78EB972a369799fb278c0243b25e5242A83", "chainId": 103 }, { "address": "0xc1CBa3fCea344f92D9239c08C0568f6F2F0ee452", "chainId": 98 }, { "address": "0x03b54A6e9a984069379fae1a4fC4dBAE93B3bCCD", "chainId": 5 }, { "address": "0xc02fE7317D4eb8753a02c35fe019786854A92001", "chainId": 492 }, { "address": "0x5979D7b546E38E414F7E9822514be443A4800529", "chainId": 61 }, { "address": "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0", "chainId": 3 } ] }, { "id": 169918, "slug": "nodle", "symbol": "NODL", "name": "Nodle", "priceUSD": "0.000305023780", "volume24hUSD": "116874.9633574116394000", "circulatingSupply": "935521185", "contracts": [ { "address": "0xBD4372e44c5eE654dd838304006E1f0f69983154", "chainId": 96 } ] }, { "id": 171109, "slug": "symbiosis-finance", "symbol": "SIS", "name": "Symbiosis Finance", "priceUSD": "0.060961263279", "volume24hUSD": "1632923.5396401369641178", "circulatingSupply": "65321769", "contracts": [ { "address": "0x1467b62A6AE5CdcB10A6a8173cfe187DD2C5a136", "chainId": 108 }, { "address": "0x9e758b8a98a42d612b3d38b66a22074dc03d7370", "chainId": 61 }, { "address": "0x6EF95B6f3b0F39508e3E04054Be96D5eE39eDE0d", "chainId": 101 }, { "address": "0xd38bb40815d2b0c2d2c866e0c72c5728ffc76dd9", "chainId": 3 }, { "address": "0xdd9f72afED3631a6C85b5369D84875e6c42f1827", "chainId": 96 }, { "address": "0xF98b660AdF2ed7d9d9D9dAACC2fb0CAce4F21835", "chainId": 4 } ] }, { "id": 171673, "slug": "izumi-finance", "symbol": "IZI", "name": "iZUMi Finance", "priceUSD": "0.004412784502", "volume24hUSD": "330746.6003961574641634", "circulatingSupply": "787400000", "contracts": [ { "address": "0x9ad37205d608B8b219e6a2573f922094CEc5c200", "chainId": 3 }, { "address": "0x60D01EC2D5E98Ac51C8B4cF84DfCCE98D527c747", "chainId": 61 }, { "address": "0x60D01EC2D5E98Ac51C8B4cF84DfCCE98D527c747", "chainId": 108 }, { "address": "0x16A9494e257703797D747540f01683952547EE5b", "chainId": 96 }, { "address": "0x60d01ec2d5e98ac51c8b4cf84dfcce98d527c747", "chainId": 103 }, { "address": null, "chainId": 4 }, { "address": "0x60D01EC2D5E98Ac51C8B4cF84DfCCE98D527c747", "chainId": 98 }, { "address": "0x91647632245cabf3d66121f86c387ae0ad295f9a", "chainId": 143 }, { "address": "0x60D01EC2D5E98Ac51C8B4cF84DfCCE98D527c747", "chainId": 101 }, { "address": "0x60D01EC2D5E98Ac51C8B4cF84DfCCE98D527c747", "chainId": 5 } ] }, { "id": 173442, "slug": "liquity-usd", "symbol": "LUSD", "name": "Liquity USD", "priceUSD": "0.995979718780", "volume24hUSD": "2758.7340448632429660", "circulatingSupply": "41318766", "contracts": [ { "address": "0x368181499736d0c0CC614DBB145E2EC1AC86b8c6", "chainId": 98 }, { "address": "0xc40F949F8a4e094D1b49a23ea9241D289B7b2819", "chainId": 84 }, { "address": "0x5f98805a4e8be255a32880fdec7f6728c6568ba0", "chainId": 3 }, { "address": "0x93b346b6BC2548dA6A1E7d98E9a421B42541425b", "chainId": 61 }, { "address": "0x503234F203fC7Eb888EEC8513210612a43Cf6115", "chainId": 96 }, { "address": "0x23001f892c0c82b79303edc9b9033cd190bb21c7", "chainId": 5 } ] }, { "id": 174671, "slug": "maverick-protocol", "symbol": "MAV", "name": "Maverick Protocol", "priceUSD": "0.055832135906", "volume24hUSD": "2049518.9217414230150462", "circulatingSupply": "603700522", "contracts": [ { "address": "0x7448c7456a97769F6cD04F1E83A4a23cCdC46aBD", "chainId": 3 }, { "address": "0x64b88c73A5DfA78D1713fE1b4c69a22d7E0faAa7", "chainId": 98 }, { "address": "0x787c09494Ec8Bcb24DcAf8659E7d5D69979eE508", "chainId": 96 }, { "address": "0xd691d9a68C887BDF34DA8c36f63487333ACfD103", "chainId": 4 } ] }, { "id": 174835, "slug": "usd", "symbol": "USD+", "name": "Overnight.fi USD+", "priceUSD": "1.000886021354", "volume24hUSD": "2318557.5607744404448812", "circulatingSupply": "85659194", "contracts": [ { "address": "0x236eeC6359fb44CCe8f97E99387aa7F8cd5cdE1f", "chainId": 5 }, { "address": "0xB79DD08EA68A908A97220C76d19A6aA9cBDE4376", "chainId": 101 }, { "address": "0xB79DD08EA68A908A97220C76d19A6aA9cBDE4376", "chainId": 98 }, { "address": "0x4fEE793d435c6D2c10C135983BB9d6D4fC7B9BBd", "chainId": 221 }, { "address": "0xe80772eaf6e2e18b651f160bc9158b2a5cafca65", "chainId": 61 }, { "address": "0xe80772eaf6e2e18b651f160bc9158b2a5cafca65", "chainId": 7 }, { "address": "0x8E86e46278518EFc1C5CEd245cBA2C7e3ef11557", "chainId": 96 }, { "address": "0x73cb180bf0521828d8849bc8cf2b920918e23032", "chainId": 84 }, { "address": "0xe80772eaf6e2e18b651f160bc9158b2a5cafca65", "chainId": 4 } ] }, { "id": 176076, "slug": "zksync", "symbol": "ZK", "name": "zkSync", "priceUSD": "0.053223305221", "volume24hUSD": "21534018.8609259087161898", "circulatingSupply": "3675000000", "contracts": [ { "address": "0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E", "chainId": 96 } ] }, { "id": 177999, "slug": "metavault-trade", "symbol": "MVX", "name": "Metavault Trade", "priceUSD": "0.134441948770", "volume24hUSD": "55.7786201251853000", "circulatingSupply": "3158978", "contracts": [ { "address": "0x0018D96C579121a94307249d47F053E2D687b5e7", "chainId": 101 }, { "address": "0x0018D96C579121a94307249d47F053E2D687b5e7", "chainId": 108 }, { "address": "0xc8ac6191cdc9c7bf846ad6b52aaaa7a0757ee305", "chainId": 96 }, { "address": "0x2760e46d9bb43dafcbecaad1f64b93207f9f0ed7", "chainId": 5 } ] }, { "id": 178476, "slug": "paxo-finance", "symbol": "WEFI", "name": "WeFi", "priceUSD": "0.030589544594", "volume24hUSD": "6155.0057772154816200", "circulatingSupply": "41883332", "contracts": [ { "address": "0xfFA188493C15DfAf2C206c97D8633377847b6a52", "chainId": 5 }, { "address": "0x81E7186947fb59AAAAEb476a47daAc60680cbbaF", "chainId": 96 }, { "address": "0x60892e742d91d16Be2cB0ffE847e85445989e30B", "chainId": 101 }, { "address": "0xfFA188493C15DfAf2C206c97D8633377847b6a52", "chainId": 4 }, { "address": "0xfFA188493C15DfAf2C206c97D8633377847b6a52", "chainId": 61 }, { "address": "0xfFA188493C15DfAf2C206c97D8633377847b6a52", "chainId": 3 } ] }, { "id": 180336, "slug": "veno-finance", "symbol": "VNO", "name": "Veno Finance", "priceUSD": "0.018190374275", "volume24hUSD": "115437.6067161836517400", "circulatingSupply": "511160922", "contracts": [ { "address": "0xe75a17b4f5c4f844688d5670b684515d7c785e63", "chainId": 96 }, { "address": "0xdb7d0a1ec37de1de924f8e8adac6ed338d4404e9", "chainId": 75 } ] }, { "id": 181064, "slug": "hypercomic", "symbol": "HYCO", "name": "HYPERCOMIC", "priceUSD": "0.000056327575", "volume24hUSD": "94095.2908919698815000", "circulatingSupply": null, "contracts": [ { "address": "0x77F76483399Dc6328456105B1db23e2Aca455bf9", "chainId": 3 }, { "address": "0x45656c02Aae856443717C34159870b90D1288203", "chainId": 96 } ] }, { "id": 181939, "slug": "zkdoge", "symbol": "ZKDOGE", "name": "zkDoge", "priceUSD": "0.000003460380", "volume24hUSD": "9.8128693700984100", "circulatingSupply": null, "contracts": [ { "address": "0xbFB4b5616044Eded03e5b1AD75141f0D9Cb1499b", "chainId": 96 } ] }, { "id": 181986, "slug": "fulcrom-finance", "symbol": "FUL", "name": "Fulcrom", "priceUSD": "0.006995472668", "volume24hUSD": "15865.7646197202946152", "circulatingSupply": "2097500155", "contracts": [ { "address": "0xe593853b4d603d5b8f21036Bb4AD0D1880097a6e", "chainId": 96 }, { "address": "0x83aFB1C32E5637ACd0a452D87c3249f4a9F0013A", "chainId": 75 } ] }, { "id": 182038, "slug": "space-fi", "symbol": "SPACE", "name": "SpaceFi", "priceUSD": "0.008479370310", "volume24hUSD": "273.3965067737609730", "circulatingSupply": "7047643", "contracts": [ { "address": "0x47260090ce5e83454d5f05a0abbb2c953835f777", "chainId": 96 } ] }, { "id": 182572, "slug": "reactorfusion", "symbol": "RF", "name": "ReactorFusion", "priceUSD": "0.001274750675", "volume24hUSD": null, "circulatingSupply": null, "contracts": [ { "address": "0x5f7CBcb391d33988DAD74D6Fd683AadDA1123E4D", "chainId": 96 } ] }, { "id": 182821, "slug": "derp-coin", "symbol": "DERP", "name": "DerpDEX", "priceUSD": "0.000000000982", "volume24hUSD": "86527.3838211341400000", "circulatingSupply": "55600000000000", "contracts": [ { "address": "0x5DfC78C4D073fD343BC6661668948178522A0DE5", "chainId": 3 }, { "address": "0x0bf4CB727b3f8092534D793893B2cC3348963dbf", "chainId": 96 }, { "address": "0xEbb78043e29F4af24E6266A7D142f5A08443969E", "chainId": 98 }, { "address": "0xEbb78043e29F4af24E6266A7D142f5A08443969E", "chainId": 110 } ] }, { "id": 183061, "slug": "zkapes", "symbol": "ZAT", "name": "zkApe", "priceUSD": "0.000000002068", "volume24hUSD": "84267.4172353719200000", "circulatingSupply": "46426356238082", "contracts": [ { "address": "0x47EF4A5641992A72CFd57b9406c9D9cefEE8e0C4", "chainId": 96 } ] }, { "id": 183169, "slug": "vesync", "symbol": "VS", "name": "veSync", "priceUSD": "0.000177133079", "volume24hUSD": "17.5539080563713875", "circulatingSupply": null, "contracts": [ { "address": "0x5756A28E2aAe01F600FC2C01358395F5C1f8ad3A", "chainId": 96 } ] }, { "id": 183206, "slug": "gravita-protocol", "symbol": "GRAI", "name": "Gravita Protocol", "priceUSD": "1.038124398700", "volume24hUSD": "254.4263305692724900", "circulatingSupply": "115838", "contracts": [ { "address": "0x15f74458aE0bFdAA1a96CA1aa779D715Cc1Eefe4", "chainId": 3 }, { "address": "0x894134a25a5faC1c2C26F1d8fBf05111a3CB9487", "chainId": 61 }, { "address": "0x5fc44e95eaa48f9eb84be17bd3ac66b6a82af709", "chainId": 96 } ] }, { "id": 185576, "slug": "holdstation", "symbol": "HOLD", "name": "Holdstation", "priceUSD": "1.047597992039", "volume24hUSD": "566623.5207217100563030", "circulatingSupply": "7903700", "contracts": [ { "address": "0xed4040fd47629e7c8fbb7da76bb50b3e7695f0f2", "chainId": 96 } ] }, { "id": 185628, "slug": "metaelfland-new", "symbol": "MELD", "name": "MetaElfLand", "priceUSD": "0.000085906805", "volume24hUSD": "104532.7469887663634000", "circulatingSupply": "549580000", "contracts": [ { "address": "0xcd2cfa60f04f3421656d6eebee122b3973b3f60c", "chainId": 96 } ] }, { "id": 185653, "slug": "karat", "symbol": "KAT", "name": "Karat", "priceUSD": "0.000561575224", "volume24hUSD": "65780.8531208648736000", "circulatingSupply": "1090715670", "contracts": [ { "address": "0xCDb7D260c107499C80B4b748e8331c64595972a1", "chainId": 96 } ] }, { "id": 185915, "slug": "zkswap-finance", "symbol": "ZF", "name": "zkSwap Finance", "priceUSD": "0.002617039976", "volume24hUSD": "140497.8235385651772904", "circulatingSupply": "551289480", "contracts": [ { "address": "0x31c2c031fdc9d33e974f327ab0d9883eae06ca4a", "chainId": 96 } ] }, { "id": 186051, "slug": "wagmi-com", "symbol": "WAGMI", "name": "Wagmi", "priceUSD": "0.006931617803", "volume24hUSD": "1934561.2104832143165772", "circulatingSupply": "1816532138", "contracts": [ { "address": "0xaf20f5f19698f1D19351028cd7103B63D30DE7d7", "chainId": 4 }, { "address": "0xb1f795776cb9ddac6e7e162f31c7419dd3d48297", "chainId": 10 }, { "address": "0xaf20f5f19698f1d19351028cd7103b63d30de7d7", "chainId": 61 }, { "address": "0x07Ed33a242BD9C08CA3C198e01189e35265024Da", "chainId": 5 }, { "address": "0x92CC36D66e9d739D50673d1f27929a371FB83a67", "chainId": 3 }, { "address": "0xaf20f5f19698f1d19351028cd7103b63d30de7d7", "chainId": 7 }, { "address": "0x0e0Ce4D450c705F8a0B6Dd9d5123e3df2787D16B", "chainId": 465 }, { "address": "0xaf20f5f19698f1D19351028cd7103B63D30DE7d7", "chainId": 314 }, { "address": "0xaf20f5f19698f1D19351028cd7103B63D30DE7d7", "chainId": 98 }, { "address": "0xaf20f5f19698f1d19351028cd7103b63d30de7d7", "chainId": 84 }, { "address": "0xaf20f5f19698f1d19351028cd7103b63d30de7d7", "chainId": 17 }, { "address": "0x3613ad277df1d5935d41400a181aa9ec1dc2dc9e", "chainId": 96 } ] }, { "id": 187519, "slug": "tarot-v-2", "symbol": "TAROT", "name": "Tarot v2", "priceUSD": "0.102369568487", "volume24hUSD": "26514.2841985293372282", "circulatingSupply": "67508541", "contracts": [ { "address": "0xf544251d25f3d243a36b07e7e7962a678f952691", "chainId": 98 }, { "address": "0x7f2fd959013eec5144269ac6edd0015cb10968fc", "chainId": 96 }, { "address": "0x2e4c7bf66d0484e44fea0ec273b85a00af92b2e3", "chainId": 81 }, { "address": "0x981bd9f77c8aafc14ebc86769503f86a3cc29af5", "chainId": 103 }, { "address": "0x13278cd824d33a7adb9f0a9a84aca7c0d2deebf7", "chainId": 89 }, { "address": "0xb092e1bf50f518b3ebf7ed26a40015183ae36ac2", "chainId": 5 }, { "address": "0x5ecfec22aa950cb5a3b4fd7249dc30b2bd160f18", "chainId": 7 }, { "address": "0x1f514a61bcde34f94bc39731235690ab9da737f7", "chainId": 84 }, { "address": "0x982e609643794a31a07f5c5b142dd3a9cf0690be", "chainId": 4 }, { "address": "0xa10bf0aba0c7953f279c4cb8192d3b5de5ea56e8", "chainId": 3 }, { "address": "0xb7c2ddb1ebac1056231ef22c1b0a13988537a274", "chainId": 10 } ] }, { "id": 187575, "slug": "zero-lend", "symbol": "ZERO", "name": "ZeroLend", "priceUSD": "0.000055572630", "volume24hUSD": "1902730.6709149503654030", "circulatingSupply": "71671379638", "contracts": [ { "address": "0x78354f8dccb269a615a7e0a24f9b0718fdc3c7a7", "chainId": 101 }, { "address": "0x27d0A2b5316b98088294378692F4EAbfB3222e36", "chainId": 96 } ] }, { "id": 188057, "slug": "libertas-omnibus", "symbol": "LIBERTAS", "name": "LIBERTAS OMNIBUS", "priceUSD": "2.012875893051", "volume24hUSD": "18.4928946922274523", "circulatingSupply": null, "contracts": [ { "address": "0xC6DaC3A53D5d6dE9D1D05AA6e28B8e9E41722601", "chainId": 96 } ] }, { "id": 188523, "slug": "koi-2", "symbol": "KOI", "name": "Koi", "priceUSD": "0.002194806500", "volume24hUSD": "205.9992931609069500", "circulatingSupply": "500000000", "contracts": [ { "address": "0xa995ad25ce5eb76972ab356168f5e1d9257e4d05", "chainId": 96 }, { "address": "0x9d14bce1daddf408d77295bb1be9b343814f44de", "chainId": 3 } ] }, { "id": 188734, "slug": "autoair-ai", "symbol": "AAI", "name": "AutoAir AI", "priceUSD": "0.002541865297", "volume24hUSD": null, "circulatingSupply": "33250000", "contracts": [ { "address": "0x144b83555d8a3119b0a69a7bc2f0a0388308fee3", "chainId": 96 } ] }, { "id": 189041, "slug": "long-2", "symbol": "LONG", "name": "Long", "priceUSD": "0.000000694095", "volume24hUSD": "0.8418805348653165", "circulatingSupply": "783813835230", "contracts": [ { "address": "0x5165ec33b491d7b67260B3143f96Bb4aC4736398", "chainId": 96 } ] }, { "id": 189524, "slug": "tevaera-zk", "symbol": "TEVA", "name": "Tevaera", "priceUSD": "0.007110085092", "volume24hUSD": "859877.3327826079846800", "circulatingSupply": "425508223", "contracts": [ { "address": "0xdbFF7c6d368904680706804645cAfA4dEfa3c224", "chainId": 96 } ] }, { "id": 189990, "slug": "wrapped-rseth", "symbol": "wrsETH", "name": "Wrapped rsETH", "priceUSD": "2642.703746000547", "volume24hUSD": "47011.5854783529306924", "circulatingSupply": null, "contracts": [ { "address": "0x87eEE96D50Fb761AD85B1c982d28A042169d61b1", "chainId": 84 }, { "address": "0xD2671165570f41BBB3B0097893300b6EB6101E6C", "chainId": 101 }, { "address": "0xe7903B1F75C534Dd8159b313d92cDCfbC62cB3Cd", "chainId": 150 }, { "address": "0xe7903B1F75C534Dd8159b313d92cDCfbC62cB3Cd", "chainId": 221 }, { "address": "0xEDfa23602D0EC14714057867A78d01e94176BEA0", "chainId": 98 }, { "address": "0xd4169E045bcF9a86cC00101225d9ED61D2F51af2", "chainId": 96 }, { "address": "0xa25b25548B4C98B0c7d3d27dcA5D5ca743d68b7F", "chainId": 108 } ] } ], "meta": { "count": 52 }, "status": { "time": "2025-06-02T14:54:40.534Z", "success": true, "code": 200, "message": "OK", "responseTime": 12, "creditsCost": 1 }} + """ + end + + defp json_tokens_2nd_page do + """ + {"data":[{"id":190444,"slug":"zyfi","symbol":"ZFI","name":"Zyfi","priceUSD":"0.004467522847","volume24hUSD":"527.5318580293320804","circulatingSupply":"228225560","contracts":[{"address":"0x5d0d7BCa050e2E98Fd4A5e8d3bA823B49f39868d","chainId":96}]},{"id":193174,"slug":"heurist","symbol":"HEU","name":"Heurist","priceUSD":"0.019624517115","volume24hUSD":"214644.7428230154531300","circulatingSupply":"155241424","contracts":[{"address":"0xEF22cb48B8483dF6152e1423b19dF5553BbD818b","chainId":98},{"address":"0xAbEc5eCBe08b6c02F5c9A2fF82696e1E7dB6f9bf","chainId":96}]}],"meta":{"count":52},"status":{"time":"2025-06-02T15:11:56.798Z","success":true,"code":200,"message":"OK","responseTime":11,"creditsCost":1}} + """ + end + + # cspell:enable + + defp json_native_coin_history do + """ + {"data":{"dates":["2025-05-28T00:00:00.000Z","2025-05-29T00:00:00.000Z","2025-05-30T00:00:00.000Z","2025-05-31T00:00:00.000Z","2025-06-01T00:00:00.000Z"],"volumes":[10438545595.534193,7970411497.8339,11568734237.80682,10646850181.382534,5666095954.980677],"prices":[2662.609005038795,2681.794582393094,2631.742984069461,2532.024768716731,2528.507007933771],"currency":"USD"},"status":{"time":"2025-06-02T15:32:53.690Z","success":true,"code":200,"message":"OK","responseTime":21,"creditsCost":1}} + """ + end + + defp json_secondary_coin_history do + """ + {"data":{"dates":["2025-05-28T00:00:00.000Z","2025-05-29T00:00:00.000Z","2025-05-30T00:00:00.000Z","2025-05-31T00:00:00.000Z","2025-06-01T00:00:00.000Z"],"volumes":[16697342.091162598,15904676.379819755,18808755.81579478,31779868.78666319,21254600.77892847],"prices":[6.669271727877,6.558096674134,6.443746862121,5.80657534093,5.903371964672],"currency":"USD"},"status":{"time":"2025-06-02T15:34:36.792Z","success":true,"code":200,"message":"OK","responseTime":49,"creditsCost":1}} + """ + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 428605795eb7..52a26a54354b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -452,7 +452,8 @@ config :explorer, Explorer.Market.Source.CryptoRank, ConfigHelper.parse_integer_or_nil_env_var("EXCHANGE_RATES_CRYPTORANK_COIN_ID"), secondary_coin_id: System.get_env("MARKET_CRYPTORANK_SECONDARY_COIN_ID") || - ConfigHelper.parse_integer_or_nil_env_var("EXCHANGE_RATES_CRYPTORANK_SECONDARY_COIN_ID") + ConfigHelper.parse_integer_or_nil_env_var("EXCHANGE_RATES_CRYPTORANK_SECONDARY_COIN_ID"), + currency: "USD" config :explorer, Explorer.Market.Source.DefiLlama, coin_id: System.get_env("MARKET_DEFILLAMA_COIN_ID"), From 024250a5366e74dafd9f360b79e754b4136f4d5b Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:14:03 +0300 Subject: [PATCH 0122/1006] fix: flaky 404 in `/api/v2/internal-transactions` (#12701) --- .../chain/cache/background_migrations.ex | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 66258b72ac31..026dde330827 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -99,133 +99,133 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do } defp handle_fallback(:transactions_denormalization_finished) do - start_migration_status_task( + set_and_return_migration_status( TransactionsDenormalization, &set_transactions_denormalization_finished/1 ) end defp handle_fallback(:tb_token_type_finished) do - start_migration_status_task( + set_and_return_migration_status( AddressTokenBalanceTokenType, &set_tb_token_type_finished/1 ) end defp handle_fallback(:ctb_token_type_finished) do - start_migration_status_task( + set_and_return_migration_status( AddressCurrentTokenBalanceTokenType, &set_ctb_token_type_finished/1 ) end defp handle_fallback(:tt_denormalization_finished) do - start_migration_status_task( + set_and_return_migration_status( TokenTransferTokenType, &set_tt_denormalization_finished/1 ) end defp handle_fallback(:sanitize_duplicated_log_index_logs_finished) do - start_migration_status_task( + set_and_return_migration_status( SanitizeDuplicatedLogIndexLogs, &set_sanitize_duplicated_log_index_logs_finished/1 ) end defp handle_fallback(:backfill_multichain_search_db_finished) do - start_migration_status_task( + set_and_return_migration_status( BackfillMultichainSearchDB, &set_backfill_multichain_search_db_finished/1 ) end defp handle_fallback(:heavy_indexes_create_logs_block_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateLogsBlockHashIndex, &set_heavy_indexes_create_logs_block_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_logs_block_number_asc_index_asc_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropLogsBlockNumberAscIndexAscIndex, &set_heavy_indexes_drop_logs_block_number_asc_index_asc_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_logs_address_hash_block_number_desc_index_desc_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateLogsAddressHashBlockNumberDescIndexDescIndex, &set_heavy_indexes_create_logs_address_hash_block_number_desc_index_desc_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_logs_address_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropLogsAddressHashIndex, &set_heavy_indexes_drop_logs_address_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_logs_address_hash_transaction_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropLogsAddressHashTransactionHashIndex, &set_heavy_indexes_drop_logs_address_hash_transaction_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_logs_index_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropLogsIndexIndex, &set_heavy_indexes_drop_logs_index_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_logs_address_hash_first_topic_block_number_index_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateLogsAddressHashFirstTopicBlockNumberIndexIndex, &set_heavy_indexes_create_logs_address_hash_first_topic_block_number_index_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_token_transfers_block_number_asc_log_index_asc_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropTokenTransfersBlockNumberAscLogIndexAscIndex, &set_heavy_indexes_drop_token_transfers_block_number_asc_log_index_asc_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_token_transfers_from_address_hash_transaction_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropTokenTransfersFromAddressHashTransactionHashIndex, &set_heavy_indexes_drop_token_transfers_from_address_hash_transaction_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_token_transfers_to_address_hash_transaction_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropTokenTransfersToAddressHashTransactionHashIndex, &set_heavy_indexes_drop_token_transfers_to_address_hash_transaction_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_token_transfers_token_contract_address_hash_transaction_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropTokenTransfersTokenContractAddressHashTransactionHashIndex, &set_heavy_indexes_drop_token_transfers_token_contract_address_hash_transaction_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_token_transfers_block_number_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropTokenTransfersBlockNumberIndex, &set_heavy_indexes_drop_token_transfers_block_number_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_internal_transactions_from_address_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( DropInternalTransactionsFromAddressHashIndex, &set_heavy_indexes_drop_internal_transactions_from_address_hash_index_finished/1 ) @@ -234,91 +234,91 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do defp handle_fallback( :heavy_indexes_create_internal_transactions_block_number_desc_transaction_index_desc_index_desc_index_finished ) do - start_migration_status_task( + set_and_return_migration_status( CreateInternalTransactionsBlockNumberDescTransactionIndexDescIndexDescIndex, &set_heavy_indexes_create_internal_transactions_block_number_desc_transaction_index_desc_index_desc_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_addresses_verified_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateAddressesVerifiedHashIndex, &set_heavy_indexes_create_addresses_verified_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_addresses_verified_transactions_count_desc_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateAddressesVerifiedTransactionsCountDescHashIndex, &set_heavy_indexes_create_addresses_verified_transactions_count_desc_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_addresses_verified_fetched_coin_balance_desc_hash_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateAddressesVerifiedFetchedCoinBalanceDescHashIndex, &set_heavy_indexes_create_addresses_verified_fetched_coin_balance_desc_hash_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_smart_contracts_language_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateSmartContractsLanguageIndex, &set_heavy_indexes_create_smart_contracts_language_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index) do - start_migration_status_task( + set_and_return_migration_status( DropTransactionsCreatedContractAddressHashWithPendingIndex, &set_heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_transactions_from_address_hash_with_pending_index) do - start_migration_status_task( + set_and_return_migration_status( DropTransactionsFromAddressHashWithPendingIndex, &set_heavy_indexes_drop_transactions_from_address_hash_with_pending_index_finished/1 ) end defp handle_fallback(:heavy_indexes_drop_transactions_to_address_hash_with_pending_index) do - start_migration_status_task( + set_and_return_migration_status( DropTransactionsToAddressHashWithPendingIndex, &set_heavy_indexes_drop_transactions_to_address_hash_with_pending_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_logs_deposits_withdrawals_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateLogsDepositsWithdrawalsIndex, &set_heavy_indexes_create_logs_deposits_withdrawals_index_finished/1 ) end defp handle_fallback(:arbitrum_da_records_normalization_finished) do - start_migration_status_task( + set_and_return_migration_status( ArbitrumDaRecordsNormalization, &set_arbitrum_da_records_normalization_finished/1 ) end defp handle_fallback(:smart_contract_language_finished) do - start_migration_status_task( + set_and_return_migration_status( SmartContractLanguage, &set_smart_contract_language_finished/1 ) end defp handle_fallback(:heavy_indexes_create_arbitrum_batch_l2_blocks_unconfirmed_blocks_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateArbitrumBatchL2BlocksUnconfirmedBlocksIndex, &set_heavy_indexes_create_arbitrum_batch_l2_blocks_unconfirmed_blocks_index_finished/1 ) end defp handle_fallback(:heavy_indexes_create_addresses_transactions_count_desc_partial_index_finished) do - start_migration_status_task( + set_and_return_migration_status( CreateAddressesTransactionsCountDescPartialIndex, &set_heavy_indexes_create_addresses_transactions_count_desc_partial_index_finished/1 ) @@ -327,7 +327,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do defp handle_fallback( :heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished ) do - start_migration_status_task( + set_and_return_migration_status( CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, &set_heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished/1 ) @@ -337,11 +337,11 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do {:return, false} end - defp start_migration_status_task(migration_module, status_setter) do - Task.start_link(fn -> - status_setter.(migration_module.migration_finished?()) - end) + defp set_and_return_migration_status(migration_module, status_setter) do + status = migration_module.migration_finished?() - {:return, false} + status_setter.(status) + + {:return, status} end end From 116455763f08a34f57465ce9e78f096d3c4d952e Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:16:13 +0300 Subject: [PATCH 0123/1006] chore: new tac microservice endpoint for search (#12448) --- .dialyzer_ignore.exs | 6 +- .../api/v2/search_controller_test.exs | 1048 +++++++++++++---- apps/explorer/lib/explorer/chain/search.ex | 253 ++-- .../tac_operation_lifecycle.ex | 79 +- apps/explorer/mix.exs | 3 +- mix.lock | 5 + 6 files changed, 1022 insertions(+), 372 deletions(-) diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index eba0066a0dc9..233b0967a97f 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -10,7 +10,7 @@ {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :exact_eq, 14}, {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 14}, ~r/lib\/phoenix\/router.ex/, - {"lib/explorer/chain/search.ex", :pattern_match, 80}, - {"lib/explorer/chain/search.ex", :pattern_match, 232}, - {"lib/explorer/chain/search.ex", :pattern_match, 327} + {"lib/explorer/chain/search.ex", :pattern_match, 100}, + {"lib/explorer/chain/search.ex", :pattern_match, 283}, + {"lib/explorer/chain/search.ex", :pattern_match, 383} ] diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 97da1d39b0d8..2e759956a252 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -1136,33 +1136,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ - Bypass.expect( + Bypass.expect_once( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search?q=#{operation_id}") @@ -1174,23 +1167,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } @@ -1199,7 +1177,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do } == json_response(request, 200) end - test "handles 404 from TAC microservice", %{conn: conn} do + test "handles no results from TAC microservice", %{conn: conn} do bypass = Bypass.open() tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) @@ -1217,16 +1195,19 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "code": 5, - "message": "cannot find operation id" + "items": [], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 404, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search?q=#{operation_id}") @@ -1257,33 +1238,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search?q=#{operation_id}") @@ -1295,23 +1269,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } in tl(response["items"]) @@ -1345,33 +1304,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search?q=#{operation_id}") @@ -1383,23 +1335,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } in tl(response["items"]) @@ -1414,6 +1351,376 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "url" => "/block/#{transaction.block_hash}" } in response["items"] end + + test "finds TAC operations by sender and paginates", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + address_hash = Address.checksum(insert(:address).hash) + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + + tac_first_response = """ + { + "items": [ + #{for i <- 10..59, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 59, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250219, + "page_size": 50 + } + } + """ + + tac_second_response = """ + { + "items": [ + #{for i <- 10..59, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "#{if i == 0, do: "2025-05-14T19:16:59.000Z", else: "2025-05-14T19:17:#{i}.000Z"}", + "type": "TON_TAC_TON" + }#{if i == 59, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250279, + "page_size": 50 + } + } + """ + + tac_third_response = """ + { + "items": [ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:18:01.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, tac_first_response) + "1747250218" -> Plug.Conn.resp(conn, 200, tac_second_response) + "1747250279" -> Plug.Conn.resp(conn, 200, tac_third_response) + _ -> raise "Unexpected page_token" + end + end + ) + + request = get(conn, "/api/v2/search?q=#{address_hash}") + assert response = json_response(request, 200) + + second_page_request = + get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + + second_page_response = json_response(second_page_request, 200) + + third_page_request = + get(conn, "/api/v2/search", second_page_response["next_page_params"] |> Query.encode() |> Query.decode()) + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => address_hash, + "timestamp" => "2025-05-14T19:18:01.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(third_page_request, 200) + end + + test "finds TAC operations by TON sender", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + tac_response = """ + { + "items": [ + { + "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type": "ROLLBACK", + "timestamp": "2025-06-05T12:21:11.000Z", + "sender": { + "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain": "TON" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["q"] do + expected_q + when expected_q in [ + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", + "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", + "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", + "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", + "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" + ] -> + Plug.Conn.resp(conn, 200, tac_response) + + q -> + raise "Unexpected 'q' parameter #{inspect(q)}" + end + end + ) + + request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + + request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + + request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + + request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + + request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + + request = + get( + conn, + "/api/v2/search?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" + ) + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] + end + + test "finds TAC operations with transaction and paginates", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + + transaction = insert(:transaction, hash: operation_id) + + tac_first_response = """ + { + "items": [ + #{for i <- 0..49, do: """ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250209, + "page_size": 50 + } + } + """ + + tac_second_response = """ + { + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:50.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, tac_first_response) + "1747250208" -> Plug.Conn.resp(conn, 200, tac_second_response) + _ -> raise "Unexpected page_token" + end + end + ) + + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) + next_page_request = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:50.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(next_page_request, 200) + end end describe "/search/check-redirect" do @@ -1725,33 +2032,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { + "items": [ + { "operation_id": "#{operation_id}", "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search/quick?q=#{operation_id}") @@ -1762,23 +2062,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } @@ -1805,33 +2090,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search/quick?q=#{operation_id}") @@ -1843,23 +2121,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } in tl(response) @@ -1893,33 +2156,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "status_history": [ - { - "is_exist": true, - "is_success": true, - "note": null, - "timestamp": "1746444308", - "transactions": [ - { - "hash": "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type": "TON" - } - ], - "type": "COLLECTED_IN_TAC" - } - ], - "timestamp": "1746444308", - "type": "PENDING" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } """ Bypass.expect( bypass, "GET", - "/api/v1/tac/operations/#{operation_id}", - fn conn -> Plug.Conn.resp(conn, 200, tac_response) end + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end ) request = get(conn, "/api/v2/search/quick?q=#{operation_id}") @@ -1931,23 +2187,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "tac_operation" => %{ "operation_id" => operation_id, "sender" => nil, - "status_history" => [ - %{ - "is_exist" => true, - "is_success" => true, - "note" => nil, - "timestamp" => "1746444308", - "transactions" => [ - %{ - "hash" => "0x5626d3aaf5f7666f0d82919178b0ba0880683e8531b6718a83ca946d337a81c9", - "type" => "TON" - } - ], - "type" => "COLLECTED_IN_TAC" - } - ], - "timestamp" => "1746444308", - "type" => "PENDING" + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" }, "type" => "tac_operation" } in tl(response) @@ -1963,6 +2204,349 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do } in response end + test "finds a lot of TAC operations with address", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + address_hash = Address.checksum(insert(:address).hash) + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + + tac_response = + """ + { + "items": [ + #{for i <- 0..49, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": "2025-05-14T19:16:49.000Z", + "page_size": 50 + } + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == address_hash + + Plug.Conn.resp(conn, 200, tac_response) + end + ) + + request = get(conn, "/api/v2/search/quick?q=#{address_hash}") + assert response = json_response(request, 200) + + correct_response = [ + %{ + "address" => address_hash, + "address_hash" => address_hash, + "certified" => false, + "ens_info" => nil, + "is_smart_contract_verified" => false, + "name" => nil, + "priority" => 0, + "type" => "address", + "url" => "/address/#{address_hash}" + } + | for( + i <- 0..48, + do: %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => address_hash, + "timestamp" => "2025-05-14T19:16:#{i}.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ) + ] + + assert correct_response == response + end + + test "finds a TAC operation with TON address", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + tac_response = """ + { + "items": [ + { + "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type": "ROLLBACK", + "timestamp": "2025-06-05T12:21:11.000Z", + "sender": { + "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain": "TON" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["q"] do + expected_q + when expected_q in [ + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", + "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", + "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", + "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", + "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" + ] -> + Plug.Conn.resp(conn, 200, tac_response) + + q -> + raise "Unexpected 'q' parameter #{inspect(q)}" + end + end + ) + + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + + request = + get( + conn, + "/api/v2/search/quick?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" + ) + + assert response = json_response(request, 200) + + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response + end + + test "finds a lot if TAC operations with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + end) + + transaction = + insert(:transaction, hash: "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2") |> with_block() + + operation_id = "#{transaction.hash}" + + tac_response = """ + { + "items": [ + #{for i <- 0..49, do: """ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": "2025-05-14T19:16:49.000Z", + "page_size": 50 + } + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + + Plug.Conn.resp(conn, 200, tac_response) + end + ) + + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) + + correct_response = [ + %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } + | for( + i <- 0..48, + do: %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:#{i}.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ) + ] + + assert correct_response == response + end + test "returns empty list and don't crash", %{conn: conn} do request = get(conn, "/api/v2/search/quick?q=qwertyuioiuytrewertyuioiuytrertyuio") assert [] = json_response(request, 200) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 137e039c9174..b5f3cd206507 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -68,53 +68,54 @@ defmodule Explorer.Chain.Search do {[], nil} {:address_hash, address_hash} -> - {address_hash - |> search_token_by_address_hash_query() - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) - |> union_all( - ^(address_hash - |> search_address_by_address_hash_query()) - ) - |> select_repo(options).all(), nil} + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - {:filecoin, filecoin_address} -> - {filecoin_address - |> address_by_filecoin_id_or_robust() - |> select_repo(options).all(), nil} + addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) - {:full_hash, full_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operation(query_string) end) + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) - transaction_block_query = - full_hash - |> search_transaction_query() - |> union_all(^search_block_by_hash_query(full_hash)) + trim_list_and_prepare_next_page_params( + addresses_and_tokens ++ tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) - transaction_block_op_query = - if UserOperation.enabled?() do - user_operation_query = search_user_operation_query(full_hash) + {:ton_address, _ton_address} -> + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - transaction_block_query - |> union_all(^user_operation_query) - else - transaction_block_query - end + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) - result_query = - if Application.get_env(:explorer, :chain_type) == :ethereum do - blob_query = search_blob_query(full_hash) + trim_list_and_prepare_next_page_params( + tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) - transaction_block_op_query - |> union_all(^blob_query) - else - transaction_block_op_query - end + {:filecoin, filecoin_address} -> + {filecoin_address + |> address_by_filecoin_id_or_robust() + |> select_repo(options).all(), nil} - results = select_repo(options).all(result_query) + {:full_hash, full_hash} -> + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + results = full_hash_search_if_first_page(paging_options, full_hash, options) - tac_operation_results = await_task(tac_operation_task) + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) - {results ++ tac_operation_results, nil} + trim_list_and_prepare_next_page_params( + results ++ tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) {:number, block_number} -> {block_number @@ -171,6 +172,50 @@ defmodule Explorer.Chain.Search do {prepared_results, next_page_params} end + defp address_hash_search_if_first_page(%PagingOptions{key: nil}, address_hash, options) do + address_hash + |> search_token_by_address_hash_query() + |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> union_all( + ^(address_hash + |> search_address_by_address_hash_query()) + ) + |> select_repo(options).all() + end + + defp address_hash_search_if_first_page(_, _address_hash, _options), do: [] + + defp full_hash_search_if_first_page(%PagingOptions{key: nil}, full_hash, options) do + transaction_block_query = + full_hash + |> search_transaction_query() + |> union_all(^search_block_by_hash_query(full_hash)) + + transaction_block_op_query = + if UserOperation.enabled?() do + user_operation_query = search_user_operation_query(full_hash) + + transaction_block_query + |> union_all(^user_operation_query) + else + transaction_block_query + end + + result_query = + if Application.get_env(:explorer, :chain_type) == :ethereum do + blob_query = search_blob_query(full_hash) + + transaction_block_op_query + |> union_all(^blob_query) + else + transaction_block_op_query + end + + select_repo(options).all(result_query) + end + + defp full_hash_search_if_first_page(_, _full_hash, _options), do: [] + defp order_and_page_text_search_result(query, paging_options) do query |> subquery() @@ -191,7 +236,7 @@ defmodule Explorer.Chain.Search do defp maybe_fetch_metadata_tags(query_string, next_page_params, false) do metadata_task = Task.async(fn -> Metadata.search_tags_by_name(query_string, next_page_params) end) - await_metadata_task(metadata_task) + await_task_with_paging(metadata_task) end defp maybe_fetch_metadata_tags(_query_string, _next_page_params, true) do @@ -210,6 +255,7 @@ defmodule Explorer.Chain.Search do @spec prepare_search_query(binary(), {:some, binary()} | :none) :: {:address_hash, Hash.Address.t()} + | {:ton_address, Ton.Address.t()} | {:filecoin, any()} | {:full_hash, Hash.t()} | {:number, non_neg_integer()} @@ -219,6 +265,7 @@ defmodule Explorer.Chain.Search do # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defp prepare_search_query(query, {:some, prepared_term}) do address_hash_result = Chain.string_to_address_hash(query) + ton_address_result = Ton.parse_address(query) filecoin_address_result = maybe_parse_filecoin_address(query) full_hash_result = Chain.string_to_full_hash(query) non_negative_integer_result = ExplorerHelper.safe_parse_non_negative_integer(query) @@ -229,6 +276,10 @@ defmodule Explorer.Chain.Search do {:ok, hash} = address_hash_result {:address_hash, hash} + match?({:ok, _address}, ton_address_result) -> + {:ok, ton_address} = ton_address_result + {:ton_address, ton_address} + match?({:ok, _address}, filecoin_address_result) -> {:ok, filecoin_address} = filecoin_address_result {:filecoin, filecoin_address} @@ -313,16 +364,21 @@ defmodule Explorer.Chain.Search do [] {:address_hash, address_hash} -> - [ - address_hash - |> search_token_by_address_hash_query() - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) - |> union_all( - ^(address_hash - |> search_address_by_address_hash_query()) - ) - |> select_repo(options).all() - ] + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) + + %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) + + [addresses_and_tokens, tac_operation_results] + + {:ton_address, _ton_address} -> + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + %{items: tac_operation_results} = + await_task_with_paging(tac_operation_task) + + [tac_operation_results] {:filecoin, filecoin_address} -> [ @@ -332,37 +388,11 @@ defmodule Explorer.Chain.Search do ] {:full_hash, full_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operation(query_string) end) - - transaction_block_query = - full_hash - |> search_transaction_query() - |> union_all(^search_block_by_hash_query(full_hash)) - - transaction_block_op_query = - if UserOperation.enabled?() do - user_operation_query = search_user_operation_query(full_hash) - - transaction_block_query - |> union_all(^user_operation_query) - else - transaction_block_query - end - - result_query = - if Application.get_env(:explorer, :chain_type) == :ethereum do - blob_query = search_blob_query(full_hash) - - transaction_block_op_query - |> union_all(^blob_query) - else - transaction_block_op_query - end + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + results = full_hash_search_if_first_page(paging_options, full_hash, options) + %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) - results = select_repo(options).all(result_query) - tac_operation_results = await_task(tac_operation_task) - - [results ++ tac_operation_results] + [results, tac_operation_results] {:number, block_number} -> [ @@ -426,7 +456,7 @@ defmodule Explorer.Chain.Search do labels_results = term |> search_label_query(paging_options) |> select_repo(options).all() - %{items: metadata_tags} = (metadata_task && await_metadata_task(metadata_task)) || %{items: []} + %{items: metadata_tags} = (metadata_task && await_task_with_paging(metadata_task)) || %{items: []} metadata_tags_addresses = metadata_tags @@ -447,8 +477,8 @@ defmodule Explorer.Chain.Search do end end - defp await_metadata_task(metadata_task) do - case Task.yield(metadata_task, 5000) || Task.shutdown(metadata_task) do + defp await_task_with_paging(task) do + case Task.yield(task, 5000) || Task.shutdown(task) do {:ok, {:ok, result}} -> result @@ -910,21 +940,34 @@ defmodule Explorer.Chain.Search do end end - defp search_tac_operation(search_query) do - case TACOperationLifecycle.get_operation_by_id(search_query) do - {:ok, operation} -> - [ - search_fields() - |> Map.merge(%{ - type: "tac_operation", - tac_operation: operation, - address_hash: nil, - timestamp: nil - }) - ] + defp search_tac_operations(search_query, paging_options) do + case paging_options do + %PagingOptions{key: %{"tac_operation" => nil}} -> {:ok, %{items: [], next_page_params: nil}} + %PagingOptions{key: %{"tac_operation" => page_params}} -> do_search_tac_operations(search_query, page_params) + _ -> do_search_tac_operations(search_query, nil) + end + end - _ -> - [] + defp do_search_tac_operations(search_query, page_params) do + case TACOperationLifecycle.get_operations_by_id_or_sender_or_transaction_hash(search_query, page_params) do + {:ok, %{items: operations, next_page_params: next_page_params}} -> + {:ok, + %{ + items: + Enum.map(operations, fn operation -> + search_fields() + |> Map.merge(%{ + type: "tac_operation", + tac_operation: operation, + address_hash: nil, + timestamp: nil + }) + end), + next_page_params: next_page_params + }} + + error -> + error end end @@ -1030,17 +1073,22 @@ defmodule Explorer.Chain.Search do |> Map.put(:priority, 1) end - @paginated_types ["label", "contract", "token", "metadata_tag"] + @paginated_types ["label", "contract", "token", "metadata_tag", "tac_operation"] defp trim_list_and_prepare_next_page_params( items, %PagingOptions{page_size: page_size, key: prev_options}, query, metadata_tags_params, - metadata_tags_next_page? + microservice_task_next_page? ) - when length(items) > page_size - 1 or metadata_tags_next_page? do - {items, [first_element_of_the_next_page | _]} = Enum.split(items, page_size - 1) + when length(items) > page_size - 1 or microservice_task_next_page? do + {items, first_element_of_the_next_page} = + case Enum.split(items, page_size - 1) do + {items, [first_element_of_the_next_page | _]} -> {items, first_element_of_the_next_page} + {items, []} -> {items, nil} + end + prev_options = prev_options || %{} base_params = @@ -1178,6 +1226,13 @@ defmodule Explorer.Chain.Search do } end + defp paging_params( + %{type: "tac_operation", tac_operation: %{"timestamp" => timestamp}}, + _ + ) do + %{"page_token" => timestamp |> DateTime.from_iso8601() |> elem(1) |> DateTime.to_unix()} + end + defp metadata_tag_to_paging_params(%{metadata: metadata}) do %{"page_token" => "#{metadata["ordinal"]},#{metadata["slug"]},#{metadata["tagType"]}", "page_size" => 50} end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex index 464466fde94f..88e95aff4067 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex @@ -9,61 +9,66 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do @request_error_msg "Error while sending request to Tac Operation Lifecycle Service" - @doc """ - Retrieves operation details from the TAC Operation Lifecycle Service by operation ID. - - Fetches complete operation information including type, timestamp, sender, and - status history from the TAC Operation Lifecycle Service. The function first - checks if the microservice is enabled before making the request. - - ## Parameters - - `operation_id`: Unique identifier for the operation to retrieve - - ## Returns - - `{:ok, map()}`: Operation details containing operation_id, type, timestamp, - sender, and status_history if the request succeeds - - `{:error, :disabled}`: If the TAC Operation Lifecycle microservice is disabled - - `{:error, :not_found}`: If the operation with the given ID does not exist - - `{:error, String.t()}`: Error message if the request fails - """ - @spec get_operation_by_id(String.t()) :: - {:ok, map()} | {:error, :disabled | :not_found | Jason.DecodeError.t() | String.t()} - def get_operation_by_id(operation_id) do + @spec get_operations_by_id_or_sender_or_transaction_hash(String.t(), nil | map()) :: + {:ok, %{items: [map()], next_page_params: map() | nil}} + | {:error, :disabled | :not_found | Jason.DecodeError.t() | String.t()} + def get_operations_by_id_or_sender_or_transaction_hash(param, page_params) do with :ok <- Microservice.check_enabled(__MODULE__) do - query_params = %{} + query_params = + %{ + "q" => param + } + |> Map.merge(page_params || %{}) - operation_id - |> operation_by_id_url() + operations_quick_search_url() |> http_get_request(query_params) + |> case do + {:ok, %{"items" => operations, "next_page_params" => next_page_params}} -> + {:ok, %{items: operations, next_page_params: next_page_params}} + + error -> + error + end end end defp http_get_request(url, query_params) do case HTTPoison.get(url, [], params: query_params) do {:ok, %Response{body: body, status_code: 200}} -> - Jason.decode(body) + case Jason.decode(body) do + {:ok, decoded_body} -> + {:ok, decoded_body} + + error -> + log_error(error) + {:error, error} + end {:ok, %Response{body: _body, status_code: 404}} -> {:error, :not_found} {_, error} -> - old_truncate = Application.get_env(:logger, :truncate) - Logger.configure(truncate: :infinity) - - Logger.error(fn -> - [ - "#{@request_error_msg}: #{url}: ", - inspect(error, limit: :infinity, printable_limit: :infinity) - ] - end) - - Logger.configure(truncate: old_truncate) + log_error(error) {:error, @request_error_msg} end end - defp operation_by_id_url(operation_id) do - "#{base_url()}/tac/operations/#{operation_id}" + defp log_error(error) do + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "#{@request_error_msg}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + end + + defp operations_quick_search_url do + "#{base_url()}/tac/operations" end defp base_url do diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 6b68866b1976..5b689d32461f 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -133,7 +133,8 @@ defmodule Explorer.Mixfile do {:utils, in_umbrella: true}, {:dns, "~> 2.4.0"}, {:inet_cidr, "~> 1.0.0"}, - {:hammer, "~> 6.0"} + {:hammer, "~> 6.0"}, + {:ton, "~> 0.5.0"} ] end diff --git a/mix.lock b/mix.lock index 9396f4a4b1a0..e1504fdea1a5 100644 --- a/mix.lock +++ b/mix.lock @@ -15,6 +15,7 @@ "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, + "cafezinho": {:hex, :cafezinho, "0.4.3", "0983519fc9a76d7a54347f563533120b73ba3f2a444a13431149d5c8bcee2d08", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "1437d5de261dde0d94c012aaab48d9ee66f4588280a1ed09ab452c9995028e72"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, @@ -47,6 +48,7 @@ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, + "evil_crc32c": {:hex, :evil_crc32c, "0.2.8", "4a522b010bcf1b11fa4dcfea8e9c7cf8fd4b4d945dbb0c9115afb0a55a5e5419", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5028fdd9e4fd66ca5fb54d4919904a7ad7ca7a6884c6d334b66e804d04c37c17"}, "evision": {:hex, :evision, "0.2.13", "15ce6e1ba70c2485ff6ea1fba67fa9d32abb7442c9189fa65f8cda20357e079e", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "49ff677cbbeaa1870841b5ac9dc4211e62d32d9585253c02a95dc98e30981e7a"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, @@ -62,6 +64,7 @@ "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, + "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.3", "a2877e7028807a494fec3e5286e82872e10d632ddc885c4e26e64eac9ece5651", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "cbb80889f0bb1154fc694ddb66807ae9b7c96cb6a59ea70021d7beaf3a4a9d08"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.4", "d771a74917eb21e9f02e52bb1d0ac9aba4e2b5118d7f515014d0a311c990e323", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "465fd788c83c24d2df47f302e8fb1011054c81a905345e377c957b159a783bfc"}, "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, @@ -106,6 +109,7 @@ "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "mnemoniac": {:hex, :mnemoniac, "0.1.3", "f2aed9ddc36c82938170d38f8944f79725f95acc1e7c08e223bad38aac9953cf", [:mix], [], "hexpm", "dc2f2da766134c55282b48358eda66133e27815c3eb5ad7ecaa1e72281a0658f"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, @@ -161,6 +165,7 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, + "ton": {:hex, :ton, "0.5.0", "be7f22a06910c00d1b3d18db70b5aec32dbcb0d6ec7bcac076cadc53cc623f0a", [:mix], [{:cafezinho, "~> 0.4.3", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.8", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.3", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.2", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "87e12d175a7144e152111285a8124b9cc75cea429b2cf32ef165396f711e892f"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"}, "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, From 8d9882ad5b6e4912d12d0b5bb8aad6856a5d10aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:32:57 +0300 Subject: [PATCH 0124/1006] chore(deps): bump ecto_sql from 3.12.1 to 3.13.2 (#12689) Bumps [ecto_sql](https://github.com/elixir-ecto/ecto_sql) from 3.12.1 to 3.13.2. - [Changelog](https://github.com/elixir-ecto/ecto_sql/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-ecto/ecto_sql/compare/v3.12.1...v3.13.2) --- updated-dependencies: - dependency-name: ecto_sql dependency-version: 3.13.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index e1504fdea1a5..1a01effe05b4 100644 --- a/mix.lock +++ b/mix.lock @@ -44,7 +44,7 @@ "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.1", "d3d76d78afd2757644b5c4f7ca37f90bcf1e05d05a06cca8526e30cefb0034a1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fba0174284fd339f69376b0405942036ce5f0ff7d59402a6ccf3b7ce2903198"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, From 7f121d8931f2801e5e3284e269968a65a03ca60f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:06:53 +0300 Subject: [PATCH 0125/1006] chore(deps): bump image from 0.59.3 to 0.60.0 (#12694) Bumps [image](https://github.com/kipcole9/image) from 0.59.3 to 0.60.0. - [Release notes](https://github.com/kipcole9/image/releases) - [Changelog](https://github.com/elixir-image/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/kipcole9/image/commits) --- updated-dependencies: - dependency-name: image dependency-version: 0.60.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 1a01effe05b4..4f294e92c128 100644 --- a/mix.lock +++ b/mix.lock @@ -87,7 +87,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.59.3", "fcc8f75e61eef3dda091018f920c1c8d931e6b9c37b6787d48536c6ab9aef3a7", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "b86cf0a17c8ea31d12cf6eb907ca3be27377cd41c0d0fe4be6f3da78e71743d5"}, + "image": {:hex, :image, "0.60.0", "84a157c002b40c902e28c1e187634131353c9347ffd345fadc9219cac8503261", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "547f08a2836b3d897b713f462b82368613207e556f62d61a399ca1fd16b66381"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -120,7 +120,7 @@ "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "number": {:hex, :number, "1.0.5", "d92136f9b9382aeb50145782f116112078b3465b7be58df1f85952b8bb399b0f", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c0733a0a90773a66582b9e92a3f01290987f395c972cb7d685f51dd927cd5169"}, "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, - "nx": {:hex, :nx, "0.9.2", "17563029c01bf749aad3c31234326d7665abd0acc33ee2acbe531a4759f29a8a", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "914d74741617d8103de8ab1f8c880353e555263e1c397b8a1109f79a3716557f"}, + "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, From b8ce714356c279922f5682bd72ddd484c91f2220 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:20:08 +0300 Subject: [PATCH 0126/1006] fix: `MarketHistory` on conflict clause (#12541) --- .../lib/explorer/market/market_history.ex | 63 +++----------- .../explorer/market/market_history_test.exs | 84 +++++++++++++++++++ .../test/explorer/market/market_test.exs | 75 ----------------- 3 files changed, 95 insertions(+), 127 deletions(-) create mode 100644 apps/explorer/test/explorer/market/market_history_test.exs diff --git a/apps/explorer/lib/explorer/market/market_history.ex b/apps/explorer/lib/explorer/market/market_history.ex index db3709d61444..56f6e8cb4097 100644 --- a/apps/explorer/lib/explorer/market/market_history.ex +++ b/apps/explorer/lib/explorer/market/market_history.ex @@ -83,61 +83,20 @@ defmodule Explorer.Market.MarketHistory do market_history in __MODULE__, update: [ set: [ - opening_price: - fragment( - """ - CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.opening_price IS NOT NULL AND EXCLUDED.opening_price > 0 - THEN EXCLUDED.opening_price - ELSE ? - END - """, - market_history.opening_price, - market_history.opening_price, - market_history.opening_price - ), - closing_price: - fragment( - """ - CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.closing_price IS NOT NULL AND EXCLUDED.closing_price > 0 - THEN EXCLUDED.closing_price - ELSE ? - END - """, - market_history.closing_price, - market_history.closing_price, - market_history.closing_price - ), - market_cap: - fragment( - """ - CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.market_cap IS NOT NULL AND EXCLUDED.market_cap > 0 - THEN EXCLUDED.market_cap - ELSE ? - END - """, - market_history.market_cap, - market_history.market_cap, - market_history.market_cap - ), - tvl: - fragment( - """ - CASE WHEN (? IS NULL OR ? = 0) AND EXCLUDED.tvl IS NOT NULL AND EXCLUDED.tvl > 0 - THEN EXCLUDED.tvl - ELSE ? - END - """, - market_history.tvl, - market_history.tvl, - market_history.tvl - ) + opening_price: fragment("COALESCE(EXCLUDED.opening_price, ?)", market_history.opening_price), + closing_price: fragment("COALESCE(EXCLUDED.closing_price, ?)", market_history.closing_price), + market_cap: fragment("COALESCE(EXCLUDED.market_cap, ?)", market_history.market_cap), + tvl: fragment("COALESCE(EXCLUDED.tvl, ?)", market_history.tvl) ] ], where: - is_nil(market_history.tvl) or market_history.tvl == 0 or is_nil(market_history.market_cap) or - market_history.market_cap == 0 or is_nil(market_history.opening_price) or - market_history.opening_price == 0 or is_nil(market_history.closing_price) or - market_history.closing_price == 0 + fragment( + "(EXCLUDED.opening_price, EXCLUDED.closing_price, EXCLUDED.market_cap, EXCLUDED.tvl) IS DISTINCT FROM (? , ?, ?, ?)", + market_history.opening_price, + market_history.closing_price, + market_history.market_cap, + market_history.tvl + ) ) end end diff --git a/apps/explorer/test/explorer/market/market_history_test.exs b/apps/explorer/test/explorer/market/market_history_test.exs new file mode 100644 index 000000000000..ab7927ec3ee7 --- /dev/null +++ b/apps/explorer/test/explorer/market/market_history_test.exs @@ -0,0 +1,84 @@ +defmodule Explorer.Market.MarketHistoryTest do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Market.MarketHistory + + describe "bulk_insert/1" do + test "replaces existing records" do + records = [ + %{date: ~D[2023-01-01], opening_price: Decimal.new("100.00"), closing_price: Decimal.new("110.00")}, + %{date: ~D[2023-01-02], opening_price: Decimal.new("7.50"), closing_price: Decimal.new("5.00")} + ] + + assert {2, _} = MarketHistory.bulk_insert(records) + + records = [ + %{date: ~D[2023-01-01], opening_price: Decimal.new("50.00"), closing_price: Decimal.new("55.00")}, + %{date: ~D[2023-01-02], opening_price: Decimal.new("10.00"), closing_price: Decimal.new("8.00")} + ] + + assert {2, _} = MarketHistory.bulk_insert(records) + + assert [date1, date2] = Repo.all(MarketHistory) + assert date1.opening_price == Decimal.new("50.00") + assert date1.closing_price == Decimal.new("55.00") + assert date2.opening_price == Decimal.new("10.00") + assert date2.closing_price == Decimal.new("8.00") + end + end + + test "inserts records" do + comparable_values = %{ + ~D[2018-04-01] => %{ + date: ~D[2018-04-01], + closing_price: Decimal.new(1), + opening_price: Decimal.new(1) + }, + ~D[2018-04-02] => %{ + date: ~D[2018-04-02], + closing_price: Decimal.new(1), + opening_price: Decimal.new(1) + }, + ~D[2018-04-03] => %{ + date: ~D[2018-04-03], + closing_price: Decimal.new(1), + opening_price: Decimal.new(1) + } + } + + insertable_records = Map.values(comparable_values) + MarketHistory.bulk_insert(insertable_records) + history = Repo.all(MarketHistory) + + missing_records = + Enum.reduce(history, comparable_values, fn record, acc -> + initial_record = Map.get(acc, record.date) + assert record.date == initial_record.date + assert record.closing_price == initial_record.closing_price + assert record.opening_price == initial_record.opening_price + Map.delete(acc, record.date) + end) + + assert missing_records == %{} + end + + test "doesn't replace existing records with zeros" do + date = ~D[2018-04-01] + + {:ok, old_record} = + Repo.insert(%MarketHistory{date: date, closing_price: Decimal.new(1), opening_price: Decimal.new(1)}) + + new_record = %{ + date: date, + closing_price: Decimal.new(0), + opening_price: Decimal.new(0) + } + + MarketHistory.bulk_insert([new_record]) + + fetched_record = Repo.get_by(MarketHistory, date: date) + assert fetched_record.closing_price == old_record.closing_price + assert fetched_record.opening_price == old_record.opening_price + end +end diff --git a/apps/explorer/test/explorer/market/market_test.exs b/apps/explorer/test/explorer/market/market_test.exs index bde06a3c3e3b..b2651d4ec28a 100644 --- a/apps/explorer/test/explorer/market/market_test.exs +++ b/apps/explorer/test/explorer/market/market_test.exs @@ -37,79 +37,4 @@ defmodule Explorer.MarketTest do assert length(history) == 30 assert Enum.at(history, 0).date == Enum.at(records, 0).date end - - describe "bulk_insert_history/1" do - test "inserts records" do - comparable_values = %{ - ~D[2018-04-01] => %{ - date: ~D[2018-04-01], - closing_price: Decimal.new(1), - opening_price: Decimal.new(1) - }, - ~D[2018-04-02] => %{ - date: ~D[2018-04-02], - closing_price: Decimal.new(1), - opening_price: Decimal.new(1) - }, - ~D[2018-04-03] => %{ - date: ~D[2018-04-03], - closing_price: Decimal.new(1), - opening_price: Decimal.new(1) - } - } - - insertable_records = Map.values(comparable_values) - MarketHistory.bulk_insert(insertable_records) - history = Repo.all(MarketHistory) - - missing_records = - Enum.reduce(history, comparable_values, fn record, acc -> - initial_record = Map.get(acc, record.date) - assert record.date == initial_record.date - assert record.closing_price == initial_record.closing_price - assert record.opening_price == initial_record.opening_price - Map.delete(acc, record.date) - end) - - assert missing_records == %{} - end - - test "doesn't replace existing records with zeros" do - date = ~D[2018-04-01] - - {:ok, old_record} = - Repo.insert(%MarketHistory{date: date, closing_price: Decimal.new(1), opening_price: Decimal.new(1)}) - - new_record = %{ - date: date, - closing_price: Decimal.new(0), - opening_price: Decimal.new(0) - } - - MarketHistory.bulk_insert([new_record]) - - fetched_record = Repo.get_by(MarketHistory, date: date) - assert fetched_record.closing_price == old_record.closing_price - assert fetched_record.opening_price == old_record.opening_price - end - - test "does not override existing records on date conflict" do - date = ~D[2018-04-01] - - {:ok, old_record} = - Repo.insert(%MarketHistory{date: date, closing_price: Decimal.new(2), opening_price: Decimal.new(2)}) - - new_record = %{ - date: date, - closing_price: Decimal.new(1), - opening_price: Decimal.new(1) - } - - MarketHistory.bulk_insert([new_record]) - - fetched_record = Repo.get_by(MarketHistory, date: date) - assert fetched_record.closing_price == old_record.closing_price - assert fetched_record.opening_price == old_record.opening_price - end - end end From 2be016bcc0edee14b7c2c9c712c8c7bbbc1b485d Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:32:02 +0300 Subject: [PATCH 0127/1006] fix: Store blocks_validated in DB for Stability Validators (#12540) * fix: Store blocks_validated in DB for Stability Validators * Remove unused alias * Fix tests * Remove unused alias --- .../lib/block_scout_web/paging_helper.ex | 9 +- .../api/v2/validator_controller_test.exs | 19 +- .../import/runner/stability/validators.ex | 117 +++++++ .../chain/import/stage/chain_type_specific.ex | 3 + .../lib/explorer/chain/stability/validator.ex | 43 ++- ...add_validator_blocks_validated_counter.exs | 23 ++ .../runner/stability/validators_test.exs | 178 +++++++++++ apps/explorer/test/support/factory.ex | 3 +- apps/indexer/lib/indexer/block/fetcher.ex | 11 +- .../indexer/fetcher/stability/validator.ex | 31 +- .../indexer/transform/stability/validators.ex | 35 +++ .../indexer/block/catchup/fetcher_test.exs | 161 ++++++++++ .../test/indexer/block/fetcher_test.exs | 8 +- .../indexer/block/realtime/fetcher_test.exs | 291 ++++++++++++++++++ .../fetcher/stability/validator_test.exs | 200 ++++++++++++ .../transform/stability/validators_test.exs | 162 ++++++++++ 16 files changed, 1242 insertions(+), 52 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/import/runner/stability/validators.ex create mode 100644 apps/explorer/priv/stability/migrations/20250602163043_add_validator_blocks_validated_counter.exs create mode 100644 apps/explorer/test/explorer/chain/import/runner/stability/validators_test.exs create mode 100644 apps/indexer/lib/indexer/transform/stability/validators.ex create mode 100644 apps/indexer/test/indexer/transform/stability/validators_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 7eefc82b5d0b..a90c9535ce66 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -9,7 +9,6 @@ defmodule BlockScoutWeb.PagingHelper do alias Explorer.Chain.InternalTransaction.CallType, as: InternalTransactionCallType alias Explorer.Chain.InternalTransaction.Type, as: InternalTransactionType - alias Explorer.Chain.Stability.Validator, as: ValidatorStability alias Explorer.Chain.{SmartContract, Transaction} alias Explorer.{Helper, PagingOptions, SortingHelper} @@ -330,12 +329,8 @@ defmodule BlockScoutWeb.PagingHelper do defp do_validators_stability_sorting("state", "desc"), do: [desc_nulls_last: :state] defp do_validators_stability_sorting("address_hash", "asc"), do: [asc_nulls_first: :address_hash] defp do_validators_stability_sorting("address_hash", "desc"), do: [desc_nulls_last: :address_hash] - - defp do_validators_stability_sorting("blocks_validated", "asc"), - do: [{:dynamic, :blocks_validated, :asc_nulls_first, ValidatorStability.dynamic_validated_blocks()}] - - defp do_validators_stability_sorting("blocks_validated", "desc"), - do: [{:dynamic, :blocks_validated, :desc_nulls_last, ValidatorStability.dynamic_validated_blocks()}] + defp do_validators_stability_sorting("blocks_validated", "asc"), do: [asc_nulls_first: :blocks_validated] + defp do_validators_stability_sorting("blocks_validated", "desc"), do: [desc_nulls_last: :blocks_validated] defp do_validators_stability_sorting(_, _), do: [] diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs index a0915ce45e14..a9afd8f17fe1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs @@ -57,14 +57,15 @@ defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do end defp compare_item({%ValidatorStability{} = validator, count}, json) do - assert json["blocks_validated_count"] == count + 1 assert compare_item(validator, json) + assert json["blocks_validated_count"] == count end describe "/validators/stability" do test "get paginated list of the validators", %{conn: conn} do validators = - insert_list(51, :validator_stability) + 51 + |> insert_list(:validator_stability) |> Enum.sort_by( fn validator -> {Keyword.fetch!(ValidatorStability.state_enum(), validator.state), validator.address_hash.bytes} @@ -84,13 +85,8 @@ defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do test "sort by blocks_validated asc", %{conn: conn} do validators = for _ <- 0..50 do - validator = insert(:validator_stability) blocks_count = Enum.random(0..50) - - _ = - for _ <- 0..blocks_count do - insert(:block, miner_hash: validator.address_hash, miner: nil) - end + validator = insert(:validator_stability, blocks_validated: blocks_count) {validator, blocks_count} end @@ -111,13 +107,8 @@ defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do test "sort by blocks_validated desc", %{conn: conn} do validators = for _ <- 0..50 do - validator = insert(:validator_stability) blocks_count = Enum.random(0..50) - - _ = - for _ <- 0..blocks_count do - insert(:block, miner_hash: validator.address_hash, miner: nil) - end + validator = insert(:validator_stability, blocks_validated: blocks_count) {validator, blocks_count} end diff --git a/apps/explorer/lib/explorer/chain/import/runner/stability/validators.ex b/apps/explorer/lib/explorer/chain/import/runner/stability/validators.ex new file mode 100644 index 000000000000..ba4d03f33d87 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/stability/validators.ex @@ -0,0 +1,117 @@ +defmodule Explorer.Chain.Import.Runner.Stability.Validators do + @moduledoc """ + Bulk updates `t:Explorer.Chain.Stability.Validator.t/0` blocks_validated counters. + """ + + require Ecto.Query + + alias Ecto.{Multi, Repo} + alias Explorer.Chain.{Import, Stability.Validator} + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2, where: 3] + + require Logger + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Validator.t()] + + @impl Import.Runner + def ecto_schema_module, do: Validator + + @impl Import.Runner + def option_key, do: :stability_validators + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s with updated counters" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :stability_validators, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> update_counters(repo, changes_list, insert_options) end, + :block_referencing, + :stability_validators, + :stability_validators + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec update_counters(Repo.t(), [map()], %{ + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [Validator.t()]} + defp update_counters(repo, changes_list, %{timeout: timeout, timestamps: timestamps}) when is_list(changes_list) do + if changes_list != [] do + # Get all address hashes from the changes + address_hashes = Enum.map(changes_list, & &1.address_hash) + + # Get existing validators that match the address hashes + existing_validators = + Validator + |> where([v], v.address_hash in ^address_hashes) + |> repo.all(timeout: timeout) + + # Update counters for each existing validator + updated_validators = + Enum.reduce(changes_list, [], fn change, acc -> + case Enum.find(existing_validators, &(&1.address_hash == change.address_hash)) do + nil -> + # Validator doesn't exist, log error and skip + Logger.error("Validator with address hash #{to_string(change.address_hash)} not found") + acc + + validator -> + # Update the blocks_validated counter + # credo:disable-for-next-line + case repo.update_all( + from(v in Validator, where: v.address_hash == ^change.address_hash), + [ + inc: [blocks_validated: change.blocks_validated], + set: [updated_at: timestamps.updated_at] + ], + timeout: timeout + ) do + {1, _} -> + # Successfully updated, add to result + updated_validator = %Validator{ + address_hash: change.address_hash, + blocks_validated: validator.blocks_validated + change.blocks_validated + } + + [updated_validator | acc] + + _ -> + # Update failed, log error and skip + Logger.error("Failed to update validator counter for address hash #{to_string(change.address_hash)}") + acc + end + end + end) + + {:ok, updated_validators} + else + {:ok, []} + end + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index d9e428e7f75a..9259d16f415e 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -71,6 +71,9 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Runner.Zilliqa.AggregateQuorumCertificates, Runner.Zilliqa.NestedQuorumCertificates, Runner.Zilliqa.QuorumCertificates + ], + stability: [ + Runner.Stability.Validators ] } diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex index e5567c09532d..8ba82db8c595 100644 --- a/apps/explorer/lib/explorer/chain/stability/validator.ex +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -5,7 +5,7 @@ defmodule Explorer.Chain.Stability.Validator do use Explorer.Schema - alias Explorer.Chain.{Address, Import} + alias Explorer.Chain.{Address, Block, Import} alias Explorer.Chain.Hash.Address, as: HashAddress alias Explorer.{Chain, Repo, SortingHelper} alias Explorer.SmartContract.Reader @@ -23,13 +23,13 @@ defmodule Explorer.Chain.Stability.Validator do typed_schema "validators_stability" do field(:address_hash, HashAddress, primary_key: true) field(:state, Ecto.Enum, values: @state_enum) - field(:blocks_validated, :integer, virtual: true) + field(:blocks_validated, :integer) has_one(:address, Address, foreign_key: :hash, references: :address_hash) timestamps() end - @required_attrs ~w(address_hash)a + @required_attrs ~w(address_hash blocks_validated)a @optional_attrs ~w(state)a def changeset(%__MODULE__{} = validator, attrs) do validator @@ -55,13 +55,6 @@ defmodule Explorer.Chain.Stability.Validator do __MODULE__ |> apply_filter_by_state(states) - |> select_merge([vs], %{ - blocks_validated: - fragment( - "SELECT count(*) FROM blocks WHERE miner_hash = ?", - vs.address_hash - ) - }) |> Chain.join_associations(necessity_by_association) |> SortingHelper.apply_sorting(sorting, @default_sorting) |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) @@ -218,7 +211,7 @@ defmodule Explorer.Chain.Stability.Validator do @spec insert_validators([map()]) :: {non_neg_integer(), nil | []} def insert_validators(validators) do Repo.insert_all(__MODULE__, validators, - on_conflict: {:replace_all_except, [:inserted_at]}, + on_conflict: {:replace_all_except, [:inserted_at, :blocks_validated]}, conflict_target: [:address_hash] ) end @@ -245,20 +238,6 @@ defmodule Explorer.Chain.Stability.Validator do @spec state_enum() :: Keyword.t() def state_enum, do: @state_enum - @doc """ - Returns dynamic query for validated blocks count. Needed for SortingHelper - """ - @spec dynamic_validated_blocks() :: Ecto.Query.dynamic_expr() - def dynamic_validated_blocks do - dynamic( - [vs], - fragment( - "SELECT count(*) FROM blocks WHERE miner_hash = ?", - vs.address_hash - ) - ) - end - @doc """ Returns total count of validators. """ @@ -286,4 +265,18 @@ defmodule Explorer.Chain.Stability.Validator do |> where([vs], vs.state == :active) |> Repo.aggregate(:count, :address_hash) end + + @doc """ + Fetch blocks validated + """ + @spec fetch_blocks_validated(list(binary())) :: list({binary(), integer()}) + def fetch_blocks_validated([_ | _] = miner_address_hashes) do + Block + |> where([b], b.miner_hash in ^miner_address_hashes) + |> group_by([b], b.miner_hash) + |> select([b], {b.miner_hash, count(b.hash)}) + |> Repo.all() + end + + def fetch_blocks_validated(_), do: [] end diff --git a/apps/explorer/priv/stability/migrations/20250602163043_add_validator_blocks_validated_counter.exs b/apps/explorer/priv/stability/migrations/20250602163043_add_validator_blocks_validated_counter.exs new file mode 100644 index 000000000000..dfbc9406ccce --- /dev/null +++ b/apps/explorer/priv/stability/migrations/20250602163043_add_validator_blocks_validated_counter.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Stability.Migrations.AddValidatorBlocksValidatedCounter do + use Ecto.Migration + + def change do + alter table(:validators_stability) do + add(:blocks_validated, :integer, null: true) + end + + execute(""" + UPDATE validators_stability v + SET blocks_validated = COALESCE( + (SELECT COUNT(*) + FROM blocks b + WHERE b.miner_hash = v.address_hash), + 0 + ); + """) + + alter table(:validators_stability) do + modify(:blocks_validated, :integer, null: false) + end + end +end diff --git a/apps/explorer/test/explorer/chain/import/runner/stability/validators_test.exs b/apps/explorer/test/explorer/chain/import/runner/stability/validators_test.exs new file mode 100644 index 000000000000..e58ebd744b92 --- /dev/null +++ b/apps/explorer/test/explorer/chain/import/runner/stability/validators_test.exs @@ -0,0 +1,178 @@ +if Application.compile_env(:explorer, :chain_type) == :stability do + defmodule Explorer.Chain.Import.Runner.Stability.ValidatorsTest do + use Explorer.DataCase + + alias Ecto.Multi + alias Explorer.Chain.Stability.Validator + alias Explorer.Chain.Import.Runner.Stability.Validators + + describe "run/1" do + test "updates blocks_validated counter for existing validators" do + # Insert some validators first + %Validator{address_hash: validator1_hash} = + insert(:validator_stability, blocks_validated: 5) + + %Validator{address_hash: validator2_hash} = + insert(:validator_stability, blocks_validated: 3) + + changes = [ + %{ + address_hash: validator1_hash, + blocks_validated: 2 + }, + %{ + address_hash: validator2_hash, + blocks_validated: 1 + } + ] + + assert {:ok, %{stability_validators: updated_validators}} = run_changes(changes) + + assert length(updated_validators) == 2 + + # Verify counters were updated in database + updated_validator1 = Repo.get(Validator, validator1_hash) + updated_validator2 = Repo.get(Validator, validator2_hash) + + # 5 + 2 + assert updated_validator1.blocks_validated == 7 + # 3 + 1 + assert updated_validator2.blocks_validated == 4 + end + + test "skips non-existent validators" do + # Insert one validator + %Validator{address_hash: existing_validator_hash} = + insert(:validator_stability, blocks_validated: 2) + + # Try to update both existing and non-existing validator + non_existing_hash = "0x1111111111111111111111111111111111111111" + + changes = [ + %{ + address_hash: existing_validator_hash, + blocks_validated: 3 + }, + %{ + address_hash: non_existing_hash, + blocks_validated: 5 + } + ] + + assert {:ok, %{stability_validators: updated_validators}} = run_changes(changes) + + # Only the existing validator should be in the result + assert length(updated_validators) == 1 + [updated_validator] = updated_validators + assert updated_validator.address_hash == existing_validator_hash + + # Verify the existing validator was updated + updated_validator_db = Repo.get(Validator, existing_validator_hash) + # 2 + 3 + assert updated_validator_db.blocks_validated == 5 + + # Verify non-existing validator wasn't created + assert Repo.get(Validator, non_existing_hash) == nil + end + + test "handles empty changes list" do + assert {:ok, %{stability_validators: []}} = run_changes([]) + end + + test "handles multiple increments for the same validator" do + %Validator{address_hash: validator_hash} = + insert(:validator_stability, blocks_validated: 10) + + changes = [ + %{ + address_hash: validator_hash, + blocks_validated: 2 + }, + %{ + address_hash: validator_hash, + blocks_validated: 3 + } + ] + + assert {:ok, %{stability_validators: updated_validators}} = run_changes(changes) + + # Should have 2 entries in the result (one for each update) + assert length(updated_validators) == 2 + + # Verify the counter was incremented twice + updated_validator = Repo.get(Validator, validator_hash) + # 10 + 2 + 3 + assert updated_validator.blocks_validated == 15 + end + + test "handles zero increment" do + %Validator{address_hash: validator_hash} = + insert(:validator_stability, blocks_validated: 7) + + changes = [ + %{ + address_hash: validator_hash, + blocks_validated: 0 + } + ] + + assert {:ok, %{stability_validators: updated_validators}} = run_changes(changes) + + assert length(updated_validators) == 1 + + # Verify the counter remained the same + updated_validator = Repo.get(Validator, validator_hash) + # 7 + 0 + assert updated_validator.blocks_validated == 7 + end + + test "handles large increment values" do + %Validator{address_hash: validator_hash} = + insert(:validator_stability, blocks_validated: 1000) + + changes = [ + %{ + address_hash: validator_hash, + blocks_validated: 999_999 + } + ] + + assert {:ok, %{stability_validators: updated_validators}} = run_changes(changes) + + assert length(updated_validators) == 1 + + # Verify the counter was updated with large value + updated_validator = Repo.get(Validator, validator_hash) + # 1000 + 999999 + assert updated_validator.blocks_validated == 1_000_999 + end + + test "is atomic - all updates succeed or all fail" do + # This test would be more complex to implement as it requires + # simulating database errors, but the Multi transaction + # ensures atomicity by design + %Validator{address_hash: validator_hash} = + insert(:validator_stability, blocks_validated: 5) + + changes = [ + %{ + address_hash: validator_hash, + blocks_validated: 2 + } + ] + + # Normal case - should succeed + assert {:ok, %{stability_validators: _}} = run_changes(changes) + end + end + + defp run_changes(changes) when is_list(changes) do + Multi.new() + |> Validators.run(changes, %{ + timeout: :infinity, + timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} + }) + |> Repo.transaction() + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 4bf35356e615..398c9de1e6be 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1319,7 +1319,8 @@ defmodule Explorer.Factory do %ValidatorStability{ address_hash: address.hash, - state: Enum.random(0..2) + state: Enum.random(0..2), + blocks_validated: Enum.random(0..100) } end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 366b73d5bbaa..9a5328466154 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -50,6 +50,8 @@ defmodule Indexer.Block.Fetcher do TransactionActions } + alias Indexer.Transform.Stability.Validators, as: StabilityValidators + alias Indexer.Transform.Optimism.Withdrawals, as: OptimismWithdrawals alias Indexer.Transform.PolygonEdge.{DepositExecutes, Withdrawals} @@ -233,6 +235,7 @@ defmodule Indexer.Block.Fetcher do transaction_actions = Enum.map(transaction_actions, fn action -> Map.put(action, :data, Map.delete(action.data, :block_number)) end), token_instances = TokenInstances.params_set(%{token_transfers_params: token_transfers}), + stability_validators = StabilityValidators.parse(blocks), basic_import_options = %{ addresses: %{params: addresses}, address_coin_balances: %{params: coin_balances_params_set}, @@ -262,7 +265,8 @@ defmodule Indexer.Block.Fetcher do shibarium_bridge_operations: shibarium_bridge_operations, celo_gas_tokens: celo_gas_tokens, celo_epochs: celo_l1_epochs ++ celo_l2_epochs, - arbitrum_messages: arbitrum_xlevel_messages + arbitrum_messages: arbitrum_xlevel_messages, + stability_validators: stability_validators } |> extend_with_zilliqa_import_options(fetched_blocks), {:ok, inserted} <- @@ -367,6 +371,11 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:zilliqa_nested_quorum_certificates, %{params: zilliqa_nested_quorum_certificates}) end + defp do_import_options(:stability, basic_import_options, %{stability_validators: stability_validators}) do + basic_import_options + |> Map.put_new(:stability_validators, %{params: stability_validators}) + end + defp do_import_options(_chain_type, basic_import_options, _chain_specific_import_options) do basic_import_options end diff --git a/apps/indexer/lib/indexer/fetcher/stability/validator.ex b/apps/indexer/lib/indexer/fetcher/stability/validator.ex index 7339cb9a1d44..c8fe6a1d8fb4 100644 --- a/apps/indexer/lib/indexer/fetcher/stability/validator.ex +++ b/apps/indexer/lib/indexer/fetcher/stability/validator.ex @@ -58,7 +58,11 @@ defmodule Indexer.Fetcher.Stability.Validator do |> ValidatorStability.append_timestamps() end) - ValidatorStability.insert_validators(active ++ inactive) + ValidatorStability.insert_validators( + (active ++ inactive) + |> add_blocks_validated(validators_from_db) + ) + ValidatorStability.delete_validators_by_address_hashes(address_hashes_to_drop_from_db) _ -> @@ -68,6 +72,31 @@ defmodule Indexer.Fetcher.Stability.Validator do {:noreply, state} end + defp add_blocks_validated([_ | _] = validators, validators_from_db) do + validators_from_db_map = + Enum.reduce(validators_from_db, %{}, fn validator, map -> Map.put(map, validator.address_hash, true) end) + + address_hashes_to_fetch_blocks_validated = + Enum.flat_map(validators, fn validator -> + if validators_from_db_map[validator.address_hash] do + [] + else + [validator.address_hash] + end + end) + + blocks_validated_map = + address_hashes_to_fetch_blocks_validated + |> ValidatorStability.fetch_blocks_validated() + |> Enum.into(%{}) + + Enum.map(validators, fn validator -> + Map.put(validator, :blocks_validated, blocks_validated_map[validator.address_hash] || 0) + end) + end + + defp add_blocks_validated(validators, _), do: validators + @spec trigger_update_validators_list() :: :ok def trigger_update_validators_list do GenServer.cast(__MODULE__, :update_validators_list) diff --git a/apps/indexer/lib/indexer/transform/stability/validators.ex b/apps/indexer/lib/indexer/transform/stability/validators.ex new file mode 100644 index 000000000000..58624b5eaa3d --- /dev/null +++ b/apps/indexer/lib/indexer/transform/stability/validators.ex @@ -0,0 +1,35 @@ +defmodule Indexer.Transform.Stability.Validators do + @moduledoc """ + Helper functions for transforming blocks into stability validator counter updates. + """ + + require Logger + + @doc """ + Returns a list of validator counter updates given a list of blocks. + Only processes blocks for stability chain type. + """ + def parse(blocks) do + chain_type = Application.get_env(:explorer, :chain_type) + + if chain_type == :stability do + do_parse(blocks) + else + [] + end + end + + defp do_parse(blocks) when is_list(blocks) do + blocks + |> Enum.filter(&(&1[:miner_hash] != nil)) + |> Enum.group_by(& &1[:miner_hash]) + |> Enum.map(fn {miner_hash, validator_blocks} -> + %{ + address_hash: miner_hash, + blocks_validated: length(validator_blocks) + } + end) + end + + defp do_parse(_), do: [] +end diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs index 99b014a4788c..0be013f9c980 100644 --- a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -694,6 +694,167 @@ defmodule Indexer.Block.Catchup.FetcherTest do assert %{from_number: 1, to_number: 0} = Repo.one(MissingBlockRange) end + + if Application.compile_env(:explorer, :chain_type) == :stability do + test "update stability validator counter", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + Application.put_env(:indexer, Indexer.Block.Catchup.Fetcher, batch_size: 1, concurrency: 10) + Application.put_env(:indexer, :block_ranges, "1..2") + start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) + CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + MissingRangesCollector.start_link([]) + MissingRangesManipulator.start_link([]) + + latest_block_number = 3 + latest_block_quantity = integer_to_quantity(latest_block_number) + + block_number = latest_block_number - 1 + block_hash = block_hash() + block_hash_1 = block_hash() + block_quantity = integer_to_quantity(block_number) + block_quantity_1 = integer_to_quantity(block_number - 1) + + validator = insert(:validator_stability) + miner_hash_data = to_string(validator.address_hash) + miner_hash_0 = address_hash() + miner_hash_0_data = to_string(miner_hash_0) + + new_block_hash = block_hash() + + refute block_hash == new_block_hash + + EthereumJSONRPC.Mox + |> expect(:json_rpc, 4, fn + %{method: "eth_getBlockByNumber", params: ["latest", false]}, _options -> + {:ok, %{"number" => latest_block_quantity}} + + [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: [^block_quantity, true] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: %{ + "hash" => to_string(block_hash), + "number" => block_quantity, + "difficulty" => "0x0", + "gasLimit" => "0x0", + "gasUsed" => "0x0", + "extraData" => "0x0", + "logsBloom" => "0x0", + "miner" => miner_hash_data, + "parentHash" => + block_hash() + |> to_string(), + "receiptsRoot" => "0x0", + "size" => "0x0", + "sha3Uncles" => "0x0", + "stateRoot" => "0x0", + "timestamp" => "0x0", + "totalDifficulty" => "0x0", + "transactions" => [], + "transactionsRoot" => "0x0", + "uncles" => [] + } + } + ]} + + [%{id: id, jsonrpc: "2.0", method: "trace_block", params: [^block_quantity]}], _options -> + { + :ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: [] + } + ] + } + + [%{id: id, jsonrpc: "2.0", method: "trace_block", params: [^block_quantity_1]}], _options -> + { + :ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: [] + } + ] + } + + [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: [^block_quantity_1, true] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: %{ + "hash" => to_string(block_hash_1), + "number" => block_quantity_1, + "difficulty" => "0x0", + "gasLimit" => "0x0", + "gasUsed" => "0x0", + "extraData" => "0x0", + "logsBloom" => "0x0", + "miner" => miner_hash_data, + "parentHash" => + block_hash() + |> to_string(), + "receiptsRoot" => "0x0", + "size" => "0x0", + "sha3Uncles" => "0x0", + "stateRoot" => "0x0", + "timestamp" => "0x0", + "totalDifficulty" => "0x0", + "transactions" => [], + "transactionsRoot" => "0x0", + "uncles" => [] + } + } + ]} + end) + + assert count(Chain.Block) == 0 + + Process.sleep(50) + + assert %{first_block_number: ^block_number, last_block_number: 1, missing_block_count: 2, shrunk: false} = + Fetcher.task(%Fetcher{ + block_fetcher: %Block.Fetcher{ + callback_module: Fetcher, + json_rpc_named_arguments: json_rpc_named_arguments + } + }) + + Process.sleep(3000) + + assert count(Chain.Block) == 2 + + validator_from_db = Repo.get!(Explorer.Chain.Stability.Validator, validator.address_hash) + assert validator_from_db.blocks_validated == validator.blocks_validated + 2 + end + end end defp count(schema) do diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 62411e127ab9..65a1592aa7d5 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -288,9 +288,11 @@ defmodule Indexer.Block.FetcherTest do } do block_number = @first_full_block_number - Indexer.Fetcher.Filecoin.AddressInfo.Supervisor.Case.start_supervised!( - json_rpc_named_arguments: json_rpc_named_arguments - ) + if Application.get_env(:explorer, :chain_type) == :filecoin do + Indexer.Fetcher.Filecoin.AddressInfo.Supervisor.Case.start_supervised!( + json_rpc_named_arguments: json_rpc_named_arguments + ) + end if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do case Keyword.fetch!(json_rpc_named_arguments, :variant) do diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index 4cf0493336bf..f0fa21cc99bb 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -847,6 +847,297 @@ defmodule Indexer.Block.Realtime.FetcherTest do Application.put_env(:indexer, :fetch_rewards_way, nil) end + + if Application.compile_env(:explorer, :chain_type) == :stability do + @tag :no_geth + test "update stability validator counter", %{ + block_fetcher: %Indexer.Block.Fetcher{} = block_fetcher, + json_rpc_named_arguments: json_rpc_named_arguments + } do + Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + UncleBlock.Supervisor.Case.start_supervised!( + block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + ) + + ReplacedTransaction.Supervisor.Case.start_supervised!() + + Indexer.Fetcher.Filecoin.AddressInfo.Supervisor.Case.start_supervised!( + json_rpc_named_arguments: json_rpc_named_arguments + ) + + # In CELO network, there is a token duality feature where CELO can be used + # as both a native chain currency and as an ERC-20 token (GoldToken). + # Transactions that transfer CELO are also counted as token transfers, and + # the TokenInstance fetcher is called. However, for simplicity, we disable + # it in this test. + Application.put_env(:indexer, Indexer.Fetcher.TokenInstance.Realtime.Supervisor, disabled?: true) + + on_exit(fn -> + Application.put_env(:indexer, Indexer.Fetcher.TokenInstance.Realtime.Supervisor, disabled?: false) + end) + + validator_1 = insert(:validator_stability) + validator_2 = insert(:validator_stability) + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["0x3C365F", true] + }, + %{ + id: 1, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["0x3C3660", true] + } + ], + _ -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: %{ + "author" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", + "difficulty" => "0xfffffffffffffffffffffffffffffffe", + "extraData" => "0xd583010b088650617269747986312e32372e32826c69", + "gasLimit" => "0x7a1200", + "gasUsed" => "0x2886e", + "hash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => to_string(validator_1.address_hash), + "number" => "0x3c365f", + "parentHash" => "0x57f6d66e07488defccd5216c4d2968dd6afd3bd32415e284de3b02af6535e8dc", + "receiptsRoot" => "0x111be72e682cea9c93e02f1ef503fb64aa821b2ef510fd9177c49b37d0af98b5", + "sealFields" => [ + "0x841246c63f", + "0xb841ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700", + "size" => "0x33e", + "stateRoot" => "0x7f73f5fb9f891213b671356126c31e9795d038844392c7aa8800ed4f52307209", + "step" => "306628159", + "timestamp" => "0x5b61df3b", + "totalDifficulty" => "0x3c365effffffffffffffffffffffffed7f0362", + "transactions" => [], + "transactionsRoot" => "0xd7c39a93eafe0bdcbd1324c13dcd674bed8c9fa8adbf8f95bf6a59788985da6f", + "uncles" => ["0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cd"] + } + }, + %{ + id: 1, + jsonrpc: "2.0", + result: %{ + "author" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", + "difficulty" => "0xfffffffffffffffffffffffffffffffe", + "extraData" => "0xd583010a068650617269747986312e32362e32826c69", + "gasLimit" => "0x7a1200", + "gasUsed" => "0x0", + "hash" => "0xfb483e511d316fa4072694da3f7abc94b06286406af45061e5e681395bdc6815", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => to_string(validator_2.address_hash), + "number" => "0x3c3660", + "parentHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sealFields" => [ + "0x841246c640", + "0xb84114db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "14db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800", + "size" => "0x243", + "stateRoot" => "0x3174c461989e9f99e08fa9b4ffb8bce8d9a281c8fc9f80694bb9d3acd4f15559", + "step" => "306628160", + "timestamp" => "0x5b61df40", + "totalDifficulty" => "0x3c365fffffffffffffffffffffffffed7f0360", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + end) + |> expect(:json_rpc, 1, fn + [ + %{id: 0, jsonrpc: "2.0", method: "trace_block", params: ["0x3C365F"]}, + %{id: 1, jsonrpc: "2.0", method: "trace_block", params: ["0x3C3660"]} + ], + _ -> + {:ok, + [ + %{id: 0, jsonrpc: "2.0", result: []}, + %{id: 1, jsonrpc: "2.0", result: []} + ]} + + [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["0x3C365F", true] + } + ], + _ -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: %{ + "author" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", + "difficulty" => "0xfffffffffffffffffffffffffffffffe", + "extraData" => "0xd583010b088650617269747986312e32372e32826c69", + "gasLimit" => "0x7a1200", + "gasUsed" => "0x2886e", + "hash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2", + "number" => "0x3c365f", + "parentHash" => "0x57f6d66e07488defccd5216c4d2968dd6afd3bd32415e284de3b02af6535e8dc", + "receiptsRoot" => "0x111be72e682cea9c93e02f1ef503fb64aa821b2ef510fd9177c49b37d0af98b5", + "sealFields" => [ + "0x841246c63f", + "0xb841ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700", + "size" => "0x33e", + "stateRoot" => "0x7f73f5fb9f891213b671356126c31e9795d038844392c7aa8800ed4f52307209", + "step" => "306628159", + "timestamp" => "0x5b61df3b", + "totalDifficulty" => "0x3c365effffffffffffffffffffffffed7f0362", + "transactions" => [], + "transactionsRoot" => "0xd7c39a93eafe0bdcbd1324c13dcd674bed8c9fa8adbf8f95bf6a59788985da6f", + "uncles" => ["0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cd"] + } + } + ]} + + [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_getBlockByNumber", + params: ["0x3C3660", true] + } + ], + _ -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: %{ + "author" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", + "difficulty" => "0xfffffffffffffffffffffffffffffffe", + "extraData" => "0xd583010a068650617269747986312e32362e32826c69", + "gasLimit" => "0x7a1200", + "gasUsed" => "0x0", + "hash" => "0xfb483e511d316fa4072694da3f7abc94b06286406af45061e5e681395bdc6815", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3", + "number" => "0x3c3660", + "parentHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sealFields" => [ + "0x841246c640", + "0xb84114db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "14db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800", + "size" => "0x243", + "stateRoot" => "0x3174c461989e9f99e08fa9b4ffb8bce8d9a281c8fc9f80694bb9d3acd4f15559", + "step" => "306628160", + "timestamp" => "0x5b61df40", + "totalDifficulty" => "0x3c365fffffffffffffffffffffffffed7f0360", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + + [ + %{ + id: 0, + jsonrpc: "2.0", + method: "trace_replayBlockTransactions", + params: [ + "0x3C3660", + ["trace"] + ] + }, + %{ + id: 1, + jsonrpc: "2.0", + method: "trace_replayBlockTransactions", + params: [ + "0x3C365F", + ["trace"] + ] + } + ], + _ -> + {:ok, + [ + %{id: 0, jsonrpc: "2.0", result: []}, + %{ + id: 1, + jsonrpc: "2.0", + result: [] + } + ]} + end) + end + + validator_1_address_hash = validator_1.address_hash + validator_2_address_hash = validator_2.address_hash + + assert {:ok, + %{ + inserted: %{ + blocks: [%Chain.Block{number: 3_946_079}, %Chain.Block{number: 3_946_080}], + stability_validators: [ + %Explorer.Chain.Stability.Validator{ + address_hash: ^validator_1_address_hash, + blocks_validated: blocks_validated_1 + }, + %Explorer.Chain.Stability.Validator{ + address_hash: ^validator_2_address_hash, + blocks_validated: blocks_validated_2 + } + ] + }, + errors: [] + }} = Indexer.Block.Fetcher.fetch_and_import_range(block_fetcher, 3_946_079..3_946_080) + + validator_from_db = Repo.get!(Explorer.Chain.Stability.Validator, validator_1.address_hash) + assert validator_from_db.blocks_validated == blocks_validated_1 + assert validator_from_db.blocks_validated == validator_1.blocks_validated + 1 + + validator_from_db = Repo.get!(Explorer.Chain.Stability.Validator, validator_2.address_hash) + assert validator_from_db.blocks_validated == blocks_validated_2 + assert validator_from_db.blocks_validated == validator_2.blocks_validated + 1 + end + end end describe "start_fetch_and_import" do diff --git a/apps/indexer/test/indexer/fetcher/stability/validator_test.exs b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs index accbebac2e7e..5af711e4d3d9 100644 --- a/apps/indexer/test/indexer/fetcher/stability/validator_test.exs +++ b/apps/indexer/test/indexer/fetcher/stability/validator_test.exs @@ -265,6 +265,206 @@ defmodule Indexer.Fetcher.Stability.ValidatorTest do assert %ValidatorStability{state: :probation} = map[validator_inactive1.address_hash.bytes] assert %ValidatorStability{state: :active} = map[validator_probation1.address_hash.bytes] end + + test "handles blocks_validated for new and existing validators" do + # Create existing validators with known blocks_validated values + existing_validator_1 = insert(:validator_stability, state: :active, blocks_validated: 100) + existing_validator_2 = insert(:validator_stability, state: :inactive, blocks_validated: 50) + + # Create some blocks for the new validators that will be added + new_validator_address_1 = insert(:address) + new_validator_address_2 = insert(:address) + + # Insert blocks to test blocks_validated functionality + insert(:block, miner: new_validator_address_1) + insert(:block, miner: new_validator_address_1) + insert(:block, miner: new_validator_address_1) + insert(:block, miner: new_validator_address_2) + insert(:block, miner: new_validator_address_2) + + start_supervised!({Indexer.Fetcher.Stability.Validator, name: Indexer.Fetcher.Stability.Validator}) + + # Mock the first call to get validator lists (existing + new validators in the network) + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn + [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xa5aa7380", to: "0x0000000000000000000000000000000000000805"}, "latest"] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [%{data: "0xe35c0f7d", to: "0x0000000000000000000000000000000000000805"}, "latest"] + } + ], + _ -> + # Return all validators (existing + new) + <<"0x", _method_id::binary-size(8), result_all::binary>> = + [@accepts_list_of_addresses] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([ + [ + existing_validator_1.address_hash.bytes, + existing_validator_2.address_hash.bytes, + new_validator_address_1.hash.bytes, + new_validator_address_2.hash.bytes + ] + ]) + + # Return active validators (subset of all) + <<"0x", _method_id::binary-size(8), result_active::binary>> = + [@accepts_list_of_addresses] + |> ABI.parse_specification() + |> Enum.at(0) + |> Encoder.encode_function_call([ + [ + existing_validator_1.address_hash.bytes, + new_validator_address_1.hash.bytes, + new_validator_address_2.hash.bytes + ] + ]) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_active + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_all + } + ]} + end) + + # Mock the second call to get missing blocks for active validators + "0x" <> existing_address_1 = to_string(existing_validator_1.address_hash) + "0x" <> new_address_1 = to_string(new_validator_address_1.hash) + "0x" <> new_address_2 = to_string(new_validator_address_2.hash) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn + [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^existing_address_1, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^new_address_1, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + }, + %{ + id: id_3, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0x41ee9a53000000000000000000000000" <> ^new_address_2, + to: "0x0000000000000000000000000000000000000805" + }, + "latest" + ] + } + ], + _ -> + # Return missing blocks for each validator + <<"0x", _method_id::binary-size(8), result_1::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + # existing validator - still active + |> Encoder.encode_function_call([0]) + + <<"0x", _method_id::binary-size(8), result_2::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + # new validator 1 - active + |> Encoder.encode_function_call([0]) + + <<"0x", _method_id::binary-size(8), result_3::binary>> = + [@accepts_integer] + |> ABI.parse_specification() + |> Enum.at(0) + # new validator 2 - active + |> Encoder.encode_function_call([0]) + + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: "0x" <> result_1 + }, + %{ + id: id_2, + jsonrpc: "2.0", + result: "0x" <> result_2 + }, + %{ + id: id_3, + jsonrpc: "2.0", + result: "0x" <> result_3 + } + ]} + end) + + # Wait for async processing + :timer.sleep(100) + + validators = ValidatorStability.get_all_validators() + assert Enum.count(validators) == 4 + + validator_map = + Enum.reduce(validators, %{}, fn validator, map -> + Map.put(map, validator.address_hash.bytes, validator) + end) + + # Verify existing validators keep their original blocks_validated values + existing_val_1 = validator_map[existing_validator_1.address_hash.bytes] + existing_val_2 = validator_map[existing_validator_2.address_hash.bytes] + + # Original value preserved + assert existing_val_1.blocks_validated == 100 + # Original value preserved + assert existing_val_2.blocks_validated == 50 + assert existing_val_1.state == :active + assert existing_val_2.state == :inactive + + # Verify new validators get blocks_validated populated from database + new_val_1 = validator_map[new_validator_address_1.hash.bytes] + new_val_2 = validator_map[new_validator_address_2.hash.bytes] + + # Should match actual blocks count + assert new_val_1.blocks_validated == 3 + # Should match actual blocks count + assert new_val_2.blocks_validated == 2 + assert new_val_1.state == :active + assert new_val_2.state == :active + end end end end diff --git a/apps/indexer/test/indexer/transform/stability/validators_test.exs b/apps/indexer/test/indexer/transform/stability/validators_test.exs new file mode 100644 index 000000000000..b135a9f5be87 --- /dev/null +++ b/apps/indexer/test/indexer/transform/stability/validators_test.exs @@ -0,0 +1,162 @@ +if Application.compile_env(:explorer, :chain_type) == :stability do + defmodule Indexer.Transform.Stability.ValidatorsTest do + use ExUnit.Case, async: true + + alias Indexer.Transform.Stability.Validators + + describe "parse/1" do + setup do + # Save original chain type and restore after each test + original_chain_type = Application.get_env(:explorer, :chain_type) + + on_exit(fn -> + Application.put_env(:explorer, :chain_type, original_chain_type) + end) + + :ok + end + + test "parses blocks for stability chain type and returns validator counter updates" do + Application.put_env(:explorer, :chain_type, :stability) + + blocks = [ + %{ + number: 100, + hash: "0xabc123", + miner_hash: "0x1234567890abcdef1234567890abcdef12345678" + }, + %{ + number: 101, + hash: "0xdef456", + miner_hash: "0x1234567890abcdef1234567890abcdef12345678" + }, + %{ + number: 102, + hash: "0x789012", + miner_hash: "0xabcdef1234567890abcdef1234567890abcdef12" + } + ] + + result = Validators.parse(blocks) + + expected = [ + %{ + address_hash: "0x1234567890abcdef1234567890abcdef12345678", + blocks_validated: 2 + }, + %{ + address_hash: "0xabcdef1234567890abcdef1234567890abcdef12", + blocks_validated: 1 + } + ] + + # Sort both lists by address_hash for comparison + sorted_result = Enum.sort_by(result, & &1.address_hash) + sorted_expected = Enum.sort_by(expected, & &1.address_hash) + + assert sorted_result == sorted_expected + end + + test "filters out blocks with nil miner_hash" do + Application.put_env(:explorer, :chain_type, :stability) + + blocks = [ + %{ + number: 100, + hash: "0xabc123", + miner_hash: "0x1234567890abcdef1234567890abcdef12345678" + }, + %{ + number: 101, + hash: "0xdef456", + miner_hash: nil + }, + %{ + number: 102, + hash: "0x789012" + # no miner_hash field + } + ] + + result = Validators.parse(blocks) + + expected = [ + %{ + address_hash: "0x1234567890abcdef1234567890abcdef12345678", + blocks_validated: 1 + } + ] + + assert result == expected + end + + test "returns empty list for non-stability chain type" do + Application.put_env(:explorer, :chain_type, :ethereum) + + blocks = [ + %{ + number: 100, + hash: "0xabc123", + miner_hash: "0x1234567890abcdef1234567890abcdef12345678" + }, + %{ + number: 101, + hash: "0xdef456", + miner_hash: "0xabcdef1234567890abcdef1234567890abcdef12" + } + ] + + result = Validators.parse(blocks) + + assert result == [] + end + + test "returns empty list for empty blocks list" do + Application.put_env(:explorer, :chain_type, :stability) + + result = Validators.parse([]) + + assert result == [] + end + + test "returns empty list for nil input" do + Application.put_env(:explorer, :chain_type, :stability) + + result = Validators.parse(nil) + + assert result == [] + end + + test "groups multiple blocks by same validator correctly" do + Application.put_env(:explorer, :chain_type, :stability) + + blocks = [ + %{number: 100, hash: "0x1", miner_hash: "0x1111"}, + %{number: 101, hash: "0x2", miner_hash: "0x1111"}, + %{number: 102, hash: "0x3", miner_hash: "0x1111"}, + %{number: 103, hash: "0x4", miner_hash: "0x2222"}, + %{number: 104, hash: "0x5", miner_hash: "0x1111"} + ] + + result = Validators.parse(blocks) + + expected = [ + %{ + address_hash: "0x1111", + blocks_validated: 4 + }, + %{ + address_hash: "0x2222", + blocks_validated: 1 + } + ] + + # Sort both lists by address_hash for comparison + sorted_result = Enum.sort_by(result, & &1.address_hash) + sorted_expected = Enum.sort_by(expected, & &1.address_hash) + + assert sorted_result == sorted_expected + end + end + end +end From fc67c74f8e7dac3900a1228432bd74afaed7cf4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:36:58 +0300 Subject: [PATCH 0128/1006] chore(deps): bump core-js in /apps/block_scout_web/assets (#12714) Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.42.0 to 3.43.0. - [Release notes](https://github.com/zloirock/core-js/releases) - [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/zloirock/core-js/commits/v3.43.0/packages/core-js) --- updated-dependencies: - dependency-name: core-js dependency-version: 3.43.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 423572a317c0..8864fafd54cd 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -17,7 +17,7 @@ "chart.js": "^4.4.9", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.42.0", + "core-js": "^3.43.0", "crypto-browserify": "^3.12.1", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", @@ -5758,9 +5758,9 @@ } }, "node_modules/core-js": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==", + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==", "hasInstallScript": true, "license": "MIT", "funding": { @@ -20631,9 +20631,9 @@ } }, "core-js": { - "version": "3.42.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.42.0.tgz", - "integrity": "sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==" + "version": "3.43.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.43.0.tgz", + "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==" }, "core-js-compat": { "version": "3.41.0", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 613f477a56e5..3ff6e1cf0e45 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -29,7 +29,7 @@ "chart.js": "^4.4.9", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", - "core-js": "^3.42.0", + "core-js": "^3.43.0", "crypto-browserify": "^3.12.1", "dropzone": "^5.9.3", "eth-net-props": "^1.0.41", From 48b467ad7f18295263f205945e1cac482c63fbb3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:37:21 +0300 Subject: [PATCH 0129/1006] chore(deps): bump sweetalert2 in /apps/block_scout_web/assets (#12706) Bumps [sweetalert2](https://github.com/sweetalert2/sweetalert2) from 11.22.0 to 11.22.2. - [Release notes](https://github.com/sweetalert2/sweetalert2/releases) - [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md) - [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.22.0...v11.22.2) --- updated-dependencies: - dependency-name: sweetalert2 dependency-version: 11.22.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 8864fafd54cd..495b6a02bf85 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -61,7 +61,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.22.0", + "sweetalert2": "^11.22.2", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", @@ -14922,9 +14922,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.22.0", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.0.tgz", - "integrity": "sha512-pSMuRGDULhh+wrFkO22O0YsIXxs8yFE0O+WVYXcqc/sTa1oRnf0JlR+vfQIRY1QM1UeFfnCjyw6DYnG75/oxiQ==", + "version": "11.22.2", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz", + "integrity": "sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==", "license": "MIT", "funding": { "type": "individual", @@ -27401,9 +27401,9 @@ } }, "sweetalert2": { - "version": "11.22.0", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.0.tgz", - "integrity": "sha512-pSMuRGDULhh+wrFkO22O0YsIXxs8yFE0O+WVYXcqc/sTa1oRnf0JlR+vfQIRY1QM1UeFfnCjyw6DYnG75/oxiQ==" + "version": "11.22.2", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz", + "integrity": "sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==" }, "symbol-tree": { "version": "3.2.4", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 3ff6e1cf0e45..ed2eb201d598 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -73,7 +73,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.22.0", + "sweetalert2": "^11.22.2", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", From e6765fbcdf29a74f3eca2495a47bad690c2e8714 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:38:07 +0300 Subject: [PATCH 0130/1006] chore(deps-dev): bump eslint-plugin-import (#12711) Bumps [eslint-plugin-import](https://github.com/import-js/eslint-plugin-import) from 2.31.0 to 2.32.0. - [Release notes](https://github.com/import-js/eslint-plugin-import/releases) - [Changelog](https://github.com/import-js/eslint-plugin-import/blob/main/CHANGELOG.md) - [Commits](https://github.com/import-js/eslint-plugin-import/compare/v2.31.0...v2.32.0) --- updated-dependencies: - dependency-name: eslint-plugin-import dependency-version: 2.32.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 1791 +++++++++++------ apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 1222 insertions(+), 571 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 495b6a02bf85..1fb0de0c8c8b 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -80,7 +80,7 @@ "css-minimizer-webpack-plugin": "^7.0.2", "eslint": "^9.28.0", "eslint-formatter-junit": "^8.40.0", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", @@ -4377,13 +4377,14 @@ } }, "node_modules/array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" }, "engines": { "node": ">= 0.4" @@ -4393,17 +4394,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4413,17 +4417,19 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -4433,15 +4439,16 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4451,15 +4458,16 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4469,19 +4477,19 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -4557,6 +4565,16 @@ "async": "^2.4.0" } }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -6442,14 +6460,15 @@ } }, "node_modules/data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6459,29 +6478,31 @@ } }, "node_modules/data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" }, "engines": { "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/inspect-js" } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -6935,57 +6956,66 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, + "license": "MIT", "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", + "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -7030,37 +7060,44 @@ } }, "node_modules/es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" } }, "node_modules/es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, + "license": "MIT", "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, + "license": "MIT", "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -7251,10 +7288,11 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^3.2.7" }, @@ -7272,34 +7310,36 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, + "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -8441,15 +8481,18 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -8468,6 +8511,7 @@ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8555,14 +8599,15 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -8640,12 +8685,14 @@ } }, "node_modules/globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, + "license": "MIT", "dependencies": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8702,10 +8749,14 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8730,10 +8781,14 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -9029,14 +9084,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9067,13 +9123,15 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -9088,26 +9146,51 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9128,9 +9211,10 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", "dependencies": { "hasown": "^2.0.2" }, @@ -9142,11 +9226,14 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, + "license": "MIT", "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -9157,12 +9244,14 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9180,6 +9269,22 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", @@ -9246,6 +9351,19 @@ "npm": ">=3" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-nan": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", @@ -9283,12 +9401,14 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9316,13 +9436,16 @@ "dev": true }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -9331,13 +9454,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -9359,12 +9496,14 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, + "license": "MIT", "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -9374,12 +9513,15 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -9408,13 +9550,47 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12338,9 +12514,13 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -12369,13 +12549,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -12418,12 +12601,14 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -12480,6 +12665,24 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -13856,16 +14059,39 @@ "node": ">= 10.13.0" } }, - "node_modules/reduce-reducers": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-1.0.4.tgz", - "integrity": "sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==" - }, - "node_modules/redux": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", - "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" - }, + "node_modules/reduce-reducers": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reduce-reducers/-/reduce-reducers-1.0.4.tgz", + "integrity": "sha512-Mb2WZ2bJF597exiqX7owBzrqJ74DHLK3yOQjCyPAaNifRncE8OD0wFIuoMhXxTnHK07+8zZ2SJEKy/qtiyR7vw==" + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13891,14 +14117,17 @@ "license": "MIT" }, "node_modules/regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" }, "engines": { @@ -14156,14 +14385,16 @@ "license": "0BSD" }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -14177,7 +14408,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/safe-buffer": { "version": "5.1.2", @@ -14193,15 +14425,40 @@ "events": "^3.0.0" } }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -14436,6 +14693,7 @@ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -14454,6 +14712,21 @@ "node": ">=0.10.0" } }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -14510,14 +14783,69 @@ } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -14634,6 +14962,20 @@ "node": ">=8" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -14709,15 +15051,19 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -14727,15 +15073,20 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15271,16 +15622,17 @@ } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -15290,17 +15642,19 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -15310,17 +15664,18 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -15351,15 +15706,19 @@ } }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -16267,16 +16626,74 @@ } }, "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, + "license": "MIT", "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -19604,81 +20021,83 @@ } }, "array-buffer-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, "requires": { - "call-bind": "^1.0.5", - "is-array-buffer": "^3.0.4" + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" } }, "array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" } }, "array.prototype.findlastindex": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", - "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", + "es-abstract": "^1.23.9", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" } }, "array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" } }, "array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" } }, "arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "requires": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" } }, "asn1": { @@ -19744,6 +20163,12 @@ "async": "^2.4.0" } }, + "async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -21101,34 +21526,34 @@ } }, "data-view-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", - "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, "requires": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" } }, "data-view-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", - "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" + "is-data-view": "^1.0.2" } }, "data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "requires": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } @@ -21464,57 +21889,65 @@ } }, "es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", "dev": true, "requires": { - "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", - "data-view-buffer": "^1.0.1", - "data-view-byte-length": "^1.0.1", - "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", - "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", - "is-array-buffer": "^3.0.4", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", + "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", - "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.2", - "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" } }, "es-define-property": { @@ -21542,34 +21975,35 @@ } }, "es-set-tostringtag": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", - "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "requires": { - "get-intrinsic": "^1.2.4", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", - "hasown": "^2.0.1" + "hasown": "^2.0.2" } }, "es-shim-unscopables": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", - "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", "dev": true, "requires": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" } }, "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" } }, "escalade": { @@ -21822,9 +22256,9 @@ } }, "eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "requires": { "debug": "^3.2.7" @@ -21842,29 +22276,29 @@ } }, "eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "requires": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "dependencies": { @@ -22668,15 +23102,17 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==" }, "function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" } }, "functional-red-black-tree": { @@ -22744,14 +23180,14 @@ "dev": true }, "get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "requires": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" } }, "getpass": { @@ -22807,12 +23243,13 @@ "dev": true }, "globalthis": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "requires": { - "define-properties": "^1.1.3" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" } }, "good-listener": { @@ -22849,9 +23286,9 @@ } }, "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true }, "has-flag": { @@ -22868,10 +23305,13 @@ } }, "has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", - "dev": true + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.0" + } }, "has-symbols": { "version": "1.1.0", @@ -23076,14 +23516,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "requires": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" } }, "interpret": { @@ -23102,13 +23542,14 @@ } }, "is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" } }, "is-arrayish": { @@ -23117,23 +23558,36 @@ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, + "is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "requires": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + } + }, "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "requires": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" } }, "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, "is-callable": { @@ -23142,29 +23596,32 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==" }, "is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "requires": { "hasown": "^2.0.2" } }, "is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "requires": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "requires": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" } }, "is-extglob": { @@ -23173,6 +23630,15 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, + "is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, "is-fn": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fn/-/is-fn-1.0.0.tgz", @@ -23217,6 +23683,12 @@ "resolved": "https://registry.npmjs.org/is-hex-prefixed/-/is-hex-prefixed-1.0.0.tgz", "integrity": "sha1-fY035q135dEnFIkTxXPggtd39VQ=" }, + "is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true + }, "is-nan": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", @@ -23239,12 +23711,13 @@ "dev": true }, "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "requires": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, "is-plain-object": { @@ -23263,22 +23736,30 @@ "dev": true }, "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" } }, + "is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true + }, "is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "requires": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" } }, "is-stream": { @@ -23288,21 +23769,24 @@ "dev": true }, "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "requires": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" } }, "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "requires": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" } }, "is-typed-array": { @@ -23318,13 +23802,29 @@ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, + "is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true + }, "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "requires": { + "call-bound": "^1.0.3" + } + }, + "is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" } }, "isarray": { @@ -25596,9 +26096,9 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==" }, "object-is": { "version": "1.1.5", @@ -25615,13 +26115,15 @@ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "requires": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, @@ -25649,12 +26151,13 @@ } }, "object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } @@ -25696,6 +26199,17 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "requires": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + } + }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -26627,6 +27141,22 @@ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" }, + "reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "requires": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + } + }, "regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -26648,14 +27178,16 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, "regexp.prototype.flags": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.3.tgz", - "integrity": "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, @@ -26862,14 +27394,15 @@ } }, "safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "requires": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "dependencies": { @@ -26894,15 +27427,33 @@ "events": "^3.0.0" } }, + "safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, "safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "requires": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" } }, "safer-buffer": { @@ -27076,6 +27627,17 @@ "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=" }, + "set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + } + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -27120,14 +27682,47 @@ "dev": true }, "side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "requires": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" } }, "signal-exit": { @@ -27213,6 +27808,16 @@ } } }, + "stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + } + }, "stream-browserify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", @@ -27270,24 +27875,28 @@ } }, "string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" } }, "string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } @@ -27640,44 +28249,45 @@ } }, "typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "requires": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" } }, "typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "requires": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" } }, "typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "requires": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" } }, "typedarray-to-buffer": { @@ -27695,15 +28305,15 @@ "peer": true }, "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "requires": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" } }, "unicode-canonical-property-names-ecmascript": { @@ -28416,16 +29026,57 @@ } }, "which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "requires": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + } + }, + "which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "requires": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } + }, + "which-collection": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", "dev": true, "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" } }, "which-module": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index ed2eb201d598..fce502e73b79 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -92,7 +92,7 @@ "css-minimizer-webpack-plugin": "^7.0.2", "eslint": "^9.28.0", "eslint-formatter-junit": "^8.40.0", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", From e00f948d92976a31fd6fcfedd2dac43724172e93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:39:02 +0300 Subject: [PATCH 0131/1006] chore(deps-dev): bump @babel/core in /apps/block_scout_web/assets (#12713) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.27.4 to 7.27.7. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.27.7/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.27.7 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 118 +++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 60 insertions(+), 60 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 1fb0de0c8c8b..6abdf2c4ba70 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -71,7 +71,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.4", + "@babel/core": "^7.27.7", "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", @@ -299,21 +299,21 @@ } }, "node_modules/@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.27.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -334,12 +334,12 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/generator": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", - "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.3", + "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", @@ -605,25 +605,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", - "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", - "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -1998,16 +1998,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", + "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -2025,9 +2025,9 @@ } }, "node_modules/@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -17140,20 +17140,20 @@ "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==" }, "@babel/core": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", - "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", + "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", + "@babel/generator": "^7.27.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.4", - "@babel/parser": "^7.27.4", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.4", - "@babel/types": "^7.27.3", + "@babel/traverse": "^7.27.7", + "@babel/types": "^7.27.7", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -17169,11 +17169,11 @@ } }, "@babel/generator": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.3.tgz", - "integrity": "sha512-xnlJYj5zepml8NXtjkG0WquFUv8RskFqyFcVgTBp5k+NaA/8uw/K+OSVf8AMGw5e9HKP2ETd5xpK5MLZQD6b4Q==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", "requires": { - "@babel/parser": "^7.27.3", + "@babel/parser": "^7.27.5", "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", @@ -17356,20 +17356,20 @@ } }, "@babel/helpers": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", - "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", "requires": { "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.6" } }, "@babel/parser": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.4.tgz", - "integrity": "sha512-BRmLHGwpUqLFR2jzx9orBuX/ABDkj2jLKOXrHDTN2aOKL+jFDDKaRNo9nyYsIl9h/UE/7lMKdDjKQQyxKKDZ7g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", + "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", "requires": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.27.7" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -18243,15 +18243,15 @@ } }, "@babel/traverse": { - "version": "7.27.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", - "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", + "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", "requires": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.3", - "@babel/parser": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/parser": "^7.27.7", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", + "@babel/types": "^7.27.7", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -18264,9 +18264,9 @@ } }, "@babel/types": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", - "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", + "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index fce502e73b79..eda798d5c8c3 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -83,7 +83,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.4", + "@babel/core": "^7.27.7", "@babel/preset-env": "^7.27.2", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", From 4b70d0f565c42b40c1714e1d306d32edee080e09 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 2 Jul 2025 09:44:20 +0300 Subject: [PATCH 0132/1006] feat: Update CodeQL action to v3 (#12697) (#12703) Signed-off-by: sellskin Co-authored-by: sellskin --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 8a4cf36f5bb4..a2e5d38487f3 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -56,7 +56,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -69,4 +69,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 5012ccf4b6d2a8e4aa3f1b796347700810b7967f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:45:11 +0300 Subject: [PATCH 0133/1006] chore(deps-dev): bump postcss in /apps/block_scout_web/assets (#12707) Bumps [postcss](https://github.com/postcss/postcss) from 8.5.4 to 8.5.6. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/postcss/postcss/compare/8.5.4...8.5.6) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 6abdf2c4ba70..30de628e0062 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -88,7 +88,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.4", + "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "sass": "^1.89.1", "sass-loader": "^14.2.1", @@ -13030,9 +13030,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -26451,9 +26451,9 @@ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==" }, "postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "requires": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index eda798d5c8c3..91cb9333e48a 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -100,7 +100,7 @@ "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "mini-css-extract-plugin": "^2.9.2", - "postcss": "^8.5.4", + "postcss": "^8.5.6", "postcss-loader": "^8.1.1", "sass": "^1.89.1", "sass-loader": "^14.2.1", From 965234028f0d7582a398578fa993711a7ad498cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:50:32 +0300 Subject: [PATCH 0134/1006] chore(deps-dev): bump eslint in /apps/block_scout_web/assets (#12715) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.30.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 143 +++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 73 insertions(+), 72 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 30de628e0062..815759280e9b 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -78,7 +78,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.2", - "eslint": "^9.28.0", + "eslint": "^9.30.1", "eslint-formatter-junit": "^8.40.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", @@ -2105,9 +2105,9 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2120,9 +2120,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2200,9 +2200,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true, "license": "MIT", "engines": { @@ -4187,10 +4187,11 @@ } }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -7197,19 +7198,19 @@ } }, "node_modules/eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -7221,9 +7222,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -7539,9 +7540,9 @@ } }, "node_modules/eslint/node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -7556,9 +7557,9 @@ } }, "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -7655,15 +7656,15 @@ } }, "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -7673,9 +7674,9 @@ } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -18323,9 +18324,9 @@ "dev": true }, "@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", "dev": true, "requires": { "@eslint/object-schema": "^2.1.6", @@ -18334,9 +18335,9 @@ } }, "@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", "dev": true }, "@eslint/core": { @@ -18389,9 +18390,9 @@ } }, "@eslint/js": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", - "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.30.1.tgz", + "integrity": "sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==", "dev": true }, "@eslint/object-schema": { @@ -19881,9 +19882,9 @@ } }, "acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true }, "acorn-globals": { @@ -22072,18 +22073,18 @@ } }, "eslint": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", - "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "version": "9.30.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", + "integrity": "sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", "@eslint/core": "^0.14.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.28.0", + "@eslint/js": "9.30.1", "@eslint/plugin-kit": "^0.3.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -22095,9 +22096,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -22155,9 +22156,9 @@ "dev": true }, "eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "requires": { "esrecurse": "^4.3.0", @@ -22165,9 +22166,9 @@ } }, "eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true }, "estraverse": { @@ -22391,20 +22392,20 @@ "dev": true }, "espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "requires": { - "acorn": "^8.14.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" + "eslint-visitor-keys": "^4.2.1" }, "dependencies": { "eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true } } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 91cb9333e48a..eb860028fc50 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -90,7 +90,7 @@ "copy-webpack-plugin": "^13.0.0", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.2", - "eslint": "^9.28.0", + "eslint": "^9.30.1", "eslint-formatter-junit": "^8.40.0", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", From 30975d60471a4c38d182ea054d452f95476157d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:52:49 +0300 Subject: [PATCH 0135/1006] chore(deps): bump chart.js in /apps/block_scout_web/assets (#12710) Bumps [chart.js](https://github.com/chartjs/Chart.js) from 4.4.9 to 4.5.0. - [Release notes](https://github.com/chartjs/Chart.js/releases) - [Commits](https://github.com/chartjs/Chart.js/compare/v4.4.9...v4.5.0) --- updated-dependencies: - dependency-name: chart.js dependency-version: 4.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 815759280e9b..45b8f23a5120 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -14,7 +14,7 @@ "assert": "^2.1.0", "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", - "chart.js": "^4.4.9", + "chart.js": "^4.5.0", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", "core-js": "^3.43.0", @@ -5534,9 +5534,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", - "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -20865,9 +20865,9 @@ "dev": true }, "chart.js": { - "version": "4.4.9", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz", - "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", "requires": { "@kurkle/color": "^0.3.0" } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index eb860028fc50..9ee31828af83 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -26,7 +26,7 @@ "assert": "^2.1.0", "bignumber.js": "^9.3.0", "bootstrap": "^4.6.0", - "chart.js": "^4.4.9", + "chart.js": "^4.5.0", "chartjs-adapter-luxon": "^1.3.1", "clipboard": "^2.0.11", "core-js": "^3.43.0", From 9298168fc710749c8221aad1553f2182e87381ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 12:14:22 +0300 Subject: [PATCH 0136/1006] chore(deps-dev): bump sass in /apps/block_scout_web/assets (#12709) Bumps [sass](https://github.com/sass/dart-sass) from 1.89.1 to 1.89.2. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.89.1...1.89.2) --- updated-dependencies: - dependency-name: sass dependency-version: 1.89.2 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 45b8f23a5120..b41a3564f69e 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -90,7 +90,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", - "sass": "^1.89.1", + "sass": "^1.89.2", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.99.9", @@ -14474,9 +14474,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sass": { - "version": "1.89.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz", - "integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "license": "MIT", "dependencies": { @@ -27463,9 +27463,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sass": { - "version": "1.89.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.1.tgz", - "integrity": "sha512-eMLLkl+qz7tx/0cJ9wI+w09GQ2zodTkcE/aVfywwdlRcI3EO19xGnbmJwg/JMIm+5MxVJ6outddLZ4Von4E++Q==", + "version": "1.89.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz", + "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==", "dev": true, "requires": { "@parcel/watcher": "^2.4.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 9ee31828af83..3ee5156d2328 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -102,7 +102,7 @@ "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", - "sass": "^1.89.1", + "sass": "^1.89.2", "sass-loader": "^14.2.1", "style-loader": "^4.0.0", "webpack": "^5.99.9", From 2d548719ffeb4648ec4c166b9078e20b1504bd60 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 2 Jul 2025 13:18:03 +0400 Subject: [PATCH 0137/1006] fix: Display correct OP Deposit origin address (#12672) * fix: Display correct OP Deposit origin address * Refactoring --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../lib/indexer/fetcher/optimism/deposit.ex | 67 +++++++++++-------- .../fetcher/optimism/withdrawal_event.ex | 24 +------ 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex b/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex index f2c7dd18dd29..83bb5c8cf34d 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/deposit.ex @@ -10,11 +10,9 @@ defmodule Indexer.Fetcher.Optimism.Deposit do import Ecto.Query - import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, quantity_to_integer: 1] + import EthereumJSONRPC, only: [quantity_to_integer: 1] import Explorer.Helper, only: [decode_data: 2] - alias EthereumJSONRPC.Block.ByNumber - alias EthereumJSONRPC.Blocks alias Explorer.{Chain, Repo} alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Optimism.Deposit @@ -167,19 +165,51 @@ defmodule Indexer.Fetcher.Optimism.Deposit do end end + # Prepares `TransactionDeposited` events to be imported to database. + # + # ## Parameters + # - `events`: The list of `TransactionDeposited` events got from `eth_getLogs` response. + # - `transaction_type`: L1 transaction type to correctly calculate the corresponding L2 transaction hash. + # - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. + # Used to get blocks info by their numbers from the RPC node. + # + # ## Returns + # - A list of prepared events. Each list item is a map outlining a deposit event. + @spec prepare_events(list(), non_neg_integer(), EthereumJSONRPC.json_rpc_named_arguments()) :: [map()] defp prepare_events(events, transaction_type, json_rpc_named_arguments) do - timestamps = + {timestamps, origins} = events - |> get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) - |> Enum.reduce(%{}, fn block, acc -> + |> Helper.get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number(), true) + |> Enum.reduce({%{}, %{}}, fn block, {timestamps_acc, origins_acc} -> block_number = quantity_to_integer(Map.get(block, "number")) {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(block, "timestamp"))) - Map.put(acc, block_number, timestamp) + + new_timestamps_acc = Map.put(timestamps_acc, block_number, timestamp) + + new_origins_acc = + block + |> Map.get("transactions", []) + |> Enum.reduce(origins_acc, fn transaction, acc -> + Map.put(acc, String.downcase(transaction["hash"]), transaction["from"]) + end) + + {new_timestamps_acc, new_origins_acc} end) - Enum.map(events, &event_to_deposit(&1, timestamps, transaction_type)) + Enum.map(events, &event_to_deposit(&1, timestamps, origins, transaction_type)) end + # Prepares `TransactionDeposited` event to be imported to database. + # + # ## Parameters + # - An event map outlining a `TransactionDeposited` event got from `eth_getLogs` response. + # - `timestamps`: A `block_number -> timestamp` map to get the timestamp of the event from its block info. + # - `origins`: A `transaction_hash -> origin` map to get the origin address of the event transaction. + # - `transaction_type`: L1 transaction type to correctly calculate the corresponding L2 transaction hash. + # + # ## Returns + # - A map with the event info ready to be imported to database. + @spec event_to_deposit(map(), map(), map(), non_neg_integer()) :: map() defp event_to_deposit( %{ "blockHash" => "0x" <> stripped_block_hash, @@ -190,6 +220,7 @@ defmodule Indexer.Fetcher.Optimism.Deposit do "data" => opaque_data }, timestamps, + origins, transaction_type ) do {_, prefixed_block_hash} = (String.pad_leading("", 64, "0") <> stripped_block_hash) |> String.split_at(-64) @@ -251,7 +282,7 @@ defmodule Indexer.Fetcher.Optimism.Deposit do l1_block_number: block_number, l1_block_timestamp: Map.get(timestamps, block_number), l1_transaction_hash: transaction_hash, - l1_transaction_origin: "0x" <> from_stripped, + l1_transaction_origin: Map.get(origins, String.downcase(transaction_hash)), l2_transaction_hash: l2_transaction_hash } end @@ -304,22 +335,4 @@ defmodule Indexer.Fetcher.Optimism.Deposit do def requires_l1_reorg_monitor? do Optimism.requires_l1_reorg_monitor?() end - - defp get_blocks_by_events(events, json_rpc_named_arguments, retries) do - request = - events - |> Enum.reduce(%{}, fn event, acc -> - Map.put(acc, event["blockNumber"], 0) - end) - |> Stream.map(fn {block_number, _} -> %{number: block_number} end) - |> id_to_params() - |> Blocks.requests(&ByNumber.request(&1, false, false)) - - error_message = &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(request)}" - - case Helper.repeated_call(&json_rpc/2, [request, json_rpc_named_arguments], error_message, retries) do - {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) - {:error, _} -> [] - end - end end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex index 9280ef6ebfb3..03c767b61798 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex @@ -10,10 +10,8 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do import Ecto.Query - import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2, quantity_to_integer: 1] + import EthereumJSONRPC, only: [quantity_to_integer: 1] - alias EthereumJSONRPC.Block.ByNumber - alias EthereumJSONRPC.Blocks alias Explorer.{Chain, Repo} alias Explorer.Chain.Optimism.WithdrawalEvent alias Explorer.Chain.RollupReorgMonitorQueue @@ -203,7 +201,7 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do defp prepare_events(events, json_rpc_named_arguments) do blocks = events - |> get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) + |> Helper.get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number(), true) transaction_hashes = events @@ -304,24 +302,6 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do Optimism.requires_l1_reorg_monitor?() end - defp get_blocks_by_events(events, json_rpc_named_arguments, retries) do - request = - events - |> Enum.reduce(%{}, fn event, acc -> - Map.put(acc, event["blockNumber"], 0) - end) - |> Stream.map(fn {block_number, _} -> %{number: block_number} end) - |> id_to_params() - |> Blocks.requests(&ByNumber.request(&1, true, false)) - - error_message = &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(request)}" - - case Helper.repeated_call(&json_rpc/2, [request, json_rpc_named_arguments], error_message, retries) do - {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) - {:error, _} -> [] - end - end - # Parses input of the prove L1 transaction and retrieves dispute game index or contract address hash # (depending on whether Super Roots are active) from that. # From 341c10e975656110dbf49243c4d795a0ff9a1985 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:47:26 +0300 Subject: [PATCH 0138/1006] perf: optimize Explorer.Chain.Cache.Blocks (#12402) * perf: optimize `Explorer.Chain.Cache.Blocks` * Add benchmark Co-Authored-By: nikitosing <32202610+nikitosing@users.noreply.github.com> --------- Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> --- .../api/v2/block_controller_benchmark.exs | 68 ++++++++++ .../benchmarks/support/benchmark_case.ex | 64 ++++++++++ .../views/api/v2/block_view.ex | 21 ++-- .../views/api/v2/ethereum_view.ex | 8 +- apps/block_scout_web/mix.exs | 4 +- apps/block_scout_web/priv/gettext/default.pot | 28 ++--- .../priv/gettext/en/LC_MESSAGES/default.po | 28 ++--- apps/explorer/lib/explorer/chain/block.ex | 116 ++++++++++++++++-- .../lib/explorer/chain/cache/blocks.ex | 27 ++++ .../lib/explorer/chain/ordered_cache.ex | 16 ++- apps/explorer/lib/explorer/helper.ex | 26 +++- .../test/explorer/chain/cache/blocks_test.exs | 3 +- 12 files changed, 347 insertions(+), 62 deletions(-) create mode 100644 apps/block_scout_web/benchmarks/block_scout_web/controllers/api/v2/block_controller_benchmark.exs create mode 100644 apps/block_scout_web/benchmarks/support/benchmark_case.ex diff --git a/apps/block_scout_web/benchmarks/block_scout_web/controllers/api/v2/block_controller_benchmark.exs b/apps/block_scout_web/benchmarks/block_scout_web/controllers/api/v2/block_controller_benchmark.exs new file mode 100644 index 000000000000..91a189ae1fc5 --- /dev/null +++ b/apps/block_scout_web/benchmarks/block_scout_web/controllers/api/v2/block_controller_benchmark.exs @@ -0,0 +1,68 @@ +defmodule BlockScoutWeb.API.V2.BlockControllerBenchmark do + @moduledoc """ + Benchmark for the Block API V2 controller. + Tests the performance of the /api/v2/blocks endpoint with various data volumes. + + To run: + ``` + cd apps/block_scout_web + mix run benchmarks/block_scout_web/controllers/api/v2/block_controller_benchmark.exs + ``` + """ + use BlockScoutWeb.BenchmarkCase + + alias Explorer.Repo + alias Explorer.Chain.Block + + @doc """ + Benchmark the first page of the /api/v2/blocks endpoint. It mainly tests the performance of `Explorer.Chain.Cache.Blocks`. + """ + def list_blocks_first_page do + benchmark_setup() + + Benchee.run( + %{ + "GET /api/v2/blocks first page (cached)" => fn %{conn: conn} -> + get(conn, "/api/v2/blocks") + end + }, + inputs: %{ + " 0 blocks 0 transactions per block" => %{block_count: 0, transaction_per_block: 0}, + "51 blocks 0 transactions per block" => %{block_count: 51, transaction_per_block: 0}, + "51 blocks 10 transactions per block" => %{block_count: 51, transaction_per_block: 10}, + "51 blocks 100 transactions per block" => %{block_count: 51, transaction_per_block: 100}, + "51 blocks 500 transactions per block" => %{block_count: 51, transaction_per_block: 500} + }, + before_scenario: fn %{block_count: block_count, transaction_per_block: transaction_per_block} = input -> + reset_db() + + Application.put_env(:explorer, Explorer.Chain.Cache.Blocks, ttl_check_interval: false, global_ttl: nil) + + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + + 0 = Repo.aggregate(Block, :count, :hash) + + block_count + |> insert_list(:block) + |> Enum.each(fn block -> + transaction_per_block + |> insert_list(:transaction) + |> with_block(block) + end) + + Map.put(input, :conn, get_conn()) + end, + formatters: [Benchee.Formatters.Console], + load: @path, + save: [ + path: @path + ], + warmup: 1, + time: 10, + memory_time: 10 + ) + end +end + +BlockScoutWeb.API.V2.BlockControllerBenchmark.list_blocks_first_page() diff --git a/apps/block_scout_web/benchmarks/support/benchmark_case.ex b/apps/block_scout_web/benchmarks/support/benchmark_case.ex new file mode 100644 index 000000000000..4539220b907d --- /dev/null +++ b/apps/block_scout_web/benchmarks/support/benchmark_case.ex @@ -0,0 +1,64 @@ +defmodule BlockScoutWeb.BenchmarkCase do + @moduledoc """ + This module defines the benchmark case to be used by benchmarks. + + This module provides common setup and utilities for benchmarking, + similar to ConnCase but optimized for benchmarking needs, including: + + - Database sandbox management + - Connection building + - Factory imports + - Common helpers + + ## Example + + ```elixir + defmodule BlockScoutWeb.MyBenchmark do + use BlockScoutWeb.BenchmarkCase + + def run do + Benchee.run(...) + end + end + + # Run the benchmark + BlockScoutWeb.MyBenchmark.run() + ``` + """ + + defmacro __using__(_opts) do + caller_file = __CALLER__.file + path = caller_file |> String.replace(Path.extname(caller_file), ".benchee") + + quote do + import Explorer.Factory + import Phoenix.ConnTest + + @endpoint BlockScoutWeb.Endpoint + @path unquote(path) + + @doc """ + Resets the database for consistent benchmarks + """ + def reset_db do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo, ownership_timeout: :infinity) + end + + @doc """ + Gets a Phoenix connection for HTTP request benchmarks + """ + def get_conn do + Phoenix.ConnTest.build_conn() + end + + @doc """ + Setup common benchmark environment + """ + def benchmark_setup do + {:ok, _} = Application.ensure_all_started(:block_scout_web) + + :ok + end + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 7c2510dc2f1e..7ec4e2e43060 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do alias BlockScoutWeb.BlockView alias BlockScoutWeb.API.V2.{ApiView, Helper} alias Explorer.Chain.Block - alias Explorer.Chain.Cache.Counters.BlockPriorityFeeCount def render("message.json", assigns) do ApiView.render("message.json", assigns) @@ -29,17 +28,14 @@ defmodule BlockScoutWeb.API.V2.BlockView do end def prepare_block(block, _conn, single_block? \\ false) do - burnt_fees = Block.burnt_fees(block.transactions, block.base_fee_per_gas) - priority_fee = block.base_fee_per_gas && BlockPriorityFeeCount.fetch(block.hash) - - transaction_fees = Block.transaction_fees(block.transactions) + block = Block.aggregate_transactions(block) %{ "height" => block.number, "timestamp" => block.timestamp, - "transactions_count" => count_transactions(block), + "transactions_count" => block.transactions_count, # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => count_transactions(block), + "transaction_count" => block.transactions_count, "internal_transactions_count" => count_internal_transactions(block), "miner" => Helper.address_with_info(nil, block.miner, block.miner_hash, false), "size" => block.size, @@ -51,17 +47,17 @@ defmodule BlockScoutWeb.API.V2.BlockView do "gas_limit" => block.gas_limit, "nonce" => block.nonce, "base_fee_per_gas" => block.base_fee_per_gas, - "burnt_fees" => burnt_fees, - "priority_fee" => priority_fee, + "burnt_fees" => block.burnt_fees, + "priority_fee" => block.priority_fees, # "extra_data" => "TODO", "uncles_hashes" => prepare_uncles(block.uncle_relations), # "state_root" => "TODO", "rewards" => prepare_rewards(block.rewards, block, single_block?), "gas_target_percentage" => Block.gas_target(block), "gas_used_percentage" => Block.gas_used_percentage(block), - "burnt_fees_percentage" => burnt_fees_percentage(burnt_fees, transaction_fees), + "burnt_fees_percentage" => burnt_fees_percentage(block.burnt_fees, block.transactions_fees), "type" => block |> BlockView.block_type() |> String.downcase(), - "transaction_fees" => transaction_fees, + "transaction_fees" => block.transactions_fees, "withdrawals_count" => count_withdrawals(block) } |> chain_type_fields(block, single_block?) @@ -97,9 +93,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do def burnt_fees_percentage(_, _), do: nil - defp count_transactions(%Block{transactions: transactions}) when is_list(transactions), do: Enum.count(transactions) - defp count_transactions(_), do: nil - defp count_internal_transactions(%Block{internal_transactions: internal_transactions}) when is_list(internal_transactions), do: Enum.count(internal_transactions) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex index 1285aea438b9..caab5e705117 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex @@ -1,12 +1,6 @@ defmodule BlockScoutWeb.API.V2.EthereumView do alias Explorer.Chain.{Block, Transaction} - defp count_blob_transactions(%Block{transactions: transactions}) when is_list(transactions), - # EIP-2718 blob transaction type - do: Enum.count(transactions, &(&1.type == 3)) - - defp count_blob_transactions(_), do: nil - def extend_transaction_json_response(out_json, %Transaction{} = transaction) do case Map.get(transaction, :beacon_blob_transaction) do nil -> @@ -29,7 +23,7 @@ defmodule BlockScoutWeb.API.V2.EthereumView do blob_gas_used = Map.get(block, :blob_gas_used) excess_blob_gas = Map.get(block, :excess_blob_gas) - blob_transaction_count = count_blob_transactions(block) + blob_transaction_count = block.blob_transactions_count extended_out_json = out_json diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 9514e1ae25c9..161fef847e7c 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -49,7 +49,8 @@ defmodule BlockScoutWeb.Mixfile do end # Specifies which paths to compile per environment. - defp elixirc_paths(:test, _), do: ["test/support", "test/block_scout_web/features/pages"] ++ elixirc_paths() + defp elixirc_paths(:test, _), + do: ["test/support", "test/block_scout_web/features/pages", "benchmarks/support"] ++ elixirc_paths() defp elixirc_paths(_, true), do: [ @@ -85,6 +86,7 @@ defmodule BlockScoutWeb.Mixfile do {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, # Absinthe support for the Relay framework {:absinthe_relay, "~> 1.5"}, + {:benchee, "~> 1.4.0", only: :test}, {:bypass, "~> 2.1", only: :test}, # To add (CORS)(https://www.w3.org/TR/cors/) {:cors_plug, "~> 3.0"}, diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 7d24722e1e1e..c298fe3a0fee 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -584,7 +584,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:277 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/views/address_view.ex:361 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -684,13 +684,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:350 +#: lib/block_scout_web/views/address_view.ex:355 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/address_view.ex:360 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -1614,7 +1614,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:347 +#: lib/block_scout_web/views/address_view.ex:352 #: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" @@ -1774,7 +1774,7 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/address_view.ex:362 #: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" @@ -2285,7 +2285,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/address_view.ex:356 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" @@ -2293,7 +2293,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/views/address_view.ex:357 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -2975,7 +2975,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:349 +#: lib/block_scout_web/views/address_view.ex:354 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/overview_view.ex:39 #: lib/block_scout_web/views/transaction_view.ex:551 @@ -2999,7 +2999,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:346 +#: lib/block_scout_web/views/address_view.ex:351 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" @@ -3172,7 +3172,7 @@ msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:348 +#: lib/block_scout_web/views/address_view.ex:353 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" @@ -3537,14 +3537,14 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/address_view.ex:358 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/views/address_view.ex:359 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -3659,7 +3659,7 @@ msgstr "" msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:34 #, elixir-autogen, elixir-format msgid "false" msgstr "" @@ -3725,7 +3725,7 @@ msgstr "" msgid "to CSV file" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:32 +#: lib/block_scout_web/views/address_contract_view.ex:33 #, elixir-autogen, elixir-format msgid "true" msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 2499baa8b4a6..5dbb1f993aaf 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -584,7 +584,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:277 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/views/address_view.ex:361 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" @@ -684,13 +684,13 @@ msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:350 +#: lib/block_scout_web/views/address_view.ex:355 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/address_view.ex:360 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" @@ -1614,7 +1614,7 @@ msgstr "" #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:347 +#: lib/block_scout_web/views/address_view.ex:352 #: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" @@ -1774,7 +1774,7 @@ msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/address_view.ex:362 #: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" @@ -2285,7 +2285,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/address_view.ex:356 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" @@ -2293,7 +2293,7 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/views/address_view.ex:357 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" @@ -2975,7 +2975,7 @@ msgstr "" #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:349 +#: lib/block_scout_web/views/address_view.ex:354 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/overview_view.ex:39 #: lib/block_scout_web/views/transaction_view.ex:551 @@ -2999,7 +2999,7 @@ msgstr "" #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:346 +#: lib/block_scout_web/views/address_view.ex:351 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" @@ -3172,7 +3172,7 @@ msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:348 +#: lib/block_scout_web/views/address_view.ex:353 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" @@ -3537,14 +3537,14 @@ msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/address_view.ex:358 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/views/address_view.ex:359 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" @@ -3659,7 +3659,7 @@ msgstr "" msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:34 #, elixir-autogen, elixir-format msgid "false" msgstr "" @@ -3725,7 +3725,7 @@ msgstr "" msgid "to CSV file" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:32 +#: lib/block_scout_web/views/address_contract_view.ex:33 #, elixir-autogen, elixir-format msgid "true" msgstr "" diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 2a269a3ddd63..22db19cc5cf5 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -153,6 +153,12 @@ defmodule Explorer.Chain.Block.Schema do field(:refetch_needed, :boolean) field(:base_fee_per_gas, Wei) field(:is_empty, :boolean) + field(:aggregated?, :boolean, virtual: true) + field(:transactions_count, :integer, virtual: true) + field(:blob_transactions_count, :integer, virtual: true) + field(:transactions_fees, :decimal, virtual: true) + field(:burnt_fees, :decimal, virtual: true) + field(:priority_fees, :decimal, virtual: true) timestamps() @@ -197,7 +203,7 @@ defmodule Explorer.Chain.Block do alias Explorer.Chain.{Block, Hash, Transaction, Wei} alias Explorer.Chain.Block.{EmissionReward, Reward} - alias Explorer.Repo + alias Explorer.{Helper, Repo} alias Explorer.Utility.MissingRangesManipulator @optional_attrs ~w(size refetch_needed total_difficulty difficulty base_fee_per_gas)a @@ -364,7 +370,7 @@ defmodule Explorer.Chain.Block do if gas_price do gas_used |> Decimal.new() - |> Decimal.mult(gas_price_to_decimal(gas_price)) + |> Decimal.mult(Helper.number_to_decimal(gas_price)) |> Decimal.add(acc) else acc @@ -382,15 +388,11 @@ defmodule Explorer.Chain.Block do if is_nil(beacon_blob_transaction) do nil else - gas_price_to_decimal(beacon_blob_transaction.blob_gas_price) + Helper.number_to_decimal(beacon_blob_transaction.blob_gas_price) end end) end - defp gas_price_to_decimal(nil), do: nil - defp gas_price_to_decimal(%Wei{} = wei), do: wei.value - defp gas_price_to_decimal(gas_price), do: Decimal.new(gas_price) - @doc """ Calculates burnt fees for the list of transactions (from a single block) """ @@ -405,7 +407,7 @@ defmodule Explorer.Chain.Block do |> Decimal.new() |> Decimal.add(acc) end) - |> Decimal.mult(gas_price_to_decimal(base_fee_per_gas)) + |> Decimal.mult(Helper.number_to_decimal(base_fee_per_gas)) end end @@ -620,4 +622,102 @@ defmodule Explorer.Chain.Block do __MODULE__ |> where([block], block.hash in ^hashes) end + + @doc """ + Calculates and aggregates transaction-related metrics for a block if not already aggregated. + + This function processes all transactions in a block to compute aggregate + statistics including transaction counts, fees, burnt fees, and priority fees. + The aggregation only occurs if the block has not been previously aggregated + (when `aggregated?` is `nil` or `false`) and contains a list of transactions. + + For each transaction, the function calculates: + - Total transaction fees (gas_used * gas_price) + - Burnt fees (gas_used * base_fee_per_gas) + - Priority fees paid to miners (min of priority fee and effective fee) + - Blob transaction detection (type 3 transactions) + + ## Parameters + - `block`: A Block struct containing transactions to be aggregated + + ## Returns + - Block struct with aggregated transaction metrics and `aggregated?` set to `true` + - Original block unchanged if already aggregated or transactions is not a list + """ + @spec aggregate_transactions(t()) :: t() + def aggregate_transactions(%__MODULE__{transactions: transactions, aggregated?: aggregated?} = block) + when is_list(transactions) and aggregated? in [nil, false] do + aggregate_results = + Enum.reduce( + transactions, + %{ + transactions_count: 0, + blob_transactions_count: 0, + transactions_fees: Decimal.new(0), + burnt_fees: Decimal.new(0), + priority_fees: Decimal.new(0) + }, + &transaction_aggregator(&1, &2, block.base_fee_per_gas) + ) + + block + |> Map.merge(aggregate_results) + |> Map.put(:aggregated?, true) + end + + def aggregate_transactions(block), do: block + + defp transaction_aggregator(transaction, acc, block_base_fee_per_gas) do + gas_used = Helper.number_to_decimal(transaction.gas_used) + + transaction_fees = + if is_nil(transaction.gas_price) do + acc.transactions_fees + else + gas_used + |> Decimal.new() + |> Decimal.mult(Helper.number_to_decimal(transaction.gas_price)) + |> Decimal.add(acc.transactions_fees) + end + + burnt_fees = + if is_nil(block_base_fee_per_gas) do + acc.burnt_fees + else + transaction.gas_used + |> Decimal.new() + |> Decimal.mult(Helper.number_to_decimal(block_base_fee_per_gas)) + |> Decimal.add(acc.burnt_fees) + end + + priority_fees = + block_base_fee_per_gas + |> is_nil() + |> if do + acc.priority_fees + else + max_fee = Helper.number_to_decimal(transaction.max_fee_per_gas || transaction.gas_price) + priority_fee = Helper.number_to_decimal(transaction.max_priority_fee_per_gas || transaction.gas_price) + + max_fee + |> Decimal.eq?(Decimal.new(0)) + |> if do + Decimal.new(0) + else + max_fee + |> Decimal.sub(Helper.number_to_decimal(block_base_fee_per_gas)) + |> Decimal.min(priority_fee) + |> Decimal.mult(gas_used) + end + |> Decimal.add(acc.priority_fees) + end + + %{ + transactions_count: acc.transactions_count + 1, + blob_transactions_count: acc.blob_transactions_count + if(transaction.type == 3, do: 1, else: 0), + transactions_fees: transaction_fees, + burnt_fees: burnt_fees, + priority_fees: priority_fees + } + end end diff --git a/apps/explorer/lib/explorer/chain/cache/blocks.ex b/apps/explorer/lib/explorer/chain/cache/blocks.ex index ca968fb56d45..5b992d391624 100644 --- a/apps/explorer/lib/explorer/chain/cache/blocks.ex +++ b/apps/explorer/lib/explorer/chain/cache/blocks.ex @@ -38,4 +38,31 @@ defmodule Explorer.Chain.Cache.Blocks do end def drop_nonconsensus(number) when not is_nil(number), do: drop_nonconsensus([number]) + + @doc """ + Prepares a block for cache update by aggregating transaction metrics and clearing the transactions list. + + This function ensures that all transaction-related statistics are calculated + and stored in the block's virtual fields before removing the transactions + list. This optimization allows the block to retain computed aggregate data + while eliminating the need to store redundant transaction references that + are maintained separately in the database. + + The function first triggers transaction aggregation to compute metrics such + as transaction counts, total fees, burnt fees, and priority fees. After + aggregation is complete, the transactions list is cleared to reduce memory + usage. + + ## Parameters + - `block`: A Block struct that may contain transactions to be aggregated + + ## Returns + - Block struct with aggregated transaction metrics and an empty transactions list + """ + @spec sanitize_before_update(Block.t()) :: Block.t() + def sanitize_before_update(block) do + block = Block.aggregate_transactions(block) + + %{block | transactions: []} + end end diff --git a/apps/explorer/lib/explorer/chain/ordered_cache.ex b/apps/explorer/lib/explorer/chain/ordered_cache.ex index 631bbf8ab73e..2371a7a4e873 100644 --- a/apps/explorer/lib/explorer/chain/ordered_cache.ex +++ b/apps/explorer/lib/explorer/chain/ordered_cache.ex @@ -117,6 +117,14 @@ defmodule Explorer.Chain.OrderedCache do """ @callback atomic_take_enough(integer()) :: [element] | nil + @doc """ + Processes the elements before updating the cache. + This function is called before the `update/1` function and can be used to + modify the elements to be inserted. Can be used to optimize memory usage along + with fetching time. + """ + @callback sanitize_before_update(element) :: element + @doc """ Adds an element, or a list of elements, to the cache. When the cache is full, only the most prevailing elements will be stored, based @@ -169,6 +177,9 @@ defmodule Explorer.Chain.OrderedCache do @impl OrderedCache def element_to_id(element), do: element + @impl OrderedCache + def sanitize_before_update(element), do: element + ### Straightforward fetching functions @impl OrderedCache @@ -250,7 +261,7 @@ defmodule Explorer.Chain.OrderedCache do |> Enum.sort_by(&element_to_id(&1), &prevails?(&1, &2)) |> Enum.take(max_size()) |> do_preloads() - |> Enum.map(&{element_to_id(&1), &1}) + |> Enum.map(&{element_to_id(&1), sanitize_before_update(&1)}) |> merge_and_update(ids || [], max_size()) # ids_list is set to never expire @@ -372,7 +383,8 @@ defmodule Explorer.Chain.OrderedCache do max_size: 0, preloads: 0, prevails?: 2, - element_to_id: 1 + element_to_id: 1, + sanitize_before_update: 1 end end end diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index cdc613766971..9757e33eccdc 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -5,7 +5,7 @@ defmodule Explorer.Helper do alias ABI.TypeDecoder alias Explorer.Chain - alias Explorer.Chain.{Data, Hash} + alias Explorer.Chain.{Data, Hash, Wei} import Ecto.Query, only: [where: 3] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @@ -532,4 +532,28 @@ defmodule Explorer.Helper do Enum.map(params, &Map.merge(&1, %{inserted_at: now, updated_at: now})) end + + @doc """ + Converts various value types to a Decimal type. + + This function handles multiple input types and ensures they are properly + converted to a Decimal representation. + + ## Parameters + - `value`: The value to convert, which can be: + - `nil`: Converted to Decimal 0 + - `%Wei{}`: The Decimal value is extracted from the struct + - `float`: Converted using Decimal.from_float/1 + - `String.t()` or `integer()`: Converted using Decimal.new/1 + - `Decimal.t()`: Returned unchanged + + ## Returns + - A Decimal representation of the input value + """ + @spec number_to_decimal(nil | Wei.t() | integer() | float() | String.t() | Decimal.t()) :: Decimal.t() + def number_to_decimal(nil), do: Decimal.new(0) + def number_to_decimal(%Wei{value: value}), do: value + def number_to_decimal(value) when is_float(value), do: Decimal.from_float(value) + def number_to_decimal(value) when is_binary(value) or is_integer(value), do: Decimal.new(value) + def number_to_decimal(%Decimal{} = value), do: value end diff --git a/apps/explorer/test/explorer/chain/cache/blocks_test.exs b/apps/explorer/test/explorer/chain/cache/blocks_test.exs index 41fe344a1e3d..384a2516238c 100644 --- a/apps/explorer/test/explorer/chain/cache/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/cache/blocks_test.exs @@ -1,6 +1,7 @@ defmodule Explorer.Chain.Cache.BlocksTest do use Explorer.DataCase + alias Explorer.Chain.Block alias Explorer.Chain.Cache.Blocks alias Explorer.Repo @@ -16,7 +17,7 @@ defmodule Explorer.Chain.Cache.BlocksTest do Blocks.update(block) - assert Blocks.all() == [block] + assert Blocks.all() == [block |> Block.aggregate_transactions() |> Map.put(:transactions, [])] end test "adds a new elements removing the oldest one" do From de900d6d9f4f8b080124abe3cff4f3003f60b269 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 2 Jul 2025 17:09:58 +0300 Subject: [PATCH 0139/1006] chore: revise Explorer.Helper.add_0x_prefix usage (#12543) * chore: revise Explorer.Helper.add_0x_prefix usage * Update apps/explorer/lib/explorer/smart_contract/helper.ex --------- Co-authored-by: Victor Baranov --- .../api/v2/advanced_filter_controller.ex | 5 +- .../controllers/api/v2/optimism_controller.ex | 6 +- .../controllers/chain_controller.ex | 14 +- .../lib/block_scout_web/routers/api_router.ex | 12 +- .../views/abi_encoded_value_view.ex | 15 +- .../views/address_contract_view.ex | 3 +- .../views/api/v2/address_badge_view.ex | 4 +- .../views/api/v2/advanced_filter_view.ex | 11 +- .../views/api/v2/optimism_view.ex | 6 +- .../views/api/v2/search_view.ex | 24 +- .../views/api/v2/suave_view.ex | 4 +- .../views/smart_contract_view.ex | 15 -- .../api/v2/address_controller_test.exs | 2 +- .../v2/advanced_filter_controller_test.exs | 48 +++- .../api/v2/optimism_controller_test.exs | 46 ++++ .../api/v2/smart_contract_controller_test.exs | 223 ++++-------------- .../api/v2/transaction_controller_test.exs | 61 ++++- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/account/notifier/email.ex | 15 +- .../explorer/arbitrum/claim_rollup_message.ex | 3 +- .../lib/explorer/chain/bridged_token.ex | 14 +- .../lib/explorer/chain/contract_method.ex | 4 +- .../lib/explorer/chain/method_identifier.ex | 12 +- apps/explorer/lib/explorer/chain/mud.ex | 10 +- .../chain/optimism/interop_message.ex | 7 +- .../chain/smart_contract/proxy/eip_7702.ex | 18 +- .../lib/explorer/chain/transaction.ex | 17 +- .../lib/explorer/smart_contract/helper.ex | 1 + .../lib/explorer/tags/address_to_tag.ex | 3 +- apps/explorer/mix.exs | 2 +- .../explorer/account/notifier/email_test.exs | 28 ++- .../smart_contract/proxy/eip_7702_test.exs | 5 + .../test/explorer/chain/transaction_test.exs | 9 + apps/explorer/test/explorer/helper_test.exs | 26 ++ .../explorer/smart_contract/helper_test.exs | 78 +++++- .../solidity/publisher_test.exs | 4 +- .../explorer/tags/address_to_tag_test.exs | 25 ++ apps/explorer/test/support/factory.ex | 18 +- .../fetcher/on_demand/token_balance.ex | 6 +- .../fetcher/on_demand/token_total_supply.ex | 3 +- .../indexer/fetcher/optimism/dispute_game.ex | 6 +- .../fetcher/optimism/interop/message.ex | 3 +- .../fetcher/optimism/interop/message_queue.ex | 14 +- .../optimism/interop/multichain_export.ex | 10 +- .../fetcher/optimism/transaction_batch.ex | 12 +- .../indexer/fetcher/polygon_zkevm/bridge.ex | 11 +- .../lib/indexer/fetcher/zksync/utils/rpc.ex | 6 +- .../indexer/pending_transactions_sanitizer.ex | 11 +- .../validator_epoch_payment_distributions.ex | 12 +- apps/indexer/mix.exs | 2 +- .../fetcher/on_demand/token_balance_test.exs | 63 +++++ .../fetcher/optimism/dispute_game_test.exs | 198 ++++++++++++++++ .../optimism/interop/message_queue_test.exs | 36 +++ .../optimism/transaction_batch_test.exs | 72 ++++++ .../indexer/fetcher/zksync/utils/rpc_test.exs | 75 ++++++ ...dator_epoch_payment_distributions_test.exs | 69 ++++++ mix.lock | 2 +- 57 files changed, 1056 insertions(+), 355 deletions(-) create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs create mode 100644 apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs create mode 100644 apps/explorer/test/explorer/helper_test.exs create mode 100644 apps/explorer/test/explorer/tags/address_to_tag_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/optimism/dispute_game_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/optimism/interop/message_queue_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/optimism/transaction_batch_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/zksync/utils/rpc_test.exs create mode 100644 apps/indexer/test/indexer/transform/celo/validator_epoch_payment_distributions_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 95b349fc5490..bd10bd29c434 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -9,7 +9,6 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{AdvancedFilter, ContractMethod, Data, Token, Transaction} alias Explorer.Chain.CsvExport.Helper, as: CsvHelper - alias Explorer.Helper, as: ExplorerHelper alias Plug.Conn action_fallback(BlockScoutWeb.API.V2.FallbackController) @@ -139,7 +138,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do case mb_contract_method do %ContractMethod{abi: %{"name" => name}, identifier: identifier} -> - render(conn, :methods, methods: [%{method_id: ExplorerHelper.add_0x_prefix(identifier), name: name}]) + render(conn, :methods, methods: [%{method_id: identifier, name: name}]) _ -> render(conn, :methods, methods: []) @@ -175,7 +174,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do |> Enum.reduce(%{}, fn contract_method, acc -> case contract_method do %ContractMethod{abi: %{"name" => name}, identifier: identifier} when is_binary(name) -> - Map.put(acc, ExplorerHelper.add_0x_prefix(identifier), name) + Map.put(acc, identifier, name) _ -> acc diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index ede497734bde..b11d5dbea2b9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -15,12 +15,12 @@ defmodule BlockScoutWeb.API.V2.OptimismController do delete_parameters_from_next_page_params: 1 ] - import Explorer.Helper, only: [add_0x_prefix: 1, hash_to_binary: 1] + import Explorer.Helper, only: [hash_to_binary: 1] alias BlockScoutWeb.API.V2.ApiView alias Explorer.Chain alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.{Hash, Token, Transaction} + alias Explorer.Chain.{Data, Hash, Token, Transaction} alias Explorer.Chain.Optimism.{ Deposit, @@ -445,7 +445,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do {:ok, public_key} <- ExSecp256k1.create_public_key(private_key) do conn |> put_status(200) - |> render(:optimism_interop_public_key, %{public_key: add_0x_prefix(public_key)}) + |> render(:optimism_interop_public_key, %{public_key: %Data{bytes: public_key}}) else _ -> Logger.error("Interop: cannot derive a public key from the private key. Private key is invalid or undefined.") diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 1986aa013e90..8a47ff18c010 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -6,11 +6,10 @@ defmodule BlockScoutWeb.ChainController do alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.{ChainView, Controller} alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.{Address, Block, Hash, Transaction} alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime, BlocksCount, GasUsageSum, TransactionsCount} alias Explorer.Chain.Search alias Explorer.Chain.Supply.RSK - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Market alias Phoenix.View @@ -99,7 +98,7 @@ defmodule BlockScoutWeb.ChainController do item = if transaction_hash_bytes do item - |> Map.replace(:transaction_hash, ExplorerHelper.add_0x_prefix(transaction_hash_bytes)) + |> Map.replace(:transaction_hash, full_hash_string(transaction_hash_bytes)) else item end @@ -107,7 +106,7 @@ defmodule BlockScoutWeb.ChainController do item = if block_hash_bytes do item - |> Map.replace(:block_hash, ExplorerHelper.add_0x_prefix(block_hash_bytes)) + |> Map.replace(:block_hash, full_hash_string(block_hash_bytes)) else item end @@ -176,4 +175,11 @@ defmodule BlockScoutWeb.ChainController do defp redirect_search_results(conn, _item, search_path) do redirect(conn, to: search_path) end + + defp full_hash_string(%Hash{} = hash), do: to_string(hash) + + defp full_hash_string(bytes) when is_binary(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + to_string(hash) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 711916ddae63..daa5f60b922e 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -123,7 +123,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/smart-contracts/:address_hash_param", V2.ImportController, :try_to_search_contract) - if @chain_type == :optimism do + chain_scope :optimism do post("/optimism/interop/", V2.OptimismController, :interop_import) end end @@ -164,7 +164,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/arbitrum-batch/:batch_number", V2.TransactionController, :arbitrum_batch) end - if @chain_type == :optimism do + chain_scope :optimism do get("/optimism-batch/:batch_number", V2.TransactionController, :optimism_batch) end @@ -217,7 +217,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/:block_hash_or_number/election-rewards/:reward_type", V2.BlockController, :celo_election_rewards) end - if @chain_type == :optimism do + chain_scope :optimism do get("/optimism-batch/:batch_number", V2.BlockController, :optimism_batch) end @@ -260,7 +260,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/transactions/watchlist", V2.MainPageController, :watchlist_transactions) get("/indexing-status", V2.MainPageController, :indexing_status) - if @chain_type == :optimism do + chain_scope :optimism do get("/optimism-deposits", V2.MainPageController, :optimism_deposits) end @@ -291,8 +291,8 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - scope "/optimism" do - if @chain_type == :optimism do + chain_scope :optimism do + scope "/optimism" do get("/txn-batches", V2.OptimismController, :transaction_batches) get("/txn-batches/count", V2.OptimismController, :transaction_batches_count) get("/txn-batches/:l2_block_range_start/:l2_block_range_end", V2.OptimismController, :transaction_batches) diff --git a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex index d4d31fdbf091..55fc039bfe03 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex @@ -10,7 +10,6 @@ defmodule BlockScoutWeb.ABIEncodedValueView do import Phoenix.LiveView.Helpers, only: [sigil_H: 2] alias ABI.FunctionSelector - alias Explorer.Helper, as: ExplorerHelper alias Phoenix.HTML alias Phoenix.HTML.Safe @@ -45,7 +44,7 @@ defmodule BlockScoutWeb.ABIEncodedValueView do end defp do_copy_text({:bytes, _type}, value) do - ExplorerHelper.add_0x_prefix(value) + "0x" <> Base.encode16(value, case: :lower) end defp do_copy_text({:array, type, _}, value) do @@ -66,11 +65,11 @@ defmodule BlockScoutWeb.ABIEncodedValueView do end defp do_copy_text(_, {:dynamic, value}) do - ExplorerHelper.add_0x_prefix(value) + "0x" <> Base.encode16(value, case: :lower) end defp do_copy_text(type, value) when type in [:bytes, :address] do - ExplorerHelper.add_0x_prefix(value) + "0x" <> Base.encode16(value, case: :lower) end defp do_copy_text({:tuple, types}, value) do @@ -161,14 +160,14 @@ defmodule BlockScoutWeb.ABIEncodedValueView do defp base_value_html(_, {:dynamic, value}, _no_links) do assigns = %{value: value} - ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>| + ~H|<%= "0x" <> Base.encode16(@value, case: :lower) %>| end defp base_value_html(:address, value, no_links) do if no_links do base_value_html(:address_text, value, no_links) else - address = ExplorerHelper.add_0x_prefix(value) + address = "0x" <> Base.encode16(value, case: :lower) path = address_path(BlockScoutWeb.Endpoint, :show, address) assigns = %{address: address, path: path} @@ -180,13 +179,13 @@ defmodule BlockScoutWeb.ABIEncodedValueView do defp base_value_html(:address_text, value, _no_links) do assigns = %{value: value} - ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>| + ~H|<%= "0x" <> Base.encode16(@value, case: :lower) %>| end defp base_value_html(:bytes, value, _no_links) do assigns = %{value: value} - ~H|<%= ExplorerHelper.add_0x_prefix(@value) %>| + ~H|<%= "0x" <> Base.encode16(@value, case: :lower) %>| end defp base_value_html(_, value, _no_links), do: HTML.html_escape(value) diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex index bb47c8ad11ab..7d07052bb8e3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex @@ -11,7 +11,6 @@ defmodule BlockScoutWeb.AddressContractView do alias Explorer.Chain.{Address, Data, InternalTransaction, Transaction} alias Explorer.Chain.SmartContract alias Explorer.Chain.SmartContract.Proxy.EIP1167 - alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.Helper, as: SmartContractHelper alias Phoenix.HTML.Safe @@ -65,7 +64,7 @@ defmodule BlockScoutWeb.AddressContractView do val_to_string_if_array(val, type, conn) type =~ "address" -> - address_hash = ExplorerHelper.add_0x_prefix(val) + address_hash = "0x" <> Base.encode16(val, case: :lower) address = Chain.string_to_address_hash_or_nil(address_hash) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex index 4e17eda4e55e..0d98e0e8c275 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeView do use BlockScoutWeb, :view - alias Explorer.Helper, as: ExplorerHelper + alias Explorer.Chain.Address def render("badge_to_address.json", %{badge_to_address_list: badge_to_address_list, status: status}) do prepare_badge_to_address(badge_to_address_list, status) @@ -28,7 +28,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeView do badge_to_address_list |> Enum.map(fn badge_to_address -> %{ - address_hash: ExplorerHelper.add_0x_prefix(badge_to_address.address_hash) + address_hash: Address.checksum(badge_to_address.address_hash) } end) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 4cd0ecd3658a..f8985f4ebd53 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -2,8 +2,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do use BlockScoutWeb, :view alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView} - alias Explorer.Chain.{Address, Data, Transaction} - alias Explorer.Helper, as: ExplorerHelper + alias Explorer.Chain.{Address, Data, MethodIdentifier, Transaction} alias Explorer.Market alias Explorer.Market.MarketHistory @@ -74,8 +73,12 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do |> Stream.map(fn advanced_filter -> method_id = case advanced_filter.input do - %{bytes: <>} -> ExplorerHelper.add_0x_prefix(method_id) - _ -> nil + %{bytes: <>} -> + {:ok, method_id} = MethodIdentifier.cast(method_id) + to_string(method_id) + + _ -> + nil end {opening_price, closing_price} = date_to_prices[DateTime.to_date(advanced_filter.timestamp)] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 75a1e3b73158..3419261cab59 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -263,7 +263,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "target_address_hash" => message.target_address_hash, # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `target_address_hash` property "target" => message.target_address_hash, - "payload" => ExplorerHelper.add_0x_prefix(message.payload) + "payload" => message.payload } # add chain info depending on whether this is incoming or outgoing message @@ -295,7 +295,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "nonce" => message.nonce, "direction" => message.direction, "status" => message.status, - "payload" => ExplorerHelper.add_0x_prefix(message.payload) + "payload" => message.payload } end @@ -331,7 +331,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "target_address_hash" => target_address_hash, "init_transaction_hash" => init_transaction_hash, "timestamp" => if(not is_nil(timestamp), do: DateTime.to_unix(timestamp)), - "payload" => ExplorerHelper.add_0x_prefix(payload) + "payload" => payload } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index 57328447da3b..d0efb14ddc06 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do alias BlockScoutWeb.{BlockView, Endpoint} alias Explorer.Chain alias Explorer.Chain.{Address, Beacon.Blob, Block, Hash, Transaction, UserOperation} - alias Explorer.Helper, as: ExplorerHelper alias Plug.Conn.Query def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do @@ -98,8 +97,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do end def prepare_search_result(%{type: "block"} = search_result) do - block_hash = ExplorerHelper.add_0x_prefix(search_result.block_hash) - {:ok, block} = Chain.hash_to_block(hash(search_result.block_hash), necessity_by_association: %{ @@ -111,8 +108,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do %{ "type" => search_result.type, "block_number" => search_result.block_number, - "block_hash" => block_hash, - "url" => block_path(Endpoint, :show, block_hash), + "block_hash" => block.hash, + "url" => block_path(Endpoint, :show, block.hash), "timestamp" => search_result.timestamp, "block_type" => block |> BlockView.block_type() |> String.downcase(), "priority" => search_result.priority @@ -120,7 +117,7 @@ defmodule BlockScoutWeb.API.V2.SearchView do end def prepare_search_result(%{type: "transaction"} = search_result) do - transaction_hash = ExplorerHelper.add_0x_prefix(search_result.transaction_hash) + transaction_hash = hash_to_string(search_result.transaction_hash) %{ "type" => search_result.type, @@ -132,22 +129,18 @@ defmodule BlockScoutWeb.API.V2.SearchView do end def prepare_search_result(%{type: "user_operation"} = search_result) do - user_operation_hash = ExplorerHelper.add_0x_prefix(search_result.user_operation_hash) - %{ "type" => search_result.type, - "user_operation_hash" => user_operation_hash, + "user_operation_hash" => hash_to_string(search_result.user_operation_hash), "timestamp" => search_result.timestamp, "priority" => search_result.priority } end def prepare_search_result(%{type: "blob"} = search_result) do - blob_hash = ExplorerHelper.add_0x_prefix(search_result.blob_hash) - %{ "type" => search_result.type, - "blob_hash" => blob_hash, + "blob_hash" => hash_to_string(search_result.blob_hash), "timestamp" => search_result.timestamp, "priority" => search_result.priority } @@ -161,6 +154,13 @@ defmodule BlockScoutWeb.API.V2.SearchView do } end + defp hash_to_string(%Hash{} = hash), do: to_string(hash) + + defp hash_to_string(bytes) do + {:ok, hash} = Hash.Full.cast(bytes) + to_string(hash) + end + defp hash(%Hash{} = hash), do: hash defp hash(bytes), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex index a267486d52c5..88b55ea2d440 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do alias Explorer.Helper, as: ExplorerHelper alias Ecto.Association.NotLoaded - alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.{Address, Hash, Transaction} @suave_bid_event "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e" @@ -115,7 +115,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do ExplorerHelper.decode_data(bid_event.data, [{:bytes, 16}, {:uint, 64}, {:array, :address}]) Enum.map(allowed_peekers, fn peeker -> - ExplorerHelper.add_0x_prefix(peeker) + Address.checksum(peeker) end) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index eaef178ab610..2b6624d07d4e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -4,7 +4,6 @@ defmodule BlockScoutWeb.SmartContractView do import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2] alias Explorer.Chain - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Address, Transaction} alias Explorer.Chain.Hash.Address, as: HashAddress alias Explorer.Chain.SmartContract @@ -166,20 +165,6 @@ defmodule BlockScoutWeb.SmartContractView do end) end - def binary_to_utf_string(item) do - case Integer.parse(to_string(item)) do - {item_integer, ""} -> - to_string(item_integer) - - _ -> - if is_binary(item) do - ExplorerHelper.add_0x_prefix(item) - else - to_string(item) - end - end - end - defp render_type_value(type, value, type) do "
(#{Helper.sanitize_input(type)}) : #{value}
" end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 3f02caed040e..37fc5a3aba7f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2528,7 +2528,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "indexed" => false, "name" => "option", "type" => "address", - "value" => "0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7" + "value" => Address.checksum("0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7") }, %{ "indexed" => false, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index 17c301b44dec..c3c817c50450 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -1,8 +1,6 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do use BlockScoutWeb.ConnCase - import Mox - alias Explorer.Chain.SmartContract alias Explorer.Chain.{AdvancedFilter, Data, Hash} alias Explorer.{Factory, TestHelper} @@ -191,6 +189,24 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do smart_contract = build(:smart_contract) + abi = + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint64"}, %{"name" => "y", "type" => "address"}], + "name" => "getAccess", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + + [parsed_method] = ABI.parse_specification([abi]) + + insert(:contract_method, + abi: abi, + identifier: parsed_method.method_id + ) + contract_address = insert(:address, hash: address_hash(), @@ -201,7 +217,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do method_id1_string = "0xa9059cbb" method_id2_string = "0xa0712d68" - method_id3_string = "0x095ea7b3" + method_id3_string = "0x3078f114" method_id4_string = "0x40993b26" {:ok, method1} = Data.cast(method_id1_string <> "ab0ba0") @@ -254,7 +270,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do 5 |> insert_list(:token_transfer, transaction: method3_transaction) 5 |> insert_list(:token_transfer, transaction: method4_transaction) - request = get(conn, "/api/v2/advanced-filters", %{"methods" => "0xa0712d68,0x095ea7b3"}) + request = get(conn, "/api/v2/advanced-filters", %{"methods" => "0xa0712d68,0x3078f114"}) assert response = json_response(request, 200) assert Enum.all?(response["items"], fn item -> @@ -1043,6 +1059,30 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do assert response = json_response(request, 200) assert response == [%{"method_id" => "0x60fe47b1", "name" => "set"}] end + + test "finds method with method id starting with 0x", %{conn: conn} do + abi = + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint64"}, %{"name" => "y", "type" => "address"}], + "name" => "getAccess", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + + [parsed_method] = ABI.parse_specification([abi]) + + insert(:contract_method, + abi: abi, + identifier: parsed_method.method_id + ) + + request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "0x3078f114"}) + assert response = json_response(request, 200) + assert response == [%{"method_id" => "0x3078f114", "name" => "getAccess"}] + end end defp check_paginated_response(all_advanced_filters, first_page, second_page) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs new file mode 100644 index 000000000000..2e2400ca5057 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs @@ -0,0 +1,46 @@ +defmodule BlockScoutWeb.API.V2.OptimismControllerTest do + use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + import Mox + + alias Explorer.Chain.Data + alias Explorer.TestHelper + + setup :set_mox_global + + describe "/optimism/interop/messages" do + if @chain_type == :optimism do + test "handles message with 0x prefixed payload", %{conn: conn} do + insert(:op_interop_message, + payload: %Data{ + bytes: <<48, 120, 120, 73, 33, 116, 36, 121, 34, 115, 113, 39, 119, 112, 117, 118, 105, 106, 108, 93>> + } + ) + + insert(:op_interop_message, payload: "0x30787849217424792273712777707576696a6c5d") + + TestHelper.get_chain_id_mock() + + conn = get(conn, "/api/v2/optimism/interop/messages") + + assert %{ + "items" => [ + %{ + "payload" => "0x30787849217424792273712777707576696a6c5d" + }, + %{ + "payload" => "0x30787849217424792273712777707576696a6c5d" + } + ], + "next_page_params" => nil + } = json_response(conn, 200) + end + else + test "returns 404 in non optimism chain type", %{conn: conn} do + conn = get(conn, "/api/v2/optimism/interop/messages") + assert json_response(conn, 404) == %{"message" => "Endpoint not available for current chain type"} + end + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 363f5fd63203..d486ce0ad3c5 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -681,32 +681,42 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do if Application.compile_env(:explorer, :chain_type) !== :zksync do describe "/smart-contracts/{address_hash} <> eth_bytecode_db" do setup do - old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) + old_fetcher_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) - :ok + old_verifier_env = + Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, []) - on_exit(fn -> - Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) - end) - end + old_chain_id = Application.get_env(:block_scout_web, :chain_id) - test "automatically verify contract", %{conn: conn} do {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) + bypass = Bypass.open() Application.put_env(:block_scout_web, :chain_id, 5) - bypass = Bypass.open() - eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_response.json") + Application.put_env( + :explorer, + Explorer.SmartContract.RustVerifierInterfaceBehaviour, + Keyword.merge( + old_verifier_env, + service_url: "http://localhost:#{bypass.port}", + enabled: true, + type: "eth_bytecode_db", + eth_bytecode_db?: true + ) + ) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_fetcher_env) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_verifier_env) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Bypass.down(bypass) + end) - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + {:ok, bypass: bypass} + end - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) + test "automatically verify contract", %{conn: conn, bypass: bypass} do + eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_response.json") address = insert(:contract_address) @@ -766,33 +776,15 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert %{"is_verified_via_eth_bytecode_db" => true} = response assert %{"is_partially_verified" => true} = response assert %{"is_fully_verified" => false} = response - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (ethBytecodeDbSources) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (ethBytecodeDbSources) endpoint", %{ + conn: conn, + bypass: bypass + } do eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_local_sources_response.json") - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -898,33 +890,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (sourcifySources) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (sourcifySources) endpoint", %{conn: conn, bypass: bypass} do eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_sourcify_sources_response.json") - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -983,35 +954,17 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert %{"is_partially_verified" => true} = response assert %{"is_fully_verified" => false} = response assert response["file_path"] == "Test.sol" - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (sourcifySources with libraries) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (sourcifySources with libraries) endpoint", %{ + conn: conn, + bypass: bypass + } do eth_bytecode_response = File.read!( "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_sourcify_sources_with_libs_response.json" ) - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -1096,33 +1049,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (allianceSources) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (allianceSources) endpoint", %{conn: conn, bypass: bypass} do eth_bytecode_response = File.read!("./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_response.json") - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -1231,35 +1163,17 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (prefer sourcify FULL match) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (prefer sourcify FULL match) endpoint", %{ + conn: conn, + bypass: bypass + } do eth_bytecode_response = File.read!( "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_partial_response.json" ) - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -1368,35 +1282,17 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end - test "automatically verify contract using search-all (take eth bytecode db FULL match) endpoint", %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - - Application.put_env(:block_scout_web, :chain_id, 5) - - bypass = Bypass.open() - + test "automatically verify contract using search-all (take eth bytecode db FULL match) endpoint", %{ + conn: conn, + bypass: bypass + } do eth_bytecode_response = File.read!( "./test/support/fixture/smart_contract/eth_bytecode_db_search_all_alliance_sources_partial_response_eth_bdb_full.json" ) - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - address = insert(:contract_address) insert(:transaction, @@ -1505,21 +1401,11 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response["additional_sources"] |> Enum.sort_by(fn x -> x["file_path"] end) == additional_sources |> Enum.sort_by(fn x -> x["file_path"] end) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end test "check fetch interval for LookUpSmartContractSourcesOnDemand and use sources:search endpoint since chain_id is unset", - %{conn: conn} do - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) - old_chain_id = Application.get_env(:block_scout_web, :chain_id) - + %{conn: conn, bypass: bypass} do Application.put_env(:block_scout_web, :chain_id, nil) - - bypass = Bypass.open() address = insert(:contract_address) topic = "addresses:#{address.hash}" @@ -1535,17 +1421,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do ) |> with_block(status: :ok) - old_env = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: "http://localhost:#{bypass.port}", - enabled: true, - type: "eth_bytecode_db", - eth_bytecode_db?: true - ) - - old_interval_env = Application.get_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand) - Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, fetch_interval: 0) Bypass.expect_once(bypass, "POST", "/api/v2/bytecodes/sources_search", fn conn -> @@ -1640,12 +1515,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do topic: ^topic }, :timer.seconds(1) - - Application.put_env(:block_scout_web, :chain_id, old_chain_id) - Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_interval_env) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) - Bypass.down(bypass) - GenServer.stop(pid) end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 66f506f7d1d2..f964f9817699 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -1,11 +1,12 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox alias Explorer.Account.{Identity, WatchlistAddress} - alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.{Address, Data, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} alias Explorer.{Repo, TestHelper} use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] @@ -319,6 +320,60 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert json_response(request, 200) end + + if @chain_type == :suave do + test "renders peeker starting with 0x", %{conn: conn} do + bid_contract = insert(:address) + + old_env = Application.get_env(:explorer, Transaction, []) + + Application.put_env( + :explorer, + Transaction, + Keyword.merge(old_env, suave_bid_contracts: to_string(bid_contract.hash)) + ) + + on_exit(fn -> + Application.put_env(:explorer, Transaction, old_env) + end) + + transaction = + insert(:transaction, + to_address_hash: bid_contract.hash, + to_address: bid_contract, + execution_node_hash: bid_contract.hash + ) + + insert(:log, + transaction_hash: transaction.hash, + transaction: transaction, + address: bid_contract, + first_topic: "0x83481d5b04dea534715acad673a8177a46fc93882760f36bdc16ccac439d504e", + data: + "0x11111111111111111111111111111111000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000003078505152535455565758595a5b5c5d5e5f6061" + ) + + request = get(conn, "/api/v2/transactions/#{transaction.hash}") + + assert %{"allowed_peekers" => ["0x3078505152535455565758595a5b5C5D5E5f6061"]} = json_response(request, 200) + end + end + + if @chain_type == :optimism do + test "returns transaction with interop message", %{conn: conn} do + transaction = insert(:transaction) + + insert(:op_interop_message, + init_transaction_hash: transaction.hash, + payload: "0x30787849009c24f10a91a327a9f2ed94ebc49ee9" + ) + + request = get(conn, "/api/v2/transactions/#{transaction.hash}") + + assert %{"op_interop" => %{"payload" => "0x30787849009c24f10a91a327a9f2ed94ebc49ee9"}} = + json_response(request, 200) + end + end end describe "/transactions/{transaction_hash}/internal-transactions" do @@ -2019,7 +2074,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do "indexed" => false, "name" => "option", "type" => "address", - "value" => "0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7" + "value" => Address.checksum("0xAeB81cbe6b19CeEB0dBE0d230CFFE35Bb40a13a7") }, %{ "indexed" => false, @@ -2090,6 +2145,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end if @chain_type == :neon do + import Ecto.Query, only: [from: 2] + describe "neon linked transactions service" do test "fetches data from the node and caches in the db", %{conn: conn} do transaction = insert(:transaction) diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index d40e1bd62320..0eff40a6a817 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -68,7 +68,7 @@ defmodule EthereumJSONRPC.MixProject do # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport` and `EthereumJSONRPC.HTTP` so we avoid hitting real chains for local testing - {:mox, "~> 1.0", only: [:test]}, + {:mox, "~> 1.1.0", only: [:test]}, {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, # Tracing {:spandex, "~> 3.0"}, diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex index 7e0c4aa531aa..06379dd3df8a 100644 --- a/apps/explorer/lib/explorer/account/notifier/email.ex +++ b/apps/explorer/lib/explorer/account/notifier/email.ex @@ -6,7 +6,7 @@ defmodule Explorer.Account.Notifier.Email do require Logger alias Explorer.Account.{Identity, Watchlist, WatchlistAddress, WatchlistNotification} - alias Explorer.Helper, as: ExplorerHelper + alias Explorer.Chain.Address alias Explorer.Repo import Bamboo.{Email, SendGridHelper} @@ -28,11 +28,11 @@ defmodule Explorer.Account.Notifier.Email do email |> with_template(template()) |> add_dynamic_field("username", username(notification)) - |> add_dynamic_field("address_hash", address_hash_string(notification)) + |> add_dynamic_field("address_hash", Address.checksum(notification.watchlist_address.address_hash)) |> add_dynamic_field("address_name", notification.watchlist_address.name) - |> add_dynamic_field("transaction_hash", ExplorerHelper.add_0x_prefix(notification.transaction_hash)) - |> add_dynamic_field("from_address_hash", ExplorerHelper.add_0x_prefix(notification.from_address_hash)) - |> add_dynamic_field("to_address_hash", ExplorerHelper.add_0x_prefix(notification.to_address_hash)) + |> add_dynamic_field("transaction_hash", to_string(notification.transaction_hash)) + |> add_dynamic_field("from_address_hash", Address.checksum(notification.from_address_hash)) + |> add_dynamic_field("to_address_hash", Address.checksum(notification.to_address_hash)) |> add_dynamic_field("block_number", notification.block_number) |> add_dynamic_field("amount", amount(notification)) |> add_dynamic_field("name", notification.name) @@ -87,11 +87,6 @@ defmodule Explorer.Account.Notifier.Email do }), do: name - defp address_hash_string(%WatchlistNotification{ - watchlist_address: %WatchlistAddress{address_hash: address_hash} - }), - do: ExplorerHelper.add_0x_prefix(address_hash.bytes) - defp direction(notification) do affect(notification) <> " " <> place(notification) end diff --git a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex index 5dd5c73df74d..f89c0ca99987 100644 --- a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex +++ b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex @@ -27,7 +27,6 @@ defmodule Explorer.Arbitrum.ClaimRollupMessage do alias Explorer.Chain.Arbitrum.Reader.Indexer.Messages, as: MessagesIndexerReader alias Explorer.Chain.{Data, Hash} alias Explorer.Chain.Hash.Address - alias Explorer.Helper, as: ExplorerHelper alias Indexer.Helper, as: IndexerHelper require Logger @@ -508,7 +507,7 @@ defmodule Explorer.Arbitrum.ClaimRollupMessage do # Converts list of binaries into the hex-encoded 0x-prefixed strings defp raw_proof_to_hex(proof) do proof - |> Enum.map(fn p -> ExplorerHelper.add_0x_prefix(p) end) + |> Enum.map(fn p -> %Data{bytes: p} |> to_string() end) end # Retrieves the size parameter (total count of L2->L1 messages) needed for outbox diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex index 8bb0a46f9efc..0cd88e5c7e7f 100644 --- a/apps/explorer/lib/explorer/chain/bridged_token.ex +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -29,8 +29,6 @@ defmodule Explorer.Chain.BridgedToken do Transaction } - alias Explorer.Helper, as: ExplorerHelper - require Logger # TODO: Consider using the `EthereumJSONRPC.ERC20` module to retrieve token metadata @@ -582,10 +580,10 @@ defmodule Explorer.Chain.BridgedToken do |> json_rpc(eth_call_foreign_json_rpc_named_arguments), token0_hash <- parse_contract_response(token0_encoded, :address), token1_hash <- parse_contract_response(token1_encoded, :address), - false <- is_nil(token0_hash), - false <- is_nil(token1_hash), - token0_hash_str <- ExplorerHelper.add_0x_prefix(token0_hash), - token1_hash_str <- ExplorerHelper.add_0x_prefix(token1_hash), + {:ok, token0_hash} <- Hash.Address.cast(token0_hash), + {:ok, token1_hash} <- Hash.Address.cast(token1_hash), + token0_hash_str <- to_string(token0_hash), + token1_hash_str <- to_string(token1_hash), {:ok, "0x" <> token0_name_encoded} <- @name_signature |> Contract.eth_call_request(token0_hash_str, 1, nil, nil) @@ -704,8 +702,8 @@ defmodule Explorer.Chain.BridgedToken do |> json_rpc(eth_call_foreign_json_rpc_named_arguments) do {:ok, "0x" <> token_encoded} -> with token_hash <- parse_contract_response(token_encoded, :address), - false <- is_nil(token_hash), - token_hash_str <- ExplorerHelper.add_0x_prefix(token_hash), + {:ok, token_hash} <- Hash.Address.cast(token_hash), + token_hash_str <- to_string(token_hash), {:ok, "0x" <> token_decimals_encoded} <- @decimals_signature |> Contract.eth_call_request(token_hash_str, 1, nil, nil) diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 021e62d4f7d9..3382e422a323 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -137,8 +137,10 @@ defmodule Explorer.Chain.ContractMethod do # we always take only the first 4 bytes of the hash. <> = selector.method_id + {:ok, method_id} = MethodIdentifier.cast(first_four_bytes) + %{ - identifier: first_four_bytes, + identifier: method_id, abi: element, type: Atom.to_string(selector.type), inserted_at: now, diff --git a/apps/explorer/lib/explorer/chain/method_identifier.ex b/apps/explorer/lib/explorer/chain/method_identifier.ex index e6097b59d941..41ec6e3a167f 100644 --- a/apps/explorer/lib/explorer/chain/method_identifier.ex +++ b/apps/explorer/lib/explorer/chain/method_identifier.ex @@ -7,7 +7,9 @@ defmodule Explorer.Chain.MethodIdentifier do use Ecto.Type - @type t :: binary + alias Explorer.Chain.Data + + @type t :: Data.t() @impl true def type, do: :integer @@ -15,12 +17,16 @@ defmodule Explorer.Chain.MethodIdentifier do @impl true @spec load(integer) :: {:ok, t()} def load(value) do - {:ok, <>} + {:ok, %Data{bytes: <>}} end @impl true @spec cast(binary) :: {:ok, t()} | :error def cast(<<_::binary-size(4)>> = identifier) do + {:ok, %Data{bytes: identifier}} + end + + def cast(%Data{bytes: <<_::binary-size(4)>>} = identifier) do {:ok, identifier} end @@ -28,7 +34,7 @@ defmodule Explorer.Chain.MethodIdentifier do @impl true @spec dump(t()) :: {:ok, integer} | :error - def dump(<>) do + def dump(%Data{bytes: <>}) do {:ok, num} end diff --git a/apps/explorer/lib/explorer/chain/mud.ex b/apps/explorer/lib/explorer/chain/mud.ex index 9b95b81eb90c..77f3cfee69d8 100644 --- a/apps/explorer/lib/explorer/chain/mud.ex +++ b/apps/explorer/lib/explorer/chain/mud.ex @@ -22,14 +22,13 @@ defmodule Explorer.Chain.Mud do Block, Data, Hash, + MethodIdentifier, Mud, Mud.Schema, Mud.Schema.FieldSchema, SmartContract } - alias Explorer.Helper, as: ExplorerHelper - require Logger @store_tables_table_id Base.decode16!("746273746f72650000000000000000005461626c657300000000000000000000", @@ -312,7 +311,10 @@ defmodule Explorer.Chain.Mud do ((system_contract && system_contract.abi) || []) |> ABI.parse_specification() |> Enum.filter(&(&1.type == :function)) - |> Enum.into(%{}, fn selector -> {ExplorerHelper.add_0x_prefix(selector.method_id), selector} end) + |> Enum.into(%{}, fn selector -> + {:ok, method_id} = MethodIdentifier.cast(selector.method_id) + {to_string(method_id), selector} + end) function_selector_signature_records |> Enum.reject(&(&1 == {nil, nil})) @@ -545,7 +547,7 @@ defmodule Explorer.Chain.Mud do int |> Integer.to_string() _ when type < 96 or type == 196 -> - ExplorerHelper.add_0x_prefix(raw) + %Data{bytes: raw} |> to_string() 96 -> raw == <<1>> diff --git a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex index 5986ebfc0968..4f94a1a86f91 100644 --- a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex +++ b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex @@ -6,10 +6,9 @@ defmodule Explorer.Chain.Optimism.InteropMessage do require Logger import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] - import Explorer.Helper, only: [add_0x_prefix: 1] alias ABI.{FunctionSelector, TypeDecoder} - alias Explorer.Chain.Hash + alias Explorer.Chain.{Data, Hash} alias Explorer.{PagingOptions, Repo} alias Indexer.Fetcher.Optimism.Interop.MessageQueue, as: InteropMessageQueue @@ -47,7 +46,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do field(:timestamp, :utc_datetime_usec) field(:relay_chain_id, :integer) field(:relay_transaction_hash, Hash.Full) - field(:payload, :binary) + field(:payload, Data) field(:failed, :boolean) field(:transfer_token_address_hash, Hash.Address) field(:transfer_from_address_hash, Hash.Address) @@ -743,7 +742,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do "target_address_hash" => message.target_address_hash, # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `target_address_hash` property "target" => message.target_address_hash, - "payload" => add_0x_prefix(message.payload) + "payload" => message.payload }, chain_info ) diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex index 7bcbc027c453..11d19996894c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex @@ -6,7 +6,6 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP7702 do alias Explorer.Chain alias Explorer.Chain.{Address, Hash} alias Explorer.Chain.SmartContract.Proxy - alias Explorer.Helper, as: ExplorerHelper @doc """ Get implementation address hash string following EIP-7702. It returns the value as array of the strings. @@ -65,18 +64,25 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP7702 do - `nil` if the delegate address is not present in the bytecode. ## Examples - iex> get_delegate_address(<<239, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>>) - "0x0102030405060708090a0b0c0d0e0f10111213" + iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<239, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>>) + "0x0102030405060708090a0b0c0d0e0f1011121314" - iex> get_delegate_address(<<1, 2, 3>>) + iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<239, 1, 0, 0x30, 0x78, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>>) + "0x3078030405060708090a0b0c0d0e0f1011121314" + + iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<1, 2, 3>>) nil """ @spec get_delegate_address(binary()) :: String.t() | nil def get_delegate_address(contract_code_bytes) do case contract_code_bytes do # 0xef0100 <> address - <<239, 1, 0>> <> <> -> ExplorerHelper.add_0x_prefix(address) - _ -> nil + <<239, 1, 0>> <> <> -> + {:ok, address_hash} = Hash.Address.cast(address) + to_string(address_hash) + + _ -> + nil end end end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index cf0de325a9c4..5ec083f6b4b3 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -320,6 +320,7 @@ defmodule Explorer.Chain.Transaction do Data, DenormalizationHelper, Hash, + MethodIdentifier, SmartContract.Proxy, TokenTransfer, Transaction, @@ -330,8 +331,6 @@ defmodule Explorer.Chain.Transaction do alias Explorer.Chain.SmartContract.Proxy.Models.Implementation - alias Explorer.Helper, as: ExplorerHelper - alias Explorer.SmartContract.SigProviderInterface @optional_attrs ~w(max_priority_fee_per_gas max_fee_per_gas block_hash block_number @@ -1040,7 +1039,8 @@ defmodule Explorer.Chain.Transaction do parse_method_name(decoded_func) {:error, :contract_not_verified, []} -> - ExplorerHelper.add_0x_prefix(method_id) + {:ok, method_id} = MethodIdentifier.cast(method_id) + to_string(method_id) _ -> "Transfer" @@ -2116,8 +2116,12 @@ defmodule Explorer.Chain.Transaction do empty_methods_map = transactions |> Enum.flat_map(fn - %{input: %{bytes: <>}} -> [method_id] - _ -> [] + %{input: %{bytes: <>}} -> + {:ok, method_id} = MethodIdentifier.cast(method_id) + [method_id] + + _ -> + [] end) |> Enum.into(%{}, &{&1, []}) @@ -2225,7 +2229,8 @@ defmodule Explorer.Chain.Transaction do skip_sc_check? ) do if skip_sc_check? || Address.smart_contract?(to_address) do - ExplorerHelper.add_0x_prefix(method_id) + {:ok, method_id} = MethodIdentifier.cast(method_id) + method_id |> to_string() else nil end diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index 1f972a99209f..78470cc7de5f 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -309,6 +309,7 @@ defmodule Explorer.SmartContract.Helper do nil end + # todo: Dangerous, fix with https://github.com/blockscout/blockscout/issues/12544 ExplorerHelper.add_0x_prefix(binary_hash) end end diff --git a/apps/explorer/lib/explorer/tags/address_to_tag.ex b/apps/explorer/lib/explorer/tags/address_to_tag.ex index 4b6d18ecc2c2..23402a970958 100644 --- a/apps/explorer/lib/explorer/tags/address_to_tag.ex +++ b/apps/explorer/lib/explorer/tags/address_to_tag.ex @@ -9,7 +9,6 @@ defmodule Explorer.Tags.AddressToTag do alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Hash} - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Tags.AddressTag # Notation.import_types(BlockScoutWeb.GraphQL.Schema.Types) @@ -71,7 +70,7 @@ defmodule Explorer.Tags.AddressToTag do current_address_hashes_strings = current_address_hashes |> Enum.map(fn address_hash -> - ExplorerHelper.add_0x_prefix(address_hash.bytes) + to_string(address_hash) end) current_address_hashes_strings_tuples = MapSet.new(current_address_hashes_strings) diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 5b689d32461f..02055cedc8c0 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -90,7 +90,7 @@ defmodule Explorer.Mixfile do {:logger_file_backend, "~> 0.0.10"}, {:math, "~> 0.7.0"}, {:mock, "~> 0.3.0", only: [:test], runtime: false}, - {:mox, "~> 1.0"}, + {:mox, "~> 1.1.0"}, {:phoenix_html, "== 3.3.4"}, {:poison, "~> 4.0.1"}, {:nimble_csv, "~> 1.1"}, diff --git a/apps/explorer/test/explorer/account/notifier/email_test.exs b/apps/explorer/test/explorer/account/notifier/email_test.exs index eb0f0413b553..6541474eb8bd 100644 --- a/apps/explorer/test/explorer/account/notifier/email_test.exs +++ b/apps/explorer/test/explorer/account/notifier/email_test.exs @@ -21,6 +21,7 @@ defmodule Explorer.Account.Notifier.EmailTest do host = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] path = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] scheme = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:scheme] + old_chain_id = Application.get_env(:block_scout_web, :chain_id) Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [scheme: "https", host: "eth.blockscout.com", path: "/", port: 443] @@ -33,10 +34,11 @@ defmodule Explorer.Account.Notifier.EmailTest do ] ) - :ok + Application.put_env(:block_scout_web, :chain_id, "30") on_exit(fn -> Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [scheme: scheme, host: host, path: path]) + Application.put_env(:block_scout_web, :chain_id, old_chain_id) end) end @@ -47,7 +49,7 @@ defmodule Explorer.Account.Notifier.EmailTest do {:ok, from_hash} = string_to_address_hash("0x092D537737E767Dae48c28aE509f34094496f030") - {:ok, to_hash} = string_to_address_hash("0xE1F4dd38f00B0D8D4d2b4B5010bE53F2A0b934E5") + {:ok, to_hash} = string_to_address_hash("0x3078dd38f00B0D8D4d2b4B5010bE53F2A0b934E5") identity = %Identity{ uid: "foo|bar", @@ -93,19 +95,31 @@ defmodule Explorer.Account.Notifier.EmailTest do private: %{ send_grid_template: %{ dynamic_template_data: %{ - "address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "address_hash" => + if(Application.get_env(:explorer, :chain_type) == :rsk, + do: "0x3078dd38F00B0d8D4D2B4b5010bE53F2A0b934E5", + else: "0x3078DD38f00B0d8D4D2B4B5010be53F2a0B934e5" + ), "address_name" => "wallet", - "address_url" => "https://eth.blockscout.com/address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "address_url" => "https://eth.blockscout.com/address/0x3078dd38f00b0d8d4d2b4b5010be53f2a0b934e5", "amount" => Decimal.new(1), "block_number" => 24_121_177, "block_url" => "https://eth.blockscout.com/block/24121177", "direction" => "received at", - "from_address_hash" => "0x092d537737e767dae48c28ae509f34094496f030", + "from_address_hash" => + if(Application.get_env(:explorer, :chain_type) == :rsk, + do: "0x092D537737e767dAE48C28aE509F34094496F030", + else: "0x092D537737E767Dae48c28aE509f34094496f030" + ), "from_url" => "https://eth.blockscout.com/address/0x092d537737e767dae48c28ae509f34094496f030", "method" => "transfer", "name" => "wallet", - "to_address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", - "to_url" => "https://eth.blockscout.com/address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5", + "to_address_hash" => + if(Application.get_env(:explorer, :chain_type) == :rsk, + do: "0x3078dd38F00B0d8D4D2B4b5010bE53F2A0b934E5", + else: "0x3078DD38f00B0d8D4D2B4B5010be53F2a0B934e5" + ), + "to_url" => "https://eth.blockscout.com/address/0x3078dd38f00b0d8d4d2b4b5010be53f2a0b934e5", "transaction_hash" => "0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d", "transaction_url" => "https://eth.blockscout.com/tx/0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d", diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs new file mode 100644 index 000000000000..d8d45d3f93d4 --- /dev/null +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs @@ -0,0 +1,5 @@ +defmodule Explorer.Chain.SmartContract.Proxy.EIP7702Test do + use Explorer.DataCase + + doctest Explorer.Chain.SmartContract.Proxy.EIP7702 +end diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index 35b4d37ddb13..f08e2ca061b4 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -867,4 +867,13 @@ defmodule Explorer.Chain.TransactionTest do ) end end + + describe "get_method_name/1" do + test "returns method name for transaction with input data starting with 0x" do + transaction = + :transaction |> insert(input: "0x3078f1140ab0ba") + + assert "0x3078f114" == Transaction.get_method_name(transaction) + end + end end diff --git a/apps/explorer/test/explorer/helper_test.exs b/apps/explorer/test/explorer/helper_test.exs new file mode 100644 index 000000000000..9791a52d6e9d --- /dev/null +++ b/apps/explorer/test/explorer/helper_test.exs @@ -0,0 +1,26 @@ +defmodule Explorer.HelperTest do + use ExUnit.Case + alias Explorer.Helper + alias Explorer.Chain.Data + + describe "decode_data/2" do + test "decodes 0x starting bytes" do + data = "0x3078f11400000000000000000000000000000000000000000000000000000000" + types = [{:uint, 32}] + + assert [21_924_702_239_838_702_630_355_123_058_721_243_447_914_074_382_437_861_546_344_133_536_048_966_795_264] == + Helper.decode_data(data, types) + end + + test "decodes 0x starting bytes with %Data{} struct" do + data = %Data{ + bytes: <<48, 120, 241, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + } + + types = [{:uint, 32}] + + assert [21_924_702_239_838_702_630_355_123_058_721_243_447_914_074_382_437_861_546_344_133_536_048_966_795_264] == + Helper.decode_data(data, types) + end + end +end diff --git a/apps/explorer/test/explorer/smart_contract/helper_test.exs b/apps/explorer/test/explorer/smart_contract/helper_test.exs index 98f67b99f537..9b105cf263e3 100644 --- a/apps/explorer/test/explorer/smart_contract/helper_test.exs +++ b/apps/explorer/test/explorer/smart_contract/helper_test.exs @@ -1,7 +1,10 @@ defmodule Explorer.SmartContract.HelperTest do use ExUnit.Case, async: true - use Explorer.DataCase + + import Mox + setup :verify_on_exit! + alias Explorer.SmartContract.Helper describe "payable?" do @@ -150,4 +153,77 @@ defmodule Explorer.SmartContract.HelperTest do refute Helper.read_with_wallet_method?(function) end end + + describe "get_binary_string_from_contract_getter/4" do + # TODO: https://github.com/blockscout/blockscout/issues/12544 + # test "returns bytes starting from 0x" do + # abi = [ + # %{ + # "type" => "function", + # "stateMutability" => "view", + # "outputs" => [%{"type" => "bytes16", "name" => "data", "internalType" => "bytes16"}], + # "name" => "getData", + # "inputs" => [] + # } + # ] + + # expect( + # EthereumJSONRPC.Mox, + # :json_rpc, + # fn [ + # %{ + # id: id, + # method: "eth_call", + # params: [%{data: "0x3bc5de30", to: "0x0000000000000000000000000000000000000001"}, _] + # } + # ], + # _options -> + # {:ok, + # [%{id: id, jsonrpc: "2.0", result: "0x3078313233343536373839404142434400000000000000000000000000000000"}]} + # end + # ) + + # assert "0x30783132333435363738394041424344" == + # Helper.get_binary_string_from_contract_getter( + # "3bc5de30", + # "0x0000000000000000000000000000000000000001", + # abi + # ) + # end + + test "returns address" do + abi = [ + %{ + "type" => "function", + "stateMutability" => "view", + "outputs" => [%{"type" => "address", "name" => "data", "internalType" => "address"}], + "name" => "getAddress", + "inputs" => [] + } + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id, + method: "eth_call", + params: [%{data: "0x38cc4831", to: "0x0000000000000000000000000000000000000001"}, _] + } + ], + _options -> + {:ok, + [%{id: id, jsonrpc: "2.0", result: "0x0000000000000000000000003078000000000000000000000000000000000001"}]} + end + ) + + assert "0x3078000000000000000000000000000000000001" == + Helper.get_binary_string_from_contract_getter( + "38cc4831", + "0x0000000000000000000000000000000000000001", + abi + ) + end + end end diff --git a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs index 2fcdec3d453d..32e792861888 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs @@ -8,7 +8,7 @@ if Application.compile_env(:explorer, :chain_type) !== :zksync do @moduletag timeout: :infinity - alias Explorer.Chain.{ContractMethod, SmartContract} + alias Explorer.Chain.{Data, ContractMethod, SmartContract} alias Explorer.{Factory, Repo} alias Explorer.SmartContract.Solidity.Publisher @@ -107,7 +107,7 @@ if Application.compile_env(:explorer, :chain_type) !== :zksync do Enum.each(contract_code_info.abi, fn selector -> [parsed] = ABI.parse_specification([selector]) - assert Repo.get_by(ContractMethod, abi: selector, identifier: parsed.method_id) + assert Repo.get_by(ContractMethod, abi: selector, identifier: %Data{bytes: parsed.method_id}) end) end diff --git a/apps/explorer/test/explorer/tags/address_to_tag_test.exs b/apps/explorer/test/explorer/tags/address_to_tag_test.exs new file mode 100644 index 000000000000..c77230b52605 --- /dev/null +++ b/apps/explorer/test/explorer/tags/address_to_tag_test.exs @@ -0,0 +1,25 @@ +defmodule Explorer.Tags.AddressToTagTest do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Tags.AddressToTag + + describe "set_tag_to_addresses/2" do + test "does not remove existing address" do + address = insert(:address, hash: "0x3078000000000000000000000000000000000001") + address_hash = address.hash + tag = insert(:address_tag) + tag_id = tag.id + att = insert(:address_to_tag, tag_id: tag.id, tag: tag, address_hash: address.hash, address: address) + att_inserted_at = att.inserted_at + + :timer.sleep(100) + + AddressToTag.set_tag_to_addresses(tag.id, [to_string(address_hash)]) + + # timestamp should be the same, no need to delete and reinsert the same address + assert [%AddressToTag{tag_id: ^tag_id, address_hash: ^address_hash, inserted_at: ^att_inserted_at}] = + Repo.all(AddressToTag) + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 398c9de1e6be..a98ddf245977 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -50,7 +50,7 @@ defmodule Explorer.Factory do Withdrawal } - alias Explorer.Chain.Optimism.OutputRoot + alias Explorer.Chain.Optimism.{InteropMessage, OutputRoot} alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Chain.Zilliqa.Hash.BLSPublicKey alias Explorer.Chain.Zilliqa.Staker, as: ZilliqaStaker @@ -1276,6 +1276,22 @@ defmodule Explorer.Factory do } end + def op_interop_message_factory do + %InteropMessage{ + sender_address_hash: insert(:address).hash, + target_address_hash: insert(:address).hash, + nonce: sequence("op_interop_message_nonce", & &1), + init_chain_id: 1, + init_transaction_hash: insert(:transaction).hash, + block_number: insert(:block).number, + timestamp: DateTime.utc_now(), + relay_chain_id: 2, + relay_transaction_hash: transaction_hash(), + payload: "0x123123", + failed: random_bool() + } + end + def op_output_root_factory do %OutputRoot{ l2_output_index: op_output_root_l2_output_index(), diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex index 3defaa43d75b..bded260c244a 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex @@ -12,7 +12,6 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Hash - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Token.BalanceReader alias Explorer.Utility.RateLimiter alias Indexer.BufferedTask @@ -127,9 +126,8 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do defp prepare_ctb_params(current_token_balances, initial_acc, block_number) do Enum.reduce(current_token_balances, initial_acc, fn %{token_id: token_id} = stale_current_token_balance, acc -> prepared_ctb = %{ - token_contract_address_hash: - ExplorerHelper.add_0x_prefix(stale_current_token_balance.token.contract_address_hash), - address_hash: ExplorerHelper.add_0x_prefix(stale_current_token_balance.address_hash), + token_contract_address_hash: to_string(stale_current_token_balance.token.contract_address_hash), + address_hash: to_string(stale_current_token_balance.address_hash), block_number: block_number, token_id: token_id && Decimal.to_integer(token_id), token_type: stale_current_token_balance.token_type diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex index 7fbdbba50a3b..0b9f14c76dc1 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex @@ -9,7 +9,6 @@ defmodule Indexer.Fetcher.OnDemand.TokenTotalSupply do alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Events.Publisher alias Explorer.Chain.{Hash, Token} - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Repo alias Explorer.Token.MetadataRetriever alias Explorer.Utility.RateLimiter @@ -51,7 +50,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenTotalSupply do if (token && !token.skip_metadata && is_nil(token.total_supply_updated_at_block)) or BlockNumber.get_max() - token.total_supply_updated_at_block > @ttl_in_blocks do - token_address_hash_string = ExplorerHelper.add_0x_prefix(address_hash) + token_address_hash_string = to_string(address_hash) token_params = MetadataRetriever.get_total_supply_of(token_address_hash_string) diff --git a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex index 580e5fac296d..3295dcffa89f 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/dispute_game.ex @@ -16,9 +16,9 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do alias EthereumJSONRPC.Contract alias Explorer.Application.Constants alias Explorer.{Chain, Helper, Repo} + alias Explorer.Chain.Data alias Explorer.Chain.Hash.Address alias Explorer.Chain.Optimism.{DisputeGame, Withdrawal} - alias Explorer.Helper, as: ExplorerHelper alias Indexer.Fetcher.Optimism alias Indexer.Helper, as: IndexerHelper @@ -345,7 +345,7 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do ] }) - calldata = ExplorerHelper.add_0x_prefix(encoded_call) + calldata = %Data{bytes: encoded_call} Contract.eth_call_request(calldata, dispute_game_factory, index, nil, nil) end) @@ -367,7 +367,7 @@ defmodule Indexer.Fetcher.Optimism.DisputeGame do [extra_data] = Helper.decode_data(extra_data_by_index[game.index], [:bytes]) game - |> Map.put(:extra_data, ExplorerHelper.add_0x_prefix(extra_data)) + |> Map.put(:extra_data, %Data{bytes: extra_data}) |> Map.put(:resolved_at, sanitize_resolved_at(resolved_at_by_index[game.index])) |> Map.put(:status, quantity_to_integer(status_by_index[game.index])) end) diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex index 779d81e5ba8b..aca7c8552dc0 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex @@ -29,6 +29,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do alias Explorer.Chain alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral + alias Explorer.Chain.Data alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.Optimism.InteropMessage alias Indexer.Fetcher.Optimism @@ -332,7 +333,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do block_number: block_number, timestamp: Map.get(timestamps, block_number), relay_chain_id: quantity_to_integer(Enum.at(event["topics"], 1)), - payload: payload, + payload: %Data{bytes: payload}, transfer_token_address_hash: transfer_token_address_hash, transfer_from_address_hash: transfer_from_address_hash, transfer_to_address_hash: transfer_to_address_hash, diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex index b9d1ea95bbe5..71b058c1dc0a 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message_queue.ex @@ -27,12 +27,12 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do require Logger - import Explorer.Helper, only: [add_0x_prefix: 1, hash_to_binary: 1] + import Explorer.Helper, only: [hash_to_binary: 1] import Indexer.Fetcher.Optimism.Interop.Helper, only: [log_cant_get_chain_id_from_rpc: 0] alias Explorer.Chain alias Explorer.Chain.Cache.Counters.LastFetchedCounter - alias Explorer.Chain.Hash + alias Explorer.Chain.{Data, Hash} alias Explorer.Chain.Optimism.InteropMessage alias Indexer.Fetcher.Optimism alias Indexer.Helper @@ -270,7 +270,6 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do @spec prepare_post_data(map(), binary()) :: {non_neg_integer(), map()} defp prepare_post_data(message, private_key) when is_nil(message.relay_transaction_hash) do timestamp = DateTime.to_unix(message.timestamp) - payload = add_0x_prefix(message.payload) data = %{ sender_address_hash: Hash.to_string(message.sender_address_hash), @@ -280,7 +279,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do init_transaction_hash: Hash.to_string(message.init_transaction_hash), timestamp: timestamp, relay_chain_id: message.relay_chain_id, - payload: payload, + payload: message.payload, signature: nil } @@ -290,7 +289,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do Integer.to_string(message.nonce) <> Integer.to_string(message.init_chain_id) <> data.init_transaction_hash <> - Integer.to_string(timestamp) <> Integer.to_string(message.relay_chain_id) <> payload + Integer.to_string(timestamp) <> Integer.to_string(message.relay_chain_id) <> to_string(message.payload) {:ok, {signature, _}} = data_to_sign @@ -334,9 +333,10 @@ defmodule Indexer.Fetcher.Optimism.Interop.MessageQueue do # - `{chain_id, post_data_signed}` tuple where # `chain_id` is the chain id from the input parameter. # `post_data_signed` is the `data` map with the `signature` field. + @doc false @spec set_post_data_signature(non_neg_integer(), map(), binary()) :: {non_neg_integer(), map()} - defp set_post_data_signature(chain_id, data, signature) do - {chain_id, %{data | signature: add_0x_prefix(signature)}} + def set_post_data_signature(chain_id, data, signature) do + {chain_id, %{data | signature: %Data{bytes: signature} |> to_string()}} end # Prepares a map to import to the `op_interop_messages` table based on the current handling message and diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex index ad2432e02fd8..2d7dc28fc685 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex @@ -18,7 +18,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MultichainExport do require Logger import Ecto.Query - import Explorer.Helper, only: [add_0x_prefix: 1, valid_url?: 1] + import Explorer.Helper, only: [valid_url?: 1] import Indexer.Fetcher.Optimism.Interop.Helper, only: [log_cant_get_chain_id_from_rpc: 0] alias Ecto.Multi @@ -196,10 +196,10 @@ defmodule Indexer.Fetcher.Optimism.Interop.MultichainExport do init_transaction_hash: Hash.to_string(message.init_transaction_hash), timestamp: Integer.to_string(DateTime.to_unix(message.timestamp)), relay_chain_id: Integer.to_string(message.relay_chain_id), - payload: add_0x_prefix(message.payload), - transfer_token_address_hash: add_0x_prefix(message.transfer_token_address_hash), - transfer_from_address_hash: add_0x_prefix(message.transfer_from_address_hash), - transfer_to_address_hash: add_0x_prefix(message.transfer_to_address_hash), + payload: message.payload, + transfer_token_address_hash: message.transfer_token_address_hash, + transfer_from_address_hash: message.transfer_from_address_hash, + transfer_to_address_hash: message.transfer_to_address_hash, transfer_amount: if(not is_nil(message.transfer_amount), do: Decimal.to_string(message.transfer_amount, :normal)) } diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index 171625cae67a..756e42a81ac2 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -26,13 +26,13 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do import EthereumJSONRPC, only: [fetch_blocks_by_range: 2, json_rpc: 2, quantity_to_integer: 1] - import Explorer.Helper, only: [add_0x_prefix: 1, hash_to_binary: 1, parse_integer: 1] + import Explorer.Helper, only: [hash_to_binary: 1, parse_integer: 1] alias Ecto.Multi alias EthereumJSONRPC.Block.ByHash alias EthereumJSONRPC.{Blocks, Contract} alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, RollupReorgMonitorQueue} + alias Explorer.Chain.{Block, Hash, RollupReorgMonitorQueue} alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Optimism.{FrameSequence, FrameSequenceBlob} alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch @@ -372,11 +372,12 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do {:noreply, state} end - defp get_block_numbers_by_hashes([], _json_rpc_named_arguments_l2) do + @doc false + def get_block_numbers_by_hashes([], _json_rpc_named_arguments_l2) do %{} end - defp get_block_numbers_by_hashes(hashes, json_rpc_named_arguments_l2) do + def get_block_numbers_by_hashes(hashes, json_rpc_named_arguments_l2) do query = from( b in Block, @@ -396,7 +397,8 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do |> Enum.filter(fn hash -> is_nil(Map.get(number_by_hash, hash)) end) |> Enum.with_index() |> Enum.map(fn {hash, id} -> - ByHash.request(%{hash: add_0x_prefix(hash), id: id}, false) + {:ok, hash} = Hash.Full.cast(hash) + ByHash.request(%{hash: hash, id: id}, false) end) chunk_size = 50 diff --git a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex index eb6257616d75..a61f041bf840 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_zkevm/bridge.ex @@ -13,10 +13,11 @@ defmodule Indexer.Fetcher.PolygonZkevm.Bridge do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - import Explorer.Helper, only: [add_0x_prefix: 1, decode_data: 2] + import Explorer.Helper, only: [decode_data: 2] alias EthereumJSONRPC.Logs alias Explorer.Chain + alias Explorer.Chain.Hash alias Explorer.Chain.PolygonZkevm.Reader alias Indexer.Helper, as: IndexerHelper alias Indexer.Transform.Addresses @@ -229,7 +230,7 @@ defmodule Indexer.Fetcher.PolygonZkevm.Bridge do [ leaf_type, origin_network, - origin_address, + origin_address_bytes, destination_network, _destination_address, amount, @@ -237,7 +238,9 @@ defmodule Indexer.Fetcher.PolygonZkevm.Bridge do deposit_count ] = decode_data(event.data, @bridge_event_params) - {token_address_by_origin_address(origin_address, origin_network, leaf_type, rollup_network_id_l2), amount, + {:ok, origin_address_hash} = Hash.Address.cast(origin_address_bytes) + + {token_address_by_origin_address(origin_address_hash, origin_network, leaf_type, rollup_network_id_l2), amount, deposit_count, destination_network} end @@ -378,7 +381,7 @@ defmodule Indexer.Fetcher.PolygonZkevm.Bridge do defp token_address_by_origin_address(origin_address, origin_network, leaf_type, rollup_network_id_l2) do with true <- leaf_type != 1, - token_address = add_0x_prefix(origin_address), + token_address = to_string(origin_address), true <- token_address != burn_address_hash_string() do if origin_network != rollup_network_id_l2 do # this is L1 address diff --git a/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex index f199a2248004..55afa4fb3d54 100644 --- a/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex +++ b/apps/indexer/lib/indexer/fetcher/zksync/utils/rpc.ex @@ -7,7 +7,7 @@ defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do alias ABI.{FunctionSelector, TypeDecoder} alias EthereumJSONRPC.ZkSync.Constants.Contracts, as: ZkSyncContracts - alias Explorer.Helper, as: ExplorerHelper + alias Explorer.Chain.Hash alias Indexer.Helper, as: IndexerHelper import Indexer.Fetcher.ZkSync.Utils.Logging, only: [log_error: 1, log_info: 1] @@ -208,7 +208,7 @@ defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do @spec fetch_transaction_by_hash(binary(), EthereumJSONRPC.json_rpc_named_arguments()) :: map() def fetch_transaction_by_hash(raw_hash, json_rpc_named_arguments) when is_binary(raw_hash) and is_list(json_rpc_named_arguments) do - hash = ExplorerHelper.add_0x_prefix(raw_hash) + {:ok, hash} = Hash.Full.cast(raw_hash) req = EthereumJSONRPC.request(%{ @@ -239,7 +239,7 @@ defmodule Indexer.Fetcher.ZkSync.Utils.Rpc do @spec fetch_transaction_receipt_by_hash(binary(), EthereumJSONRPC.json_rpc_named_arguments()) :: map() def fetch_transaction_receipt_by_hash(raw_hash, json_rpc_named_arguments) when is_binary(raw_hash) and is_list(json_rpc_named_arguments) do - hash = ExplorerHelper.add_0x_prefix(raw_hash) + {:ok, hash} = Hash.Full.cast(raw_hash) req = EthereumJSONRPC.request(%{ diff --git a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex index 71706b32a023..0f3ef9545915 100644 --- a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex +++ b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex @@ -14,7 +14,6 @@ defmodule Indexer.PendingTransactionsSanitizer do alias Ecto.Changeset alias Explorer.{Chain, Repo} alias Explorer.Chain.{Block, Transaction} - alias Explorer.Helper, as: ExplorerHelper defstruct interval: nil, json_rpc_named_arguments: [] @@ -122,20 +121,18 @@ defmodule Indexer.PendingTransactionsSanitizer do end defp fetch_pending_transaction_and_delete(transaction) do - pending_transaction_hash_string = ExplorerHelper.add_0x_prefix(transaction.hash) - with %{block_hash: nil} <- Repo.reload(transaction), changeset = Changeset.change(transaction), {:ok, _transaction} <- Repo.delete(changeset, timeout: :infinity) do Logger.debug( - "Transaction with hash #{pending_transaction_hash_string} successfully deleted from Blockscout DB because it doesn't exist in the archive node anymore", + "Transaction with hash #{transaction.hash} successfully deleted from Blockscout DB because it doesn't exist in the archive node anymore", fetcher: :pending_transactions_to_refetch ) else {:error, changeset} -> Logger.debug( [ - "Deletion of pending transaction with hash #{pending_transaction_hash_string} from Blockscout DB failed", + "Deletion of pending transaction with hash #{transaction.hash} from Blockscout DB failed", inspect(changeset) ], fetcher: :pending_transactions_to_refetch @@ -143,7 +140,7 @@ defmodule Indexer.PendingTransactionsSanitizer do _transaction -> Logger.debug( - "Transaction with hash #{pending_transaction_hash_string} is already included in block, cancel deletion", + "Transaction with hash #{transaction.hash} is already included in block, cancel deletion", fetcher: :pending_transactions_to_refetch ) end @@ -187,7 +184,7 @@ defmodule Indexer.PendingTransactionsSanitizer do Repo.update(changeset) Logger.debug( - "Pending transaction with hash #{ExplorerHelper.add_0x_prefix(pending_transaction.hash)} assigned to block ##{block.number} with hash #{block.hash}" + "Pending transaction with hash #{pending_transaction.hash} assigned to block ##{block.number} with hash #{block.hash}" ) end end diff --git a/apps/indexer/lib/indexer/transform/celo/validator_epoch_payment_distributions.ex b/apps/indexer/lib/indexer/transform/celo/validator_epoch_payment_distributions.ex index 2e83684ffd1b..9a265b4e1e7b 100644 --- a/apps/indexer/lib/indexer/transform/celo/validator_epoch_payment_distributions.ex +++ b/apps/indexer/lib/indexer/transform/celo/validator_epoch_payment_distributions.ex @@ -7,7 +7,6 @@ defmodule Indexer.Transform.Celo.ValidatorEpochPaymentDistributions do alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.Chain.{Hash, Log} - alias Explorer.Helper, as: ExplorerHelper require Logger @@ -56,16 +55,19 @@ defmodule Indexer.Transform.Celo.ValidatorEpochPaymentDistributions do |> Enum.map(fn log -> {:ok, %FunctionSelector{}, [ - {"validator", "address", true, validator_address}, + {"validator", "address", true, validator_address_bytes}, {"validatorPayment", "uint256", false, validator_payment}, - {"group", "address", true, group_address}, + {"group", "address", true, group_address_bytes}, {"groupPayment", "uint256", false, group_payment} ]} = Log.find_and_decode(@event_abi, log, log.block_hash) + {:ok, validator_address} = Hash.Address.cast(validator_address_bytes) + {:ok, group_address} = Hash.Address.cast(group_address_bytes) + %{ - validator_address: ExplorerHelper.add_0x_prefix(validator_address), + validator_address: validator_address, validator_payment: validator_payment, - group_address: ExplorerHelper.add_0x_prefix(group_address), + group_address: group_address, group_payment: group_payment } end) diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 7fd34a1f73b5..76e6638f64ca 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -63,7 +63,7 @@ defmodule Indexer.MixProject do # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing - {:mox, "~> 1.0"}, + {:mox, "~> 1.1.0"}, {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, # Tracing {:spandex, "~> 3.0"}, diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs index ac853b38fdee..d1ee6c7c85ee 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs @@ -145,4 +145,67 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do ) end end + + describe "run/2" do + setup do + now = Timex.now() + + Enum.each(0..101, fn i -> + insert(:block, number: i, timestamp: Timex.shift(now, hours: -(102 - i) * 50)) + end) + + insert(:block, number: 102, timestamp: now) + AverageBlockTime.refresh() + + :ok + end + + test "fetches token balance for an address" do + address = insert(:address, hash: "0x3078000000000000000000000000000000000001") + token_contract_address = insert(:address, hash: "0x3078000000000000000000000000000000000002") + + token = + insert(:token, + contract_address_hash: token_contract_address.hash, + contract_address: token_contract_address + ) + + insert(:address_current_token_balance, + address_hash: address.hash, + address: address, + token_contract_address_hash: token_contract_address.hash, + token: token, + token_type: "ERC-20", + value_fetched_at: nil, + value: nil + ) + + insert_list(2, :block) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" + } + ]} + end + ) + + assert TokenBalanceOnDemand.run( + [{:fetch, address.hash}], + nil + ) == :ok + + token_balance_updated = Repo.get_by(CurrentTokenBalance, address_hash: address.hash) + + assert token_balance_updated.value == Decimal.new(1_000_000_000_000_000_000_000_000) + assert token_balance_updated.value_fetched_at != nil + end + end end diff --git a/apps/indexer/test/indexer/fetcher/optimism/dispute_game_test.exs b/apps/indexer/test/indexer/fetcher/optimism/dispute_game_test.exs new file mode 100644 index 000000000000..d2b6581227cd --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/optimism/dispute_game_test.exs @@ -0,0 +1,198 @@ +if Application.get_env(:explorer, :chain_type) == :optimism do + defmodule Indexer.Fetcher.Optimism.DisputeGameTest do + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.Data + alias Indexer.Fetcher.Optimism.DisputeGame + + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end + + describe "handle_info/2" do + defp mox_handle_continue_calls(%{ + id: id, + method: "eth_call", + params: [ + %{data: data, to: _}, + _ + ] + }) + when data in ~w(0x3c9f397c 0x19effeb4 0x200d2ed2), + do: %{id: id, jsonrpc: "2.0", result: "0x0"} + + defp mox_handle_continue_calls(%{ + id: id, + method: "eth_call", + params: [ + %{ + data: %Data{bytes: <<187, 138, 161, 252, index::integer-256>>}, + to: "0x3078000000000000000000000000000000000001" + }, + _ + ] + }) do + %{ + id: id, + jsonrpc: "2.0", + result: + "0x" <> + (ABI.encode("(uint32,uint64,address)", [ + {1, index + 1_740_000_000, 0x3078A00000000000000000000000000000000000 + index} + ]) + |> Base.encode16(case: :lower)) + } + end + + defp mox_handle_continue_calls(%{ + id: id, + method: "eth_call", + params: [ + %{data: "0x609d3334", to: "0x3078a0000000000000000000000000000000000" <> index}, + _ + ] + }) do + {index, ""} = Integer.parse(index) + + %{ + id: id, + jsonrpc: "2.0", + result: + "0x" <> + (ABI.TypeEncoder.encode([4, <<46 + index, 120, 32, 32 + index>>], [:bytes]) + |> Base.encode16(case: :lower)) + } + end + + defp mox_handle_continue_calls(%{ + id: id, + method: "eth_call", + params: [ + %{ + data: "0x609d3334", + to: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<48, 120, 160, 0::128, index>> + } + }, + _ + ] + }) do + %{ + id: id, + jsonrpc: "2.0", + result: + "0x" <> + (ABI.TypeEncoder.encode([<<46 + index, 120, 32, 32 + index>>], [:bytes]) |> Base.encode16(case: :lower)) + } + end + + defp mox_handle_continue_calls(%{ + id: id, + method: "eth_call", + params: [ + %{data: "0x4d1975b4", to: _}, + _ + ] + }) do + %{id: id, jsonrpc: "2.0", result: "0x5"} + end + + test "handles :continue", %{json_rpc_named_arguments: json_rpc_named_arguments} do + old_env = Application.get_env(:indexer, DisputeGame, []) + + Application.put_env( + :indexer, + DisputeGame, + Keyword.merge(old_env, json_rpc_named_arguments: json_rpc_named_arguments) + ) + + on_exit(fn -> + Application.put_env(:indexer, DisputeGame, old_env) + end) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + 8, + fn + requests, _options when is_list(requests) -> + {:ok, Enum.map(requests, &mox_handle_continue_calls/1)} + + request, _options -> + {:ok, mox_handle_continue_calls(request).result} + end + ) + + assert {:noreply, + %{ + optimism_portal: "0x3078000000000000000000000000000000000002", + dispute_game_factory: "0x3078000000000000000000000000000000000001", + end_index: 4, + start_index: 4 + }} = + DisputeGame.handle_info(:continue, %{ + dispute_game_factory: "0x3078000000000000000000000000000000000001", + optimism_portal: "0x3078000000000000000000000000000000000002", + start_index: 0, + end_index: 3, + json_rpc_named_arguments: json_rpc_named_arguments + }) + + assert [ + %Explorer.Chain.Optimism.DisputeGame{ + index: 0, + game_type: 1, + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<48, 120, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> + }, + extra_data: %Explorer.Chain.Data{bytes: ".x "}, + resolved_at: nil, + status: 0 + }, + %Explorer.Chain.Optimism.DisputeGame{ + index: 1, + game_type: 1, + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<48, 120, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1>> + }, + extra_data: %Explorer.Chain.Data{bytes: "/x !"}, + resolved_at: nil, + status: 0 + }, + %Explorer.Chain.Optimism.DisputeGame{ + index: 2, + game_type: 1, + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<48, 120, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2>> + }, + extra_data: %Explorer.Chain.Data{bytes: "0x \""}, + resolved_at: nil, + status: 0 + }, + %Explorer.Chain.Optimism.DisputeGame{ + index: 3, + game_type: 1, + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<48, 120, 160, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3>> + }, + extra_data: %Explorer.Chain.Data{bytes: "1x #"}, + resolved_at: nil, + status: 0 + } + ] = Repo.all(Explorer.Chain.Optimism.DisputeGame) + end + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/optimism/interop/message_queue_test.exs b/apps/indexer/test/indexer/fetcher/optimism/interop/message_queue_test.exs new file mode 100644 index 000000000000..d835b41d3fee --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/optimism/interop/message_queue_test.exs @@ -0,0 +1,36 @@ +defmodule Indexer.Fetcher.Optimism.Interop.MessageQueueTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + alias Indexer.Fetcher.Optimism.Interop.MessageQueue + + describe "set_post_data_signature/3" do + test "handles signature starting with 0x properly" do + message_nonce = 1 + init_chain_id = 2 + relay_chain_id = 3 + relay_transaction_hash = "0xf463ce43ac52251f60f437a0346b7168874e6eb13a9689d7efde4ce907826897" + message_failed = false + + data_to_sign = + Integer.to_string(message_nonce) <> + Integer.to_string(init_chain_id) <> + Integer.to_string(relay_chain_id) <> relay_transaction_hash <> to_string(message_failed) + + private_key = + <<51, 169, 186, 160, 251, 109, 12, 35, 225, 13, 110, 62, 216, 253, 27, 181, 187, 222, 222, 75, 79, 84, 185, 24, + 245, 213, 28, 21, 76, 179, 162, 16>> + + {:ok, {signature, _}} = + data_to_sign + |> ExKeccak.hash_256() + |> ExSecp256k1.sign_compact(private_key) + + assert {^init_chain_id, + %{ + signature: + "0x3078a19168c4ab6d5aebcb8556d4c6bdf2df53e8b4562f6974ccfaea60639ae13437ee19c4bd7328a5d8451e1c11075d1581e0abf4e470c97d7a89b20e4c68d9" + }} = MessageQueue.set_post_data_signature(init_chain_id, %{signature: nil}, signature) + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/optimism/transaction_batch_test.exs b/apps/indexer/test/indexer/fetcher/optimism/transaction_batch_test.exs new file mode 100644 index 000000000000..fd13ad80dc5b --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/optimism/transaction_batch_test.exs @@ -0,0 +1,72 @@ +if Application.get_env(:explorer, :chain_type) == :optimism do + defmodule Indexer.Fetcher.Optimism.TransactionBatchTest do + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import Mox + + alias Indexer.Fetcher.Optimism.TransactionBatch + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end + + describe "get_block_numbers_by_hashes/2" do + test "processes empty list" do + assert TransactionBatch.get_block_numbers_by_hashes([], %{}) == %{} + end + + test "processes list of hashes", %{json_rpc_named_arguments: json_rpc_named_arguments} do + hashA = <<1::256>> + + hashB = + <<48, 120, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32>> + + hashes = [ + hashA, + hashB + ] + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: id1, + method: "eth_getBlockByHash", + params: [ + %Explorer.Chain.Hash{byte_count: 32, bytes: hashA}, + false + ] + }, + %{ + id: id2, + method: "eth_getBlockByHash", + params: [ + %Explorer.Chain.Hash{byte_count: 32, bytes: hashB}, + false + ] + } + ], + _options -> + {:ok, + [ + %{ + id: id1, + jsonrpc: "2.0", + result: %{"number" => 1, "hash" => "0x0000000000000000000000000000000000000000000000000000000000000001"} + }, + %{ + id: id2, + jsonrpc: "2.0", + result: %{"number" => 2, "hash" => "0x3078202020202020202020202020202020202020202020202020202020202020"} + } + ]} + end) + + assert %{hashA => 1, hashB => 2} == + TransactionBatch.get_block_numbers_by_hashes(hashes, json_rpc_named_arguments) + end + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/zksync/utils/rpc_test.exs b/apps/indexer/test/indexer/fetcher/zksync/utils/rpc_test.exs new file mode 100644 index 000000000000..76bea9c80258 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/zksync/utils/rpc_test.exs @@ -0,0 +1,75 @@ +defmodule Indexer.Fetcher.ZkSync.Utils.RpcTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.Hash + alias Indexer.Fetcher.ZkSync.Utils.Rpc, as: ZksyncRpc + + setup :set_mox_global + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end + + describe "fetch_transaction_by_hash/2" do + test "returns transaction data for a valid transaction hash", %{json_rpc_named_arguments: json_rpc_named_arguments} do + transaction_hash = "0x3078313131313131313131313131313131313131313131313131313131313131" + raw_transaction_hash = "3078313131313131313131313131313131313131313131313131313131313131" |> Base.decode16!() + + EthereumJSONRPC.Mox + |> expect( + :json_rpc, + fn + %{id: _id, method: "eth_getTransactionByHash", params: [^transaction_hash]}, _options -> + {:ok, + %{ + "hash" => transaction_hash + }} + + %{id: _id, method: "eth_getTransactionByHash", params: [%Hash{bytes: ^raw_transaction_hash}]}, _options -> + {:ok, + %{ + "hash" => transaction_hash + }} + end + ) + + assert %{"hash" => ^transaction_hash} = + ZksyncRpc.fetch_transaction_by_hash(raw_transaction_hash, json_rpc_named_arguments) + end + end + + describe "fetch_transaction_receipt_by_hash/2" do + test "returns transaction receipt data for a valid transaction hash", + %{json_rpc_named_arguments: json_rpc_named_arguments} do + transaction_hash = "0x3078313131313131313131313131313131313131313131313131313131313131" + raw_transaction_hash = "3078313131313131313131313131313131313131313131313131313131313131" |> Base.decode16!() + + EthereumJSONRPC.Mox + |> expect( + :json_rpc, + fn + %{id: _id, method: "eth_getTransactionReceipt", params: [^transaction_hash]}, _options -> + {:ok, + %{ + "transactionHash" => transaction_hash + }} + + %{id: _id, method: "eth_getTransactionReceipt", params: [%Hash{bytes: ^raw_transaction_hash}]}, _options -> + {:ok, + %{ + "transactionHash" => transaction_hash + }} + end + ) + + assert %{"transactionHash" => ^transaction_hash} = + ZksyncRpc.fetch_transaction_receipt_by_hash(raw_transaction_hash, json_rpc_named_arguments) + end + end +end diff --git a/apps/indexer/test/indexer/transform/celo/validator_epoch_payment_distributions_test.exs b/apps/indexer/test/indexer/transform/celo/validator_epoch_payment_distributions_test.exs new file mode 100644 index 000000000000..edd4db17981b --- /dev/null +++ b/apps/indexer/test/indexer/transform/celo/validator_epoch_payment_distributions_test.exs @@ -0,0 +1,69 @@ +defmodule Indexer.Transform.Celo.ValidatorEpochPaymentDistributionsTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + alias Explorer.Chain.Hash + alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions + + describe "parse/1" do + setup do + old_env = Application.get_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, []) + + validator = insert(:address) + + Application.put_env( + :explorer, + Explorer.Chain.Cache.CeloCoreContracts, + Keyword.merge(old_env, + contracts: %{ + "addresses" => %{ + "Validators" => [ + %{"address" => to_string(validator.hash), "updated_at_block_number" => 0} + ] + } + } + ) + ) + + on_exit(fn -> Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, old_env) end) + + %{validator: validator} + end + + test "parses log", %{validator: validator} do + transaction = + :transaction + |> insert() + |> with_block() + + log = + insert(:log, + transaction_hash: transaction.hash, + transaction: transaction, + address_hash: validator.hash, + address: validator, + first_topic: ValidatorEpochPaymentDistributions.signature(), + second_topic: "0x0000000000000000000000003078323232323232323232323232323232323232", + third_topic: "0x0000000000000000000000003078333333333333333333333333333333333333", + data: "0x" <> Base.encode16(ABI.encode("(uint256,uint256)", [{123, 456}]), case: :lower) + ) + + logs = [log] + + assert [ + %{ + group_address: %Hash{ + byte_count: 20, + bytes: "3078333333333333333333333333333333333333" |> Base.decode16!(case: :lower) + }, + group_payment: 456, + validator_address: %Hash{ + byte_count: 20, + bytes: "3078323232323232323232323232323232323232" |> Base.decode16!(case: :lower) + }, + validator_payment: 123 + } + ] == ValidatorEpochPaymentDistributions.parse(logs) + end + end +end diff --git a/mix.lock b/mix.lock index 4f294e92c128..6ac4754cd33e 100644 --- a/mix.lock +++ b/mix.lock @@ -111,7 +111,7 @@ "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mnemoniac": {:hex, :mnemoniac, "0.1.3", "f2aed9ddc36c82938170d38f8944f79725f95acc1e7c08e223bad38aac9953cf", [:mix], [], "hexpm", "dc2f2da766134c55282b48358eda66133e27815c3eb5ad7ecaa1e72281a0658f"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, - "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, + "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, "nimble_csv": {:hex, :nimble_csv, "1.3.0", "b7f998dc62b222bce9596e46f028c7a5af04cb5dde6df2ea197c583227c54971", [:mix], [], "hexpm", "41ccdc18f7c8f8bb06e84164fc51635321e80d5a3b450761c4997d620925d619"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, From f17b5fb6d7b97b3fc55e1819a0306a43672ca971 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 3 Jul 2025 14:30:25 +0300 Subject: [PATCH 0140/1006] chore: Duplicate block countdown endpoint in API v2 (#12704) * chore: Duplicate block countdown endpoint in API v2 * Add spec & doc * Change namings * Return block numbers as int --- .../lib/block_scout_web/chain.ex | 1 + .../controllers/api/v2/block_controller.ex | 41 +++++++++++++++++++ .../controllers/api/v2/fallback_controller.ex | 21 ++++++++++ .../lib/block_scout_web/routers/api_router.ex | 1 + .../views/api/v2/block_view.ex | 14 +++++++ 5 files changed, 78 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index d02e57a4293d..e021a6cef30d 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -579,6 +579,7 @@ defmodule BlockScoutWeb.Chain do %PagingOptions{options | page_number: new_page_number, page_size: new_page_size} end + @spec param_to_block_number(binary()) :: {:ok, integer()} | {:error, :invalid} def param_to_block_number(formatted_number) when is_binary(formatted_number) do case Integer.parse(formatted_number) do {number, ""} -> {:ok, number} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index aec5124daf63..d26ed059132b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -7,6 +7,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params: 3, next_page_params: 4, paging_options: 1, + param_to_block_number: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1, parse_block_hash_or_number_param: 1 @@ -31,9 +32,11 @@ defmodule BlockScoutWeb.API.V2.BlockController do alias Explorer.Chain alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader + alias Explorer.Chain.Cache.{BlockNumber, Counters.AverageBlockTime} alias Explorer.Chain.InternalTransaction alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Scroll.Reader, as: ScrollReader + alias Timex.Duration case @chain_type do :ethereum -> @@ -369,6 +372,44 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end + @doc """ + Function to handle GET requests to `/api/v2/blocks/:block_number/countdown` endpoint. + Calculates the estimated time remaining until a specified block number is reached + based on the current block number and average block time. + + ## Parameters + - `conn`: The connection struct + - `params`: Map containing the target block number + + ## Returns + - Renders countdown data with current block, target block, remaining blocks, and estimated time + - Returns appropriate error responses via fallback controller for various failure cases + """ + @spec block_countdown(Plug.Conn.t(), map()) :: + Plug.Conn.t() + | {:format, {:error, :invalid}} + | {:max_block, nil} + | {:average_block_time, {:error, :disabled}} + | {:remaining_blocks, 0} + def block_countdown(conn, %{"block_number" => block_number}) do + with {:format, {:ok, target_block_number}} <- {:format, param_to_block_number(block_number)}, + {:max_block, current_block_number} when not is_nil(current_block_number) <- + {:max_block, BlockNumber.get_max()}, + {:average_block_time, average_block_time} when is_struct(average_block_time) <- + {:average_block_time, AverageBlockTime.average_block_time()}, + {:remaining_blocks, remaining_blocks} when remaining_blocks > 0 <- + {:remaining_blocks, target_block_number - current_block_number} do + estimated_time_in_sec = Float.round(remaining_blocks * Duration.to_seconds(average_block_time), 1) + + render(conn, :block_countdown, + current_block: current_block_number, + countdown_block: target_block_number, + remaining_blocks: remaining_blocks, + estimated_time_in_sec: estimated_time_in_sec + ) + end + end + defp block_param_to_block(block_hash_or_number, options \\ @api_true) do with {:ok, type, value} <- parse_block_hash_or_number_param(block_hash_or_number) do fetch_block(type, value, options) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 3369e577d356..37b44cb23a04 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -318,6 +318,27 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> render(:message, %{message: @not_a_smart_contract}) end + def call(conn, {:average_block_time, {:error, :disabled}}) do + conn + |> put_status(501) + |> put_view(ApiView) + |> render(:message, %{message: "Average block time calculating is disabled, so getblockcountdown is not available"}) + end + + def call(conn, {stage, _}) when stage in ~w(max_block average_block_time)a do + conn + |> put_status(200) + |> put_view(ApiView) + |> render(:message, %{message: "Chain is indexing now, try again later"}) + end + + def call(conn, {:remaining_blocks, _}) do + conn + |> put_status(200) + |> put_view(ApiView) + |> render(:message, %{message: "Error! Block number already pass"}) + end + def call(conn, {code, response}) when is_integer(code) do conn |> put_status(code) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index daa5f60b922e..a12b3cdf0745 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -207,6 +207,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/:block_hash_or_number/transactions", V2.BlockController, :transactions) get("/:block_hash_or_number/internal-transactions", V2.BlockController, :internal_transactions) get("/:block_hash_or_number/withdrawals", V2.BlockController, :withdrawals) + get("/:block_number/countdown", V2.BlockController, :block_countdown) if @chain_type == :arbitrum do get("/arbitrum-batch/:batch_number", V2.BlockController, :arbitrum_batch) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 7ec4e2e43060..87db1c7074d1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -27,6 +27,20 @@ defmodule BlockScoutWeb.API.V2.BlockView do prepare_block(block, nil, false) end + def render("block_countdown.json", %{ + current_block: current_block, + countdown_block: countdown_block, + remaining_blocks: remaining_blocks, + estimated_time_in_sec: estimated_time_in_sec + }) do + %{ + current_block_number: current_block, + countdown_block_number: countdown_block, + remaining_blocks_count: remaining_blocks, + estimated_time_in_seconds: to_string(estimated_time_in_sec) + } + end + def prepare_block(block, _conn, single_block? \\ false) do block = Block.aggregate_transactions(block) From f914f338466a4994f728088e4d5976b5f681c35c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 3 Jul 2025 14:46:08 +0300 Subject: [PATCH 0141/1006] fix: Multichain search export: retry only on failed chunks (#12459) * fix: Multichain search export: retry only on failed chunks * Process review comments * Process review comment: remove successful chunks from the retry queue --- .../multichain_search.ex | 44 ++++-- .../multichain_search_test.exs | 67 +++++++-- .../multichain_search_db_export/retry.ex | 134 ++++++++++++++---- .../retry_test.exs | 124 ++++++++++++++-- 4 files changed, 312 insertions(+), 57 deletions(-) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index ee60a19b2566..bb49c02afcc4 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -15,7 +15,6 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @max_concurrency 5 @post_timeout :timer.minutes(5) - @request_error_msg "Error while sending request to Multichain Search DB Service" @unspecified "UNSPECIFIED" @doc """ @@ -35,11 +34,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @spec batch_import( %{ addresses: [Address.t()], - blocks: [Block.t()], - transactions: [Transaction.t()] + blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], + transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] }, boolean() - ) :: {:error, :disabled | String.t() | Jason.DecodeError.t()} | {:ok, any()} + ) :: {:error, map()} | {:ok, any()} def batch_import(params, retry? \\ false) do if enabled?() do params_chunks = extract_batch_import_params_into_chunks(params) @@ -56,11 +55,29 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do {:ok, {:ok, _result}}, acc -> acc - {:ok, {:error, error}}, _acc -> + {:ok, {:error, error}}, acc -> on_error(error, retry?) - {:error, @request_error_msg} - {:exit, {export_body, reason}}, _acc -> + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, + %{ + addresses: error.data_to_retry.addresses, + block_ranges: error.data_to_retry.block_ranges, + hashes: error.data_to_retry.hashes + }} + + {:error, data_to_retry} -> + merged_data_to_retry = %{ + addresses: error.data_to_retry.addresses ++ data_to_retry.addresses, + block_ranges: error.data_to_retry.block_ranges ++ data_to_retry.block_ranges, + hashes: error.data_to_retry.hashes ++ data_to_retry.hashes + } + + {:error, merged_data_to_retry} + end + + {:exit, {export_body, reason}}, acc -> on_error( %{ url: url, @@ -70,7 +87,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do retry? ) - {:error, @request_error_msg} + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, export_body} + + {:error, acc} -> + {:error, Map.merge(acc, export_body)} + end end) else {:ok, :service_disabled} @@ -222,8 +246,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do """ @spec extract_batch_import_params_into_chunks(%{ addresses: [Address.t()], - blocks: [Block.t()], - transactions: [Transaction.t()] + blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], + transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] }) :: [ %{ api_key: String.t(), diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index ede50d4719f9..8a5906779787 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -76,7 +76,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 end - test "returns {:error, reason} when an error occurs during processing and 'multichain_search_db_export_retry_queue' table is populated" do + test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_export_retry_queue' table is populated" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -115,7 +115,22 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do transactions: [transaction_1, transaction_2] } - assert {:error, @error_msg} = MultichainSearch.batch_import(params) + assert {:error, + %{ + addresses: [ + address_export_data(address_1), + address_export_data(address_2) + ], + block_ranges: [ + %{max_block_number: to_string(block_2.number), min_block_number: to_string(block_1.number)} + ], + hashes: [ + block_export_data(block_1), + block_export_data(block_2), + transaction_export_data(transaction_1), + transaction_export_data(transaction_2) + ] + }} == MultichainSearch.batch_import(params) assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 6 records = Repo.all(MultichainSearchDbExportRetryQueue) @@ -130,7 +145,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do end) end - test "returns {:error, reason} when at least one chunk is failed" do + test "returns {:error, data_to_retry} when at least one chunk is failed" do Application.put_env(:explorer, MultichainSearch, service_url: "http://localhost:1234", api_key: "12345", @@ -179,12 +194,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do transactions: [transaction_1, transaction_2] } - assert {:error, @error_msg} = MultichainSearch.batch_import(params) + assert {:error, results} = MultichainSearch.batch_import(params) + assert Enum.count(results.addresses) == 7000 + assert Enum.count(results.block_ranges) == 1 + assert Enum.count(results.hashes) == 4 # 7000 addresses + 2 blocks + 2 transactions assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 7004 end - test "returns {:error, reason} when an error occurs in all chunks during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -218,12 +236,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do transactions: [] } - assert {:error, @error_msg} = MultichainSearch.batch_import(params) + assert {:error, results} = MultichainSearch.batch_import(params) + assert Enum.count(results.addresses) == 10 + assert Enum.count(results.block_ranges) == 0 + assert Enum.count(results.hashes) == 0 assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 10 end - test "returns {:error, reason} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -257,7 +278,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do transactions: [] } - assert {:error, @error_msg} = MultichainSearch.batch_import(params) + assert {:error, results} = MultichainSearch.batch_import(params) + assert Enum.count(results.addresses) == 15 + assert Enum.count(results.block_ranges) == 0 + assert Enum.count(results.hashes) == 0 assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 15 end @@ -556,4 +580,31 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert chunk[:hashes] == [] end end + + defp transaction_export_data(transaction) do + %{ + hash: "0x" <> Base.encode16(transaction.hash.bytes, case: :lower), + hash_type: "TRANSACTION" + } + end + + defp block_export_data(block) do + %{ + hash: "0x" <> Base.encode16(block.hash.bytes, case: :lower), + hash_type: "BLOCK" + } + end + + defp address_export_data(address) do + %{ + hash: "0x" <> Base.encode16(address.hash.bytes, case: :lower), + token_type: "UNSPECIFIED", + is_contract: false, + token_name: nil, + contract_name: nil, + ens_name: nil, + is_token: false, + is_verified_contract: false + } + end end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex index 133029733add..25ba444e5f57 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex @@ -9,7 +9,7 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do use Spandex.Decorators alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, Hash, MultichainSearchDbExportRetryQueue} + alias Explorer.Chain.{Address, Block, Hash, MultichainSearchDbExportRetryQueue, Transaction} alias Explorer.MicroserviceInterfaces.MultichainSearch alias Indexer.BufferedTask @@ -38,14 +38,112 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do initial_acc, fn data, acc -> IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) - end + end, + true ) acc end @impl BufferedTask - def run(export_data, _json_rpc_named_arguments) when is_list(export_data) do + def run(data, _json_rpc_named_arguments) when is_list(data) do + prepared_export_data = prepare_export_data(data) + + case MultichainSearch.batch_import(prepared_export_data) do + {:ok, result} -> + unless result == :service_disabled do + hashes = all_hashes(prepared_export_data) + + hashes + |> MultichainSearchDbExportRetryQueue.by_hashes_query() + |> Repo.delete_all() + end + + :ok + + {:error, data_to_retry} -> + Logger.error(fn -> + ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] + end) + + hashes = all_hashes(prepared_export_data) + failed_hashes = failed_hashes(data_to_retry) + + successful_hashes = hashes -- failed_hashes + + successful_hash_binaries = + successful_hashes + |> Enum.map(fn hash -> + "0x" <> hex = hash + Base.decode16!(hex) + end) + + successful_hash_binaries + |> MultichainSearchDbExportRetryQueue.by_hashes_query() + |> Repo.delete_all() + + {:retry, [data_to_retry]} + end + end + + defp all_hashes(prepared_export_data) do + transaction_hashes = + prepared_export_data[:transactions] |> Enum.map(&Map.get(&1, :hash)) + + block_hashes = prepared_export_data[:blocks] |> Enum.map(&to_string(Map.get(&1, :hash))) + + address_hashes = prepared_export_data[:addresses] |> Enum.map(&to_string(Map.get(&1, :hash))) + + transaction_hashes ++ block_hashes ++ address_hashes + end + + defp failed_hashes(data_to_retry) do + block_transaction_hashes = + data_to_retry.hashes + |> Enum.map(&Map.get(&1, :hash)) + + address_hashes = + data_to_retry.addresses + |> Enum.map(&Map.get(&1, :hash)) + + block_transaction_hashes ++ address_hashes + end + + @doc """ + Prepares export data by categorizing input hashes into addresses, blocks, and transactions, + then enriches the result with resolved address and block data. + + ## Parameters + + - `export_data`: A list of maps, each containing a `:hash` (binary) and a `:hash_type` (`:address`, `:block`, or `:transaction`). + + ## Returns + + - A map containing: + - `:addresses` - a list of resolved address structs from the given address hashes. + - `:blocks` - a list of block structs fetched from the database using the block hashes or list of hash and hash_type maps. + - `:transactions` - a list Transaction.t() objects or list of hash and hash_type maps. + + ## Example + + iex> prepare_export_data([ + ...> %{hash: <<1,2,3>>, hash_type: :address}, + ...> %{hash: <<4,5,6>>, hash_type: :block}, + ...> %{hash: <<7,8,9>>, hash_type: :transaction} + ...> ]) + %{ + addresses: [...], + blocks: [...], + transactions: [...] + } + + """ + @spec prepare_export_data([%{hash: binary, hash_type: atom}]) :: %{ + addresses: [Address.t()], + blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], + transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] + } + def prepare_export_data(export_data) do pre_prepared_export_data = export_data |> Enum.reduce(%{address_hashes: [], block_hashes: [], transactions: []}, fn %{ @@ -89,31 +187,11 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do |> Block.by_hashes_query() |> Repo.all() - prepared_export_data = - pre_prepared_export_data - |> Map.put(:addresses, addresses) - |> Map.put(:blocks, blocks) - |> Map.drop([:address_hashes]) - |> Map.drop([:block_hashes]) - - case MultichainSearch.batch_import(prepared_export_data, true) do - {:ok, result} -> - unless result == :service_disabled do - export_data - |> Enum.map(&Map.get(&1, :hash)) - |> MultichainSearchDbExportRetryQueue.by_hashes_query() - |> Repo.delete_all() - end - - :ok - - {:error, _} -> - Logger.error(fn -> - ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] - end) - - {:retry, export_data} - end + pre_prepared_export_data + |> Map.put(:addresses, addresses) + |> Map.put(:blocks, blocks) + |> Map.drop([:address_hashes]) + |> Map.drop([:block_hashes]) end defp defaults do diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs index 0f3688ecb1ad..5fa56e09d16a 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs @@ -3,7 +3,9 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do use Explorer.DataCase import ExUnit.CaptureLog, only: [capture_log: 1] + import Mox + alias Explorer.Chain.MultichainSearchDbExportRetryQueue alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.TestHelper alias Indexer.Fetcher.MultichainSearchDbExport.Retry, as: MultichainSearchDbExportRetry @@ -112,33 +114,133 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do assert :ok = MultichainSearchDbExportRetry.run(export_data, nil) end - test "returns {:retry, export_data} on error", %{bypass: bypass} do - address = insert(:address) + test "returns {:retry, failed_data} on error where failed_data is only chunks that failed to export", %{ + bypass: _bypass + } do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + addresses_chunk_size: 1 + ) + + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Application.put_env(:explorer, :http_adapter, HTTPoison) + end) + + address_1 = insert(:address) + address_2 = insert(:address) + address_2_hash_string = to_string(address_2) |> String.downcase() + address_3 = insert(:address) + address_3_hash_string = to_string(address_3) |> String.downcase() block = insert(:block) + block_hash_string = to_string(block.hash) + block_number = to_string(block.number) transaction = insert(:transaction) |> with_block(block) + transaction_hash_string = to_string(transaction.hash) export_data = [ - %{hash: address.hash.bytes, hash_type: :address}, + %{hash: address_1.hash.bytes, hash_type: :address}, + %{hash: address_2.hash.bytes, hash_type: :address}, + %{hash: address_3.hash.bytes, hash_type: :address}, %{hash: block.hash.bytes, hash_type: :block}, %{hash: transaction.hash.bytes, hash_type: :transaction} ] TestHelper.get_chain_id_mock() - Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> - Conn.resp( - conn, - 500, - Jason.encode!(%{"status" => "ok"}) - ) - end) + for _ <- 0..2 do + Explorer.Mox.HTTPoison + |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", + body, + [{"Content-Type", "application/json"}], + _options -> + case Jason.decode(body) do + {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> + {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + {:ok, %{"addresses" => [%{"hash" => ^address_2_hash_string}]}} -> + {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end) + end log = capture_log(fn -> - assert {:retry, ^export_data} = MultichainSearchDbExportRetry.run(export_data, nil) + assert {:retry, + [ + %{ + addresses: [ + %{ + hash: ^address_2_hash_string, + token_type: "UNSPECIFIED", + is_contract: false, + token_name: nil, + contract_name: nil, + ens_name: nil, + is_token: false, + is_verified_contract: false + }, + %{ + hash: ^address_3_hash_string, + token_type: "UNSPECIFIED", + is_contract: false, + token_name: nil, + contract_name: nil, + ens_name: nil, + is_token: false, + is_verified_contract: false + } + ], + block_ranges: [%{max_block_number: ^block_number, min_block_number: ^block_number}], + hashes: [ + %{hash: ^block_hash_string, hash_type: "BLOCK"}, + %{ + hash: ^transaction_hash_string, + hash_type: "TRANSACTION" + } + ] + } + ]} = MultichainSearchDbExportRetry.run(export_data, nil) end) + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 4 + assert log =~ "Batch export retry to the Multichain Search DB failed" + + export_data_2 = [ + %{hash: address_2.hash.bytes, hash_type: :address}, + %{hash: address_3.hash.bytes, hash_type: :address}, + %{hash: block.hash.bytes, hash_type: :block}, + %{hash: transaction.hash.bytes, hash_type: :transaction} + ] + + TestHelper.get_chain_id_mock() + + for _ <- 0..1 do + Explorer.Mox.HTTPoison + |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", + body, + [{"Content-Type", "application/json"}], + _options -> + case Jason.decode(body) do + {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> + {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end) + end + + MultichainSearchDbExportRetry.run(export_data_2, nil) + + assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 3 end end end From 275b855b235bb0846eac3662f7073900086238f5 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:54:41 +0300 Subject: [PATCH 0142/1006] fix: internal transactions balance extraction (#12654) * fix: internal transactions balance extraction * Process review comments --- .../transform/address_coin_balances.ex | 12 +++-- .../transform/address_coin_balances_test.exs | 47 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex index 3787261d552f..a5ddb2f2428d 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex @@ -71,16 +71,18 @@ defmodule Indexer.Transform.AddressCoinBalances do defp internal_transactions_params_reducer(%{block_number: block_number} = internal_transaction_params, acc) when is_integer(block_number) do case internal_transaction_params do - %{type: "call"} = params -> + %{error: _} -> + acc + + %{type: "call", call_type: call_type, value: value} = params when call_type in ~w(call invalid) and value > 0 -> acc |> process_internal_transaction_field(params, :from_address_hash, block_number) |> process_internal_transaction_field(params, :to_address_hash, block_number) - %{type: "create", error: _} -> + %{type: type} = params when type in ~w(create create2) -> acc - - %{type: "create"} = params -> - process_internal_transaction_field(acc, params, :created_contract_address_hash, block_number) + |> process_internal_transaction_field(params, :from_address_hash, block_number) + |> process_internal_transaction_field(params, :created_contract_address_hash, block_number) %{type: "selfdestruct", from_address_hash: from_address_hash, to_address_hash: to_address_hash} when is_binary(from_address_hash) and is_binary(to_address_hash) -> diff --git a/apps/indexer/test/indexer/transform/address_coin_balances_test.exs b/apps/indexer/test/indexer/transform/address_coin_balances_test.exs index a230b43e5d5d..91985ac51621 100644 --- a/apps/indexer/test/indexer/transform/address_coin_balances_test.exs +++ b/apps/indexer/test/indexer/transform/address_coin_balances_test.exs @@ -36,6 +36,7 @@ defmodule Indexer.Transform.AddressCoinBalancesTest do :internal_transaction |> Factory.params_for() |> Map.put(:type, "call") + |> Map.put(:call_type, "call") |> Map.put(:block_number, block_number) |> Map.put(:from_address_hash, from_address_hash) |> Map.put(:to_address_hash, to_address_hash) @@ -80,6 +81,52 @@ defmodule Indexer.Transform.AddressCoinBalancesTest do assert MapSet.member?(params_set, %{address_hash: created_contract_address_hash, block_number: block_number}) end + test "with create2 internal transaction without error extracts created_contract_address_hash and from_address_hash" do + block_number = 1 + + created_contract_address_hash = + Factory.address_hash() + |> to_string() + + from_address_hash = + Factory.address_hash() + |> to_string() + + internal_transaction_params = + :internal_transaction_create + |> Factory.params_for() + |> Map.put(:type, "create2") + |> Map.put(:block_number, block_number) + |> Map.put(:created_contract_address_hash, created_contract_address_hash) + |> Map.put(:from_address_hash, from_address_hash) + + params_set = AddressCoinBalances.params_set(%{internal_transactions_params: [internal_transaction_params]}) + + assert MapSet.size(params_set) == 2 + assert MapSet.member?(params_set, %{address_hash: created_contract_address_hash, block_number: block_number}) + assert MapSet.member?(params_set, %{address_hash: from_address_hash, block_number: block_number}) + end + + test "ignores call internal transaction with call type that does not change balances" do + block_number = 1 + + from_address_hash = + Factory.address_hash() + |> to_string() + + internal_transaction_params = + :internal_transaction + |> Factory.params_for() + |> Map.put(:type, "call") + |> Map.put(:call_type, "staticcall") + |> Map.put(:block_number, block_number) + |> Map.put(:from_address_hash, from_address_hash) + + params_set = AddressCoinBalances.params_set(%{internal_transactions_params: [internal_transaction_params]}) + + assert MapSet.size(params_set) == 0 + end + test "with self-destruct internal transaction extracts from_address_hash and to_address_hash" do block_number = 1 From d131f34d55bdd3f887ccc469a0c478c5c559aa13 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:00:24 +0300 Subject: [PATCH 0143/1006] feat: Integrate Open API Spex lib (#11886) * feat: Integrate Open API Spex lib * Update for celo after rebase * Update spec --- .formatter.exs | 3 +- .github/workflows/config.yml | 9 +- .github/workflows/generate-swagger.yml | 211 ++++++ .gitignore | 4 +- apps/block_scout_web/lib/block_scout_web.ex | 7 + .../lib/block_scout_web/api_spec.ex | 28 + .../lib/block_scout_web/chain.ex | 192 +++-- .../controllers/api/v2/address_controller.ex | 422 ++++++++++- .../lib/block_scout_web/paging_helper.ex | 15 +- .../lib/block_scout_web/routers/api_router.ex | 2 + .../block_scout_web/schemas/api/v2/address.ex | 105 +++ .../schemas/api/v2/address/counters.ex | 21 + .../schemas/api/v2/address/response.ex | 100 +++ .../schemas/api/v2/address/tabs_counters.ex | 24 + .../schemas/api/v2/address/token_balance.ex | 20 + .../block_scout_web/schemas/api/v2/block.ex | 330 +++++++++ .../schemas/api/v2/celo/election_reward.ex | 29 + .../api/v2/celo/election_reward/type.ex | 8 + .../schemas/api/v2/coin_balance.ex | 27 + .../schemas/api/v2/coin_balance_by_day.ex | 18 + .../schemas/api/v2/error_responses.ex | 29 + .../block_scout_web/schemas/api/v2/general.ex | 691 ++++++++++++++++++ .../schemas/api/v2/internal_transaction.ex | 70 ++ .../lib/block_scout_web/schemas/api/v2/log.ex | 35 + .../schemas/api/v2/nft_collection.ex | 37 + .../schemas/api/v2/proxy/metadata.ex | 18 + .../schemas/api/v2/proxy/metadata_tag.ex | 21 + .../schemas/api/v2/signed_authorization.ex | 23 + .../block_scout_web/schemas/api/v2/token.ex | 101 +++ .../schemas/api/v2/token_instance.ex | 58 ++ .../schemas/api/v2/token_instance_in_list.ex | 25 + .../schemas/api/v2/token_transfer.ex | 99 +++ .../schemas/api/v2/transaction.ex | 474 ++++++++++++ .../schemas/api/v2/transaction/fee.ex | 21 + .../schemas/api/v2/withdrawal.ex | 25 + .../views/api/v2/arbitrum_view.ex | 6 +- .../block_scout_web/views/api/v2/helper.ex | 1 + .../views/api/v2/token_view.ex | 1 + .../views/api/v2/zksync_view.ex | 8 + apps/block_scout_web/mix.exs | 4 +- ...s_internal_transaction_controller_test.exs | 8 +- .../api/v2/address_controller_test.exs | 633 ++++++++++++++-- .../api/v2/transaction_controller_test.exs | 1 + .../lib/explorer/account/notifier/email.ex | 37 +- .../explorer/chain/smart_contract/proxy.ex | 4 +- .../lib/explorer/chain/token/instance.ex | 12 +- .../lib/explorer/chain/transaction.ex | 16 +- .../lib/explorer/chain/transaction_action.ex | 54 +- apps/utils/lib/utils/helper.ex | 43 ++ bin/version_bump.sh | 2 +- cspell.json | 5 + mix.lock | 2 + 52 files changed, 3900 insertions(+), 239 deletions(-) create mode 100644 .github/workflows/generate-swagger.yml create mode 100644 apps/block_scout_web/lib/block_scout_web/api_spec.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex create mode 100644 apps/utils/lib/utils/helper.ex diff --git a/.formatter.exs b/.formatter.exs index 69ae0d25d2c6..563ddfd8e03a 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -7,5 +7,6 @@ "mix.exs", "{config}/**/*.{ex,exs}" ], - line_length: 120 + line_length: 120, + import_deps: [:open_api_spex] ] diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 89fb2de8531d..baad1e211910 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -32,8 +32,8 @@ on: env: MIX_ENV: test - OTP_VERSION: ${{ github.ref_name == '10284/merge' && '27.1' || vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ github.ref_name == '10284/merge' && '1.17.3' || vars.ELIXIR_VERSION }} + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} ACCOUNT_AUTH0_DOMAIN: "blockscoutcom.us.auth0.com" jobs: @@ -290,6 +290,7 @@ jobs: mix gettext.extract --merge | tee stdout.txt grep "Wrote priv/gettext/en/LC_MESSAGES/default.po (0 new messages, 0 removed, " stdout.txt working-directory: "apps/block_scout_web" + sobelow: name: Sobelow security analysis runs-on: ubuntu-latest @@ -420,6 +421,7 @@ jobs: - run: ./node_modules/.bin/eslint --format=junit --output-file="test/eslint/junit.xml" js/** working-directory: apps/block_scout_web/assets + jest: name: JS Tests runs-on: ubuntu-latest @@ -552,6 +554,7 @@ jobs: ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox" ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} + test_nethermind_mox_explorer: strategy: fail-fast: false @@ -627,6 +630,7 @@ jobs: ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true" + test_nethermind_mox_indexer: strategy: fail-fast: false @@ -694,6 +698,7 @@ jobs: ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true" + test_nethermind_mox_block_scout_web: strategy: fail-fast: false diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml new file mode 100644 index 000000000000..c73ce6356d7c --- /dev/null +++ b/.github/workflows/generate-swagger.yml @@ -0,0 +1,211 @@ +name: Blockscout + +on: + push: + branches: + - master + - np-integrate-open-api-spex + paths-ignore: + - "CHANGELOG.md" + - "**/README.md" + - "docker/*" + - "docker-compose/*" + workflow_dispatch: + release: + types: [published] + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + RELEASE_VERSION: 8.1.0 + +jobs: + matrix-builder: + name: Build matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - id: set-matrix + run: | + echo "matrix=$(node -e ' + + // Add/remove CI matrix chain types here + const defaultChainTypes = ["default"]; + + const chainTypes = [ + "arbitrum", + "berachain", + "blackfort", + "celo", + "ethereum", + "filecoin", + "optimism", + "polygon_zkevm", + "rsk", + "scroll", + "shibarium", + "stability", + "zetachain", + "zilliqa", + "zksync", + "neon" + ]; + + const extraChainTypes = ["suave", "polygon_edge"]; + + // Chain type matrix we use in master branch + const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes); + + const matrix = { "chain-type": ${{ 'allChainTypes' }} }; + console.log(JSON.stringify(matrix)); + ')" >> $GITHUB_OUTPUT + + build-and-cache: + name: Build and Cache deps + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex + + - name: "ELIXIR_VERSION.lock" + run: echo "${ELIXIR_VERSION}" > ELIXIR_VERSION.lock + + - name: "OTP_VERSION.lock" + run: echo "${OTP_VERSION}" > OTP_VERSION.lock + + - name: Restore Mix Deps Cache + uses: actions/cache@v4 + id: deps-cache + with: + path: | + deps + _build + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- + + - name: Conditionally build Mix deps cache + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + mix local.hex --force + mix local.rebar --force + mix deps.get + mix deps.compile --skip-umbrella-children + + generate-swagger: + strategy: + fail-fast: false + matrix: ${{ fromJson(needs.matrix-builder.outputs.matrix) }} + name: Generate Open API spec + runs-on: ubuntu-latest + needs: + - build-and-cache + - matrix-builder + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: ${{ env.OTP_VERSION }} + elixir-version: ${{ env.ELIXIR_VERSION }} + hexpm-mirrors: | + https://builds.hex.pm + https://cdn.jsdelivr.net/hex + + - name: Mix Deps Cache + uses: actions/cache/restore@v4 + id: deps-cache + with: + path: | + deps + _build + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash-${{ hashFiles('mix.lock') }} + restore-keys: | + ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash- + + - name: mix openapi.spec.yaml + run: | + mix openapi.spec.yaml --spec BlockScoutWeb.ApiSpec openapi.${{ matrix.chain-type }}.yaml --start-app=false + env: + CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} + + - name: Upload OpenAPI spec + uses: actions/upload-artifact@v4 + with: + name: openapi-spec-${{ matrix.chain-type }} + path: openapi.${{ matrix.chain-type }}.yaml + retention-days: 1 + + push-specs: + needs: + - generate-swagger + - matrix-builder + runs-on: ubuntu-latest + name: Push all OpenAPI specs + steps: + - name: Validate required secrets + run: | + if [ -z "${{ secrets.API_SPECS_PAT }}" ]; then + echo "Error: API_SPECS_PAT secret is not set" + exit 1 + fi + + - name: Checkout specs repository + uses: actions/checkout@v4 + with: + repository: ${{ vars.API_SPECS_REPOSITORY }} + token: ${{ secrets.API_SPECS_PAT }} + path: api-specs + + - name: Download all swagger specs + uses: actions/download-artifact@v4 + with: + pattern: openapi-spec-* + merge-multiple: true + path: temp-specs + + - name: Create specs directory structure + run: | + VERSION=${{ github.event_name == 'release' && env.RELEASE_VERSION || 'master' }} + + for SPEC_FILE in temp-specs/*; do + if [ -f "$SPEC_FILE" ]; then + # Extract chain type from filename + CHAIN_TYPE=$(echo $SPEC_FILE | cut -d'.' -f2) + + # Create directory + mkdir -p "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}" + + # Copy spec files + cp "$SPEC_FILE" "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}/swagger.yaml" + fi + done + + + - name: Commit and push changes + working-directory: api-specs + run: | + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git config --local user.name "github-actions[bot]" + + git add . + + # Only commit if there are changes + if git diff --staged --quiet; then + echo "No changes to commit" + else + git commit -m "[SKIP-GH-PAGES] create OpenAPI specs for Blockscout ${{ github.event_name == 'release' && env.RELEASE_VERSION || github.sha }}" + git push + fi + + - name: Clean up + if: always() + run: | + rm -rf temp-specs + rm -rf api-specs \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16545281812b..16880a0aef09 100644 --- a/.gitignore +++ b/.gitignore @@ -74,4 +74,6 @@ queue_storage tasks_in_progress /dets -/temp \ No newline at end of file +/temp + +openapi.yaml \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web.ex b/apps/block_scout_web/lib/block_scout_web.ex index e0fffdb280be..ddc521e086fa 100644 --- a/apps/block_scout_web/lib/block_scout_web.ex +++ b/apps/block_scout_web/lib/block_scout_web.ex @@ -34,6 +34,13 @@ defmodule BlockScoutWeb do only: [proxy_implementations_association: 0, proxy_implementations_smart_contracts_association: 0] alias BlockScoutWeb.Routers.AdminRouter.Helpers, as: AdminRoutes + + alias BlockScoutWeb.Schemas.API.V2, as: Schemas + alias OpenApiSpex.{Schema, Reference} + alias OpenApiSpex.JsonErrorResponse + alias Schemas.ErrorResponses.ForbiddenResponse + + import BlockScoutWeb.Schemas.API.V2.General end end diff --git a/apps/block_scout_web/lib/block_scout_web/api_spec.ex b/apps/block_scout_web/lib/block_scout_web/api_spec.ex new file mode 100644 index 000000000000..09e09051781a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/api_spec.ex @@ -0,0 +1,28 @@ +defmodule BlockScoutWeb.ApiSpec do + @moduledoc """ + This module defines the API specification for the BlockScoutWeb application. + """ + alias BlockScoutWeb.{Routers.ApiRouter} + alias OpenApiSpex.{Contact, Info, OpenApi, Paths, Server} + alias Utils.Helper + + @behaviour OpenApi + + @impl OpenApi + def spec do + %OpenApi{ + servers: [ + %Server{url: to_string(Helper.instance_url() |> URI.append_path("/api"))} + ], + info: %Info{ + title: "Blockscout", + version: to_string(Application.spec(:block_scout_web, :vsn)), + contact: %Contact{ + email: "info@blockscout.com" + } + }, + paths: Paths.from_router(ApiRouter) + } + |> OpenApiSpex.resolve_schema_modules() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index e021a6cef30d..fa0d97b9e0e5 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -44,7 +44,8 @@ defmodule BlockScoutWeb.Chain do Transaction, Transaction.StateChange, UserOperation, - Wei + Wei, + Withdrawal } alias Explorer.Chain.Optimism.Deposit, as: OptimismDeposit @@ -70,7 +71,7 @@ defmodule BlockScoutWeb.Chain do def current_filter(%{paging_options: paging_options} = params) do params - |> Map.get("filter") + |> (&(Map.get(&1, "filter") || Map.get(&1, :filter))).() |> case do "to" -> [direction: :to, paging_options: paging_options] "from" -> [direction: :from, paging_options: paging_options] @@ -80,7 +81,7 @@ defmodule BlockScoutWeb.Chain do def current_filter(params) do params - |> Map.get("filter") + |> (&(Map.get(&1, "filter") || Map.get(&1, :filter))).() |> case do "to" -> [direction: :to] "from" -> [direction: :from] @@ -167,9 +168,9 @@ defmodule BlockScoutWeb.Chain do @spec paging_options(any) :: [{:paging_options, Explorer.PagingOptions.t()}, ...] | Explorer.PagingOptions.t() def paging_options(%{ - "hash" => hash_string, - "fetched_coin_balance" => fetched_coin_balance_string, - "transactions_count" => transactions_count_string + hash: hash_string, + fetched_coin_balance: fetched_coin_balance_string, + transactions_count: transactions_count_string }) when is_binary(hash_string) do case string_to_address_hash(hash_string) do @@ -191,30 +192,31 @@ defmodule BlockScoutWeb.Chain do end def paging_options(%{ - "fee" => fee_string, - "value" => value_string, - "block_number" => block_number_string, - "index" => index_string, - "inserted_at" => inserted_at_string, - "hash" => hash_string + fee: fee_string, + value: value_string, + block_number: block_number_string, + index: index_string, + inserted_at: inserted_at, + hash: hash_string }) do - with {:ok, hash} <- string_to_full_hash(hash_string), - {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string) do - [ - paging_options: %{ - @default_paging_options - | key: %{ - fee: decimal_parse(fee_string), - value: decimal_parse(value_string), - block_number: parse_integer(block_number_string), - index: parse_integer(index_string), - inserted_at: inserted_at, - hash: hash - } - } - ] - else - _ -> [paging_options: @default_paging_options] + case string_to_full_hash(hash_string) do + {:ok, hash} -> + [ + paging_options: %{ + @default_paging_options + | key: %{ + fee: decimal_parse(fee_string), + value: decimal_parse(value_string), + block_number: parse_integer(block_number_string), + index: parse_integer(index_string), + inserted_at: inserted_at, + hash: hash + } + } + ] + + _ -> + [paging_options: @default_paging_options] end end @@ -274,6 +276,14 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{ + block_number: block_number, + transaction_index: transaction_index, + index: index + }) do + [paging_options: %{@default_paging_options | key: {block_number, transaction_index, index}}] + end + def paging_options(%{ "block_number" => block_number_string, "index" => index_string, @@ -283,7 +293,7 @@ defmodule BlockScoutWeb.Chain do "index_in_batch" => index_in_batch_string }) when is_binary(block_number_string) and is_binary(index_string) and is_binary(batch_log_index_string) and - is_binary(batch_transaction_hash_string) and is_binary(index_in_batch_string) and + is_binary(batch_block_hash_string) and is_binary(batch_transaction_hash_string) and is_binary(index_in_batch_string) do with {block_number, ""} <- Integer.parse(block_number_string), {index, ""} <- Integer.parse(index_string), @@ -328,6 +338,52 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{ + block_number: block_number, + index: index, + batch_log_index: batch_log_index, + batch_block_hash: batch_block_hash_string, + batch_transaction_hash: batch_transaction_hash_string, + index_in_batch: index_in_batch + }) + when is_binary(batch_transaction_hash_string) and is_binary(batch_block_hash_string) do + with {:ok, batch_transaction_hash} <- string_to_full_hash(batch_transaction_hash_string), + {:ok, batch_block_hash} <- string_to_full_hash(batch_block_hash_string) do + [ + paging_options: %{ + @default_paging_options + | key: {block_number, index}, + batch_key: {batch_block_hash, batch_transaction_hash, batch_log_index, index_in_batch} + } + ] + else + _ -> + [paging_options: @default_paging_options] + end + end + + def paging_options(%{ + batch_log_index: batch_log_index, + batch_block_hash: batch_block_hash_string, + batch_transaction_hash: batch_transaction_hash_string, + index_in_batch: index_in_batch + }) + when is_binary(batch_block_hash_string) and + is_binary(batch_transaction_hash_string) do + with {:ok, batch_transaction_hash} <- string_to_full_hash(batch_transaction_hash_string), + {:ok, batch_block_hash} <- string_to_full_hash(batch_block_hash_string) do + [ + paging_options: %{ + @default_paging_options + | batch_key: {batch_block_hash, batch_transaction_hash, batch_log_index, index_in_batch} + } + ] + else + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(%{"block_number" => block_number_string, "index" => index_string}) when is_binary(block_number_string) and is_binary(index_string) do with {block_number, ""} <- Integer.parse(block_number_string), @@ -339,6 +395,10 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{block_number: block_number, index: index}) do + [paging_options: %{@default_paging_options | key: {block_number, index}}] + end + def paging_options(%{"block_number" => block_number_string}) when is_binary(block_number_string) do case Integer.parse(block_number_string) do {block_number, ""} -> @@ -349,6 +409,10 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{block_number: block_number}) do + [paging_options: %{@default_paging_options | key: {block_number}}] + end + def paging_options(%{"index" => index_string}) when is_binary(index_string) do case Integer.parse(index_string) do {index, ""} -> @@ -363,6 +427,10 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {index}}] end + def paging_options(%{index: index}) do + [paging_options: %{@default_paging_options | key: {index}}] + end + def paging_options(%{"nonce" => nonce_string}) when is_binary(nonce_string) do case Integer.parse(nonce_string) do {nonce, ""} -> @@ -402,6 +470,16 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{inserted_at: inserted_at, hash: hash_string}) when is_binary(hash_string) do + case string_to_full_hash(hash_string) do + {:ok, hash} -> + [paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_transaction: true}] + + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}), do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}] @@ -424,6 +502,20 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{fiat_value: fiat_value_string, value: value_string, id: id}) + when is_binary(fiat_value_string) and is_binary(value_string) do + with {value, ""} <- Decimal.parse(value_string), + {_id, _value, {fiat_value, ""}} <- {id, value, Decimal.parse(fiat_value_string)} do + [paging_options: %{@default_paging_options | key: {fiat_value, value, id}}] + else + {id, value, :error} -> + [paging_options: %{@default_paging_options | key: {nil, value, id}}] + + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(%{"value" => value_string, "id" => id_string}) when is_binary(value_string) and is_binary(id_string) do with {id, ""} <- Integer.parse(id_string), @@ -491,14 +583,14 @@ defmodule BlockScoutWeb.Chain do end def paging_options(%{ - "token_contract_address_hash" => token_contract_address_hash, - "token_id" => token_id, - "token_type" => token_type + token_contract_address_hash: token_contract_address_hash, + token_id: token_id, + token_type: token_type }) do [paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_id, token_type}}] end - def paging_options(%{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type}) do + def paging_options(%{token_contract_address_hash: token_contract_address_hash, token_type: token_type}) do [paging_options: %{@default_paging_options | key: {token_contract_address_hash, token_type}}] end @@ -651,9 +743,9 @@ defmodule BlockScoutWeb.Chain do transactions_count: transactions_count }) do %{ - "hash" => hash, - "fetched_coin_balance" => fetched_coin_balance && Wei.to(fetched_coin_balance, :wei), - "transactions_count" => transactions_count + hash: hash, + fetched_coin_balance: fetched_coin_balance && Wei.to(fetched_coin_balance, :wei), + transactions_count: transactions_count } end @@ -705,16 +797,16 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%Block{number: number}) do - %{"block_number" => number} + %{block_number: number} end defp paging_params(%InternalTransaction{index: index, transaction_hash: transaction_hash}) do {:ok, %Transaction{block_number: block_number, index: transaction_index}} = hash_to_transaction(transaction_hash) - %{"block_number" => block_number, "transaction_index" => transaction_index, "index" => index} + %{block_number: block_number, transaction_index: transaction_index, index: index} end defp paging_params(%Log{index: index, block_number: block_number}) do - %{"block_number" => block_number, "index" => index} + %{block_number: block_number, index: index} end defp paging_params(%Transaction{block_number: nil, inserted_at: inserted_at, hash: hash}) do @@ -726,7 +818,7 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%TokenTransfer{block_number: block_number, log_index: index}) do - %{"block_number" => block_number, "index" => index} + %{block_number: block_number, index: index} end defp paging_params(%Address.Token{name: name, type: type, inserted_at: inserted_at}) do @@ -740,7 +832,7 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%CoinBalance{block_number: block_number}) do - %{"block_number" => block_number} + %{block_number: block_number} end defp paging_params(%SmartContract{address: %NotLoaded{}} = smart_contract) do @@ -778,6 +870,10 @@ defmodule BlockScoutWeb.Chain do %{"index" => index} end + defp paging_params(%Withdrawal{index: index}) do + %{index: index} + end + defp paging_params(%{msg_nonce: nonce}) do %{"nonce" => nonce} end @@ -815,10 +911,10 @@ defmodule BlockScoutWeb.Chain do end @spec paging_params_with_fiat_value(CurrentTokenBalance.t()) :: %{ - required(String.t()) => Decimal.t() | non_neg_integer() | nil + required(atom()) => Decimal.t() | non_neg_integer() | nil } def paging_params_with_fiat_value(%CurrentTokenBalance{id: id, value: value} = ctb) do - %{"fiat_value" => ctb.fiat_value, "value" => value, "id" => id} + %{fiat_value: ctb.fiat_value, value: value, id: id} end defp block_or_transaction_or_operation_or_blob_from_param(param) do @@ -878,10 +974,10 @@ defmodule BlockScoutWeb.Chain do params |> Map.merge(new_params) |> Map.merge(%{ - "batch_log_index" => current_token_transfer.log_index, - "batch_block_hash" => current_token_transfer.block_hash, - "batch_transaction_hash" => current_token_transfer.transaction_hash, - "index_in_batch" => current_token_transfer.index_in_batch + batch_log_index: current_token_transfer.log_index, + batch_block_hash: current_token_transfer.block_hash, + batch_transaction_hash: current_token_transfer.transaction_hash, + index_in_batch: current_token_transfer.index_in_batch }) else Map.merge(params, paging_params(List.last(list))) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 12680adb3203..e516a813e1c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do use BlockScoutWeb, :controller use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Chain, only: [ @@ -132,12 +133,27 @@ defmodule BlockScoutWeb.API.V2.AddressController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags ["addresses"] + + operation :address, + summary: "Retrieve detailed information about a specific address or contract", + description: + "Retrieves detailed information for a specific address, including balance, transaction count, and metadata.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Detailed information about the specified address.", "application/json", Schemas.Address.Response}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/addresses/:address_hash_param` endpoint. Returns 200 on any valid address_hash, even if the address is not found in the database. """ @spec address(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def address(conn, %{"address_hash_param" => address_hash_string} = params) do + def address(conn, %{address_hash_param: address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do @@ -181,6 +197,17 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :counters, + summary: "Get activity count stats for a specific address", + description: + "Retrieves count statistics for an address, including transactions, token transfers, gas usage, and validations.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Count statistics for the specified address", "application/json", Schemas.Address.Counters}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/counters` endpoint. @@ -194,7 +221,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the operation is successful. """ @spec counters(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def counters(conn, %{"address_hash_param" => address_hash_string} = params) do + def counters(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, address} -> @@ -222,6 +249,19 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :token_balances, + summary: "List all token balances held by a specific address", + description: + "Retrieves all token balances held by a specific address, including ERC-20, ERC-721, ERC-1155 and ERC-404 tokens.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: + {"All token balances for the specified address.", "application/json", + %Schema{title: "AddressTokenBalances", type: :array, items: Schemas.Address.TokenBalance}}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/token-balances` endpoint (retrieves the token balances for a given address) @@ -237,7 +277,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec token_balances(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def token_balances(conn, %{"address_hash_param" => address_hash_string} = params) do + def token_balances(conn, %{address_hash_param: address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do @@ -261,6 +301,47 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :transactions, + summary: "List transactions involving a specific address with to-from filtering", + description: + "Retrieves transactions involving a specific address, with optional filtering for transactions sent from or to the address.", + parameters: + base_params() ++ + [ + address_hash_param(), + direction_filter_param(), + sort_param(["block_number", "value", "fee"]), + order_param() + ] ++ + define_paging_params([ + "block_number_nullable", + "index_nullable", + "inserted_at", + "hash", + "value", + "fee", + "items_count" + ]), + responses: [ + ok: + {"All transactions for the specified address.", "application/json", + paginated_response( + items: Schemas.Transaction, + next_page_params_example: %{ + "block_number" => 22_566_361, + "fee" => "19206937428000", + "hash" => "0xe38d616dade747097354b0731b5560f581536dacf22121feb4bb4a0b776018aa", + "index" => 103, + "inserted_at" => "2025-05-26T10:26:51.474448Z", + "items_count" => 50, + "value" => "24741049597737" + }, + title_prefix: "AddressTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/transactions` endpoint (retrieves transactions for a given address) @@ -276,7 +357,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec transactions(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def transactions(conn, %{"address_hash_param" => address_hash_string} = params) do + def transactions(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -317,6 +398,38 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :token_transfers, + summary: "List token transfers involving a specific address with filtering options", + description: + "Retrieves token transfers involving a specific address, with optional filtering by token type, direction, and specific token.", + parameters: + base_params() ++ + [address_hash_param(), direction_filter_param(), token_type_param(), token_filter_param()] ++ + define_paging_params([ + "block_number", + "index", + "items_count", + "batch_log_index", + "batch_block_hash", + "batch_transaction_hash", + "index_in_batch" + ]), + responses: [ + ok: + {"All token transfers for the specified address.", "application/json", + paginated_response( + items: Schemas.TokenTransfer, + next_page_params_example: %{ + "block_number" => 12_345_678, + "index" => 0, + "items_count" => 50 + }, + title_prefix: "AddressTokenTransfers" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/token-transfers` endpoint (retrieves token transfers for a given address) @@ -337,9 +450,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do | {:not_found, {:error, :not_found}} | {:restricted_access, true} | Plug.Conn.t() - def token_transfers(conn, %{"address_hash_param" => address_hash_string} = params) do + def token_transfers(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), - {:ok, token_address_hash} <- validate_optional_address_hash(params["token"], params), + {:ok, token_address_hash} <- validate_optional_address_hash(params[:token], params), token_address_exists <- (token_address_hash && Chain.check_token_exists(token_address_hash)) || :ok do case {Chain.hash_to_address(address_hash, @address_options), token_address_exists} do {{:ok, _address}, :ok} -> @@ -386,6 +499,31 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :internal_transactions, + summary: "List all internal transactions involving a specific address", + description: + "Retrieves all internal transactions involving a specific address, with optional filtering for internal transactions sent from or to the address.", + parameters: + base_params() ++ + [address_hash_param(), direction_filter_param()] ++ + define_paging_params(["block_number", "index", "items_count", "transaction_index"]), + responses: [ + ok: + {"All internal transactions for the specified address.", "application/json", + paginated_response( + items: Schemas.InternalTransaction, + next_page_params_example: %{ + "block_number" => 22_530_770, + "index" => 8, + "items_count" => 50, + "transaction_index" => 8 + }, + title_prefix: "AddressInternalTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/internal-transactions` endpoint (retrieves internal transactions for a given address) @@ -401,7 +539,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec internal_transactions(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def internal_transactions(conn, %{"address_hash_param" => address_hash_string} = params) do + def internal_transactions(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -445,6 +583,24 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :logs, + summary: "List event logs emitted by or involving a specific address", + description: "Retrieves event logs emitted by or involving a specific address.", + parameters: + base_params() ++ + [address_hash_param(), topic_param()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"Event logs for the specified address, with pagination.", "application/json", + paginated_response( + items: Schemas.Log, + next_page_params_example: %{"block_number" => 22_546_398, "index" => 268, "items_count" => 50}, + title_prefix: "AddressLogs" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/logs` endpoint (retrieves logs for a given address) @@ -460,9 +616,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec logs(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def logs(conn, %{"address_hash_param" => address_hash_string} = params) do + def logs(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), - {:ok, topic} <- validate_optional_topic(params["topic"]) do + {:ok, topic} <- validate_optional_topic(params[:topic]) do case Chain.hash_to_address(address_hash, @api_true) do {:ok, _address} -> options = @@ -502,6 +658,23 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :blocks_validated, + summary: "List blocks validated (mined) by a specific validator/miner address", + description: + "Retrieves blocks that were validated (mined) by a specific address. Useful for tracking validator/miner performance.", + parameters: base_params() ++ [address_hash_param()] ++ define_paging_params(["block_number", "items_count"]), + responses: [ + ok: + {"Blocks validated by the specified address, with pagination.", "application/json", + paginated_response( + items: Schemas.Block, + next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50}, + title_prefix: "AddressBlocksValidated" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/blocks-validated` endpoint (retrieves validated by a given address blocks) @@ -517,7 +690,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec blocks_validated(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def blocks_validated(conn, %{"address_hash_param" => address_hash_string} = params) do + def blocks_validated(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -553,6 +726,23 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :coin_balance_history, + summary: "Get native coin balance history for an address showing all balance changes", + description: + "Retrieves historical native coin balance changes for a specific address, tracking how an address's balance has changed over time.", + parameters: base_params() ++ [address_hash_param()] ++ define_paging_params(["block_number", "items_count"]), + responses: [ + ok: + {"Historical coin balance changes for the specified address, with pagination.", "application/json", + paginated_response( + items: Schemas.CoinBalance, + next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50}, + title_prefix: "AddressCoinBalanceHistory" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/coin-balance-history` endpoint (retrieves coin balance history for given address) @@ -568,7 +758,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec coin_balance_history(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def coin_balance_history(conn, %{"address_hash_param" => address_hash_string} = params) do + def coin_balance_history(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, address} -> @@ -593,6 +783,27 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :coin_balance_history_by_day, + summary: "Get daily native coin balance snapshots for an address from previous 10 days", + description: + "Retrieves daily snapshots of native coin balance for a specific address. Useful for generating balance-over-time charts.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: + {"Daily coin balance history for the specified address.", "application/json", + %Schema{ + title: "AddressCoinBalanceHistoryByDay", + type: :object, + properties: %{ + days: %Schema{type: :integer, nullable: false}, + items: %Schema{type: :array, items: Schemas.CoinBalanceByDay} + }, + nullable: false + }}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/coin-balance-history-by-day` endpoint (retrieves coin balance history by day for given address) @@ -609,7 +820,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do """ @spec coin_balance_history_by_day(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def coin_balance_history_by_day(conn, %{"address_hash_param" => address_hash_string} = params) do + def coin_balance_history_by_day(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -629,6 +840,31 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :tokens, + summary: "List token balances for an address with pagination and type filtering", + description: + "Retrieves token balances for a specific address with pagination and filtering by token type. Useful for displaying large token portfolios.", + parameters: + base_params() ++ + [address_hash_param(), token_type_param()] ++ + define_paging_params(["fiat_value_nullable", "id", "items_count", "value"]), + responses: [ + ok: + {"Token balances for the specified address with pagination.", "application/json", + paginated_response( + items: Schemas.Address.TokenBalance, + next_page_params_example: %{ + "fiat_value" => nil, + "id" => 12_519_063_346, + "items_count" => 50, + "value" => "3750000000000000000000" + }, + title_prefix: "AddressTokens" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/tokens` endpoint (retrieves token balances for given address) @@ -644,7 +880,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec tokens(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def tokens(conn, %{"address_hash_param" => address_hash_string} = params) do + def tokens(conn, %{address_hash_param: address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do @@ -684,6 +920,24 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :withdrawals, + summary: "List validator withdrawals involving a specific address", + description: + "Retrieves withdrawals involving a specific address, typically for proof-of-stake networks supporting validator withdrawals.", + parameters: base_params() ++ [address_hash_param()] ++ define_paging_params(["index", "items_count"]), + responses: [ + ok: + {"Withdrawals for the specified address, with pagination. Note that receiver field is not included in this endpoint.", + "application/json", + paginated_response( + items: Schemas.Withdrawal, + next_page_params_example: %{"index" => 88_192_653, "items_count" => 50}, + title_prefix: "AddressWithdrawals" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/withdrawals` endpoint (retrieves withdrawals for given address) @@ -699,7 +953,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec withdrawals(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def withdrawals(conn, %{"address_hash_param" => address_hash_string} = params) do + def withdrawals(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -729,6 +983,41 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :addresses_list, + summary: "List addresses holding native coins sorted by balance - top accounts", + description: "Retrieves a paginated list of addresses holding the native coin, sorted by balance.", + parameters: + base_params() ++ + [sort_param(["balance", "transactions_count"]), order_param()] ++ + define_paging_params(["fetched_coin_balance", "address_hash", "items_count", "transactions_count"]), + responses: [ + ok: + {"List of native coin holders with their balances, with pagination.", "application/json", + %Schema{ + title: "AddressesList", + allOf: [ + paginated_response( + items: Schemas.Address, + next_page_params_example: %{ + "fetched_coin_balance" => "124355417998347240251800", + "hash" => "0x59708733fbbf64378d9293ec56b977c011a08fd2", + "items_count" => 50, + "transactions_count" => nil + }, + title_prefix: "AddressList" + ), + %Schema{ + properties: %{ + exchange_rate: Schemas.General.FloatStringNullable, + total_supply: Schemas.General.FloatStringNullable + }, + required: [:exchange_rate, :total_supply] + } + ] + }}, + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses` endpoint (retrieves addresses list) @@ -766,6 +1055,16 @@ defmodule BlockScoutWeb.API.V2.AddressController do }) end + operation :tabs_counters, + summary: "Get counters for address tabs", + description: "Retrieves counters for various address-related entities (max counter value is 51).", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Counters for address tabs.", "application/json", Schemas.Address.TabsCounters}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ Handles GET requests to `/api/v2/addresses/:address_hash_param/tabs-counters` endpoint (retrieves counter for each entity (max counter value is 51) for given address) @@ -781,7 +1080,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec tabs_counters(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def tabs_counters(conn, %{"address_hash_param" => address_hash_string} = params) do + def tabs_counters(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do counter_name_to_json_field_name = %{ validations: :validations_count, @@ -830,8 +1129,33 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :nft_list, + summary: "List NFTs owned by a specific address with optional type filtering", + description: + "Retrieves a list of NFTs (non-fungible tokens) owned by a specific address, with optional filtering by token type.", + parameters: + base_params() ++ + [address_hash_param(), nft_token_type_param()] ++ + define_paging_params(["items_count", "token_contract_address_hash", "token_id", "token_type"]), + responses: [ + ok: + {"NFTs owned by the specified address, with pagination.", "application/json", + paginated_response( + items: Schemas.TokenInstanceInList, + next_page_params_example: %{ + "items_count" => 50, + "token_contract_address_hash" => "0x1ffe11b9fb7f6ff1b153ab8608cf403ecaf9d44a", + "token_id" => "24950", + "token_type" => "ERC-721" + }, + title_prefix: "AddressNFTs" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ - Handles GET requests to `/api/v2/addresses/:address_hash_param/nft-list` endpoint (retrieves NFTs for given address) + Handles GET requests to `/api/v2/addresses/:address_hash_param/nft` endpoint (retrieves NFTs for given address) ## Parameters @@ -845,7 +1169,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec nft_list(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def nft_list(conn, %{"address_hash_param" => address_hash_string} = params) do + def nft_list(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -882,8 +1206,32 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :nft_collections, + summary: "List NFTs owned by an address grouped by collection/project", + description: + "Retrieves NFTs owned by a specific address, organized by collection. Useful for displaying an address's NFT portfolio grouped by project.", + parameters: + base_params() ++ + [address_hash_param(), nft_token_type_param()] ++ + define_paging_params(["items_count", "token_contract_address_hash", "token_type"]), + responses: [ + ok: + {"NFTs owned by the specified address, grouped by collection, with pagination.", "application/json", + paginated_response( + items: Schemas.NFTCollection, + next_page_params_example: %{ + "items_count" => 50, + "token_contract_address_hash" => "0x1ffe11b9fb7f6ff1b153ab8608cf403ecaf9d44a", + "token_type" => "ERC-721" + }, + title_prefix: "AddressNFTCollections" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ - Handles GET requests to `/api/v2/addresses/:address_hash_param/nft-collections` endpoint (retrieves NFTs grouped by collections for given address) + Handles GET requests to `/api/v2/addresses/:address_hash_param/nft/collections` endpoint (retrieves NFTs grouped by collections for given address) ## Parameters @@ -897,7 +1245,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do - `Plug.Conn.t()` if the request is successful. """ @spec nft_collections(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def nft_collections(conn, %{"address_hash_param" => address_hash_string} = params) do + def nft_collections(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, _address} -> @@ -934,11 +1282,35 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :celo_election_rewards, + summary: "List Celo election rewards for a specific address", + description: "Retrieves Celo election rewards for a specific address.", + parameters: + base_params() ++ + [address_hash_param()] ++ + define_paging_params(["epoch_number", "amount", "associated_account_address_hash", "type"]), + responses: [ + ok: + {"Celo election rewards for the specified address.", "application/json", + paginated_response( + items: Schemas.Celo.ElectionReward, + next_page_params_example: %{ + "block_number" => 100, + "amount" => "1000000000000000000", + "associated_account_address_hash" => "0x1234567890123456789012345678901234567890", + "type" => "validator" + }, + title_prefix: "AddressCeloElectionRewards" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + @doc """ - Function to handle GET requests to `/api/v2/addresses/:address_hash_param/election-rewards` endpoint. + Handles GET requests to `/api/v2/addresses/:address_hash_param/election-rewards` endpoint. """ @spec celo_election_rewards(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() - def celo_election_rewards(conn, %{"address_hash_param" => address_hash_string} = params) do + def celo_election_rewards(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), {:ok, _address} <- Chain.hash_to_address(address_hash, api?: true) do full_options = @@ -988,10 +1360,10 @@ defmodule BlockScoutWeb.API.V2.AddressController do @spec celo_election_rewards_paging_options(map()) :: PagingOptions.t() defp celo_election_rewards_paging_options(params) do with %{ - "epoch_number" => epoch_number_string, - "amount" => amount_string, - "associated_account_address_hash" => associated_account_address_hash_string, - "type" => type_string + epoch_number: epoch_number_string, + amount: amount_string, + associated_account_address_hash: associated_account_address_hash_string, + type: type_string } when is_binary(epoch_number_string) and is_binary(amount_string) and diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index a90c9535ce66..4cfa005a53e5 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -80,13 +80,19 @@ defmodule BlockScoutWeb.PagingHelper do ] end + def token_transfers_types_options(%{type: filters}) do + [ + token_type: filters_to_list(filters, @allowed_token_transfer_type_labels) + ] + end + def token_transfers_types_options(_), do: [token_type: []] @doc """ Parse 'type' query parameter from request option map """ @spec nft_types_options(map()) :: [{:token_type, list}] - def nft_types_options(%{"type" => filters}) do + def nft_types_options(%{type: filters}) do [ token_type: filters_to_list(filters, @allowed_nft_type_labels) ] @@ -221,6 +227,7 @@ defmodule BlockScoutWeb.PagingHelper do def delete_parameters_from_next_page_params(params) when is_map(params) do params |> Map.drop([ + :address_hash_param, "block_hash_or_number", "transaction_hash_param", "address_hash_param", @@ -282,10 +289,10 @@ defmodule BlockScoutWeb.PagingHelper do defp do_tokens_sorting("circulating_market_cap", "desc"), do: [desc_nulls_last: :circulating_market_cap] defp do_tokens_sorting(_, _), do: [] - @spec address_transactions_sorting(%{required(String.t()) => String.t()}) :: [ + @spec address_transactions_sorting(%{required(atom()) => String.t()}) :: [ {:sorting, SortingHelper.sorting_params()} ] - def address_transactions_sorting(%{"sort" => sort_field, "order" => order}) do + def address_transactions_sorting(%{sort: sort_field, order: order}) do [sorting: do_address_transaction_sorting(sort_field, order)] end @@ -390,7 +397,7 @@ defmodule BlockScoutWeb.PagingHelper do @spec addresses_sorting(%{required(String.t()) => String.t()}) :: [ {:sorting, SortingHelper.sorting_params()} ] - def addresses_sorting(%{"sort" => sort_field, "order" => order}) do + def addresses_sorting(%{sort: sort_field, order: order}) do [sorting: do_addresses_sorting(sort_field, order)] end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index a12b3cdf0745..a0c81c67de56 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -76,6 +76,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do plug(:fetch_session) plug(:protect_from_forgery) plug(RateLimit) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end pipeline :api_v2_no_session do @@ -130,6 +131,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do scope "/v2", as: :api_v2 do pipe_through(:api_v2) + get("/openapi", OpenApiSpex.Plug.RenderSpec, []) scope "/search" do get("/", V2.SearchController, :search) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex new file mode 100644 index 000000000000..a4ac085aab9e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -0,0 +1,105 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do + @moduledoc false + require OpenApiSpex + + alias Ecto.Enum, as: EctoEnum + alias Explorer.Chain.Address + alias OpenApiSpex.Schema + + @filecoin_robust_address_schema %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + schema + |> put_in([:properties, :filecoin], %Schema{ + type: :object, + properties: %{ + id: %Schema{type: :string, example: "f03248220", nullable: true}, + robust: @filecoin_robust_address_schema, + actor_type: %Schema{ + type: :string, + enum: EctoEnum.values(Address, :filecoin_actor_type), + nullable: true + } + } + }) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Address do + @moduledoc """ + This module defines the schema for address struct, returned by BlockScoutWeb.API.V2.Helper.address_with_info/5. + + Note that BlockScoutWeb.Schemas.API.V2.Address.Response is defined in __after_compile__/2 callback. This is done to reuse the Address schema in the AddressResponse schema. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations + alias BlockScoutWeb.Schemas.API.V2.{General, Proxy} + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + description: "Address", + type: :object, + properties: %{ + hash: General.AddressHash, + is_contract: %Schema{type: :boolean, description: "Has address contract code?", nullable: true}, + name: %Schema{type: :string, description: "Name associated with the address", nullable: true}, + is_scam: %Schema{type: :boolean, description: "Has address scam badge?", nullable: false}, + proxy_type: General.ProxyType, + implementations: %Schema{ + description: "Implementations linked with the contract", + type: :array, + items: General.Implementation + }, + is_verified: %Schema{type: :boolean, description: "Has address associated source code?", nullable: false}, + ens_domain_name: %Schema{ + type: :string, + description: "ENS domain name associated with the address", + nullable: true + }, + metadata: %Schema{allOf: [Proxy.Metadata], nullable: true}, + private_tags: %Schema{ + description: "Private tags associated with the address", + type: :array, + items: General.Tag + }, + watchlist_names: %Schema{ + description: "Watch list name associated with the address", + type: :array, + items: General.WatchlistName + }, + public_tags: %Schema{ + description: "Public tags associated with the address", + type: :array, + items: General.Tag + } + }, + required: [ + :hash, + :is_contract, + :name, + :is_scam, + :proxy_type, + :implementations, + :is_verified, + :ens_domain_name, + :metadata, + :private_tags, + :watchlist_names, + :public_tags + ] + } + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex new file mode 100644 index 000000000000..6a82016c3dec --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Address.Counters do + @moduledoc """ + This module defines the schema for address counters. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + title: "AddressCounters", + description: "Address counters", + type: :object, + properties: %{ + transactions_count: General.IntegerString, + token_transfers_count: General.IntegerString, + gas_usage_count: General.IntegerString, + validations_count: General.IntegerString + }, + required: [:transactions_count, :token_transfers_count, :gas_usage_count, :validations_count] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex new file mode 100644 index 000000000000..90ec30950ed8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -0,0 +1,100 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations do + @moduledoc false + require OpenApiSpex + + alias OpenApiSpex.Schema + + @filecoin_robust_address_schema %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + schema + |> put_in([:properties, :creator_filecoin_robust_address], @filecoin_robust_address_schema) + |> update_in([:required], &[:creator_filecoin_robust_address | &1]) + + :zilliqa -> + schema + |> put_in([:properties, :is_scilla_contract], %Schema{type: :boolean, nullable: false}) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Address.Response do + @moduledoc """ + This module defines the schema for address response from /api/v2/addresses/:hash. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations + alias BlockScoutWeb.Schemas.API.V2.{General, Token} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "AddressResponse", + description: "Address response", + type: :object, + allOf: [ + BlockScoutWeb.Schemas.API.V2.Address, + struct( + Schema, + %{ + type: :object, + properties: %{ + creator_address_hash: General.AddressHashNullable, + creation_transaction_hash: General.FullHashNullable, + token: %Schema{allOf: [Token], nullable: true}, + coin_balance: General.IntegerStringNullable, + exchange_rate: General.FloatStringNullable, + block_number_balance_updated_at: %Schema{type: :integer, minimum: 0, nullable: true}, + has_validated_blocks: %Schema{type: :boolean, nullable: false}, + has_logs: %Schema{type: :boolean, nullable: false}, + has_tokens: %Schema{type: :boolean, nullable: false}, + has_token_transfers: %Schema{type: :boolean, nullable: false}, + watchlist_address_id: %Schema{type: :integer, nullable: true}, + has_beacon_chain_withdrawals: %Schema{type: :boolean, nullable: false}, + creation_status: %Schema{ + type: :string, + description: "Creation status of the contract", + enum: ["success", "failed", "selfdestructed"], + nullable: true + } + }, + required: [ + :creator_address_hash, + :creation_transaction_hash, + :token, + :coin_balance, + :exchange_rate, + :block_number_balance_updated_at, + :has_validated_blocks, + :has_logs, + :has_tokens, + :has_token_transfers, + :watchlist_address_id, + :has_beacon_chain_withdrawals, + :creation_status + ] + } + |> ChainTypeCustomizations.chain_type_fields() + ) + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex new file mode 100644 index 000000000000..3291980daaaf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Address.TabsCounters do + @moduledoc """ + This module defines the schema for the Address.TabsCounters struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "AddressTabsCounters", + description: "Counters for address tabs", + type: :object, + properties: %{ + transactions_count: %Schema{type: :integer, nullable: false}, + token_transfers_count: %Schema{type: :integer, nullable: false}, + token_balances_count: %Schema{type: :integer, nullable: false}, + logs_count: %Schema{type: :integer, nullable: false}, + withdrawals_count: %Schema{type: :integer, nullable: false}, + internal_transactions_count: %Schema{type: :integer, nullable: false}, + validations_count: %Schema{type: :integer, nullable: false}, + celo_election_rewards_count: %Schema{type: :integer, nullable: false} + } + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex new file mode 100644 index 000000000000..573d92abe9b7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex @@ -0,0 +1,20 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do + @moduledoc """ + This module defines the schema for the TokenBalance struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{General, Token, TokenInstance} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + value: General.IntegerString, + token: %Schema{allOf: [Token], nullable: true}, + token_id: General.IntegerStringNullable, + token_instance: %Schema{allOf: [TokenInstance], nullable: true} + }, + required: [:value, :token, :token_id, :token_instance] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex new file mode 100644 index 000000000000..398e13e5c61b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -0,0 +1,330 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do + @moduledoc false + alias BlockScoutWeb.API.V2.ZkSyncView + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias OpenApiSpex.Schema + + @zksync_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + batch_number: %Schema{type: :integer, nullable: true}, + status: %Schema{ + type: :string, + enum: ZkSyncView.batch_status_enum(), + nullable: false + }, + commit_transaction_hash: General.FullHashNullable, + commit_transaction_timestamp: General.TimestampNullable, + prove_transaction_hash: General.FullHashNullable, + prove_transaction_timestamp: General.TimestampNullable, + execute_transaction_hash: General.FullHashNullable, + execute_transaction_timestamp: General.TimestampNullable + }, + required: [ + :batch_number, + :status, + :commit_transaction_hash, + :commit_transaction_timestamp, + :prove_transaction_hash, + :prove_transaction_timestamp, + :execute_transaction_hash, + :execute_transaction_timestamp + ] + } + + @arbitrum_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + batch_number: %Schema{type: :integer, nullable: true}, + batch_data_container: %Schema{ + type: :string, + nullable: true, + enum: ["in_blob4844", "in_calldata", "in_celestia", "in_anytrust"] + }, + status: %Schema{ + type: :string, + enum: ["Confirmed on base", "Sent to base", "Sealed on rollup", "Processed on rollup"], + nullable: false + }, + commitment_transaction: %Schema{ + type: :object, + nullable: false, + properties: %{ + hash: General.FullHashNullable, + timestamp: General.TimestampNullable, + status: %Schema{type: :string, enum: ["unfinalized", "finalized"], nullable: true} + }, + required: [:hash, :timestamp, :status] + }, + confirmation_transaction: %Schema{ + type: :object, + nullable: false, + properties: %{ + hash: General.FullHashNullable, + timestamp: General.TimestampNullable, + status: %Schema{type: :string, enum: ["unfinalized", "finalized"], nullable: true} + }, + required: [:hash, :timestamp, :status] + }, + delayed_messages: %Schema{type: :integer, nullable: false}, + l1_block_number: %Schema{type: :integer, nullable: true}, + send_count: %Schema{type: :integer, nullable: true}, + send_root: General.FullHashNullable + }, + required: [:batch_number, :status, :commitment_transaction, :confirmation_transaction] + } + + @blob4844_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + hash: General.FullHashNullable, + l1_transaction_hash: General.FullHashNullable, + l1_timestamp: General.TimestampNullable + }, + required: [:hash, :l1_transaction_hash, :l1_timestamp] + } + + @celestia_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + height: %Schema{type: :integer, nullable: true}, + namespace: %Schema{type: :string, nullable: true}, + commitment: %Schema{type: :string, nullable: true}, + l1_transaction_hash: General.FullHashNullable, + l1_timestamp: General.TimestampNullable + }, + required: [:height, :namespace, :commitment, :l1_transaction_hash, :l1_timestamp] + } + + @optimism_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + number: %Schema{type: :integer, nullable: true}, + l1_timestamp: General.TimestampNullable, + l1_transaction_hashes: %Schema{type: :array, items: General.FullHash, nullable: false}, + batch_data_container: %Schema{type: :string, nullable: false, enum: ["in_blob4844", "in_celestia", "in_calldata"]}, + blobs: %Schema{type: :array, items: %Schema{anyOf: [@blob4844_schema, @celestia_schema]}, nullable: false} + }, + required: [:number, :l1_timestamp, :l1_transaction_hashes, :batch_data_container] + } + + @celo_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + is_epoch_block: %Schema{type: :boolean, nullable: false}, + epoch_number: %Schema{type: :integer, nullable: false}, + base_fee: %Schema{ + type: :object, + nullable: true, + properties: %{ + recipient: Address, + amount: General.IntegerString, + token: Token, + breakdown: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + address: Address, + amount: General.IntegerString, + percentage: %Schema{type: :number, nullable: false} + }, + required: [:address, :amount, :percentage] + }, + nullable: false + } + }, + required: [:recipient, :amount, :token, :breakdown] + } + }, + required: [:is_epoch_block, :epoch_number] + } + + @zilliqa_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + view: %Schema{type: :integer, nullable: true}, + quorum_certificate: %Schema{ + type: :object, + nullable: false, + properties: %{ + view: %Schema{type: :integer, nullable: false}, + signature: General.HexString, + signers: %Schema{type: :array, items: %Schema{type: :integer, nullable: false}, nullable: false} + }, + required: [:view, :signature, :signers] + }, + aggregate_quorum_certificate: %Schema{ + type: :object, + nullable: false, + properties: %{ + view: %Schema{type: :integer, nullable: false}, + signature: General.HexString, + signers: %Schema{type: :array, items: %Schema{type: :integer, nullable: false}, nullable: false}, + nested_quorum_certificates: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + view: %Schema{type: :integer, nullable: false}, + signature: General.HexString, + proposed_by_validator_index: %Schema{type: :integer, nullable: false}, + signers: %Schema{type: :array, items: %Schema{type: :integer, nullable: false}, nullable: false} + }, + required: [:view, :signature, :proposed_by_validator_index, :signers] + }, + nullable: false + } + }, + required: [:view, :signature, :signers, :nested_quorum_certificates] + } + }, + required: [:view] + } + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :rsk -> + schema + |> put_in([:properties, :minimum_gas_price], General.IntegerString) + |> put_in([:properties, :bitcoin_merged_mining_header], General.HexString) + |> put_in([:properties, :bitcoin_merged_mining_coinbase_transaction], General.HexString) + |> put_in([:properties, :bitcoin_merged_mining_merkle_proof], General.HexString) + |> put_in([:properties, :hash_for_merged_mining], General.FullHash) + + :optimism -> + schema + |> put_in([:properties, :optimism], @optimism_schema) + + :zksync -> + schema + |> put_in([:properties, :zksync], @zksync_schema) + + :arbitrum -> + schema + |> put_in([:properties, :arbitrum], @arbitrum_schema) + + :ethereum -> + schema + |> put_in([:properties, :blob_transactions_count], %Schema{type: :integer, nullable: false}) + |> put_in([:properties, :blob_gas_used], General.IntegerStringNullable) + |> put_in([:properties, :excess_blob_gas], General.IntegerStringNullable) + |> put_in([:properties, :blob_gas_price], General.IntegerString) + |> put_in([:properties, :burnt_blob_fees], General.IntegerString) + |> update_in([:required], &([:blob_transactions_count, :blob_gas_used, :excess_blob_gas] ++ &1)) + + :celo -> + schema + |> put_in([:properties, :celo], @celo_schema) + |> update_in([:required], &[:celo | &1]) + + :zilliqa -> + schema + |> put_in([:properties, :zilliqa], @zilliqa_schema) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Block do + @moduledoc """ + This module defines the schema for the Block struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, Block.ChainTypeCustomizations, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + type: :object, + properties: %{ + height: %Schema{type: :integer, nullable: false}, + timestamp: General.Timestamp, + transactions_count: %Schema{type: :integer, nullable: false}, + internal_transactions_count: %Schema{type: :integer, nullable: true}, + miner: Address, + size: %Schema{type: :integer, nullable: false}, + hash: General.FullHash, + parent_hash: General.FullHash, + difficulty: General.IntegerString, + total_difficulty: General.IntegerString, + gas_used: General.IntegerString, + gas_limit: General.IntegerString, + nonce: General.HexStringNullable, + base_fee_per_gas: General.IntegerStringNullable, + burnt_fees: General.IntegerStringNullable, + priority_fee: General.IntegerStringNullable, + uncles_hashes: %Schema{ + type: :array, + items: %Schema{type: :object, properties: %{hash: General.FullHash}, required: [:hash], nullable: false}, + nullable: false + }, + rewards: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + type: %Schema{type: :string, nullable: false}, + reward: General.IntegerString + }, + required: [:type, :reward] + }, + nullable: false + }, + gas_target_percentage: %Schema{type: :number, format: :float, nullable: false}, + gas_used_percentage: %Schema{type: :number, format: :float, nullable: false}, + burnt_fees_percentage: %Schema{type: :number, format: :float, nullable: true}, + type: %Schema{type: :string, nullable: false, enum: ["block", "uncle", "reorg"]}, + transaction_fees: General.IntegerString, + withdrawals_count: %Schema{type: :integer, nullable: true} + }, + required: [ + :height, + :timestamp, + :transactions_count, + :internal_transactions_count, + :miner, + :size, + :hash, + :parent_hash, + :difficulty, + :total_difficulty, + :gas_used, + :gas_limit, + :nonce, + :base_fee_per_gas, + :burnt_fees, + :priority_fee, + :uncles_hashes, + :rewards, + :gas_target_percentage, + :gas_used_percentage, + :burnt_fees_percentage, + :type, + :transaction_fees, + :withdrawals_count + ] + } + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex new file mode 100644 index 000000000000..c272bbd956fb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex @@ -0,0 +1,29 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward do + @moduledoc """ + This module defines the schema for the CeloElectionReward struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, Celo.ElectionReward.Type, General, Token} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + block_number: %Schema{type: :integer, nullable: false}, + block_timestamp: General.Timestamp, + block_hash: General.FullHash, + account: Address, + associated_account: Address, + amount: General.IntegerString, + type: Type, + epoch_number: %Schema{type: :integer, nullable: false}, + token: Token + }, + required: [ + :amount, + :account, + :associated_account + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex new file mode 100644 index 000000000000..7a4e6f59599e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex @@ -0,0 +1,8 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type do + @moduledoc false + require OpenApiSpex + + alias Explorer.Chain.Celo.ElectionReward + + OpenApiSpex.schema(%{type: :string, nullable: false, enum: ElectionReward.types()}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex new file mode 100644 index 000000000000..bc7e297317a4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.Schemas.API.V2.CoinBalance do + @moduledoc """ + This module defines the schema for the CoinBalance struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + transaction_hash: General.FullHashNullable, + block_number: %Schema{type: :integer, nullable: false}, + delta: General.IntegerString, + value: General.IntegerString, + block_timestamp: General.Timestamp + }, + required: [ + :transaction_hash, + :block_number, + :delta, + :value, + :block_timestamp + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex new file mode 100644 index 000000000000..56b3885bcced --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.CoinBalanceByDay do + @moduledoc """ + This module defines the schema for the CoinBalanceByDay struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + date: %Schema{type: :string, format: :date, nullable: false}, + value: General.IntegerString + }, + required: [:date, :value] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex new file mode 100644 index 000000000000..d07b02ef6c08 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex @@ -0,0 +1,29 @@ +defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do + @moduledoc """ + This module contains the schemas for the error responses. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + defmodule ForbiddenResponse do + @moduledoc false + OpenApiSpex.schema(%{ + title: "ForbiddenResponse", + description: "Response returned when the user is not authorized to access the resource", + type: :object, + properties: %{ + message: %Schema{ + type: :string, + description: "Error message indicating the user is not authorized to access the resource", + example: "Restricted access" + } + } + }) + + @spec response() :: {String.t(), String.t(), module()} + def response do + {"Forbidden", "application/json", __MODULE__} + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex new file mode 100644 index 000000000000..46564d91ca2f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -0,0 +1,691 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General do + @moduledoc """ + This module defines the schema for general types used in the API. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type, as: CeloElectionRewardType + alias BlockScoutWeb.Schemas.API.V2.Token.Type, as: TokenType + alias OpenApiSpex.{Parameter, Schema} + + defmodule AddressHash do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{40})$", nullable: false}) + end + + defmodule AddressHashNullable do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{40})$", nullable: true}) + end + + defmodule FullHash do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: false}) + end + + defmodule FullHashNullable do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: true}) + end + + defmodule HexString do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]*)$", nullable: false}) + end + + defmodule HexStringNullable do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]*)$", nullable: true}) + end + + defmodule ProxyType do + @moduledoc false + alias Ecto.Enum + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + + OpenApiSpex.schema(%{ + type: :string, + enum: Enum.values(Implementation, :proxy_type), + nullable: true + }) + end + + defmodule Implementation.ChainTypeCustomizations do + @moduledoc false + alias OpenApiSpex.Schema + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + schema + |> put_in( + [:properties, :filecoin_robust_address], + %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + ) + |> update_in([:required], &[:filecoin_robust_address | &1]) + + _ -> + schema + end + end + end + + defmodule Implementation do + @moduledoc false + require OpenApiSpex + + alias Implementation.ChainTypeCustomizations + + OpenApiSpex.schema( + %{ + description: "Proxy smart contract implementation", + type: :object, + properties: %{ + address_hash: AddressHash, + name: %Schema{type: :string, nullable: true} + }, + required: [:address_hash, :name] + } + |> ChainTypeCustomizations.chain_type_fields() + ) + end + + defmodule Tag do + @moduledoc false + OpenApiSpex.schema(%{ + description: "Address tag struct", + type: :object, + properties: %{ + address_hash: AddressHash, + display_name: %Schema{type: :string, nullable: false}, + label: %Schema{type: :string, nullable: false} + }, + required: [:address_hash, :display_name, :label] + }) + end + + defmodule WatchlistName do + @moduledoc false + OpenApiSpex.schema(%{ + description: "Watch list name struct", + type: :object, + properties: %{ + display_name: %Schema{type: :string, nullable: false}, + label: %Schema{type: :string, nullable: false} + }, + required: [:display_name, :label] + }) + end + + defmodule FloatString do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^([1-9][0-9]*|0)(\.[0-9]+)?$"}) + end + + defmodule FloatStringNullable do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^([1-9][0-9]*|0)(\.[0-9]+)?$", nullable: true}) + end + + defmodule IntegerStringNullable do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^-?([1-9][0-9]*|0)$", nullable: true}) + end + + defmodule IntegerString do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^-?([1-9][0-9]*|0)$", nullable: false}) + end + + defmodule URLNullable do + @moduledoc false + OpenApiSpex.schema(%{ + type: :string, + format: :uri, + example: "https://example.com", + nullable: true + }) + end + + defmodule Timestamp do + @moduledoc false + OpenApiSpex.schema(%{ + type: :string, + format: :"date-time", + nullable: false + }) + end + + defmodule TimestampNullable do + @moduledoc false + OpenApiSpex.schema(%{ + type: :string, + format: :"date-time", + nullable: true + }) + end + + defmodule DecodedInput do + @moduledoc false + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + method_id: %Schema{type: :string, nullable: true}, + method_call: %Schema{type: :string, nullable: true}, + parameters: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + type: %Schema{type: :string, nullable: false}, + value: %Schema{ + anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + nullable: false + } + }, + nullable: false + } + } + }, + required: [:method_id, :method_call, :parameters], + nullable: false + }) + end + + defmodule MethodNameNullable do + @moduledoc false + OpenApiSpex.schema(%{ + type: :string, + nullable: true, + example: "transfer", + description: "Method name or hex method id" + }) + end + + defmodule DecodedLogInput do + @moduledoc false + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + method_id: %Schema{type: :string, nullable: true}, + method_call: %Schema{type: :string, nullable: true}, + parameters: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + type: %Schema{type: :string, nullable: false}, + indexed: %Schema{type: :boolean, nullable: false}, + value: %Schema{ + anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + nullable: false + } + }, + required: [:name, :type, :indexed, :value], + nullable: false + }, + nullable: false + } + }, + required: [:method_id, :method_call, :parameters], + nullable: false + }) + end + + defmodule EmptyString do + @moduledoc false + OpenApiSpex.schema(%{type: :string, minLength: 0, maxLength: 0}) + end + + defmodule NullString do + @moduledoc false + OpenApiSpex.schema(%{type: :string, pattern: ~r"^null$"}) + end + + @doc """ + Returns a parameter definition for an address hash in the path. + """ + @spec address_hash_param() :: Parameter.t() + def address_hash_param do + %Parameter{ + name: :address_hash_param, + in: :path, + schema: AddressHash, + required: true + } + end + + @doc """ + Returns a parameter definition for filtering transactions by direction (to/from). + """ + @spec direction_filter_param() :: Parameter.t() + def direction_filter_param do + %Parameter{ + name: :filter, + in: :query, + schema: %Schema{type: :string, enum: ["to", "from"]}, + required: false, + description: """ + Filter transactions by direction: + * to - Only show transactions sent to this address + * from - Only show transactions sent from this address + If omitted, all transactions involving the address are returned. + """ + } + end + + @doc """ + Returns a parameter definition for sorting transactions by specified fields. + """ + @spec sort_param([String.t()]) :: Parameter.t() + def sort_param(sort_fields) do + %Parameter{ + name: :sort, + in: :query, + schema: %Schema{ + type: :string, + enum: sort_fields + }, + required: false, + description: """ + Sort transactions by: + * block_number - Sort by block number + * value - Sort by transaction value + * fee - Sort by transaction fee + Should be used together with `order` parameter. + """ + } + end + + @doc """ + Returns a parameter definition for sorting order (asc/desc). + """ + @spec order_param() :: Parameter.t() + def order_param do + %Parameter{ + in: :query, + schema: %Schema{ + type: :string, + enum: ["asc", "desc"] + }, + required: false, + description: """ + Sort order: + * asc - Ascending order + * desc - Descending order + Should be used together with `sort` parameter. + """, + name: :order + } + end + + @doc """ + Returns a parameter definition for filtering by token type. + """ + @spec token_type_param() :: Parameter.t() + def token_type_param do + %Parameter{ + in: :query, + schema: %Schema{ + anyOf: [ + EmptyString, + %Schema{ + type: :string, + pattern: ~r"^(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*$" + } + ] + }, + required: false, + description: """ + Filter by token type. Comma-separated list of: + * ERC-20 - Fungible tokens + * ERC-721 - Non-fungible tokens + * ERC-1155 - Multi-token standard + * ERC-404 - Hybrid fungible/non-fungible tokens + + Example: `ERC-20,ERC-721` to show both fungible and NFT transfers + """, + name: :type + } + end + + @doc """ + Returns a parameter definition for filtering by NFT token type. + """ + @spec nft_token_type_param() :: Parameter.t() + def nft_token_type_param do + %Parameter{ + in: :query, + schema: %Schema{ + anyOf: [ + EmptyString, + %Schema{ + type: :string, + pattern: ~r"^(ERC-721|ERC-1155|ERC-404)(,(ERC-721|ERC-1155|ERC-404))*$" + } + ] + }, + required: false, + description: """ + Filter by token type. Comma-separated list of: + * ERC-721 - Non-fungible tokens + * ERC-1155 - Multi-token standard + * ERC-404 - Hybrid fungible/non-fungible tokens + + Example: `ERC-721,ERC-1155` to show both NFT and multi-token transfers + """, + name: :type + } + end + + @doc """ + Returns a parameter definition for filtering logs by topic. + """ + @spec topic_param() :: Parameter.t() + def topic_param do + %Parameter{ + in: :query, + schema: HexString, + required: false, + description: "Filter logs by topic", + name: :topic + } + end + + @doc """ + Returns a parameter definition for filtering token transfers by token contract address. + """ + @spec token_filter_param() :: Parameter.t() + def token_filter_param do + %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Filter token transfers by token contract address.", + name: :token + } + end + + @doc """ + Returns a parameter definition for API key used in rate limiting. + """ + @spec api_key_param() :: Parameter.t() + def api_key_param do + %Parameter{ + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "API key for rate limiting", + name: :api_key + } + end + + @doc """ + Returns a parameter definition for secret key used to access restricted resources. + """ + @spec key_param() :: Parameter.t() + def key_param do + %Parameter{ + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Secret key for getting access to restricted resources", + name: :key + } + end + + @doc """ + Returns a list of base parameters (api_key and key). + """ + @spec base_params() :: [Parameter.t()] + def base_params do + [api_key_param(), key_param()] + end + + @doc """ + Returns a schema definition for paginated response. + """ + @spec paginated_response(Keyword.t()) :: Schema.t() + def paginated_response(options) do + items_schema = Keyword.fetch!(options, :items) + next_page_params_example = Keyword.fetch!(options, :next_page_params_example) + title_prefix = Keyword.fetch!(options, :title_prefix) + + %Schema{ + title: "#{title_prefix}PaginatedResponse", + type: :object, + properties: %{ + items: %Schema{type: :array, items: items_schema, nullable: false}, + next_page_params: %Schema{ + type: :object, + nullable: true, + example: next_page_params_example + } + }, + required: [:items, :next_page_params], + nullable: false + } + end + + # `%Schema{anyOf: [%Schema{type: :integer}, EmptyString]}` is used because, + # `allowEmptyValue: true` does not allow empty string for some reasons (at least in this case) + + @paging_params %{ + "block_number" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Block number for paging", + name: :block_number + }, + "block_number_nullable" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, + required: false, + description: "Block number for paging", + name: :block_number + }, + "block_number_no_casting" => %Parameter{ + in: :query, + schema: IntegerString, + required: false, + description: "Block number for paging", + name: :block_number + }, + "epoch_number" => %Parameter{ + in: :query, + schema: IntegerString, + required: false, + description: "Epoch number for paging", + name: :epoch_number + }, + "index" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Transaction index for paging", + name: :index + }, + "index_nullable" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, + required: false, + description: "Transaction index for paging", + name: :index + }, + "inserted_at" => %Parameter{ + in: :query, + schema: %Schema{type: :string, format: :"date-time"}, + required: false, + description: "Inserted at timestamp for paging (ISO8601)", + name: :inserted_at + }, + "hash" => %Parameter{ + in: :query, + schema: FullHash, + required: false, + description: "Transaction hash for paging", + name: :hash + }, + # TODO: consider refactoring, to avoid ambiguity with hash param (the same name) + "address_hash" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Address hash for paging", + name: :hash + }, + "value" => %Parameter{ + in: :query, + schema: IntegerString, + required: false, + description: "Transaction value for paging", + name: :value + }, + "fee" => %Parameter{ + in: :query, + schema: IntegerString, + required: false, + description: "Transaction fee for paging", + name: :fee + }, + "items_count" => %Parameter{ + in: :query, + schema: %Schema{type: :integer, minimum: 1, maximum: 50}, + required: false, + description: "Number of items returned per page", + name: :items_count + }, + "batch_log_index" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Batch log index for paging", + name: :batch_log_index + }, + "batch_block_hash" => %Parameter{ + in: :query, + schema: FullHash, + required: false, + description: "Batch block hash for paging", + name: :batch_block_hash + }, + "batch_transaction_hash" => %Parameter{ + in: :query, + schema: FullHash, + required: false, + description: "Batch transaction hash for paging", + name: :batch_transaction_hash + }, + "index_in_batch" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Index in batch for paging", + name: :index_in_batch + }, + "transaction_index" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Transaction index for paging", + name: :transaction_index + }, + "fiat_value_nullable" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, + required: false, + description: "Fiat value for paging", + name: :fiat_value + }, + "id" => %Parameter{ + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "ID for paging", + name: :id + }, + "fetched_coin_balance" => %Parameter{ + in: :query, + schema: IntegerStringNullable, + required: false, + description: "Fetched coin balance for paging", + name: :fetched_coin_balance + }, + "transactions_count" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, + required: false, + description: "Transactions count for paging", + name: :transactions_count + }, + "token_contract_address_hash" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Token contract address hash for paging", + name: :token_contract_address_hash + }, + "token_id" => %Parameter{ + in: :query, + schema: IntegerStringNullable, + required: false, + description: "Token ID for paging", + name: :token_id + }, + "token_type" => %Parameter{ + in: :query, + schema: TokenType, + required: false, + description: "Token type for paging", + name: :token_type + }, + "amount" => %Parameter{ + in: :query, + schema: IntegerStringNullable, + required: false, + description: "Amount for paging", + name: :amount + }, + "associated_account_address_hash" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Associated account address hash for paging", + name: :associated_account_address_hash + }, + "type" => %Parameter{ + in: :query, + schema: CeloElectionRewardType, + required: false, + description: "Type for paging", + name: :type + } + } + + @doc """ + Returns a list of paging parameters based on the provided field names. + """ + @spec define_paging_params([String.t()]) :: [Parameter.t()] + def define_paging_params(fields) do + Enum.map(fields, fn field -> + Map.get(@paging_params, field) || raise "Unknown paging param: #{field}" + end) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex new file mode 100644 index 000000000000..776e54635337 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex @@ -0,0 +1,70 @@ +defmodule BlockScoutWeb.Schemas.API.V2.InternalTransaction do + @moduledoc """ + This module defines the schema for the InternalTransaction struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + error: %Schema{ + type: :string, + nullable: true + }, + success: %Schema{ + type: :boolean, + nullable: false + }, + type: %Schema{ + type: :string, + nullable: false, + description: "Type of the internal transaction (call, create, etc.)" + }, + transaction_hash: General.FullHash, + transaction_index: %Schema{ + type: :integer, + nullable: false, + description: "The index of the parent transaction inside the block." + }, + from: Address, + to: Address, + created_contract: %Schema{allOf: [Address], nullable: true}, + value: General.IntegerString, + block_number: %Schema{ + type: :integer, + nullable: false + }, + timestamp: General.Timestamp, + index: %Schema{ + type: :integer, + description: "The index of this internal transaction inside the transaction.", + nullable: false + }, + gas_limit: General.IntegerStringNullable, + block_index: %Schema{ + type: :integer, + description: "The index of this internal transaction inside the block.", + nullable: false + } + }, + required: [ + :error, + :success, + :type, + :transaction_hash, + :transaction_index, + :from, + :to, + :created_contract, + :value, + :block_number, + :timestamp, + :index, + :gas_limit, + :block_index + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex new file mode 100644 index 000000000000..72cffc212253 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex @@ -0,0 +1,35 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Log do + @moduledoc """ + This module defines the schema for the Log struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + transaction_hash: General.FullHash, + address: Address, + topics: %Schema{type: :array, items: General.HexStringNullable, nullable: false}, + data: General.HexString, + index: %Schema{type: :integer, nullable: false}, + decoded: %Schema{allOf: [General.DecodedInput], nullable: true}, + smart_contract: %Schema{oneOf: [Address], nullable: true}, + block_hash: General.FullHash, + block_number: %Schema{type: :integer, nullable: false} + }, + required: [ + :transaction_hash, + :address, + :topics, + :data, + :index, + :decoded, + :smart_contract, + :block_hash, + :block_number + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex new file mode 100644 index 000000000000..6be7a4ca698e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex @@ -0,0 +1,37 @@ +defmodule BlockScoutWeb.Schemas.API.V2.NFTCollection do + @moduledoc """ + This module defines the schema for the NFTCollection struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{General, Token, Token.Type, TokenInstance} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + token: Token, + amount: General.IntegerStringNullable, + token_instances: %Schema{ + type: :array, + items: %Schema{ + allOf: [ + TokenInstance, + %Schema{ + type: :object, + nullable: false, + properties: %{ + token_type: Type, + value: General.IntegerStringNullable + }, + required: [:token_type, :value] + } + ], + nullable: true + }, + nullable: false + } + }, + required: [:token, :amount, :token_instances] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex new file mode 100644 index 000000000000..9d0503121aa3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.Metadata do + @moduledoc """ + This module defines the schema for the Metadata struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Proxy.MetadataTag + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Metadata struct", + type: :object, + properties: %{ + tags: %Schema{description: "Metadata tags linked with the address", type: :array, items: MetadataTag} + }, + required: [:tags] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex new file mode 100644 index 000000000000..04072169cda8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.MetadataTag do + @moduledoc """ + This module defines the schema for the MetadataTag struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Metadata tag struct", + type: :object, + properties: %{ + slug: %Schema{type: :string, nullable: false}, + name: %Schema{type: :string, nullable: false}, + tagType: %Schema{type: :string, nullable: false}, + ordinal: %Schema{type: :integer, nullable: false}, + meta: %Schema{type: :object, nullable: false} + }, + required: [:slug, :name, :tagType, :ordinal, :meta] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex new file mode 100644 index 000000000000..41b587bd4837 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.Schemas.API.V2.SignedAuthorization do + @moduledoc """ + This module defines the schema for the SignedAuthorization struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + address_hash: General.AddressHash, + chain_id: %Schema{type: :integer, nullable: false}, + nonce: General.IntegerString, + r: General.IntegerString, + s: General.IntegerString, + v: %Schema{type: :integer, nullable: false}, + authority: General.AddressHash + }, + required: [:address_hash, :chain_id, :nonce, :r, :s, :v, :authority] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex new file mode 100644 index 000000000000..c5d2f578b0b3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -0,0 +1,101 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Token.ChainTypeCustomizations do + @moduledoc false + require OpenApiSpex + + alias Explorer.Chain.BridgedToken + alias OpenApiSpex.Schema + + @filecoin_robust_address_schema %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + schema + |> put_in([:properties, :filecoin_robust_address], @filecoin_robust_address_schema) + |> update_in([:required], &[:filecoin_robust_address | &1]) + + _ -> + schema + end + end + + def maybe_append_bridged_info(schema) do + if BridgedToken.enabled?() do + schema + |> put_in([:properties, :is_bridged], %Schema{type: :boolean, nullable: false}) + |> update_in([:required], &[:is_bridged | &1]) + else + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Token do + @moduledoc """ + This module defines the schema for the Token struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.API.V2.Token.{ChainTypeCustomizations, Type} + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + description: "Token struct", + type: :object, + properties: %{ + address: General.AddressHash, + symbol: %Schema{type: :string, nullable: false}, + name: %Schema{type: :string, nullable: false}, + decimals: General.IntegerStringNullable, + type: %Schema{ + anyOf: [Type], + nullable: true + }, + holders: General.IntegerStringNullable, + exchange_rate: General.FloatStringNullable, + volume_24h: General.FloatStringNullable, + total_supply: General.IntegerStringNullable, + icon_url: General.URLNullable, + circulating_market_cap: General.FloatStringNullable + }, + required: [ + :address, + :symbol, + :name, + :decimals, + :type, + :holders, + :exchange_rate, + :volume_24h, + :total_supply, + :icon_url, + :circulating_market_cap + ] + } + |> ChainTypeCustomizations.chain_type_fields() + |> ChainTypeCustomizations.maybe_append_bridged_info() + ) +end + +defmodule BlockScoutWeb.Schemas.API.V2.Token.Type do + @moduledoc """ + This module defines the schema for the Token type. + """ + require OpenApiSpex + + OpenApiSpex.schema(%{ + type: :string, + enum: [ + "ERC-20", + "ERC-721", + "ERC-1155", + "ERC-404" + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex new file mode 100644 index 000000000000..58214a8c2fe0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex @@ -0,0 +1,58 @@ +defmodule BlockScoutWeb.Schemas.API.V2.TokenInstance do + @moduledoc """ + This module defines the schema for the TokenInstance struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + id: General.IntegerString, + metadata: %Schema{ + type: :object, + nullable: true, + example: %{"name" => "Test", "description" => "Test", "image" => "https://example.com/image.png"} + }, + owner: %Schema{allOf: [Address], nullable: true}, + token: %Schema{allOf: [Token], nullable: true}, + external_app_url: General.URLNullable, + animation_url: General.URLNullable, + image_url: General.URLNullable, + is_unique: %Schema{type: :boolean, nullable: true}, + thumbnails: %Schema{ + type: :object, + properties: %{ + "500x500" => %Schema{type: :string, format: :uri}, + "250x250" => %Schema{type: :string, format: :uri}, + "60x60" => %Schema{type: :string, format: :uri}, + "original" => %Schema{type: :string, format: :uri} + }, + required: ["original"], + nullable: true + }, + media_type: %Schema{ + type: :string, + example: "image/png", + description: "Mime type of the media in media_url", + nullable: true + }, + media_url: General.URLNullable + }, + required: [ + :id, + :metadata, + :owner, + :token, + :external_app_url, + :animation_url, + :image_url, + :is_unique, + :thumbnails, + :media_type, + :media_url + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex new file mode 100644 index 000000000000..9b0ef8c4e6ba --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.Schemas.API.V2.TokenInstanceInList do + @moduledoc """ + This module defines the schema for the TokenInstanceInList struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{General, Token.Type, TokenInstance} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + allOf: [ + TokenInstance, + %Schema{ + type: :object, + nullable: false, + properties: %{ + token_type: Type, + value: General.IntegerStringNullable + }, + required: [:token_type, :value] + } + ], + type: :object + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex new file mode 100644 index 000000000000..d87d2499754f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -0,0 +1,99 @@ +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do + @moduledoc """ + Schema for token transfer + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.API.V2.TokenTransfer.{Total, TotalERC1155, TotalERC721} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + transaction_hash: General.FullHash, + from: Address, + to: Address, + total: %Schema{ + anyOf: [ + TotalERC721, + TotalERC1155, + Total + ], + nullable: true + }, + token: Token, + type: %Schema{type: :string, enum: ["token_burning", "token_minting", "token_spawning", "token_transfer"]}, + timestamp: General.TimestampNullable, + method: General.MethodNameNullable, + block_hash: General.FullHash, + block_number: %Schema{type: :integer, nullable: false}, + log_index: %Schema{type: :integer, nullable: false} + }, + required: [ + :transaction_hash, + :from, + :to, + :total, + :token, + :type, + :timestamp, + :method, + :block_hash, + :block_number, + :log_index + ] + }) +end + +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TotalERC721 do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{General, TokenInstance} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + token_id: General.IntegerStringNullable, + token_instance: %Schema{type: :object, anyOf: [TokenInstance], nullable: true} + }, + required: [:token_id, :token_instance] + }) +end + +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TotalERC1155 do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{General, TokenInstance} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + token_id: General.IntegerStringNullable, + value: General.IntegerStringNullable, + decimals: General.IntegerStringNullable, + token_instance: %Schema{type: :object, anyOf: [TokenInstance], nullable: true} + }, + required: [:token_id, :value, :decimals, :token_instance] + }) +end + +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.Total do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + value: General.IntegerStringNullable, + decimals: General.IntegerStringNullable + }, + required: [:value, :decimals] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex new file mode 100644 index 000000000000..804730f15878 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -0,0 +1,474 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do + @moduledoc false + alias BlockScoutWeb.API.V2.ZkSyncView + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.API.V2.Transaction.Fee + alias OpenApiSpex.Schema + + @polygon_edge_deposit_withdrawal_schema %Schema{type: :object, nullable: true} + + @zksync_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + batch_number: %Schema{type: :integer, nullable: true}, + status: %Schema{ + type: :string, + enum: ZkSyncView.batch_status_enum(), + nullable: true + }, + commit_transaction_hash: General.FullHashNullable, + commit_transaction_timestamp: General.TimestampNullable, + prove_transaction_hash: General.FullHashNullable, + prove_transaction_timestamp: General.TimestampNullable, + execute_transaction_hash: General.FullHashNullable, + execute_transaction_timestamp: General.TimestampNullable + }, + required: [ + :batch_number, + :status, + :commit_transaction_hash, + :commit_transaction_timestamp, + :prove_transaction_hash, + :prove_transaction_timestamp, + :execute_transaction_hash, + :execute_transaction_timestamp + ] + } + + @arbitrum_commitment_transaction_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + hash: General.FullHashNullable, + timestamp: General.TimestampNullable, + status: %Schema{type: :string, enum: ["unfinalized", "finalized"], nullable: true} + }, + required: [:hash, :timestamp, :status] + } + + @arbitrum_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + batch_data_container: %Schema{type: :string, nullable: true}, + batch_number: %Schema{type: :integer, nullable: true}, + status: %Schema{ + type: :string, + enum: ["Confirmed on base", "Sent to base", "Sealed on rollup", "Processed on rollup"], + nullable: false + }, + commitment_transaction: @arbitrum_commitment_transaction_schema, + confirmation_transaction: @arbitrum_commitment_transaction_schema, + contains_message: %Schema{type: :string, enum: ["incoming", "outcoming"], nullable: true}, + message_related_info: %Schema{ + type: :object, + nullable: false, + properties: %{ + message_id: %Schema{type: :integer, nullable: false}, + associated_l1_transaction_hash: General.FullHashNullable, + message_status: %Schema{ + type: :string, + enum: [ + "Syncing with base layer", + "Relayed", + "Settlement pending", + "Waiting for confirmation", + "Ready for relay" + ], + nullable: false + } + } + }, + gas_used_for_l1: General.IntegerString, + gas_used_for_l2: General.IntegerString, + poster_fee: General.IntegerString, + network_fee: General.IntegerString + }, + required: [:gas_used_for_l1, :gas_used_for_l2, :poster_fee, :network_fee] + } + + @optimism_withdrawal_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + nonce: %Schema{type: :integer}, + status: %Schema{type: :string, nullable: false}, + l1_transaction_hash: General.FullHashNullable + }, + required: [:nonce, :status, :l1_transaction_hash] + } + + @scroll_schema %Schema{ + type: :object, + nullable: false, + properties: %{ + l1_fee_scalar: %Schema{type: :integer, nullable: true}, + l1_fee_commit_scalar: %Schema{type: :integer, nullable: true}, + l1_fee_blob_scalar: %Schema{type: :integer, nullable: true}, + l1_fee_overhead: %Schema{type: :integer, nullable: true}, + l1_base_fee: %Schema{type: :integer, nullable: true}, + l1_blob_base_fee: %Schema{type: :integer, nullable: true}, + l1_gas_used: %Schema{type: :integer, nullable: true}, + l2_fee: Fee, + l2_block_status: %Schema{ + type: :string, + enum: ["Committed", "Finalized", "Confirmed by Sequencer"], + nullable: false + }, + l1_fee: General.IntegerString, + queue_index: %Schema{type: :integer, nullable: false} + }, + required: [ + :l1_fee_scalar, + :l1_fee_commit_scalar, + :l1_fee_blob_scalar, + :l1_fee_overhead, + :l1_base_fee, + :l1_blob_base_fee, + :l1_gas_used, + :l2_fee, + :l2_block_status + ] + } + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + # credo:disable-for-next-line + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :polygon_edge -> + schema + |> put_in([:properties, :polygon_edge_deposit], @polygon_edge_deposit_withdrawal_schema) + |> put_in([:properties, :polygon_edge_withdrawal], @polygon_edge_deposit_withdrawal_schema) + + :polygon_zkevm -> + schema + |> put_in([:properties, :zkevm_batch_number], %Schema{type: :integer, nullable: true}) + |> put_in([:properties, :zkevm_sequence_hash], General.FullHash) + |> put_in([:properties, :zkevm_verify_hash], General.FullHash) + |> put_in([:properties, :zkevm_status], %Schema{ + type: :string, + enum: ["Confirmed by Sequencer", "L1 Confirmed"], + nullable: false + }) + + :zksync -> + schema + |> put_in([:properties, :zksync], @zksync_schema) + + :arbitrum -> + schema + |> put_in([:properties, :arbitrum], @arbitrum_schema) + + :optimism -> + schema + |> put_in([:properties, :l1_fee], General.IntegerString) + |> put_in([:properties, :l1_fee_scalar], General.IntegerString) + |> put_in([:properties, :l1_gas_price], General.IntegerString) + |> put_in([:properties, :l1_gas_used], General.IntegerString) + |> put_in([:properties, :op_withdrawals], %Schema{ + type: :array, + items: @optimism_withdrawal_schema, + nullable: false + }) + |> put_in([:properties, :op_interop], %Schema{ + type: :object, + nullable: false, + properties: %{ + nonce: %Schema{type: :integer}, + status: %Schema{type: :string, nullable: false, enum: ["Sent", "Relayed", "Failed"]}, + sender_address_hash: General.AddressHashNullable, + target_address_hash: General.AddressHashNullable, + payload: General.HexString, + relay_chain: %Schema{type: :object, nullable: true}, + relay_transaction_hash: General.FullHashNullable, + init_chain: %Schema{type: :object, nullable: true}, + init_transaction_hash: General.FullHashNullable + }, + required: [:nonce, :status, :sender_address_hash, :target_address_hash, :payload] + }) + + :scroll -> + schema + |> put_in([:properties, :scroll], @scroll_schema) + + :suave -> + schema + |> put_in([:properties, :allowed_peekers], %Schema{type: :array, items: General.AddressHash, nullable: false}) + |> put_in([:properties, :execution_node], Address) + |> put_in([:properties, :wrapped], %Schema{ + type: :object, + nullable: false, + properties: %{ + type: %Schema{type: :integer}, + nonce: %Schema{type: :integer, nullable: true}, + to: Address, + gas_limit: General.IntegerStringNullable, + gas_price: General.IntegerStringNullable, + fee: Fee, + max_priority_fee_per_gas: General.IntegerStringNullable, + max_fee_per_gas: General.IntegerStringNullable, + value: General.IntegerStringNullable, + hash: General.FullHash, + method: General.MethodNameNullable, + decoded_input: %Schema{allOf: [General.DecodedInput], nullable: true}, + raw_input: General.HexString + } + }) + + :stability -> + schema + |> put_in([:properties, :stability_fee], %Schema{ + type: :object, + nullable: false, + properties: %{ + token: Token, + validator_address: Address, + dapp_address: Address, + total_fee: General.IntegerString, + dapp_fee: General.IntegerString, + validator_fee: General.IntegerString + }, + required: [:token, :validator_address, :dapp_address, :total_fee, :dapp_fee, :validator_fee] + }) + + :ethereum -> + schema + |> put_in([:properties, :max_fee_per_blob_gas], General.IntegerString) + |> put_in([:properties, :blob_versioned_hashes], %Schema{type: :array, items: General.FullHash, nullable: false}) + |> put_in([:properties, :blob_gas_used], General.IntegerString) + |> put_in([:properties, :blob_gas_price], General.IntegerString) + |> put_in([:properties, :burnt_blob_fee], General.IntegerString) + + :celo -> + schema + |> put_in([:properties, :celo], %Schema{ + type: :object, + nullable: false, + properties: %{gas_token: %Schema{oneOf: [Token], nullable: true}}, + required: [:gas_token] + }) + |> update_in([:required], &[:celo | &1]) + + :zilliqa -> + schema + |> put_in([:properties, :zilliqa], %Schema{ + type: :object, + nullable: false, + properties: %{ + is_scilla: %Schema{type: :boolean, nullable: false} + }, + required: [:is_scilla] + }) + |> update_in([:required], &[:zilliqa | &1]) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Transaction do + @moduledoc """ + This module defines the schema for the Transaction struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General, SignedAuthorization, TokenTransfer} + alias BlockScoutWeb.Schemas.API.V2.Transaction.{ChainTypeCustomizations, Fee} + alias Explorer.Chain.TransactionAction + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + type: :object, + properties: %{ + hash: General.FullHash, + result: %Schema{ + anyOf: [ + %Schema{ + type: :string, + enum: ["pending", "awaiting_internal_transactions", "success", "dropped/replaced"] + }, + %Schema{ + type: :string, + description: "Error message", + example: "out of gas" + } + ], + nullable: false + }, + status: %Schema{ + type: :string, + enum: ["ok", "error"], + nullable: true + }, + block_number: %Schema{ + type: :integer, + nullable: true + }, + timestamp: General.TimestampNullable, + from: Address, + to: Address, + created_contract: %Schema{allOf: [Address], nullable: true}, + confirmations: %Schema{ + type: :integer, + minimum: 0 + }, + confirmation_duration: %Schema{ + type: :array, + items: %Schema{ + type: :integer, + minimum: 0, + description: "Duration in milliseconds" + }, + description: + "Array of time intervals in milliseconds. Can be empty [] (no info), single value [interval] (means that the transaction was confirmed within {interval} milliseconds), or two values [short_interval, long_interval] (means that the transaction's confirmation took from {short_interval} to {long_interval} milliseconds)", + example: [1000, 2000], + maxItems: 2 + }, + value: General.IntegerString, + fee: Fee, + gas_price: General.IntegerStringNullable, + type: %Schema{ + type: :integer, + nullable: true + }, + gas_used: General.IntegerStringNullable, + gas_limit: General.IntegerString, + max_fee_per_gas: General.IntegerStringNullable, + max_priority_fee_per_gas: General.IntegerStringNullable, + base_fee_per_gas: General.IntegerStringNullable, + priority_fee: General.IntegerStringNullable, + transaction_burnt_fee: General.IntegerStringNullable, + nonce: %Schema{ + type: :integer, + nullable: false, + minimum: 0 + }, + position: %Schema{ + type: :integer, + nullable: true, + minimum: 0 + }, + revert_reason: %Schema{ + oneOf: [ + General.DecodedInput, + %Schema{ + type: :object, + properties: %{raw: %Schema{anyOf: [General.HexString, %Schema{type: :string}], nullable: true}}, + required: [:raw], + nullable: false + } + ], + nullable: true + }, + raw_input: General.HexString, + decoded_input: %Schema{allOf: [General.DecodedInput], nullable: true}, + token_transfers: %Schema{type: :array, items: TokenTransfer, nullable: true}, + token_transfers_overflow: %Schema{type: :boolean, nullable: true}, + actions: %Schema{ + type: :array, + items: %Schema{ + type: :object, + required: [:protocol, :type, :data], + properties: %{ + protocol: %Schema{ + type: :string, + enum: TransactionAction.supported_protocols(), + nullable: false + }, + type: %Schema{ + type: :string, + enum: TransactionAction.supported_types(), + nullable: false + }, + data: %Schema{ + type: :object, + description: "Transaction action details (json formatted)", + nullable: false + } + } + }, + nullable: true + }, + exchange_rate: General.FloatStringNullable, + historic_exchange_rate: General.FloatStringNullable, + method: General.MethodNameNullable, + transaction_types: %Schema{ + type: :array, + items: %Schema{ + type: :string, + enum: [ + "coin_transfer", + "contract_call", + "contract_creation", + "rootstock_bridge", + "rootstock_remasc", + "token_creation", + "token_transfer", + "blob_transaction", + "set_code_transaction" + ] + } + }, + transaction_tag: %Schema{ + type: :string, + nullable: true, + example: "personal", + description: "Transaction tag set in My Account" + }, + has_error_in_internal_transactions: %Schema{type: :boolean, nullable: true}, + authorization_list: %Schema{type: :array, items: SignedAuthorization, nullable: true} + }, + required: [ + :hash, + :result, + :status, + :block_number, + :timestamp, + :from, + :to, + :created_contract, + :confirmations, + :confirmation_duration, + :value, + :fee, + :gas_price, + :type, + :gas_used, + :gas_limit, + :max_fee_per_gas, + :max_priority_fee_per_gas, + :base_fee_per_gas, + :priority_fee, + :transaction_burnt_fee, + :nonce, + :position, + :revert_reason, + :raw_input, + :decoded_input, + :token_transfers, + :token_transfers_overflow, + :actions, + :exchange_rate, + :historic_exchange_rate, + :method, + :transaction_types, + :transaction_tag, + :has_error_in_internal_transactions, + :authorization_list + ] + } + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex new file mode 100644 index 000000000000..299809cdebec --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Fee do + @moduledoc """ + This module defines the schema for the Transaction fee. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + required: [:type, :value], + properties: %{ + type: %Schema{ + type: :string, + enum: ["maximum", "actual"] + }, + value: General.IntegerStringNullable + } + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex new file mode 100644 index 000000000000..f2a1e81d9c29 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Withdrawal do + @moduledoc """ + This module defines the schema for the Withdrawal struct. + """ + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + index: %Schema{type: :integer, nullable: false}, + validator_index: %Schema{type: :integer, nullable: false}, + amount: General.IntegerString, + block_number: %Schema{type: :integer, nullable: false}, + receiver: Address, + timestamp: General.Timestamp + }, + required: [ + :index, + :validator_index, + :amount + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex index 005e54bc67ca..f0645649e9ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex @@ -686,10 +686,10 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do # Map.get is only needed for the case when the module is compiled with # chain_type different from "arbitrum", `|| 0` is used to avoid nil values # for the transaction prior to the migration to Arbitrum specific BS build. - gas_used_for_l1 = Map.get(arbitrum_transaction, :gas_used_for_l1, 0) || 0 + gas_used_for_l1 = Map.get(arbitrum_transaction, :gas_used_for_l1) || Decimal.new(0) - gas_used = Map.get(arbitrum_transaction, :gas_used, 0) || 0 - gas_price = Map.get(arbitrum_transaction, :gas_price, 0) || 0 + gas_used = Map.get(arbitrum_transaction, :gas_used) || Decimal.new(0) + gas_price = Map.get(arbitrum_transaction, :gas_price) || %Wei{value: Decimal.new(0)} gas_used_for_l2 = gas_used diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 11837656a86e..c12b5f664617 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -111,6 +111,7 @@ defmodule BlockScoutWeb.API.V2.Helper do "hash" => Address.checksum(address_hash), "is_contract" => false, "name" => nil, + "is_scam" => false, "proxy_type" => nil, "implementations" => [], "is_verified" => nil, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index b896ea82c037..43d178f240be 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -21,6 +21,7 @@ defmodule BlockScoutWeb.API.V2.TokenView do # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 "holders" => nil, "exchange_rate" => nil, + "volume_24h" => nil, "total_supply" => nil, "icon_url" => nil, "circulating_market_cap" => nil diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex index 03327049c479..25f6bd447294 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex @@ -204,4 +204,12 @@ defmodule BlockScoutWeb.API.V2.ZkSyncView do true -> "Processed on L2" end end + + @doc """ + Returns a list of possible batch statuses. + """ + @spec batch_status_enum() :: [String.t()] + def batch_status_enum do + ["Executed on L1", "Validated on L1", "Sent to L1", "Sealed on L2", "Processed on L2"] + end end diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 161fef847e7c..4a117d9a0017 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -161,7 +161,9 @@ defmodule BlockScoutWeb.Mixfile do {:ueberauth_auth0, "~> 2.0"}, {:utils, in_umbrella: true}, {:bureaucrat, "~> 0.2.9", only: :test}, - {:logger_json, "~> 5.1"} + {:logger_json, "~> 5.1"}, + {:open_api_spex, "~> 3.21"}, + {:ymlr, "~> 2.0"} ] end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs index bfb0e2c80d77..fe92511fe0a5 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs @@ -528,10 +528,10 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do expected_response = address_internal_transaction_path(BlockScoutWeb.Endpoint, :index, address.hash, %{ - "block_number" => number, - "index" => 11, - "transaction_index" => transaction_index, - "items_count" => "50" + block_number: number, + index: 11, + transaction_index: transaction_index, + items_count: "50" }) assert expected_response == json_response(conn, 200)["next_page_path"] diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 37fc5a3aba7f..525d981663e7 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -29,6 +29,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox + import OpenApiSpex.TestAssertions @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" @instances_amount_in_collection 9 @@ -84,13 +85,27 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end) request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") - check_response(correct_response, json_response(request, 200)) + json_response = json_response(request, 200) + check_response(correct_response, json_response) + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do @@ -127,10 +142,14 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end) request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") - check_response(correct_response, json_response(request, 200)) + json_response = json_response(request, 200) + check_response(correct_response, json_response) + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}") - check_response(correct_response, json_response(request, 200)) + json_response = json_response(request, 200) + check_response(correct_response, json_response) + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "returns successful creation transaction for a contract when both failed and successful transactions exist", @@ -160,6 +179,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["is_contract"] assert response["creation_transaction_hash"] == to_string(succeeded_transaction.hash) assert response["creation_status"] == "success" + + assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "returns failed creation transaction for a contract", @@ -181,6 +202,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["is_contract"] assert response["creation_transaction_hash"] == to_string(failed_transaction.hash) assert response["creation_status"] == "failed" + + assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end defp check_response(pattern_response, response) do @@ -289,6 +312,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{Address.checksum(proxy_address.hash)}") + json_response = json_response(request, 200) + assert %{ "hash" => ^address_hash, "is_contract" => true, @@ -307,7 +332,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "name" => ^name } ] - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get EIP-1967 proxy contract info", %{conn: conn} do @@ -339,6 +366,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{Address.checksum(smart_contract.address_hash)}") + json_response = json_response(request, 200) + assert %{ "hash" => ^address_hash, "is_contract" => true, @@ -358,7 +387,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "name" => nil } ] - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get Resolved Delegate Proxy contract info", %{conn: conn} do @@ -412,6 +443,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do ) request = get(conn, "/api/v2/addresses/#{checksummed_proxy_address_hash}") + json_response = json_response(request, 200) assert %{ "hash" => ^checksummed_proxy_address_hash, @@ -432,7 +464,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "name" => nil } ] - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get watchlist id", %{conn: conn} do @@ -467,6 +501,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert response["watchlist_address_id"] == watchlist_address.id + + assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "broadcasts fetched_bytecode event", %{conn: conn} do @@ -497,7 +533,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> subscribe_and_join(topic) request = get(conn, "/api/v2/addresses/#{address.hash}") - assert _response = json_response(request, 200) + assert response = json_response(request, 200) + assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) assert_receive %Phoenix.Socket.Message{ payload: %{fetched_bytecode: ^contract_code}, @@ -514,18 +551,34 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + json_response = json_response(request, 200) + assert %{ "transactions_count" => "0", "token_transfers_count" => "0", "gas_usage_count" => "0", "validations_count" => "0" - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/counters") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get counters with 0s", %{conn: conn} do @@ -533,12 +586,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/counters") + json_response = json_response(request, 200) + assert %{ "transactions_count" => "0", "token_transfers_count" => "0", "gas_usage_count" => "0", "validations_count" => "0" - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end test "get counters", %{conn: conn} do @@ -569,7 +626,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do Counters.gas_usage_count(address) request = get(conn, "/api/v2/addresses/#{address.hash}/counters") - + json_response = json_response(request, 200) gas_used = to_string(transaction_from.gas_used) assert %{ @@ -577,7 +634,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "token_transfers_count" => "2", "gas_usage_count" => ^gas_used, "validations_count" => "1" - } = json_response(request, 200) + } = json_response + + assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end end @@ -586,14 +645,29 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") + json_response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = json_response + + assert_schema(json_response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/transactions") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant transaction", %{conn: conn} do @@ -608,7 +682,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get pending transaction", %{conn: conn} do @@ -625,8 +701,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 2 assert response["next_page_params"] == nil + compare_item(pending_transaction, Enum.at(response["items"], 0)) compare_item(transaction, Enum.at(response["items"], 1)) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :to transaction", %{conn: conn} do @@ -640,7 +718,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :from transactions", %{conn: conn} do @@ -654,7 +734,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(transaction, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "validated transactions can paginate", %{conn: conn} do @@ -669,6 +751,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "pending transactions can paginate", %{conn: conn} do @@ -683,6 +767,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "pending + validated transactions can paginate", %{conn: conn} do @@ -694,9 +779,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") assert response = json_response(request, 200) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) assert response_2nd_page = json_response(request_2nd_page, 200) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert Enum.count(response["items"]) == 50 assert response["next_page_params"] != nil compare_item(Enum.at(transactions_pending, 50), Enum.at(response["items"], 0)) @@ -716,6 +805,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response, transactions_validated ++ [Enum.at(transactions_pending, 0)] ) + + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":to transactions can paginate", %{conn: conn} do @@ -734,6 +825,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from transactions can paginate", %{conn: conn} do @@ -752,6 +845,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from + :to transactions can paginate", %{conn: conn} do @@ -777,30 +872,42 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions_from, 49), Enum.at(response_2nd_page["items"], 1)) compare_item(Enum.at(transactions_from, 1), Enum.at(response_2nd_page["items"], 49)) - request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) - assert response = json_response(request, 200) + request_3rd_page = + get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"]) - check_paginated_response(response_2nd_page, response, transactions_from ++ [Enum.at(transactions_to, 0)]) + assert response_3rd_page = json_response(request_3rd_page, 200) + + check_paginated_response(response_2nd_page, response_3rd_page, transactions_from ++ [Enum.at(transactions_to, 0)]) + + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_3rd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end - test "ignores wrong ordering params", %{conn: conn} do + test "422 on wrong ordering params", %{conn: conn} do address = insert(:address) - transactions = insert_list(51, :transaction, from_address: address) |> with_block() + insert_list(51, :transaction, from_address: address) |> with_block() request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"sort" => "foo", "order" => "bar"}) - assert response = json_response(request, 200) + assert json_response = json_response(request, 422) - request_2nd_page = - get( - conn, - "/api/v2/addresses/#{address.hash}/transactions", - %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"]) - ) - - assert response_2nd_page = json_response(request_2nd_page, 200) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid value for enum", + "source" => %{"pointer" => "/sort"}, + "title" => "Invalid value" + }, + %{ + "detail" => "Invalid value for enum", + "source" => %{"pointer" => "/order"}, + "title" => "Invalid value" + } + ] + } = json_response - check_paginated_response(response, response_2nd_page, transactions) + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "backward compatible with legacy paging params", %{conn: conn} do @@ -824,6 +931,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "backward compatible with legacy paging params for pending transactions", %{conn: conn} do @@ -849,6 +958,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page_pending = json_response(request_2nd_page_pending, 200) check_paginated_response(response, response_2nd_page_pending, transactions) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page_pending, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by fee ascending", %{conn: conn} do @@ -888,6 +999,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by fee descending", %{conn: conn} do @@ -927,6 +1040,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by value ascending", %{conn: conn} do @@ -961,6 +1076,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by value descending", %{conn: conn} do @@ -995,6 +1112,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by block number ascending", %{conn: conn} do @@ -1033,6 +1152,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by block number descending", %{conn: conn} do @@ -1071,6 +1192,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -1079,14 +1202,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + json_response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = json_response + assert_schema(json_response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/token-transfers") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 200 on non existing address of token", %{conn: conn} do @@ -1095,8 +1232,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do token = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => to_string(token.hash)}) + json_response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = json_response + assert_schema(json_response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid token address hash", %{conn: conn} do @@ -1104,7 +1243,19 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"token" => "0x"}) - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/token"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant token transfer", %{conn: conn} do @@ -1131,7 +1282,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "method in token transfer could be decoded", %{conn: conn} do @@ -1183,6 +1336,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) assert Enum.at(response["items"], 0)["method"] == "mint" + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant token transfer filtered by token", %{conn: conn} do @@ -1222,7 +1377,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "token transfers by token can paginate", %{conn: conn} do @@ -1260,6 +1417,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :to token transfer", %{conn: conn} do @@ -1287,7 +1447,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :from token transfer", %{conn: conn} do @@ -1315,7 +1477,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(token_transfer, Enum.at(response["items"], 0)) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "token transfers can paginate", %{conn: conn} do @@ -1340,6 +1504,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":to token transfers can paginate", %{conn: conn} do @@ -1378,6 +1544,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from token transfers can paginate", %{conn: conn} do @@ -1416,6 +1584,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from + :to tt can paginate", %{conn: conn} do @@ -1462,10 +1632,15 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(tt_from, 49), Enum.at(response_2nd_page["items"], 1)) compare_item(Enum.at(tt_from, 1), Enum.at(response_2nd_page["items"], 49)) - request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"]) - assert response = json_response(request, 200) + request_3rd_page = + get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"]) + + assert response_3rd_page = json_response(request_3rd_page, 200) - check_paginated_response(response_2nd_page, response, tt_from ++ [Enum.at(tt_to, 0)]) + check_paginated_response(response_2nd_page, response_3rd_page, tt_from ++ [Enum.at(tt_to, 0)]) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_3rd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check token type filters", %{conn: conn} do @@ -1532,6 +1707,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_20_tt) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # -- ERC-721 -- @@ -1545,6 +1722,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_721_tt) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # -- ERC-1155 -- @@ -1558,6 +1737,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_1155_tt) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # two filters simultaneously @@ -1593,6 +1774,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_3rd_page["next_page_params"] == nil compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_3rd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- end @@ -1643,12 +1827,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) request_2nd_page = - get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter)) + get( + conn, + "/api/v2/addresses/#{address.hash}/token-transfers", + Map.merge(response["next_page_params"], filter) + ) assert response_2nd_page = json_response(request_2nd_page, 200) - check_paginated_response(response, response_2nd_page, erc_721_tt) - + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) assert response = json_response(request, 200) @@ -1659,6 +1847,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_721_tt) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) @@ -1670,6 +1860,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_20_tt) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that same token_ids within batch squashes", %{conn: conn} do @@ -1710,6 +1902,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works for 721 tokens", %{conn: conn} do @@ -1740,6 +1934,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #1 (large batch) + check filters", %{conn: conn} do @@ -1776,11 +1972,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155", "filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert response = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second", @@ -1847,6 +2048,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3]) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #3", %{conn: conn} do @@ -1900,6 +2103,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -1908,14 +2113,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/internal-transactions") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get internal transaction and filter working", %{conn: conn} do @@ -1956,13 +2175,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(internal_transaction_from, Enum.at(response["items"], 1)) compare_item(internal_transaction_to, Enum.at(response["items"], 0)) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"}) assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(internal_transaction_from, Enum.at(response["items"], 0)) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"}) @@ -1970,6 +2192,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil compare_item(internal_transaction_to, Enum.at(response["items"], 0)) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "internal transactions can paginate", %{conn: conn} do @@ -2002,6 +2225,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_from) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) internal_transactions_to = for i <- 52..102 do @@ -2030,6 +2255,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_to) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) @@ -2045,6 +2272,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_from) + assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2053,14 +2282,27 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") - - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert response = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/blocks-validated") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant block validated", %{conn: conn} do @@ -2075,6 +2317,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(block, Enum.at(response["items"], 0)) + assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "blocks validated can be paginated", %{conn: conn} do @@ -2089,6 +2332,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, blocks) + assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2097,14 +2342,27 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") - - assert json_response(request, 200) == [] + assert response = json_response(request, 200) + assert response == [] + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/token-balances") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token balance", %{conn: conn} do @@ -2123,6 +2381,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do for i <- 0..50 do compare_item(Enum.at(ctbs, i), Enum.at(response, i)) end + + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end end @@ -2131,14 +2391,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history") + response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/coin-balance-history") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance history", %{conn: conn} do @@ -2154,6 +2428,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(acb, Enum.at(response["items"], 0)) + assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance with transaction", %{conn: conn} do @@ -2219,6 +2494,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, acbs) + assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2231,16 +2508,32 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do days_count = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance)[:coin_balance_history_days] + response = json_response(request, 200) + assert %{ "days" => ^days_count, "items" => [] - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressCoinBalanceHistoryByDay", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/coin-balance-history-by-day") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance history by day", %{conn: conn} do @@ -2265,6 +2558,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %{"date" => _, "value" => "1000"} ] } = response + + assert_schema(response, "AddressCoinBalanceHistoryByDay", BlockScoutWeb.ApiSpec.spec()) end end @@ -2273,14 +2568,27 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/logs") - - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + response = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/logs") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get log", %{conn: conn} do @@ -2305,7 +2613,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end # for some reasons test does not work if run as single test @@ -2334,6 +2644,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/logs", response["next_page_params"]) assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, logs) + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end # https://github.com/blockscout/blockscout/issues/9926 @@ -2392,7 +2704,17 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do 200, Jason.encode!(%{ "addresses" => %{ - to_string(address) => %{"tags" => [%{"slug" => "tag", "meta" => "{\"styles\":\"danger_high\"}"}]} + to_string(address) => %{ + "tags" => [ + %{ + "name" => "Proposer Fee Recipient", + "ordinal" => 0, + "slug" => "proposer-fee-recipient", + "tagType" => "generic", + "meta" => "{\"styles\":\"danger_high\"}" + } + ] + } } }) ) @@ -2403,10 +2725,24 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + log = Enum.at(response["items"], 0) assert log["address"]["ens_domain_name"] == "test.eth" - assert log["address"]["metadata"] == %{"tags" => [%{"slug" => "tag", "meta" => %{"styles" => "danger_high"}}]} + + assert log["address"]["metadata"] == %{ + "tags" => [ + %{ + "slug" => "proposer-fee-recipient", + "name" => "Proposer Fee Recipient", + "ordinal" => 0, + "tagType" => "generic", + "meta" => %{"styles" => "danger_high"} + } + ] + } Application.put_env(:block_scout_web, :chain_id, old_chain_id) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, old_env_bens) @@ -2450,7 +2786,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil + compare_item(log, Enum.at(response["items"], 0)) + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "log could be decoded via verified implementation", %{conn: conn} do @@ -2550,6 +2888,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } + + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test corner case, when preload functions face absent smart contract", %{conn: conn} do @@ -2590,7 +2930,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil log_from_api = Enum.at(response["items"], 0) + compare_item(log, log_from_api) + assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "ignore logs without topics when trying to decode with sig provider", %{conn: conn} do @@ -2659,13 +3001,27 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/tokens") - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + response = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/tokens") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get tokens", %{conn: conn} do @@ -2714,6 +3070,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_20) + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721"} request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) @@ -2725,6 +3083,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_721) + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) @@ -2736,6 +3096,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_1155) + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -3025,14 +3387,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals") + response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/withdrawals") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get withdrawals", %{conn: conn} do @@ -3046,6 +3422,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, address.withdrawals) + assert_schema(response, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -3061,6 +3439,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert pattern_response["items"] == response["items"] assert pattern_response["next_page_params"] == response["next_page_params"] assert pattern_response["total_supply"] == response["total_supply"] + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check pagination", %{conn: conn} do @@ -3080,27 +3459,33 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.at(response["items"], 0)["coin_balance"] == to_string(Enum.at(addresses, 50).fetched_coin_balance.value) + + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check nil", %{conn: conn} do address = insert(:address, transactions_count: 2, fetched_coin_balance: 1) request = get(conn, "/api/v2/addresses") - - assert %{"items" => [address_json], "next_page_params" => nil} = json_response(request, 200) + response = json_response(request, 200) + assert %{"items" => [address_json], "next_page_params" => nil} = response compare_item(address, address_json) + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check smart contract preload", %{conn: conn} do smart_contract = insert(:smart_contract, address_hash: insert(:contract_address, fetched_coin_balance: 1).hash) request = get(conn, "/api/v2/addresses") - assert %{"items" => [address]} = json_response(request, 200) + response = json_response(request, 200) + assert %{"items" => [address]} = response assert String.downcase(address["hash"]) == to_string(smart_contract.address_hash) assert address["is_contract"] == true assert address["is_verified"] == true + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by balance asc", %{conn: conn} do @@ -3122,6 +3507,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response_2nd_page, Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :desc) ) + + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by transactions count asc", %{conn: conn} do @@ -3139,6 +3527,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :desc)) + + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by balance desc", %{conn: conn} do @@ -3160,6 +3551,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response_2nd_page, Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :asc) ) + + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by transactions count desc", %{conn: conn} do @@ -3177,6 +3571,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :asc)) + + assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end end @@ -3185,6 +3582,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 0, @@ -3195,19 +3593,34 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "withdrawals_count" => 0, "internal_transactions_count" => 0, "celo_election_rewards_count" => 0 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/addresses/0x/tabs-counters") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get counters with 0s", %{conn: conn} do address = insert(:address) request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 0, @@ -3217,7 +3630,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "logs_count" => 0, "withdrawals_count" => 0, "internal_transactions_count" => 0 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "get counters and check that cache works", %{conn: conn} do @@ -3280,6 +3695,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 1, @@ -3289,7 +3705,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "logs_count" => 51, "withdrawals_count" => 51, "internal_transactions_count" => 2 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) for x <- 3..4 do insert(:internal_transaction, @@ -3304,6 +3722,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 1, @@ -3313,7 +3732,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "logs_count" => 51, "withdrawals_count" => 51, "internal_transactions_count" => 2 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "check counters cache ttl", %{conn: conn} do @@ -3376,6 +3797,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 1, @@ -3385,7 +3807,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "logs_count" => 51, "withdrawals_count" => 51, "internal_transactions_count" => 2 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) old_env = Application.get_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount) Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, ttl: 200) @@ -3407,6 +3831,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do insert(:transaction, to_address: address) |> with_block() request = get(conn, "/api/v2/addresses/#{address.hash}/tabs-counters") + response = json_response(request, 200) assert %{ "validations_count" => 1, @@ -3416,7 +3841,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "logs_count" => 51, "withdrawals_count" => 51, "internal_transactions_count" => 4 - } = json_response(request, 200) + } = response + + assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, old_env) end @@ -3431,14 +3858,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do address = build(:address) request = get(conn, endpoint.(address.hash)) + response = json_response(request, 200) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do request = get(conn, endpoint.("0x")) - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do @@ -3466,6 +3907,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do @@ -3502,6 +3945,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated ERC-404 nft", %{conn: conn, endpoint: endpoint} do @@ -3538,6 +3983,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test filters", %{conn: conn, endpoint: endpoint} do @@ -3589,6 +4036,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances_721) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, endpoint.(address.hash), filter) @@ -3598,6 +4047,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances_1155) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "return all token instances", %{conn: conn, endpoint: endpoint} do @@ -3667,6 +4118,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) + + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_3rd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -3680,13 +4135,28 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, endpoint.(address.hash)) - assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + response = json_response(request, 200) + assert %{"items" => [], "next_page_params" => nil} = response + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do request = get(conn, endpoint.("0x")) - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + json_response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response + + assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated erc-721 collection", %{conn: conn, endpoint: endpoint} do @@ -3734,6 +4204,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated erc-1155 collection", %{conn: conn, endpoint: endpoint} do @@ -3780,6 +4252,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, collections) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test filters", %{conn: conn, endpoint: endpoint} do @@ -3860,6 +4334,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, endpoint.(address.hash), filter) @@ -3869,6 +4345,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, collections) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "return all collections", %{conn: conn, endpoint: endpoint} do @@ -3967,6 +4445,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(collections_1155, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(collections_1155, 0), Enum.at(response_3rd_page["items"], 1)) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_3rd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index f964f9817699..2a0d2274e0f1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -4,6 +4,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox + import Ecto.Query, only: [from: 2] alias Explorer.Account.{Identity, WatchlistAddress} alias Explorer.Chain.{Address, Data, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex index 06379dd3df8a..46abfc7519cf 100644 --- a/apps/explorer/lib/explorer/account/notifier/email.ex +++ b/apps/explorer/lib/explorer/account/notifier/email.ex @@ -8,6 +8,7 @@ defmodule Explorer.Account.Notifier.Email do alias Explorer.Account.{Identity, Watchlist, WatchlistAddress, WatchlistNotification} alias Explorer.Chain.Address alias Explorer.Repo + alias Utils.Helper import Bamboo.{Email, SendGridHelper} @@ -112,45 +113,15 @@ defmodule Explorer.Account.Notifier.Email do end defp address_url(address_hash) do - uri() |> URI.append_path("/address/#{address_hash}") |> to_string() + Helper.instance_url() |> URI.append_path("/address/#{address_hash}") |> to_string() end defp block_url(notification) do - uri() |> URI.append_path("/block/#{notification.block_number}") |> to_string() + Helper.instance_url() |> URI.append_path("/block/#{notification.block_number}") |> to_string() end defp transaction_url(notification) do - uri() |> URI.append_path("/tx/#{notification.transaction_hash}") |> to_string() - end - - defp url_params do - Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url] - end - - defp uri do - %URI{scheme: scheme(), host: host(), port: port(), path: path()} - end - - defp scheme do - Keyword.get(url_params(), :scheme, "http") - end - - defp host do - url_params()[:host] - end - - defp port do - url_params()[:http][:port] - end - - defp path do - raw_path = url_params()[:path] - - if raw_path |> String.ends_with?("/") do - raw_path |> String.slice(0..-2//1) - else - raw_path - end + Helper.instance_url() |> URI.append_path("/tx/#{notification.transaction_hash}") |> to_string() end defp sender do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex index e86cf6a98323..7a75b59bfb1c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -499,7 +499,7 @@ defmodule Explorer.Chain.SmartContract.Proxy do case address do %Hash{} = address_hash -> [ - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + UPDATE PATTERN MATCHING in `chain_type_fields()` function %{ "address_hash" => Address.checksum(address_hash), "address" => Address.checksum(address_hash), @@ -513,7 +513,7 @@ defmodule Explorer.Chain.SmartContract.Proxy do with {:ok, address_hash} <- string_to_address_hash(address), checksummed_address <- Address.checksum(address_hash) do [ - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + UPDATE PATTERN MATCHING in `chain_type_fields()` function %{"address_hash" => checksummed_address, "address" => checksummed_address, "name" => name} |> chain_type_fields(implementations_info) | acc diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index ff94261f908a..c1587bfc20f0 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -290,21 +290,21 @@ defmodule Explorer.Chain.Token.Instance do @doc """ Function to be used in BlockScoutWeb.Chain.next_page_params/4 """ - @spec nft_list_next_page_params(__MODULE__.t()) :: %{binary() => any} + @spec nft_list_next_page_params(__MODULE__.t()) :: %{atom() => any} def nft_list_next_page_params(%__MODULE__{ current_token_balance: %CurrentTokenBalance{}, token_contract_address_hash: token_contract_address_hash, token_id: token_id, token: token }) do - %{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => token.type} + %{token_contract_address_hash: token_contract_address_hash, token_id: token_id, token_type: token.type} end def nft_list_next_page_params(%__MODULE__{ token_contract_address_hash: token_contract_address_hash, token_id: token_id }) do - %{"token_contract_address_hash" => token_contract_address_hash, "token_id" => token_id, "token_type" => "ERC-721"} + %{token_contract_address_hash: token_contract_address_hash, token_id: token_id, token_type: "ERC-721"} end @preloaded_nfts_limit 9 @@ -488,20 +488,20 @@ defmodule Explorer.Chain.Token.Instance do Function to be used in BlockScoutWeb.Chain.next_page_params/4 """ @spec nft_collections_next_page_params(%{:token_contract_address_hash => any, optional(any) => any}) :: %{ - binary() => any + atom() => any } def nft_collections_next_page_params(%{ token_contract_address_hash: token_contract_address_hash, token: %Token{type: token_type} }) do - %{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type} + %{token_contract_address_hash: token_contract_address_hash, token_type: token_type} end def nft_collections_next_page_params(%{ token_contract_address_hash: token_contract_address_hash, token_type: token_type }) do - %{"token_contract_address_hash" => token_contract_address_hash, "token_type" => token_type} + %{token_contract_address_hash: token_contract_address_hash, token_type: token_type} end @spec token_instances_by_holder_address_hash(Token.t(), binary() | Hash.Address.t(), keyword) :: [__MODULE__.t()] diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 5ec083f6b4b3..02ae71978daa 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -1919,19 +1919,19 @@ defmodule Explorer.Chain.Transaction do Returns next page params based on the provided transaction. """ @spec address_transactions_next_page_params(Explorer.Chain.Transaction.t()) :: %{ - required(String.t()) => Decimal.t() | Wei.t() | non_neg_integer | DateTime.t() | Hash.t() + required(atom()) => Decimal.t() | Wei.t() | non_neg_integer | DateTime.t() | Hash.t() } def address_transactions_next_page_params( %__MODULE__{block_number: block_number, index: index, inserted_at: inserted_at, hash: hash, value: value} = transaction ) do %{ - "fee" => transaction |> fee(:wei) |> elem(1), - "value" => value, - "block_number" => block_number, - "index" => index, - "inserted_at" => inserted_at, - "hash" => hash + fee: transaction |> fee(:wei) |> elem(1), + value: value, + block_number: block_number, + index: index, + inserted_at: inserted_at, + hash: hash } end @@ -1964,7 +1964,7 @@ defmodule Explorer.Chain.Transaction do {:actual, Decimal.new(4)} """ - @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t()} | {:actual, Decimal.t() | nil} + @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil} def fee(%Transaction{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} def fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil} = transaction, unit) do diff --git a/apps/explorer/lib/explorer/chain/transaction_action.ex b/apps/explorer/lib/explorer/chain/transaction_action.ex index 795892a6f167..c1514615e76b 100644 --- a/apps/explorer/lib/explorer/chain/transaction_action.ex +++ b/apps/explorer/lib/explorer/chain/transaction_action.ex @@ -10,7 +10,29 @@ defmodule Explorer.Chain.TransactionAction do @required_attrs ~w(hash protocol data type log_index)a @supported_protocols [:uniswap_v3, :opensea_v1_1, :wrapping, :approval, :zkbob, :aave_v3] - + @supported_types [ + :mint_nft, + :mint, + :burn, + :collect, + :swap, + :sale, + :cancel, + :transfer, + :wrap, + :unwrap, + :approve, + :revoke, + :withdraw, + :deposit, + :borrow, + :supply, + :repay, + :flash_loan, + :enable_collateral, + :disable_collateral, + :liquidation_call + ] @typedoc """ * `hash` - transaction hash * `protocol` - name of the action protocol (see possible values for Enum of the db table field) @@ -24,29 +46,7 @@ defmodule Explorer.Chain.TransactionAction do field(:data, :map, null: false) field(:type, Ecto.Enum, - values: [ - :mint_nft, - :mint, - :burn, - :collect, - :swap, - :sale, - :cancel, - :transfer, - :wrap, - :unwrap, - :approve, - :revoke, - :withdraw, - :deposit, - :borrow, - :supply, - :repay, - :flash_loan, - :enable_collateral, - :disable_collateral, - :liquidation_call - ], + values: @supported_types, null: false ) @@ -70,7 +70,13 @@ defmodule Explorer.Chain.TransactionAction do |> foreign_key_constraint(:hash) end + @spec supported_protocols() :: [atom()] def supported_protocols do @supported_protocols end + + @spec supported_types() :: [atom()] + def supported_types do + @supported_types + end end diff --git a/apps/utils/lib/utils/helper.ex b/apps/utils/lib/utils/helper.ex new file mode 100644 index 000000000000..bf891ec462b9 --- /dev/null +++ b/apps/utils/lib/utils/helper.ex @@ -0,0 +1,43 @@ +defmodule Utils.Helper do + @moduledoc """ + This module contains helper functions for the entire application. + """ + + @doc """ + Retrieves the host URL for the application. + + ## Returns + + - A string containing the host URL for the application. + """ + @spec instance_url :: URI.t() + def instance_url do + %URI{scheme: scheme(), host: host(), port: port(), path: path()} + end + + defp url_params do + Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url] + end + + defp scheme do + Keyword.get(url_params(), :scheme, "http") + end + + defp host do + url_params()[:host] + end + + defp port do + url_params()[:http][:port] + end + + defp path do + raw_path = url_params()[:path] + + if raw_path |> String.ends_with?("/") do + raw_path |> String.slice(0..-2//1) + else + raw_path + end + end +end diff --git a/bin/version_bump.sh b/bin/version_bump.sh index 512400a554d0..4ec967a3a9f6 100755 --- a/bin/version_bump.sh +++ b/bin/version_bump.sh @@ -14,7 +14,7 @@ CONFIG_FILE="$(pwd)/rel/config.exs" DOCKER_COMPOSE_FILE="$(pwd)/docker-compose/docker-compose.yml" DOCKER_COMPOSE_NO_SERVICES_FILE="$(pwd)/docker-compose/no-services.yml" MAKE_FILE="$(pwd)/docker/Makefile" -WORKFLOW_FILES=($(find "$(pwd)/.github/workflows" -type f \( -name "pre-release*" -o -name "release*" -o -name "publish-regular-docker-image-on-demand*" -o -name "publish-docker-image-*" \))) +WORKFLOW_FILES=($(find "$(pwd)/.github/workflows" -type f \( -name "pre-release*" -o -name "release*" -o -name "publish-regular-docker-image-on-demand*" -o -name "publish-docker-image-*" -o -name "generate-swagger*" \))) METADATA_RETRIEVER_FILE="$(pwd)/apps/explorer/lib/explorer/token/metadata_retriever.ex" # Function to bump version diff --git a/cspell.json b/cspell.json index 97dda466143d..0ab5bc6af3d8 100644 --- a/cspell.json +++ b/cspell.json @@ -119,6 +119,7 @@ "celo", "certifi", "cfasync", + "cfbljvn", "chainid", "chainlink", "Chainscout", @@ -283,6 +284,7 @@ "giga", "Gitter", "goldtoken", + "goqtclhifepvfnicv", "gqz", "granitegrey", "graphiql", @@ -382,6 +384,7 @@ "Merkle", "metaprogramming", "metatags", + "mfmmvq", "microsecs", "millis", "mintings", @@ -579,6 +582,7 @@ "sourcecode", "sourcify", "sparkline", + "spex", "splitted", "srcset", "stabletoken", @@ -730,6 +734,7 @@ "xnonsense", "xzzz", "yellowgreen", + "ymlr", "zaphod", "zeppelinos", "zerion", diff --git a/mix.lock b/mix.lock index 6ac4754cd33e..bab49b93a4f7 100644 --- a/mix.lock +++ b/mix.lock @@ -122,6 +122,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, + "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, @@ -177,4 +178,5 @@ "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, + "ymlr": {:hex, :ymlr, "2.0.0", "7525b6da40250777c35456017ef44f7faec06da254eafcf9f9cfb0d65f4c8cb7", [:mix], [], "hexpm", "f9301ad7ea377213b506f6e58ddffd1a7743e24238bb70e572ee510bdc2d1d5a"}, } From d5b35310e6f8831be048fd07de08bf2341f82c4b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 4 Jul 2025 17:09:20 +0300 Subject: [PATCH 0144/1006] Remove Berachain from GA workflow configs --- .github/workflows/config.yml | 1 - .github/workflows/generate-swagger.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index baad1e211910..fd26e53e91e0 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -52,7 +52,6 @@ jobs: const chainTypes = [ "arbitrum", - "berachain", "blackfort", "celo", "ethereum", diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index c73ce6356d7c..5644363187b2 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -35,7 +35,6 @@ jobs: const chainTypes = [ "arbitrum", - "berachain", "blackfort", "celo", "ethereum", From ab0b4385434e435aba8d5150f42b00806796343c Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 8 Jul 2025 19:09:02 +0300 Subject: [PATCH 0145/1006] fix: multichain retry hex decoding (#12742) --- .../lib/indexer/fetcher/multichain_search_db_export/retry.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex index 25ba444e5f57..bf0818062a6d 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex @@ -75,7 +75,7 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do successful_hashes |> Enum.map(fn hash -> "0x" <> hex = hash - Base.decode16!(hex) + Base.decode16!(hex, case: :mixed) end) successful_hash_binaries From 18e1e3521e63367031e0ce0a7c5163214caaa823 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 9 Jul 2025 00:05:36 +0400 Subject: [PATCH 0146/1006] chore: Allow resending reindexed OP interop messages to Multichain service (#12626) * Allow resending reindexed OP interop messages to Multichain service * sent_to_multichain is redundant in where clause * Add 2-second sleep to RollupL1ReorgMonitor --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../chain/import/runner/optimism/interop_messages.ex | 1 + .../lib/explorer/chain/optimism/interop_message.ex | 4 ++-- .../20250619093450_op_sent_to_multichain_nullable.exs | 9 +++++++++ .../lib/indexer/fetcher/optimism/interop/message.ex | 6 +++--- .../lib/indexer/fetcher/rollup_l1_reorg_monitor.ex | 3 +++ 5 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 apps/explorer/priv/optimism/migrations/20250619093450_op_sent_to_multichain_nullable.exs diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex index 5451966d8682..ea8f67a27680 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/interop_messages.ex @@ -103,6 +103,7 @@ defmodule Explorer.Chain.Import.Runner.Optimism.InteropMessages do transfer_to_address_hash: fragment("COALESCE(EXCLUDED.transfer_to_address_hash, ?)", message.transfer_to_address_hash), transfer_amount: fragment("COALESCE(EXCLUDED.transfer_amount, ?)", message.transfer_amount), + sent_to_multichain: fragment("COALESCE(EXCLUDED.sent_to_multichain, ?)", message.sent_to_multichain), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", message.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", message.updated_at) ] diff --git a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex index 4f94a1a86f91..86e70ab32ac9 100644 --- a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex +++ b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex @@ -33,7 +33,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do * `transfer_from_address_hash` - The cross-chain transfer `from` address. Can be `nil` (if this is not transfer operation). * `transfer_to_address_hash` - The cross-chain transfer `to` address. Can be `nil` (if this is not transfer operation). * `transfer_amount` - The cross-chain transfer amount. Can be `nil` (if this is not transfer operation). - * `sent_to_multichain` - Equals to `true` if message details are sent to multichain service. Defaults to `false`. + * `sent_to_multichain` - Equals to `true` if message details are sent to multichain service. Defaults to `nil`. """ @primary_key false typed_schema "op_interop_messages" do @@ -215,7 +215,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do where: ((not is_nil(m.init_transaction_hash) and m.init_chain_id == ^current_chain_id) or (not is_nil(m.relay_transaction_hash) and m.relay_chain_id == ^current_chain_id)) and - not m.sent_to_multichain, + (is_nil(m.sent_to_multichain) or m.sent_to_multichain == false), order_by: [desc: m.block_number], limit: ^limit ) diff --git a/apps/explorer/priv/optimism/migrations/20250619093450_op_sent_to_multichain_nullable.exs b/apps/explorer/priv/optimism/migrations/20250619093450_op_sent_to_multichain_nullable.exs new file mode 100644 index 000000000000..d7edd502e077 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20250619093450_op_sent_to_multichain_nullable.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Optimism.Migrations.OPSentToMultichainNullable do + use Ecto.Migration + + def change do + alter table(:op_interop_messages) do + modify(:sent_to_multichain, :boolean, null: true, default: nil) + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex index aca7c8552dc0..9de880a79049 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/message.ex @@ -63,8 +63,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do @impl GenServer def init(args) do - json_rpc_named_arguments = args[:json_rpc_named_arguments] - {:ok, %{}, {:continue, json_rpc_named_arguments}} + {:ok, %{}, {:continue, args[:json_rpc_named_arguments]}} end # Initialization function which is used instead of `init` to avoid Supervisor's stop in case of any critical issues @@ -337,7 +336,8 @@ defmodule Indexer.Fetcher.Optimism.Interop.Message do transfer_token_address_hash: transfer_token_address_hash, transfer_from_address_hash: transfer_from_address_hash, transfer_to_address_hash: transfer_to_address_hash, - transfer_amount: transfer_amount + transfer_amount: transfer_amount, + sent_to_multichain: false } else %{ diff --git a/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex index 39b9f84fa758..f8bdaf1bfcd0 100644 --- a/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex +++ b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex @@ -106,6 +106,9 @@ defmodule Indexer.Fetcher.RollupL1ReorgMonitor do def handle_continue(:ok, _state) do Logger.metadata(fetcher: @fetcher_name) + # two seconds pause needed to avoid exceeding Supervisor restart intensity when RPC issues + :timer.sleep(2000) + modules_using_reorg_monitor = modules_can_use_reorg_monitor() |> Enum.filter(& &1.requires_l1_reorg_monitor?()) From dee54e92c925dbd4c2b2207a095dc3d88077b14e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:48:18 +0300 Subject: [PATCH 0147/1006] chore(deps): bump image from 0.60.0 to 0.61.0 (#12739) Bumps [image](https://github.com/kipcole9/image) from 0.60.0 to 0.61.0. - [Release notes](https://github.com/kipcole9/image/releases) - [Changelog](https://github.com/elixir-image/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/kipcole9/image/compare/v0.60.0...v0.61.0) --- updated-dependencies: - dependency-name: image dependency-version: 0.61.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index bab49b93a4f7..c25ce20057f1 100644 --- a/mix.lock +++ b/mix.lock @@ -87,7 +87,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.60.0", "84a157c002b40c902e28c1e187634131353c9347ffd345fadc9219cac8503261", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "547f08a2836b3d897b713f462b82368613207e556f62d61a399ca1fd16b66381"}, + "image": {:hex, :image, "0.61.0", "3797e9486aded811a58d49027cbb8a6484971351456abd4db1c6d18afe4760fa", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "be6d82b1fb169413c5f37f3e88d7e8f795c321079b51418e13e10f03a2c32e66"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -132,7 +132,7 @@ "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, - "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, + "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, From 05a475a6f16803c834ca09ee768ddf9760392f80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:04:10 +0300 Subject: [PATCH 0148/1006] chore(deps): bump open_api_spex from 3.21.2 to 3.21.4 (#12740) Bumps [open_api_spex](https://github.com/open-api-spex/open_api_spex) from 3.21.2 to 3.21.4. - [Release notes](https://github.com/open-api-spex/open_api_spex/releases) - [Changelog](https://github.com/open-api-spex/open_api_spex/blob/master/CHANGELOG.md) - [Commits](https://github.com/open-api-spex/open_api_spex/compare/v3.21.2...v3.21.4) --- updated-dependencies: - dependency-name: open_api_spex dependency-version: 3.21.4 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c25ce20057f1..8138ad193511 100644 --- a/mix.lock +++ b/mix.lock @@ -122,7 +122,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, - "open_api_spex": {:hex, :open_api_spex, "3.21.2", "6a704f3777761feeb5657340250d6d7332c545755116ca98f33d4b875777e1e5", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "f42ae6ed668b895ebba3e02773cfb4b41050df26f803f2ef634c72a7687dc387"}, + "open_api_spex": {:hex, :open_api_spex, "3.21.4", "f066dab3257a2804cdb54efd84ed98bc46612dc60bf1d6313de373b776b83639", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "6a4e692222eef9472e1c6ce6aeccc89fa8d585aa1729fb4ececba06012743aa3"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, From 32bfbdbb0c625c5a567fe9dc80ddefe8ac57cf03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:45:18 +0300 Subject: [PATCH 0149/1006] chore(deps): bump ymlr from 2.0.0 to 5.1.3 (#12741) Bumps [ymlr](https://github.com/ufirstgroup/ymlr) from 2.0.0 to 5.1.3. - [Release notes](https://github.com/ufirstgroup/ymlr/releases) - [Changelog](https://github.com/ufirstgroup/ymlr/blob/main/CHANGELOG.md) - [Commits](https://github.com/ufirstgroup/ymlr/compare/v2.0.0...v5.1.3) --- updated-dependencies: - dependency-name: ymlr dependency-version: 5.1.3 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 4a117d9a0017..0973361a2d78 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -163,7 +163,7 @@ defmodule BlockScoutWeb.Mixfile do {:bureaucrat, "~> 0.2.9", only: :test}, {:logger_json, "~> 5.1"}, {:open_api_spex, "~> 3.21"}, - {:ymlr, "~> 2.0"} + {:ymlr, "~> 5.1"} ] end diff --git a/mix.lock b/mix.lock index 8138ad193511..3bcb7908837a 100644 --- a/mix.lock +++ b/mix.lock @@ -178,5 +178,5 @@ "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, - "ymlr": {:hex, :ymlr, "2.0.0", "7525b6da40250777c35456017ef44f7faec06da254eafcf9f9cfb0d65f4c8cb7", [:mix], [], "hexpm", "f9301ad7ea377213b506f6e58ddffd1a7743e24238bb70e572ee510bdc2d1d5a"}, + "ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"}, } From 2eb58c6d60bfe0d6a26a053bf9540a35efcfc277 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 9 Jul 2025 16:07:48 +0300 Subject: [PATCH 0150/1006] feat: Rate limits refactoring (#12386) * feat: Rate limits refactoring * Process review comments * Add isolate_rate_limit? param to rate limit config * Fix tests * Ignore store_rate_limit_config if disabled api * Allow recaptcha headers in CORS * Tuning * Increase limits --- apps/block_scout_web/.sobelow-conf | 4 +- apps/block_scout_web/config/config.exs | 15 - .../lib/block_scout_web/application.ex | 14 +- .../account/api/v2/authenticate_controller.ex | 44 +- .../controllers/api/eth_rpc/eth_controller.ex | 72 +-- .../controllers/api/rpc/rpc_translator.ex | 5 - .../api/v2/advanced_filter_controller.ex | 45 +- .../controllers/api/v2/api_key_controller.ex | 2 +- .../api/v2/csv_export_controller.ex | 13 +- .../api/v2/smart_contract_controller.ex | 3 +- .../controllers/api/v2/token_controller.ex | 3 +- .../lib/block_scout_web/endpoint.ex | 12 +- .../lib/block_scout_web/plug/rate_limit.ex | 111 +++- .../lib/block_scout_web/rate_limit.ex | 376 ++++++++++++++ .../lib/block_scout_web/rate_limit/hammer.ex | 38 ++ .../lib/block_scout_web/router.ex | 14 +- .../routers/address_badges_v2_router.ex | 4 +- .../lib/block_scout_web/routers/api_router.ex | 13 +- .../routers/smart_contracts_api_v2_router.ex | 4 +- .../routers/tokens_api_v2_router.ex | 4 +- .../routers/utils_api_v2_router.ex | 3 +- .../utility/rate_limit_config_helper.ex | 165 ++++++ .../block_scout_web/views/access_helper.ex | 207 +------- apps/block_scout_web/mix.exs | 3 +- .../priv/rate_limit_config.json | 111 ++++ apps/block_scout_web/rate_limits.md | 99 ++++ .../api/v2/csv_export_controller_test.exs | 147 ++++-- .../api/v2/token_controller_test.exs | 246 +++++---- .../block_scout_web/plug/rate_limit_test.exs | 477 ++++++++++++++++++ .../test/block_scout_web/rate_limit_test.exs | 426 ++++++++++++++++ .../utility/rate_limit_config_helper_test.exs | 294 +++++++++++ .../views/access_helper_test.exs | 70 --- apps/explorer/config/config.exs | 3 + apps/explorer/config/runtime/test.exs | 3 + apps/explorer/lib/explorer/application.ex | 8 +- apps/explorer/lib/explorer/utility/hammer.ex | 41 ++ .../lib/explorer/utility/rate_limiter.ex | 5 +- apps/explorer/mix.exs | 4 +- .../explorer/utility/rate_limiter_test.exs | 9 +- apps/utils/lib/utils/config_helper.ex | 19 + config/config_helper.exs | 13 +- config/runtime.exs | 53 +- docker-compose/envs/common-blockscout.env | 6 +- mix.lock | 4 +- 44 files changed, 2626 insertions(+), 586 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/rate_limit.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/rate_limit/hammer.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex create mode 100644 apps/block_scout_web/priv/rate_limit_config.json create mode 100644 apps/block_scout_web/rate_limits.md create mode 100644 apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/rate_limit_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs delete mode 100644 apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs create mode 100644 apps/explorer/lib/explorer/utility/hammer.ex create mode 100644 apps/utils/lib/utils/config_helper.ex diff --git a/apps/block_scout_web/.sobelow-conf b/apps/block_scout_web/.sobelow-conf index 45fc3453b994..7c1fc3762411 100644 --- a/apps/block_scout_web/.sobelow-conf +++ b/apps/block_scout_web/.sobelow-conf @@ -10,6 +10,8 @@ "apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex", - "apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex" + "apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex", + "apps/block_scout_web/lib/block_scout_web/rate_limit.ex" ] ] diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 73d91877a0a1..705ee05abbff 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -118,21 +118,6 @@ config :ueberauth, Ueberauth, } ] -redis_url = System.get_env("API_RATE_LIMIT_HAMMER_REDIS_URL") - -if is_nil(redis_url) or redis_url == "" do - config :hammer, backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]} -else - config :hammer, - backend: - {Hammer.Backend.Redis, - [ - delete_buckets_timeout: 60_000 * 10, - expiry_ms: 60_000 * 60 * 4, - redis_url: redis_url - ]} -end - # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index a3bf07f2d4bb..861da966c860 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -6,11 +6,16 @@ defmodule BlockScoutWeb.Application do use Application use Utils.CompileTimeEnvHelper, disable_api?: [:block_scout_web, :disable_api?] - alias BlockScoutWeb.{Endpoint, HealthEndpoint} + alias BlockScoutWeb.{Endpoint, HealthEndpoint, RateLimit.Hammer} + alias BlockScoutWeb.Utility.RateLimitConfigHelper def start(_type, _args) do opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + if not @disable_api? do + RateLimitConfigHelper.store_rate_limit_config() + end + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) else @@ -53,10 +58,6 @@ defmodule BlockScoutWeb.Application do Exporter.setup() PublicExporter.setup() - APILogger.message( - "Current global API rate limit #{inspect(Application.get_env(:block_scout_web, :api_rate_limit)[:global_limit])} reqs/sec" - ) - APILogger.message( "Current API rate limit by key #{inspect(Application.get_env(:block_scout_web, :api_rate_limit)[:limit_by_key])} reqs/sec" ) @@ -75,7 +76,8 @@ defmodule BlockScoutWeb.Application do {BlocksIndexedCounter, name: BlocksIndexedCounter}, {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, {EventHandlersMetrics, []}, - {ChainMetrics, []} + {ChainMetrics, []}, + Hammer.child_for_supervisor() ], [ {Absinthe.Subscription, Endpoint} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex index c4785608abac..100897a3ecd4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.Account.API.V2.UserView alias BlockScoutWeb.API.V2.ApiView alias Explorer.Account.Identity @@ -72,27 +72,24 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do :error | {:error, String.t()} | {:interval, integer()} - | {:recaptcha, false} | Conn.t() - def send_otp(conn, %{"email" => email} = params) do - with {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)} do - case conn |> Conn.fetch_session() |> current_user() do - nil -> - with :ok <- Auth0.send_otp(email, AccessHelper.conn_to_ip_string(conn)) do - conn |> put_status(200) |> json(%{message: "Success"}) - end - - %{email: nil} -> - with :ok <- Auth0.send_otp_for_linking(email, AccessHelper.conn_to_ip_string(conn)) do - conn |> put_status(200) |> json(%{message: "Success"}) - end - - %{} -> - conn - |> put_status(500) - |> put_view(ApiView) - |> render(:message, %{message: "This account already has an email"}) - end + def send_otp(conn, %{"email" => email}) do + case conn |> Conn.fetch_session() |> current_user() do + nil -> + with :ok <- Auth0.send_otp(email, AccessHelper.conn_to_ip_string(conn)) do + conn |> put_status(200) |> json(%{message: "Success"}) + end + + %{email: nil} -> + with :ok <- Auth0.send_otp_for_linking(email, AccessHelper.conn_to_ip_string(conn)) do + conn |> put_status(200) |> json(%{message: "Success"}) + end + + %{} -> + conn + |> put_status(500) + |> put_view(ApiView) + |> render(:message, %{message: "This account already has an email"}) end end @@ -216,9 +213,8 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do information. """ @spec authenticate_via_wallet(Conn.t(), map()) :: :error | {:error, any()} | Conn.t() - def authenticate_via_wallet(conn, %{"message" => message, "signature" => signature} = params) do - with {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)}, - {:ok, auth} <- Auth0.get_auth_with_web3(message, signature) do + def authenticate_via_wallet(conn, %{"message" => message, "signature" => signature}) do + with {:ok, auth} <- Auth0.get_auth_with_web3(message, signature) do put_auth_to_session(conn, auth) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex index f67ef4afb9dd..a42116389d0f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/eth_rpc/eth_controller.ex @@ -1,7 +1,6 @@ defmodule BlockScoutWeb.API.EthRPC.EthController do use BlockScoutWeb, :controller - alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView alias BlockScoutWeb.API.RPC.RPCView alias Explorer.EthRPC @@ -9,8 +8,7 @@ defmodule BlockScoutWeb.API.EthRPC.EthController do def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do eth_json_rpc_max_batch_size = Application.get_env(:block_scout_web, :api_rate_limit)[:eth_json_rpc_max_batch_size] - with :ok <- AccessHelper.check_rate_limit(conn), - {:batch_size, true} <- {:batch_size, Enum.count(requests) <= eth_json_rpc_max_batch_size} do + if Enum.count(requests) <= eth_json_rpc_max_batch_size do responses = EthRPC.responses(requests) conn @@ -18,55 +16,39 @@ defmodule BlockScoutWeb.API.EthRPC.EthController do |> put_view(EthRPCView) |> render("responses.json", %{responses: responses}) else - :rate_limit_reached -> - AccessHelper.handle_rate_limit_deny(conn) - - {:batch_size, _} -> - conn - |> put_status(413) - |> put_view(RPCView) - |> render(:error, %{:error => "Payload Too Large. Max batch size is #{eth_json_rpc_max_batch_size}"}) + conn + |> put_status(413) + |> put_view(RPCView) + |> render(:error, %{:error => "Payload Too Large. Max batch size is #{eth_json_rpc_max_batch_size}"}) end end def eth_request(%{body_params: %{"_json" => request}} = conn, _) do - case AccessHelper.check_rate_limit(conn) do - :ok -> - [response] = EthRPC.responses([request]) + [response] = EthRPC.responses([request]) - conn - |> put_status(200) - |> put_view(EthRPCView) - |> render("response.json", %{response: response}) - - :rate_limit_reached -> - AccessHelper.handle_rate_limit_deny(conn) - end + conn + |> put_status(200) + |> put_view(EthRPCView) + |> render("response.json", %{response: response}) end def eth_request(conn, request) do - case AccessHelper.check_rate_limit(conn) do - :ok -> - # In the case that the JSON body is sent up w/o a json content type, - # Phoenix encodes it as a single key value pair, with the value being - # nil and the body being the key (as in a CURL request w/ no content type header) - decoded_request = - with [{single_key, nil}] <- Map.to_list(request), - {:ok, decoded} <- Jason.decode(single_key) do - decoded - else - _ -> request - end - - [response] = EthRPC.responses([decoded_request]) - - conn - |> put_status(200) - |> put_view(EthRPCView) - |> render("response.json", %{response: response}) - - :rate_limit_reached -> - AccessHelper.handle_rate_limit_deny(conn) - end + # In the case that the JSON body is sent up w/o a json content type, + # Phoenix encodes it as a single key value pair, with the value being + # nil and the body being the key (as in a CURL request w/ no content type header) + decoded_request = + with [{single_key, nil}] <- Map.to_list(request), + {:ok, decoded} <- Jason.decode(single_key) do + decoded + else + _ -> request + end + + [response] = EthRPC.responses([decoded_request]) + + conn + |> put_status(200) + |> put_view(EthRPCView) + |> render("response.json", %{response: response}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index 17fd203f6cb9..365404354e27 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -18,7 +18,6 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do import Plug.Conn import Phoenix.Controller, only: [put_view: 2] - alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.API.RPC.RPCView alias Phoenix.Controller @@ -33,7 +32,6 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do {:ok, {controller, write_actions}} <- translate_module(translations, module), {:ok, action} <- translate_action(action), true <- action_accessed?(action, write_actions), - :ok <- AccessHelper.check_rate_limit(conn), {:ok, conn} <- call_controller(conn, controller, action) do conn else @@ -62,9 +60,6 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do |> Controller.render(:error, error: "Something went wrong.") |> halt() - :rate_limit_reached -> - AccessHelper.handle_rate_limit_deny(conn) - {:valid_api_v1_request, false} -> conn |> put_status(404) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index bd10bd29c434..ee71ac2f715f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -4,7 +4,6 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do import BlockScoutWeb.Chain, only: [split_list_by_page: 1, next_page_params: 4, fetch_scam_token_toggle: 2] import Explorer.PagingOptions, only: [default_paging_options: 0] - alias BlockScoutWeb.CaptchaHelper alias BlockScoutWeb.API.V2.{AdvancedFilterView, CsvExportController} alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{AdvancedFilter, ContractMethod, Data, Token, Transaction} @@ -87,30 +86,28 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do """ @spec list_csv(Plug.Conn.t(), map()) :: Plug.Conn.t() def list_csv(conn, params) do - with {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)} do - full_options = - params - |> extract_filters() - |> Keyword.merge(paging_options(params)) - |> Keyword.update(:paging_options, %PagingOptions{page_size: CsvHelper.limit()}, fn paging_options -> - %PagingOptions{paging_options | page_size: CsvHelper.limit()} - end) - |> Keyword.put(:timeout, :timer.minutes(5)) - - full_options - |> AdvancedFilter.list() - |> AdvancedFilterView.to_csv_format() - |> CsvHelper.dump_to_stream() - |> Enum.reduce_while(CsvExportController.put_resp_params(conn), fn chunk, conn -> - case Conn.chunk(conn, chunk) do - {:ok, conn} -> - {:cont, conn} - - {:error, :closed} -> - {:halt, conn} - end + full_options = + params + |> extract_filters() + |> Keyword.merge(paging_options(params)) + |> Keyword.update(:paging_options, %PagingOptions{page_size: CsvHelper.limit()}, fn paging_options -> + %PagingOptions{paging_options | page_size: CsvHelper.limit()} end) - end + |> Keyword.put(:timeout, :timer.minutes(5)) + + full_options + |> AdvancedFilter.list() + |> AdvancedFilterView.to_csv_format() + |> CsvHelper.dump_to_stream() + |> Enum.reduce_while(CsvExportController.put_resp_params(conn), fn chunk, conn -> + case Conn.chunk(conn, chunk) do + {:ok, conn} -> + {:cont, conn} + + {:error, :closed} -> + {:halt, conn} + end + end) end @doc """ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex index 995d063863a8..6d23516eee70 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.API.V2.APIKeyController do """ @spec get_key(Plug.Conn.t(), nil | map) :: {:recaptcha, any} | Plug.Conn.t() def get_key(conn, params) do - ttl = Application.get_env(:block_scout_web, :api_rate_limit)[:api_v2_token_ttl_seconds] + ttl = div(Application.get_env(:block_scout_web, :api_rate_limit)[:api_v2_token_ttl], 1000) with {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)} do conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index bd25ca12efd2..dff5132cc795 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -1,8 +1,7 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do use BlockScoutWeb, :controller - alias BlockScoutWeb.{AccessHelper, CaptchaHelper} - alias BlockScoutWeb.API.V2.ApiView + alias BlockScoutWeb.AccessHelper alias Explorer.Chain alias Explorer.Chain.Address alias Explorer.Chain.Address.CurrentTokenBalance @@ -31,7 +30,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do def export_token_holders(conn, %{"address_hash_param" => address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)}, {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do token_holders = Chain.fetch_token_holders_from_token_hash_for_csv(address_hash, options()) @@ -73,8 +71,7 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do ) when is_binary(address_hash_string) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:address_exists, true} <- {:address_exists, Address.address_exists?(address_hash)}, - {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)} do + {:address_exists, true} <- {:address_exists, Address.address_exists?(address_hash)} do filter_type = Map.get(params, "filter_type") filter_value = Map.get(params, "filter_value") @@ -95,12 +92,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do {:address_exists, false} -> not_found(conn) - - {:recaptcha, false} -> - conn - |> put_status(:forbidden) - |> put_view(ApiView) - |> render(:message, %{message: "Invalid reCAPTCHA response"}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index fb473d19a5a1..5d3fc3609bec 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do import Explorer.Helper, only: [parse_integer: 1] - alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias BlockScoutWeb.AccessHelper alias Explorer.Chain alias Explorer.Chain.{Address, SmartContract} alias Explorer.Chain.SmartContract.AuditReport @@ -235,7 +235,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do def audit_report_submission(conn, %{"address_hash" => address_hash_string} = params) do with {:disabled, true} <- {:disabled, Application.get_env(:explorer, :air_table_audit_reports)[:enabled]}, {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string), - {:recaptcha, _} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)}, audit_report_params <- %{ address_hash: address_hash, submitter_name: params["submitter_name"], diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 115253e7cdbb..dc28ba7e7aaa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do use BlockScoutWeb, :controller use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] - alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias Explorer.{Chain, Helper, PagingOptions} alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} @@ -382,7 +382,6 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params, :token_instance_refetch_metadata)}, {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)}, diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index d02ba286f1ee..6edc1dd63e33 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -78,7 +78,17 @@ defmodule BlockScoutWeb.Endpoint do # 'x-apollo-tracing' header for https://www.graphqlbin.com to work with our GraphQL endpoint # 'updated-gas-oracle' header for /api/v2/stats endpoint, added to support cross-origin requests (e.g. multichain search explorer) - plug(CORSPlug, headers: ["x-apollo-tracing", "updated-gas-oracle"] ++ CORSPlug.defaults()[:headers]) + plug(CORSPlug, + headers: + [ + "x-apollo-tracing", + "updated-gas-oracle", + "recaptcha-v2-response", + "recaptcha-v3-response", + "recaptcha-bypass-token", + "scoped-recaptcha-bypass-token" + ] ++ CORSPlug.defaults()[:headers] + ) plug(BlockScoutWeb.Router) end diff --git a/apps/block_scout_web/lib/block_scout_web/plug/rate_limit.ex b/apps/block_scout_web/lib/block_scout_web/plug/rate_limit.ex index 5f6de5f4fef1..6d01b08d90ff 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/rate_limit.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/rate_limit.ex @@ -2,30 +2,115 @@ defmodule BlockScoutWeb.Plug.RateLimit do @moduledoc """ Rate limiting """ - alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.{AccessHelper, RateLimit} + alias Plug.Conn def init(opts), do: opts - def call(conn, graphql?: true) do - case AccessHelper.check_rate_limit(conn, graphql?: true) do - :ok -> - conn + def call(conn, _opts) do + config = fetch_rate_limit_config(conn) - true -> + conn + |> handle_call(config) + |> case do + {:deny, _time_to_reset, _limit, _period} = result -> conn + |> set_rate_limit_headers(result) + |> set_rate_limit_headers_for_frontend(config) + |> AccessHelper.handle_rate_limit_deny(!api_v2?(conn)) - _ -> - AccessHelper.handle_rate_limit_deny(conn, true) + result -> + conn + |> set_rate_limit_headers(result) + |> set_rate_limit_headers_for_frontend(config) end end - def call(conn, _opts) do - case AccessHelper.check_rate_limit(conn) do - :ok -> + defp set_rate_limit_headers(conn, result) do + case result do + {:allow, -1} -> + conn + |> Conn.put_resp_header("x-ratelimit-limit", "-1") + |> Conn.put_resp_header("x-ratelimit-remaining", "-1") + |> Conn.put_resp_header("x-ratelimit-reset", "-1") + + {:allow, count, limit, period} -> + now = System.system_time(:millisecond) + window = div(now, period) + expires_at = (window + 1) * period + conn + |> Conn.put_resp_header("x-ratelimit-limit", "#{limit}") + |> Conn.put_resp_header("x-ratelimit-remaining", "#{limit - count}") + |> Conn.put_resp_header("x-ratelimit-reset", "#{expires_at - now}") - :rate_limit_reached -> - AccessHelper.handle_rate_limit_deny(conn, true) + {:deny, time_to_reset, limit, _time_interval} -> + conn + |> Conn.put_resp_header("x-ratelimit-limit", "#{limit}") + |> Conn.put_resp_header("x-ratelimit-remaining", "0") + |> Conn.put_resp_header("x-ratelimit-reset", "#{time_to_reset}") end end + + defp set_rate_limit_headers_for_frontend(conn, config) do + user_agent = RateLimit.get_user_agent(conn) + + option = + cond do + config[:recaptcha_to_bypass_429] && user_agent -> "recaptcha" + config[:temporary_token] && user_agent -> "temporary_token" + !is_nil(config) -> "no_bypass" + true -> "no_bypass" + end + + conn + |> Conn.put_resp_header("bypass-429-option", option) + end + + defp handle_call(conn, config) do + if graphql?(conn) do + RateLimit.check_rate_limit_graphql(conn, 1) + else + RateLimit.rate_limit_with_config(conn, config) + end + end + + defp fetch_rate_limit_config(conn) do + request_path = request_path(conn) + config = :persistent_term.get(:rate_limit_config) + + if res = config[:static_match][request_path] do + res + else + find_endpoint_config(config, conn.path_info) || config[:static_match]["default"] + end + end + + defp find_endpoint_config(config, request_path_parts) do + config[:parametrized_match] + |> Enum.find({nil, nil}, fn {key, _config} -> + length(key) == length(request_path_parts) && + key |> Enum.zip(request_path_parts) |> Enum.all?(fn {k, r} -> k == r || k == ":param" end) + end) + |> elem(1) || + config[:wildcard_match] + |> Enum.find({nil, nil}, fn {{key, length}, _config} when is_integer(length) -> + Enum.take(request_path_parts, length) == key + end) + |> elem(1) + end + + defp graphql?(conn) do + request_path = request_path(conn) + request_path == "api/v1/graphql" or request_path == "graphiql" + end + + defp request_path(conn) do + Enum.join(conn.path_info, "/") + end + + defp api_v2?(conn) do + path_part = conn.path_info |> Enum.take(2) + path_part == ["api", "v2"] or path_part == ["api", "account"] + end end diff --git a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex new file mode 100644 index 000000000000..9cd9615a3f79 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex @@ -0,0 +1,376 @@ +defmodule BlockScoutWeb.RateLimit do + @moduledoc """ + Rate limiting + """ + alias BlockScoutWeb.RateLimit.Hammer + alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias Explorer.Account.Api.Key, as: ApiKey + alias Plug.Conn + + require Logger + + @doc """ + Checks, if rate limit reached before making a new request. It is applied to GraphQL API. + """ + @spec check_rate_limit_graphql(Plug.Conn.t(), integer()) :: + {:allow, -1} | {:deny, integer(), integer(), integer()} | {:allow, integer(), integer(), integer()} + def check_rate_limit_graphql(conn, multiplier) do + config = Application.get_env(:block_scout_web, Api.GraphQL) + no_rate_limit_api_key = config[:no_rate_limit_api_key] + + cond do + config[:rate_limit_disabled?] -> + {:allow, -1} + + check_no_rate_limit_api_key(conn, no_rate_limit_api_key) -> + {:allow, -1} + + true -> + check_graphql_rate_limit_inner(conn, config, multiplier) + end + end + + defp check_graphql_rate_limit_inner(conn, config, multiplier) do + static_api_key = config[:static_api_key] + + ip_string = AccessHelper.conn_to_ip_string(conn) + + user_api_key = get_api_key(conn) + + with {:api_key, false} <- {:api_key, has_api_key_param?(conn) && user_api_key == static_api_key}, + {:plan, plan} when plan in [false, nil] <- {:plan, has_api_key_param?(conn) && get_plan(conn.query_params)} do + ip_result = + rate_limit("graphql_#{ip_string}", config[:time_interval_limit_by_ip], config[:limit_by_ip], multiplier) + + if match?({:allow, _}, ip_result) or match?({:allow, _, _, _}, ip_result) do + maybe_replace_result( + ip_result, + rate_limit("graphql", config[:time_interval_limit], config[:global_limit], multiplier) + ) + else + ip_result + end + else + {:api_key, true} -> + rate_limit(static_api_key, config[:time_interval_limit], config[:limit_by_key], multiplier) + + {:plan, {plan, api_key}} -> + rate_limit( + api_key, + config[:time_interval_limit], + min(plan.max_req_per_second, config[:limit_by_key]), + multiplier + ) + end + end + + defp maybe_replace_result(ip_result, {:allow, -1}) do + ip_result + end + + defp maybe_replace_result(ip_result, {:allow, _, _, _}) do + ip_result + end + + defp maybe_replace_result(_ip_result, global_result) do + global_result + end + + @spec rate_limit_with_config(Plug.Conn.t(), map()) :: + {:allow, -1} | {:deny, integer(), integer(), integer()} | {:allow, integer(), integer(), integer()} + def rate_limit_with_config(conn, config) do + config + |> prepare_pipeline(conn) + |> Enum.reject(&(is_nil(&1) || &1 == false)) + |> Enum.reduce_while(nil, fn fun, _acc -> + case fun.(conn) do + :skip -> {:cont, nil} + result -> {:halt, result} + end + end) + |> maybe_check_recaptcha_response( + conn, + get_user_agent(conn), + config[:recaptcha_to_bypass_429], + config[:bypass_token_scope] + ) + |> case do + nil -> + Logger.error("Misconfiguration issue for #{conn.request_path}") + {:allow, -1} + + result -> + result + end + end + + defp prepare_pipeline(config, conn) do + global_config = Application.get_env(:block_scout_web, :api_rate_limit) + + [ + global_config[:disabled] && fn _ -> {:allow, -1} end, + config[:ignore] && fn _ -> {:allow, -1} end, + check_no_rate_limit_api_key(conn, global_config[:no_rate_limit_api_key_value]) && fn _ -> {:allow, -1} end, + config[:temporary_token] && + (&rate_limit_by_temporary_token( + &1, + config[:temporary_token], + global_config[:temporary_token], + config[:bucket_key_prefix] + )), + config[:static_api_key] && + (&rate_limit_by_static_api_key( + &1, + config[:static_api_key], + global_config[:static_api_key], + global_config, + config[:bucket_key_prefix] + )), + config[:account_api_key] && + (&rate_limit_by_account_api_key( + &1, + config[:account_api_key], + global_config[:account_api_key], + config[:bucket_key_prefix] + )), + config[:whitelisted_ip] && + (&rate_limit_by_whitelisted_ip( + &1, + config[:whitelisted_ip], + global_config[:whitelisted_ip], + global_config, + config[:bucket_key_prefix] + )), + config[:ip] && + (&rate_limit_by_ip(&1, config[:ip], global_config[:ip], config[:bucket_key_prefix])) + ] + end + + defp maybe_check_recaptcha_response(result, conn, user_agent, true, scope) when not is_nil(user_agent) do + case result do + {:deny, _time_to_reset, limit, time_interval} -> + conn + |> check_recaptcha(scope) + |> case do + true -> + {:allow, limit, limit, time_interval} + + false -> + result + end + + _ -> + result + end + end + + defp maybe_check_recaptcha_response(result, _, _, _, _) do + result + end + + defp check_recaptcha(conn, scope) when is_binary(scope) do + conn + |> collect_recaptcha_headers() + |> CaptchaHelper.recaptcha_passed?(String.to_atom(scope)) + end + + defp check_recaptcha(conn, _) do + conn + |> collect_recaptcha_headers() + |> CaptchaHelper.recaptcha_passed?() + end + + defp collect_recaptcha_headers(conn) do + recaptcha_response = get_header_or_nil(conn, "recaptcha-v2-response") + recaptcha_v3_response = get_header_or_nil(conn, "recaptcha-v3-response") + recaptcha_bypass_token = get_header_or_nil(conn, "recaptcha-bypass-token") + scoped_recaptcha_bypass_token = get_header_or_nil(conn, "scoped-recaptcha-bypass-token") + + cond do + recaptcha_response -> + %{ + "recaptcha_response" => recaptcha_response + } + + recaptcha_v3_response -> + %{ + "recaptcha_v3_response" => recaptcha_v3_response + } + + scoped_recaptcha_bypass_token -> + %{ + "scoped_recaptcha_bypass_token" => scoped_recaptcha_bypass_token + } + + recaptcha_bypass_token -> + %{ + "recaptcha_bypass_token" => recaptcha_bypass_token + } + + true -> + %{} + end + end + + defp get_header_or_nil(conn, header_name) do + case Conn.get_req_header(conn, header_name) do + [response] -> + response + + _ -> + nil + end + end + + defp rate_limit_by_static_api_key(conn, route_config, default_config, global_config, bucket_key_prefix) do + config = config_or_default(route_config, default_config) + static_api_key = global_config[:static_api_key_value] + + if has_api_key_param?(conn) && get_api_key(conn) == static_api_key do + rate_limit(static_api_key, config[:period], config[:limit], config[:cost] || 1, bucket_key_prefix) + else + :skip + end + end + + @spec rate_limit_by_account_api_key(any(), any(), any(), String.t()) :: + :skip | {:allow, -1} | {:deny, integer(), integer(), integer()} | {:allow, integer(), integer(), integer()} + defp rate_limit_by_account_api_key(conn, route_config, global_config, bucket_key_prefix) do + config = config_or_default(route_config, global_config) + plan = get_plan(conn.query_params) + + if plan do + {plan, api_key} = plan + + rate_limit( + api_key, + config[:period], + config[:limit] || plan.max_req_per_second, + config[:cost] || 1, + bucket_key_prefix + ) + else + :skip + end + end + + defp rate_limit_by_whitelisted_ip(conn, route_config, default_config, global_config, bucket_key_prefix) do + config = config_or_default(route_config, default_config) + ip_string = AccessHelper.conn_to_ip_string(conn) + + if Enum.member?(whitelisted_ips(global_config), ip_string) do + rate_limit(ip_string, config[:period], config[:limit], config[:cost] || 1, bucket_key_prefix) + else + :skip + end + end + + defp rate_limit_by_temporary_token(conn, route_config, default_config, bucket_key_prefix) do + config = config_or_default(route_config, default_config) + ip_string = AccessHelper.conn_to_ip_string(conn) + token = get_ui_v2_token(conn, ip_string) + + if token && !is_nil(get_user_agent(conn)) do + rate_limit(token, config[:period], config[:limit], config[:cost] || 1, bucket_key_prefix) + else + :skip + end + end + + defp rate_limit_by_ip(conn, route_config, default_config, bucket_key_prefix) do + config = config_or_default(route_config, default_config) + ip_string = AccessHelper.conn_to_ip_string(conn) + + rate_limit(ip_string, config[:period], config[:limit], config[:cost] || 1, bucket_key_prefix) + end + + @spec config_or_default(any(), any()) :: any() + defp config_or_default(config, default) do + if is_map(config) do + config + else + default + end + end + + @spec rate_limit(String.t(), integer(), integer(), integer(), String.t()) :: + {:allow, integer(), integer(), integer()} | {:deny, integer(), integer(), integer()} | {:allow, -1} + def rate_limit(key, time_interval, limit, multiplier, bucket_key_prefix \\ "") do + case Hammer.hit(construct_bucket_key(key, bucket_key_prefix), time_interval, limit, multiplier) do + {:allow, count} -> + {:allow, count, limit, time_interval} + + {:deny, time_to_reset} -> + {:deny, time_to_reset, limit, time_interval} + + {:error, error} -> + Logger.error(fn -> ["Rate limit check error: ", inspect(error)] end) + {:allow, -1} + end + end + + @spec construct_bucket_key(String.t(), String.t()) :: String.t() + defp construct_bucket_key(key, bucket_key_prefix) do + chain_id = Application.get_env(:block_scout_web, :chain_id) + "#{chain_id}_#{bucket_key_prefix}#{key}" + end + + defp check_no_rate_limit_api_key(conn, no_rate_limit_api_key) do + user_api_key = get_api_key(conn) + + has_api_key_param?(conn) && !is_nil(user_api_key) && String.trim(user_api_key) !== "" && + user_api_key == no_rate_limit_api_key + end + + @doc """ + Get the user agent from the request headers. + """ + @spec get_user_agent(Plug.Conn.t()) :: nil | binary() + def get_user_agent(conn) do + case Conn.get_req_header(conn, "user-agent") do + [agent] -> + agent + + _ -> + nil + end + end + + defp get_ui_v2_token(conn, ip_string) do + api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key) + conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key]) + + case conn.cookies[api_v2_temp_token_key] do + %{ip: ^ip_string} -> + conn.req_cookies[api_v2_temp_token_key] + + _ -> + nil + end + end + + defp whitelisted_ips(api_rate_limit_object) do + case api_rate_limit_object && api_rate_limit_object |> Keyword.fetch(:whitelisted_ips) do + {:ok, whitelisted_ips_string} -> + if whitelisted_ips_string, do: String.split(whitelisted_ips_string, ","), else: [] + + _ -> + [] + end + end + + defp has_api_key_param?(conn), do: Map.has_key?(conn.query_params, "apikey") + + defp get_api_key(conn), do: Map.get(conn.query_params, "apikey") + + defp get_plan(query_params) do + with true <- query_params && Map.has_key?(query_params, "apikey"), + api_key_value <- Map.get(query_params, "apikey"), + api_key when not is_nil(api_key) <- ApiKey.api_key_with_plan_by_value(api_key_value) do + {api_key.identity.plan, to_string(api_key.value)} + else + _ -> + nil + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/rate_limit/hammer.ex b/apps/block_scout_web/lib/block_scout_web/rate_limit/hammer.ex new file mode 100644 index 000000000000..fdfe5fcd1606 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/rate_limit/hammer.ex @@ -0,0 +1,38 @@ +defmodule BlockScoutWeb.RateLimit.Hammer.ETS do + @moduledoc false + use Hammer, backend: Hammer.ETS +end + +defmodule BlockScoutWeb.RateLimit.Hammer.Redis do + @moduledoc false + use Hammer, backend: Hammer.Redis +end + +defmodule BlockScoutWeb.RateLimit.Hammer do + @moduledoc """ + Wrapper for the rate limit functions. Defines union of all functions from `BlockScoutWeb.RateLimit.Hammer.ETS` and `BlockScoutWeb.RateLimit.Hammer.Redis`. Resolves the backend to use based on `Application.get_env(:block_scout_web, :rate_limit_backend)` in runtime. + """ + alias BlockScoutWeb.RateLimit.Hammer.{ETS, Redis} + + functions = + (ETS.__info__(:functions) ++ Redis.__info__(:functions)) + |> Enum.uniq() + + for {name, arity} <- functions do + args = Macro.generate_arguments(arity, nil) + + def unquote(name)(unquote_splicing(args)) do + apply(Application.get_env(:block_scout_web, :api_rate_limit)[:rate_limit_backend], unquote(name), unquote(args)) + end + end + + def child_for_supervisor do + redis_url = Application.get_env(:block_scout_web, :api_rate_limit)[:redis_url] + + if redis_url do + {BlockScoutWeb.RateLimit.Hammer.Redis, [url: redis_url]} + else + {BlockScoutWeb.RateLimit.Hammer.ETS, []} + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 05292b44f702..2882274689ab 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -44,6 +44,7 @@ defmodule BlockScoutWeb.Router do ) plug(BlockScoutWeb.Plug.Logger, application: :api) + plug(BlockScoutWeb.Plug.RateLimit) plug(:accepts, ["json"]) end @@ -57,12 +58,21 @@ defmodule BlockScoutWeb.Router do plug(BlockScoutWeb.Plug.Logger, application: :api) plug(:accepts, ["json"]) - plug(BlockScoutWeb.Plug.RateLimit, graphql?: true) + plug(BlockScoutWeb.Plug.RateLimit) + end + + pipeline :rate_limit do + plug(:fetch_query_params) + plug(:accepts, ["json"]) + plug(BlockScoutWeb.Plug.RateLimit) end match(:*, "/auth/*path", AccountRouter, []) - forward("/api", ApiRouter) + scope "/api" do + pipe_through(:rate_limit) + forward("/", ApiRouter) + end scope "/graphiql" do pipe_through(:api_v1_graphql) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex index 36edce1b8c70..71b7571ac704 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.Routers.AddressBadgesApiV2Router do """ use BlockScoutWeb, :router alias BlockScoutWeb.API.V2 - alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + alias BlockScoutWeb.Plug.CheckApiV2 @max_query_string_length 5_000 @@ -23,7 +23,6 @@ defmodule BlockScoutWeb.Routers.AddressBadgesApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(RateLimit) end pipeline :api_v2_no_forgery_protect do @@ -39,7 +38,6 @@ defmodule BlockScoutWeb.Routers.AddressBadgesApiV2Router do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(RateLimit) plug(:fetch_session) end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index a0c81c67de56..34ff45afd3d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -34,7 +34,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do UtilsApiV2Router } - alias BlockScoutWeb.Plug.{CheckApiV2, CheckFeature, RateLimit} + alias BlockScoutWeb.Plug.{CheckApiV2, CheckFeature} alias BlockScoutWeb.Routers.AccountRouter @max_query_string_length 5_000 @@ -75,7 +75,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(RateLimit) plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end @@ -91,7 +90,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(RateLimit) end pipeline :api_v1_graphql do @@ -104,7 +102,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do plug(BlockScoutWeb.Plug.Logger, application: :api) plug(:accepts, ["json"]) - plug(RateLimit, graphql?: true) plug(BlockScoutWeb.Plug.GraphQLSchemaIntrospection) end @@ -536,13 +533,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do # leave the same endpoint in v1 in order to keep backward compatibility get("/search", SearchController, :search) - # todo: remove these old CSV export related endpoints in 7.2.0 or higher since they are moved to /api/v2/** path. - # Related frontend task https://github.com/blockscout/frontend/issues/2718. - get("/transactions-csv", V2.CsvExportController, :transactions_csv) - get("/token-transfers-csv", V2.CsvExportController, :token_transfers_csv) - get("/internal-transactions-csv", V2.CsvExportController, :internal_transactions_csv) - get("/logs-csv", V2.CsvExportController, :logs_csv) - if @chain_type == :celo do get("/celo-election-rewards-csv", V2.CsvExportController, :celo_election_rewards_csv) end @@ -578,7 +568,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/multichain-search-export", BlockScoutWeb.API.HealthController, :multichain_search_db_export) end - # For backward compatibility. Should be removed scope "/" do pipe_through(:api) alias BlockScoutWeb.API.{EthRPC, RPC} diff --git a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex index 0218d273994e..37e1750f7542 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias BlockScoutWeb.API.V2 - alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + alias BlockScoutWeb.Plug.CheckApiV2 @max_query_string_length 5_000 @@ -25,7 +25,6 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(RateLimit) end pipeline :api_v2_no_forgery_protect do @@ -41,7 +40,6 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(RateLimit) plug(:fetch_session) end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex index b61781cca79b..ad161bc73084 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] alias BlockScoutWeb.API.V2 - alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + alias BlockScoutWeb.Plug.CheckApiV2 @max_query_string_length 5_000 @@ -25,7 +25,6 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(RateLimit) end pipeline :api_v2_no_forgery_protect do @@ -41,7 +40,6 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(RateLimit) plug(:fetch_session) end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex index 2e9b8ef66fd3..04eeae92f2a3 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Routers.UtilsApiV2Router do Router for /api/v2/utils. This route has separate router in order to ignore sobelow's warning about missing CSRF protection """ use BlockScoutWeb, :router - alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + alias BlockScoutWeb.Plug.CheckApiV2 pipeline :api_v2_no_forgery_protect do plug( @@ -19,7 +19,6 @@ defmodule BlockScoutWeb.Routers.UtilsApiV2Router do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(CheckApiV2) - plug(RateLimit) plug(:fetch_session) end diff --git a/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex b/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex new file mode 100644 index 000000000000..3a8a32de9ca8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex @@ -0,0 +1,165 @@ +defmodule BlockScoutWeb.Utility.RateLimitConfigHelper do + @moduledoc """ + Fetches the rate limit config from the config url and parses it into a map. + """ + require Logger + + alias Utils.ConfigHelper + + @doc """ + Fetches the rate limit config from the config url and puts it into the persistent term under the key `:rate_limit_config`. + """ + @spec store_rate_limit_config() :: :ok + def store_rate_limit_config do + :persistent_term.put(:rate_limit_config, fetch_config()) + end + + defp fetch_config do + url = Application.get_env(:block_scout_web, :api_rate_limit)[:config_url] + + with {:ok, config} <- download_config(url), + parsed_config <- parse_config(config) do + parsed_config + else + {:error, reason} -> + Logger.error("Failed to fetch rate limit config: #{inspect(reason)}. Fallback to local config.") + fallback_config() + end + rescue + error -> + Logger.error("Failed to fetch config: #{inspect(error)}. Fallback to local config.") + fallback_config() + end + + defp fallback_config do + with {:ok, config} <- + :block_scout_web + |> Application.app_dir("priv/rate_limit_config.json") + |> File.read(), + {:ok, config} <- decode_config(config), + config <- parse_config(config) do + config + else + {:error, reason} -> + Logger.error("Failed to parse local config: #{inspect(reason)}. Using default rate limits from ENVs.") + %{} + end + end + + defp download_config(url) when is_binary(url) do + url + |> HTTPoison.get([], follow_redirect: true) + |> case do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + decode_config(body) + + {:ok, %HTTPoison.Response{status_code: status}} -> + Logger.error("Failed to fetch config from #{url}: #{status}") + {:error, status} + + {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} + end + end + + defp download_config(_), do: {:error, :invalid_config_url} + + defp decode_config(config) do + to_atom? = + Map.from_keys( + [ + "account_api_key", + "bypass_token_scope", + "cost", + "ip", + "ignore", + "limit", + "period", + "recaptcha_to_bypass_429", + "static_api_key", + "temporary_token", + "whitelisted_ip", + "isolate_rate_limit?" + ], + true + ) + + config + |> Jason.decode( + keys: fn key -> + if to_atom?[key] do + String.to_atom(key) + else + key + end + end + ) + end + + defp parse_config(config) do + config = decode_time_values(config) + + config + |> Map.keys() + |> Enum.reduce(%{wildcard_match: %{}, parametrized_match: %{}, static_match: %{}}, fn key, acc -> + {type, value} = process_endpoint_path(key) + + prefix = + if config[key][:isolate_rate_limit?] do + "#{key}_" + else + "" + end + + config = Map.put(config[key], :bucket_key_prefix, prefix) + + Map.update(acc, type, %{value => config}, fn existing_value -> + Map.put(existing_value, value, config) + end) + end) + end + + defp process_endpoint_path(key) do + path_parts = key |> String.trim("/") |> String.split("/") + + cond do + String.contains?(key, "*") -> + if Enum.find_index(path_parts, &Kernel.==(&1, "*")) == length(path_parts) - 1 do + {:wildcard_match, {Enum.drop(path_parts, -1), length(path_parts) - 1}} + else + raise "wildcard `*` allowed only at the end of the path" + end + + String.contains?(key, ":param") -> + {:parametrized_match, path_parts} + + true -> + {:static_match, key} + end + end + + defp decode_time_values(config) when is_map(config) do + config + |> Enum.map(fn + {:period, value} when is_binary(value) -> + {:period, parse_time_string(value)} + + {key, value} when is_map(value) -> + {key, decode_time_values(value)} + + entry -> + entry + end) + |> Enum.into(%{}) + end + + defp parse_time_string(value) do + case ConfigHelper.parse_time_value(value) do + :error -> + raise "Invalid time format in rate limit config: #{value}" + + time -> + time + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex index c8c27436ea52..ba9d7bffb2a1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex @@ -10,7 +10,6 @@ defmodule BlockScoutWeb.AccessHelper do alias BlockScoutWeb.API.V2.ApiView alias BlockScoutWeb.Routers.WebRouter.Helpers alias Explorer.{AccessHelper, Chain} - alias Explorer.Account.Api.Key, as: ApiKey alias Plug.Conn alias RemoteIp @@ -67,164 +66,30 @@ defmodule BlockScoutWeb.AccessHelper do apply(Helpers, path, full_args) end - def handle_rate_limit_deny(conn, api_v2? \\ false) do + @doc """ + Handles a rate limit deny. + + ## Parameters + - conn: The connection to handle. + - api_v1?: Whether the API is v1. + + ## Returns + - A connection with the status code 429 and the view set to ApiView if api_v2? is true, otherwise the view is set to RPCView. + """ + @spec handle_rate_limit_deny(Plug.Conn.t(), boolean()) :: Plug.Conn.t() + def handle_rate_limit_deny(conn, api_v1?) do APILogger.message("API rate limit reached") - view = if api_v2?, do: ApiView, else: RPCView - tag = if api_v2?, do: :message, else: :error + view = if api_v1?, do: RPCView, else: ApiView + tag = if api_v1?, do: :error, else: :message conn |> Conn.put_status(429) |> put_view(view) - |> render(tag, %{tag => "429 Too Many Requests"}) + |> render(tag, %{tag => "Too Many Requests"}) |> Conn.halt() end - @doc """ - Checks, if rate limit reached before making a new request. It is applied to GraphQL API. - """ - @spec check_rate_limit(Plug.Conn.t(), list()) :: :ok | :rate_limit_reached | true | false - def check_rate_limit(conn, graphql?: true) do - rate_limit_config = Application.get_env(:block_scout_web, Api.GraphQL) - no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key] - - cond do - rate_limit_config[:rate_limit_disabled?] -> - :ok - - check_no_rate_limit_api_key(conn, no_rate_limit_api_key) -> - :ok - - true -> - check_graphql_rate_limit_inner(conn, rate_limit_config) - end - end - - defp check_graphql_rate_limit_inner(conn, rate_limit_config) do - global_limit = rate_limit_config[:global_limit] - limit_by_key = rate_limit_config[:limit_by_key] - time_interval_limit = rate_limit_config[:time_interval_limit] - static_api_key = rate_limit_config[:static_api_key] - limit_by_ip = rate_limit_config[:limit_by_ip] - time_interval_by_ip = rate_limit_config[:time_interval_limit_by_ip] - - ip_string = conn_to_ip_string(conn) - plan = get_plan(conn.query_params) - - user_api_key = get_api_key(conn) - - cond do - check_api_key(conn) && user_api_key == static_api_key -> - rate_limit(static_api_key, time_interval_limit, limit_by_key) - - check_api_key(conn) && !is_nil(plan) -> - rate_limit(user_api_key, time_interval_limit, min(plan.max_req_per_second, limit_by_key)) - - true -> - rate_limit("graphql_#{ip_string}", limit_by_ip, time_interval_by_ip) == :ok && - rate_limit("graphql", time_interval_limit, global_limit) == :ok - end - end - - @doc """ - Checks, if rate limit reached before making a new request. It is applied to API v1, ETH RPC API. - """ - @spec check_rate_limit(Plug.Conn.t()) :: :ok | :rate_limit_reached - def check_rate_limit(conn) do - rate_limit_config = Application.get_env(:block_scout_web, :api_rate_limit) - no_rate_limit_api_key = rate_limit_config[:no_rate_limit_api_key] - - cond do - rate_limit_config[:disabled] -> - :ok - - check_no_rate_limit_api_key(conn, no_rate_limit_api_key) -> - :ok - - true -> - check_rate_limit_inner(conn, rate_limit_config) - end - end - - defp check_no_rate_limit_api_key(conn, no_rate_limit_api_key) do - user_api_key = get_api_key(conn) - - check_api_key(conn) && !is_nil(user_api_key) && String.trim(user_api_key) !== "" && - user_api_key == no_rate_limit_api_key - end - - # credo:disable-for-next-line /Complexity/ - defp check_rate_limit_inner(conn, rate_limit_config) do - global_limit = rate_limit_config[:global_limit] - limit_by_key = rate_limit_config[:limit_by_key] - limit_by_whitelisted_ip = rate_limit_config[:limit_by_whitelisted_ip] - time_interval_limit = rate_limit_config[:time_interval_limit] - static_api_key = rate_limit_config[:static_api_key] - limit_by_ip = rate_limit_config[:limit_by_ip] - api_v2_ui_limit = rate_limit_config[:api_v2_ui_limit] - time_interval_by_ip = rate_limit_config[:time_interval_limit_by_ip] - - ip_string = conn_to_ip_string(conn) - - plan = get_plan(conn.query_params) - token = get_ui_v2_token(conn, ip_string) - - user_agent = get_user_agent(conn) - - user_api_key = get_api_key(conn) - - cond do - check_api_key(conn) && user_api_key == static_api_key -> - rate_limit(static_api_key, time_interval_limit, limit_by_key) - - check_api_key(conn) && !is_nil(plan) -> - rate_limit(user_api_key, time_interval_limit, plan.max_req_per_second) - - Enum.member?(whitelisted_ips(rate_limit_config), ip_string) -> - rate_limit(ip_string, time_interval_limit, limit_by_whitelisted_ip) - - api_v2_request?(conn) && !is_nil(token) && !is_nil(user_agent) -> - rate_limit(token, time_interval_limit, api_v2_ui_limit) - - api_v2_request?(conn) && !is_nil(user_agent) -> - rate_limit(ip_string, time_interval_by_ip, limit_by_ip) - - true -> - rate_limit("api", time_interval_limit, global_limit) - end - end - - defp check_api_key(conn), do: Map.has_key?(conn.query_params, "apikey") - - defp get_api_key(conn), do: Map.get(conn.query_params, "apikey") - - defp get_plan(query_params) do - with true <- query_params && Map.has_key?(query_params, "apikey"), - api_key_value <- Map.get(query_params, "apikey"), - api_key <- ApiKey.api_key_with_plan_by_value(api_key_value), - false <- is_nil(api_key) do - api_key.identity.plan - else - _ -> - nil - end - end - - @spec rate_limit(String.t(), integer(), integer()) :: :ok | :rate_limit_reached - defp rate_limit(key, time_interval, limit) do - case Hammer.check_rate(key, time_interval, limit) do - {:allow, _count} -> - :ok - - {:deny, _limit} -> - :rate_limit_reached - - {:error, error} -> - Logger.error(fn -> ["Rate limit check error: ", inspect(error)] end) - :ok - end - end - defp get_restricted_key(%Phoenix.Socket{}), do: nil defp get_restricted_key(conn) do @@ -232,19 +97,16 @@ defmodule BlockScoutWeb.AccessHelper do conn_with_params.query_params["key"] end - defp whitelisted_ips(api_rate_limit_object) do - case api_rate_limit_object && api_rate_limit_object |> Keyword.fetch(:whitelisted_ips) do - {:ok, whitelisted_ips_string} -> - if whitelisted_ips_string, do: String.split(whitelisted_ips_string, ","), else: [] - - _ -> - [] - end - end + @doc """ + Converts a connection to an IP string. - defp api_v2_request?(%Plug.Conn{request_path: "/api/v2/" <> _}), do: true - defp api_v2_request?(_), do: false + ## Parameters + - conn: The connection to convert. + ## Returns + - A string representing the IP address. If the connection is behind a proxy, the IP address from the headers is used. + """ + @spec conn_to_ip_string(Plug.Conn.t()) :: String.t() def conn_to_ip_string(conn) do is_blockscout_behind_proxy = Application.get_env(:block_scout_web, :api_rate_limit)[:is_blockscout_behind_proxy] @@ -253,27 +115,4 @@ defmodule BlockScoutWeb.AccessHelper do to_string(:inet_parse.ntoa(ip)) end - - defp get_ui_v2_token(conn, ip_string) do - api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key) - conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key]) - - case api_v2_request?(conn) && conn.cookies[api_v2_temp_token_key] do - %{ip: ^ip_string} -> - conn.req_cookies[api_v2_temp_token_key] - - _ -> - nil - end - end - - defp get_user_agent(conn) do - case Conn.get_req_header(conn, "user-agent") do - [agent] -> - agent - - _ -> - nil - end - end end diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 0973361a2d78..5e865d3d01ee 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -109,7 +109,8 @@ defmodule BlockScoutWeb.Mixfile do {:floki, "~> 0.31"}, {:flow, "~> 1.2"}, {:gettext, "~> 0.26.1"}, - {:hammer, "~> 6.0"}, + {:hammer, "~> 7.0"}, + {:hammer_backend_redis, "~> 7.0"}, {:httpoison, "~> 2.0"}, {:indexer, in_umbrella: true, runtime: false}, # JSON parser and generator diff --git a/apps/block_scout_web/priv/rate_limit_config.json b/apps/block_scout_web/priv/rate_limit_config.json new file mode 100644 index 000000000000..8208d59f52e5 --- /dev/null +++ b/apps/block_scout_web/priv/rate_limit_config.json @@ -0,0 +1,111 @@ +{ + "api/v2/key": { + "ignore": true + }, + "api/account/v2/authenticate_via_wallet": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 1 + }, + "isolate_rate_limit?": true + }, + "api/account/v2/send_otp": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 1 + }, + "isolate_rate_limit?": true + }, + "api/v2/tokens/:param/instances/:param/refetch-metadata": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "bypass_token_scope": "token_instance_refetch_metadata", + "isolate_rate_limit?": true + }, + "api/v2/advanced-filters/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/tokens/:param/holders/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/addresses/:param/transactions/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/addresses/:param/token-transfers/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/addresses/:param/internal-transactions/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/addresses/:param/logs/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/addresses/:param/election-rewards/csv": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/smart-contracts/:param/audit-reports": { + "temporary_token": { + "period": "1h", + "limit": 50 + }, + "ip": { + "period": "1h", + "limit": 50 + }, + "isolate_rate_limit?": true + }, + "api/v2/*": { + "static_api_key": true, + "account_api_key": true, + "whitelisted_ip": true, + "ip": true, + "temporary_token": true + }, + "default": { + "static_api_key": true, + "account_api_key": true, + "whitelisted_ip": true, + "ip": true, + "temporary_token": false + } +} diff --git a/apps/block_scout_web/rate_limits.md b/apps/block_scout_web/rate_limits.md new file mode 100644 index 000000000000..6eaa1eaf6f5c --- /dev/null +++ b/apps/block_scout_web/rate_limits.md @@ -0,0 +1,99 @@ +# Rate Limits Configuration + +Rate limits can be defined in a JSON configuration file `apps/block_scout_web/priv/rate_limit_config.json` or passed to `API_RATE_LIMIT_CONFIG_URL` as a URL to the JSON file. + +## Configuration Structure + +The JSON configuration is a map where: +- **Keys**: API endpoint paths +- **Values**: Rate limit configuration + +### Example Configuration +```json +"api/account/v2/send_otp": { + "recaptcha_to_bypass_429": true, + "ip": { + "period": "1h", + "limit": 1 + } +} +``` + +### Path Rules +- Paths should not contain query parameters +- Paths should not contain trailing slashes +- Paths can contain: + - `*` - Works as a wildcard (matches any path starting from the asterisk) + - Example: `api/v2/*` matches `api/v2` and `api/v2/addresses` + - ⚠️ Wildcard `*` allowed only at the end of the path + - `:param` - Represents a variable parameter in the endpoint path + - Example: `api/v2/addresses/:param` matches `api/v2/addresses/0x00000..000` +- ⚠️ It's not allowed to use `*` and `:param` simultaneously + +### Path Matching Priority +1. Paths without `:param` and `*` +2. Paths with `:param` +3. Paths with `*` + +### Default Configuration +The config must contain a `default` key that defines the default API rate limit configuration. This will be used for endpoints that don't match any defined paths in the config. + +> **Note**: GraphQL endpoints are out of scope for this config. Their rate limits are based on ENVs: `API_GRAPHQL_RATE_LIMIT_*` + +## Rate Limit Options + +Each rate limit entry can contain the following keys: + +### Rate Limit Methods +- `account_api_key` - Allows using API key emitted in My Account + > **Important**: When overriding `account_api_key`, ensure your limits are much lower than the default ones +- `whitelisted_ip` - Allows rate limiting by whitelisted IP +- `static_api_key` - Allows rate limiting by static API key +- `temporary_token` - Allows rate limiting by temporary token (cookie) issued by `/api/v2/key` +- `ip` - Allows rate limiting by IP address + +### Additional Options +- `cost` - Integer value used to decrease allowed limit (default: `1`) +- `ignore` - If `true`, the endpoint won't be rate limited +- `recaptcha_to_bypass_429` - If `true`, allows passing recaptcha header with response to bypass 429 errors. +- `bypass_token_scope` - Scope of recaptcha bypass token (currently only supports `token_instance_refetch_metadata`) +- `isolate_rate_limit?` - If `true`, creates a separate rate limit bucket for this endpoint. Instead of using the shared rate limit key, it prepends the endpoint path to create an isolated bucket (e.g., `api/v2/address_127.0.0.1` for the `/api/v2/address` endpoint with IP-based rate limiting). + +⚠️ It is recommended to use either `recaptcha_to_bypass_429` or `temporary_token`, not both. + +### Rate Limit Option Values +Each rate limit method can have one of these values: +- `true` - Rate limit option is allowed, limits pulled from ENVs +- `false` or omitted - Rate limit option is disabled +- Map with configuration: + - `limit` - Integer value representing max requests allowed per period + - `period` - Rate limit time period in [time format](https://docs.blockscout.com/setup/env-variables/backend-env-variables#time-format) + +## ReCAPTCHA Implementation + +ReCAPTCHA responses should be passed via headers: +- `recaptcha-v2-response` - For V2 captcha +- `recaptcha-v3-response` - For V3 captcha +- `recaptcha-bypass-token` - For non-scoped bypass recaptcha token +- `scoped-recaptcha-bypass-token` - For scoped bypass recaptcha token (currently only supports `token_instance_refetch_metadata` scope) + +> **Note**: ReCAPTCHA for `/api/v2/key` endpoint should be sent in the request body. + +## Rate Limit Headers + +The backend returns informational headers: +- `X-RateLimit-Limit` - Total limit per timeframe +- `X-RateLimit-Remaining` - Remaining requests within current time window +- `X-RateLimit-Reset` - Time to reset rate limits in milliseconds + +These headers may return `-1` in case of: +- Internal errors +- `API_NO_RATE_LIMIT_API_KEY` is used +- Rate limits are disabled on the backend +- The endpoint has `"ignore": true` parameter set + +### Bypass Options Header +The `bypass-429-option` header indicates how to bypass rate limits: +- `recaptcha` - Use ReCAPTCHA response in headers +- `temporary_token` - Get temporary cookie from `/api/v2/key` endpoint +- `no_bypass` - No way to bypass 429 error diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index 9c9168ca8039..327e8e531f25 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -12,7 +12,8 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do csv_setup() end - test "do not export token transfers to csv without recaptcha recaptcha_response provided", %{conn: conn} do + test "do not export token transfers to csv after rate limit is reached (1 per hour) without recaptcha recaptcha_response provided", + %{conn: conn} do address = insert(:address) transaction = @@ -29,16 +30,28 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do to_period = now |> DateTime.to_iso8601() conn = - get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + conn + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 200 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) - assert conn.status == 403 + assert conn.status == 429 end - test "do not export token transfers to csv without recaptcha passed", %{ + test "do not export token transfers to csv after rate limit is reached without recaptcha passed", %{ conn: conn, v2_secret_key: recaptcha_secret_key } do @@ -68,14 +81,27 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period }) - assert conn.status == 403 + assert conn.status == 200 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("recaptcha-v2-response", "123") + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 429 end - test "exports token transfers to csv without recaptcha if recaptcha is disabled", %{conn: conn} do + test "exports token transfers to csv after rate limit is reached without recaptcha if recaptcha is disabled", %{ + conn: conn + } do init_config = Application.get_env(:block_scout_web, :recaptcha) Application.put_env(:block_scout_web, :recaptcha, is_disabled: true) @@ -101,6 +127,19 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do "to_period" => to_period }) + assert conn.status == 200 + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 Application.put_env(:block_scout_web, :recaptcha, init_config) @@ -141,10 +180,23 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period }) + assert conn.status == 200 + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("recaptcha-v2-response", "123") + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 end end @@ -189,10 +241,23 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period + }) + + assert conn.status == 200 + assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("recaptcha-v2-response", "123") + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period }) + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 4 end end @@ -274,10 +339,23 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/internal-transactions/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period }) + assert conn.status == 200 + assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 + + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("recaptcha-v2-response", "123") + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/internal-transactions/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) + + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 end end @@ -353,29 +431,27 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ "address_id" => Address.checksum(address.hash), "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period }) + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 - end - test "handles null filter", %{conn: conn, v2_secret_key: recaptcha_secret_key} do - expected_body = "secret=#{recaptcha_secret_key}&response=123" + conn = + Phoenix.ConnTest.build_conn() + |> put_req_header("recaptcha-v2-response", "123") + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ + "address_id" => Address.checksum(address.hash), + "from_period" => from_period, + "to_period" => to_period + }) - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + assert conn.status == 200 + assert conn.resp_body |> String.split("\n") |> Enum.count() == 5 + end + test "handles null filter", %{conn: conn} do address = insert(:address) transaction = @@ -402,10 +478,10 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do "filter_type" => "null", "filter_value" => "null", "from_period" => from_period, - "to_period" => to_period, - "recaptcha_response" => "123" + "to_period" => to_period }) + assert conn.status == 200 assert conn.resp_body |> String.split("\n") |> Enum.count() == 3 end end @@ -413,6 +489,7 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do defp csv_setup() do old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) + original_api_rate_limit = Application.get_env(:block_scout_web, :api_rate_limit) v2_secret_key = "v2_secret_key" v3_secret_key = "v3_secret_key" @@ -425,9 +502,13 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do Application.put_env(:block_scout_web, :http_adapter, Explorer.Mox.HTTPoison) + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_api_rate_limit, :disabled, false)) + on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) + Application.put_env(:block_scout_web, :api_rate_limit, original_api_rate_limit) end) {:ok, %{v2_secret_key: v2_secret_key, v3_secret_key: v3_secret_key}} diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index b5d23b4d79cc..ffba606adefe 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1538,6 +1538,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) old_explorer_http_adapter = Application.get_env(:explorer, :http_adapter) + original_api_rate_limit = Application.get_env(:block_scout_web, :api_rate_limit) v2_secret_key = "v2_secret_key" v3_secret_key = "v3_secret_key" @@ -1564,31 +1565,21 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do Subscriber.to(:fetched_token_instance_metadata, :on_demand) Subscriber.to(:not_fetched_token_instance_metadata, :on_demand) + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_api_rate_limit, :disabled, false)) + on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) Application.put_env(:explorer, :http_adapter, old_explorer_http_adapter) + Application.put_env(:block_scout_web, :api_rate_limit, original_api_rate_limit) + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) end) {:ok, %{v2_secret_key: v2_secret_key, v3_secret_key: v3_secret_key}} end - test "token instance metadata on-demand re-fetcher is called", %{conn: conn, v2_secret_key: v2_secret_key} do - expected_body = "secret=#{v2_secret_key}&response=123" - - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) - + test "token instance metadata on-demand re-fetcher is called; recaptcha is required after rate limit is exceeded", + %{conn: conn, v2_secret_key: v2_secret_key} do token = insert(:token, type: "ERC-721") token_id = 1 @@ -1619,9 +1610,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> subscribe_and_join(topic) request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "recaptcha_response" => "123" - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) @@ -1644,12 +1633,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert(token_instance_from_db) assert token_instance_from_db.metadata == metadata - end - test "don't fetch token instance metadata for non-existent token instance", %{ - conn: conn, - v2_secret_key: v2_secret_key - } do expected_body = "secret=#{v2_secret_key}&response=123" Explorer.Mox.HTTPoison @@ -1665,38 +1649,67 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do }} end) + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert json_response(request, 429) + + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("recaptcha-v2-response", "123") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + event: "fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + end + + test "don't fetch token instance metadata for non-existent token instance", %{ + conn: conn, + v2_secret_key: v2_secret_key + } do token = insert(:token, type: "ERC-721") token_id = 0 insert(:token_instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/1/refetch-metadata", %{ - "recaptcha_response" => "123" - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/1/refetch-metadata", %{}) assert %{"message" => "Not found"} = json_response(request, 404) end test "fetch token instance metadata for existing token instance with no metadata", %{ - conn: conn, - v2_secret_key: v2_secret_key + conn: conn } do - expected_body = "secret=#{v2_secret_key}&response=123" - - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) - token = insert(:token, type: "ERC-721") token_id = 1 @@ -1727,9 +1740,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> subscribe_and_join(topic) request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "recaptcha_response" => "123" - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) @@ -1755,24 +1766,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do end test "emit not_fetched_token_instance_metadata event when fetching token instance metadata fails", %{ - conn: conn, - v2_secret_key: v2_secret_key + conn: conn } do - expected_body = "secret=#{v2_secret_key}&response=123" - - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) - token = insert(:token, type: "ERC-721") token_id = 1 @@ -1879,9 +1874,49 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> subscribe_and_join(topic) request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "scoped_recaptcha_bypass_token" => scoped_bypass_token - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert %{"message" => "OK"} = json_response(request, 200) + + :timer.sleep(100) + + assert_receive( + {:chain_event, :fetched_token_instance_metadata, :on_demand, + [^token_contract_address_hash_string, ^token_id, ^metadata]} + ) + + assert_receive %Phoenix.Socket.Message{ + payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + event: "fetched_token_instance_metadata", + topic: ^topic + }, + :timer.seconds(1) + + token_instance_from_db = + Repo.get_by(Instance, token_id: token_id, token_contract_address_hash: token.contract_address_hash) + + assert(token_instance_from_db) + assert token_instance_from_db.metadata == metadata + + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert json_response(request, 429) + + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} + end) + + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("scoped-recaptcha-bypass-token", scoped_bypass_token) + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) @@ -1946,13 +1981,24 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - # First request with wrong scoped token - should fail + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"name" => "test"})}} + end) + request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "scoped_recaptcha_bypass_token" => "wrong_scoped_token" - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert %{"message" => "OK"} = json_response(request, 200) + + # First request after hitting rate limit with wrong scoped token - should fail + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("scoped-recaptcha-bypass-token", "wrong_scoped_token") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" @@ -1970,6 +2016,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do }} end) + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + Explorer.Mox.HTTPoison |> expect(:get, fn ^url, _headers, _options -> {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} @@ -1984,9 +2032,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do # Second request with correct reCAPTCHA token - should work request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "recaptcha_response" => "correct_recaptcha_token" - }) + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("recaptcha-v2-response", "correct_recaptcha_token") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) @@ -2041,28 +2090,42 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do metadata = %{"name" => "Super Token"} url = "http://metadata.endpoint.com" token_contract_address_hash_string = to_string(token.contract_address_hash) + TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + + Explorer.Mox.HTTPoison + |> expect(:get, fn ^url, _headers, _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"name" => "test"})}} + end) - # First request with a scoped token that isn't configured - should fail request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "scoped_recaptcha_bypass_token" => "some_token_that_does_not_exist" - }) + patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + assert %{"message" => "OK"} = json_response(request, 200) + # First request after hitting rate limit with a scoped token that isn't configured - should fail request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "scoped_recaptcha_bypass_token" => "" - }) + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("scoped-recaptcha-bypass-token", "some_token_that_does_not_exist") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "scoped_recaptcha_bypass_token" => nil - }) + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("scoped-recaptcha-bypass-token", "some_token_that_does_not_exist") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "Invalid reCAPTCHA response"} = json_response(request, 403) + assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) + + request = + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("scoped-recaptcha-bypass-token", "") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) + + assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" @@ -2089,9 +2152,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do # Second request with correct reCAPTCHA token - should work request = - patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{ - "recaptcha_response" => "correct_recaptcha_token" - }) + Phoenix.ConnTest.build_conn() + |> put_req_header("user-agent", "test-agent") + |> put_req_header("recaptcha-v2-response", "correct_recaptcha_token") + |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) diff --git a/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs b/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs new file mode 100644 index 000000000000..14f2bc8c3a7b --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs @@ -0,0 +1,477 @@ +defmodule BlockScoutWeb.Plug.RateLimitTest do + use BlockScoutWeb.ConnCase, async: false + + setup do + # Store original config + original_config = :persistent_term.get(:rate_limit_config) + + original_recaptcha_config = Application.get_env(:block_scout_web, :recaptcha) + original_rate_limit_config = Application.get_env(:block_scout_web, :api_rate_limit) + original_graphql_config = Application.get_env(:block_scout_web, Api.GraphQL) + + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_rate_limit_config, :disabled, false)) + + Application.put_env( + :block_scout_web, + Api.GraphQL, + Keyword.put(original_graphql_config, :rate_limit_disabled?, false) + ) + + on_exit(fn -> + :persistent_term.put(:rate_limit_config, original_config) + + Application.put_env(:block_scout_web, :recaptcha, original_recaptcha_config) + Application.put_env(:block_scout_web, :api_rate_limit, original_rate_limit_config) + Application.put_env(:block_scout_web, Api.GraphQL, original_graphql_config) + + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) + end) + end + + describe "rate limiting" do + test "sets rate limit headers for allowed and denied requests", %{conn: conn} do + config = %{ + static_match: %{ + "api/v2/blocks" => %{ + ip: %{ + period: 60_000, + limit: 1 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request - allowed + first_request = conn |> get("/api/v2/blocks") + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(first_request, "bypass-429-option") == ["no_bypass"] + + # Second request - should be denied with 429 + second_request = conn |> get("/api/v2/blocks") + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(second_request, "bypass-429-option") == ["no_bypass"] + end + + test "handles recaptcha bypass option", %{conn: conn} do + config = %{ + static_match: %{ + "api/v2/addresses" => %{ + ip: %{ + period: 60_000, + limit: 1 + }, + recaptcha_to_bypass_429: true + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request with user agent + first_request = + conn + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses") + + assert first_request.status == 200 + + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "bypass-429-option") == ["recaptcha"] + + # Second request - should be denied with 429 + second_request = + conn + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/addresses") + + assert second_request.status == 429 + + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "bypass-429-option") == ["recaptcha"] + end + + test "handles temporary token bypass option", %{conn: conn} do + config = %{ + static_match: %{ + "api/v2/transactions" => %{ + ip: %{ + period: 60_000, + limit: 1 + }, + temporary_token: true + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request with user agent + first_request = + conn + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/transactions") + + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "bypass-429-option") == ["temporary_token"] + + # Second request - should be denied with 429 + second_request = + conn + |> put_req_header("user-agent", "test-agent") + |> get("/api/v2/transactions") + + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "bypass-429-option") == ["temporary_token"] + end + + test "handles GraphQL requests", %{conn: conn} do + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + limit_by_ip: 1, + time_interval_limit_by_ip: 60_000, + global_limit: 500, + time_interval_limit: 60_000 + ) + + # First request + first_request = conn |> post("/api/v1/graphql", %{query: "{}"}) + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + + # This request should be denied due to IP limit + last_request = conn |> post("/api/v1/graphql", %{query: "{}"}) + assert last_request.status == 429 + assert get_resp_header(last_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(last_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(last_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "handles GraphQL requests with global limit", %{conn: conn} do + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + limit_by_ip: 100, + time_interval_limit_by_ip: 60_000, + global_limit: 1, + time_interval_limit: 60_000 + ) + + # First request + first_request = conn |> post("/api/v1/graphql", %{query: "{}"}) + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["100"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["99"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + + # This request should be denied due to IP limit + last_request = conn |> post("/api/v1/graphql", %{query: "{}"}) + assert last_request.status == 429 + assert get_resp_header(last_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(last_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(last_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "handles parametrized paths", %{conn: conn} do + token = insert(:token) + + config = %{ + static_match: %{}, + wildcard_match: %{}, + parametrized_match: %{ + ["api", "v2", "tokens", ":param"] => %{ + ip: %{ + period: 60_000, + limit: 1 + } + } + } + } + + :persistent_term.put(:rate_limit_config, config) + + # First request to parametrized path + first_request = conn |> get("/api/v2/tokens/#{token.contract_address_hash}") + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + + # Second request - should be denied with 429 + second_request = conn |> get("/api/v2/tokens/123") + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "handles wildcard paths", %{conn: conn} do + config = %{ + static_match: %{}, + wildcard_match: %{ + {["api", "v2"], 2} => %{ + ip: %{ + period: 60_000, + limit: 1 + } + } + }, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request to a path matching wildcard + first_request = conn |> get("/api/v2/main-page/transactions") + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + + # Second request - should be denied with 429 + second_request = conn |> get("/api/v2/blocks") + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "falls back to default config", %{conn: conn} do + config = %{ + static_match: %{ + "default" => %{ + ip: %{ + period: 60_000, + limit: 1 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request to a path with no specific config + first_request = conn |> get("/api/v2/transactions") + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + + # Second request - should be denied with 429 + second_request = conn |> get("/api/v2/blocks") + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "rate limit is disabled when configured", %{conn: conn} do + config = %{ + static_match: %{ + "default" => %{ + ip: %{ + period: 60_000, + limit: 0 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + Application.put_env( + :block_scout_web, + :api_rate_limit, + Keyword.put(Application.get_env(:block_scout_web, :api_rate_limit), :disabled, true) + ) + + request = conn |> get("/api/v2/transactions") + assert request.status == 200 + assert get_resp_header(request, "x-ratelimit-limit") == ["-1"] + assert get_resp_header(request, "x-ratelimit-remaining") == ["-1"] + assert get_resp_header(request, "x-ratelimit-reset") == ["-1"] + end + + test "bypasses rate limit with valid API key", %{conn: conn} do + config = %{ + static_match: %{ + "default" => %{ + ip: %{ + period: 60_000, + limit: 0 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + Application.put_env( + :block_scout_web, + :api_rate_limit, + Keyword.put(Application.get_env(:block_scout_web, :api_rate_limit), :no_rate_limit_api_key_value, "123") + ) + + request = conn |> get("/api/v2/transactions", %{"apikey" => "123"}) + assert request.status == 200 + assert get_resp_header(request, "x-ratelimit-limit") == ["-1"] + assert get_resp_header(request, "x-ratelimit-remaining") == ["-1"] + assert get_resp_header(request, "x-ratelimit-reset") == ["-1"] + end + + test "enforces rate limit with invalid API key", %{conn: conn} do + config = %{ + static_match: %{ + "default" => %{ + ip: %{ + period: 60_000, + limit: 0 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + Application.put_env( + :block_scout_web, + :api_rate_limit, + Keyword.put(Application.get_env(:block_scout_web, :api_rate_limit), :no_rate_limit_api_key_value, nil) + ) + + request = conn |> get("/api/v2/transactions", %{"apikey" => nil}) + assert request.status == 429 + assert get_resp_header(request, "x-ratelimit-limit") == ["0"] + assert get_resp_header(request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "enforces rate limit with empty API key", %{conn: conn} do + config = %{ + static_match: %{ + "default" => %{ + ip: %{ + period: 60_000, + limit: 0 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + Application.put_env( + :block_scout_web, + :api_rate_limit, + Keyword.put(Application.get_env(:block_scout_web, :api_rate_limit), :no_rate_limit_api_key_value, "") + ) + + request = conn |> get("/api/v2/transactions", %{"apikey" => " "}) + assert request.status == 429 + assert get_resp_header(request, "x-ratelimit-limit") == ["0"] + assert get_resp_header(request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + end + + test "rate limit requests with isolated rate limit", %{conn: conn} do + config = %{ + static_match: %{ + "api/v2/blocks" => %{ + ip: %{ + period: 60_000, + limit: 1 + } + }, + "api/v2/transactions" => %{ + ip: %{ + period: 60_000, + limit: 1 + }, + isolate_rate_limit?: true, + bucket_key_prefix: "api/v2/transactions_" + }, + "api/v2/addresses" => %{ + ip: %{ + period: 60_000, + limit: 1 + } + } + }, + wildcard_match: %{}, + parametrized_match: %{} + } + + :persistent_term.put(:rate_limit_config, config) + + # First request - allowed + first_request = conn |> get("/api/v2/blocks") + assert first_request.status == 200 + assert get_resp_header(first_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(first_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(first_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(first_request, "bypass-429-option") == ["no_bypass"] + + # Second request - should be denied with 429 + second_request = conn |> get("/api/v2/blocks") + assert second_request.status == 429 + assert get_resp_header(second_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(second_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(second_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(second_request, "bypass-429-option") == ["no_bypass"] + + # Third request - should be allowed + third_request = conn |> get("/api/v2/transactions") + assert third_request.status == 200 + assert get_resp_header(third_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(third_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(third_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(third_request, "bypass-429-option") == ["no_bypass"] + + # Fourth request - should be allowed + fourth_request = conn |> get("/api/v2/transactions") + assert fourth_request.status == 429 + assert get_resp_header(fourth_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(fourth_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(fourth_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(fourth_request, "bypass-429-option") == ["no_bypass"] + + # Fifth request - should be denied with 429 + fifth_request = conn |> get("/api/v2/addresses") + assert fifth_request.status == 429 + assert get_resp_header(fifth_request, "x-ratelimit-limit") == ["1"] + assert get_resp_header(fifth_request, "x-ratelimit-remaining") == ["0"] + assert get_resp_header(fifth_request, "x-ratelimit-reset") |> hd() |> String.to_integer() > 0 + assert get_resp_header(fifth_request, "bypass-429-option") == ["no_bypass"] + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs b/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs new file mode 100644 index 000000000000..f490a76c7880 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs @@ -0,0 +1,426 @@ +defmodule BlockScoutWeb.RateLimitTest do + use BlockScoutWeb.ConnCase, async: false + alias BlockScoutWeb.RateLimit + + describe "check_rate_limit_graphql/3" do + setup do + original_config = Application.get_env(:block_scout_web, Api.GraphQL) + Application.put_env(:block_scout_web, Api.GraphQL, Keyword.put(original_config, :rate_limit_disabled?, false)) + + on_exit(fn -> + Application.put_env(:block_scout_web, Api.GraphQL, original_config) + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) + end) + end + + test "returns {:allow, -1} when rate limit is disabled" do + Application.put_env(:block_scout_web, Api.GraphQL, rate_limit_disabled?: true) + conn = build_conn() + + assert RateLimit.check_rate_limit_graphql(conn, 1) == {:allow, -1} + end + + test "returns {:allow, -1} when using no_rate_limit_api_key" do + no_rate_limit_api_key = "no_limit_key" + Application.put_env(:block_scout_web, Api.GraphQL, no_rate_limit_api_key: no_rate_limit_api_key) + conn = build_conn() |> Map.put(:query_params, %{"apikey" => no_rate_limit_api_key}) + + assert RateLimit.check_rate_limit_graphql(conn, 1) == {:allow, -1} + end + + test "applies rate limit for IP when no API key is provided" do + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + limit_by_ip: 100, + time_interval_limit_by_ip: 60_000, + time_interval_limit: 60_000, + global_limit: 1000 + ) + + conn = build_conn() + + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 100 + assert period == 60_000 + end + + test "global limit is applied when no API key is provided" do + static_api_key = "static_key" + + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + limit_by_ip: 100, + time_interval_limit_by_ip: 50_000, + time_interval_limit: 60_000, + global_limit: 1, + limit_by_key: 100, + static_api_key: static_api_key + ) + + conn = build_conn() + + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 100 + assert period == 50_000 + + assert {:deny, time_to_reset, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert time_to_reset > 0 and time_to_reset < 60_000 + assert limit == 1 + assert period == 60_000 + + conn = build_conn() |> Map.put(:query_params, %{"apikey" => static_api_key}) + + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 100 + assert period == 60_000 + end + + test "bypass 429 rate limit with static API key" do + static_api_key = "static_key" + + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + limit_by_ip: 1, + time_interval_limit_by_ip: 60_000, + time_interval_limit: 1_000, + global_limit: 1000, + static_api_key: static_api_key, + limit_by_key: 500 + ) + + conn = build_conn() + + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 1 + assert period == 60_000 + + assert {:deny, time_to_reset, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert time_to_reset > 0 and time_to_reset < 60_000 + assert limit == 1 + assert period == 60_000 + + conn = build_conn() |> Map.put(:query_params, %{"apikey" => static_api_key}) + + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 500 + assert period == 1_000 + end + + test "applies rate limit for static API key" do + static_api_key = "static_key" + time_interval_limit = 500 + + Application.put_env(:block_scout_web, Api.GraphQL, + rate_limit_disabled?: false, + static_api_key: static_api_key, + time_interval_limit: time_interval_limit, + limit_by_key: 500 + ) + + conn = build_conn() |> Map.put(:query_params, %{"apikey" => static_api_key}) + + # Make 500 requests to hit the limit + Enum.each(1..500, fn i -> + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == i + assert limit == 500 + assert period == time_interval_limit + end) + + # Next request should be denied + assert {:deny, time_to_reset, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert limit == 500 + assert period == time_interval_limit + assert time_to_reset > 0 and time_to_reset < time_interval_limit + + Process.sleep(time_to_reset) + + # Make another request to check if it's allowed again + assert {:allow, count, limit, period} = RateLimit.check_rate_limit_graphql(conn, 1) + assert count == 1 + assert limit == 500 + assert period == time_interval_limit + end + end + + describe "rate_limit_with_config/2" do + setup do + original_config = Application.get_env(:block_scout_web, :api_rate_limit) + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_config, :disabled, false)) + original_recaptcha_config = Application.get_env(:block_scout_web, :recaptcha) + + on_exit(fn -> + Application.put_env(:block_scout_web, :api_rate_limit, original_config) + Application.put_env(:block_scout_web, :recaptcha, original_recaptcha_config) + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) + end) + end + + test "returns {:allow, -1} when rate limit is disabled globally" do + Application.put_env(:block_scout_web, :api_rate_limit, disabled: true) + conn = build_conn() + + assert RateLimit.rate_limit_with_config(conn, %{}) == {:allow, -1} + end + + test "returns {:allow, -1} when endpoint is ignored" do + conn = build_conn() + + assert RateLimit.rate_limit_with_config(conn, %{ignore: true}) == {:allow, -1} + end + + test "applies rate limit by IP" do + config = %{ + ip: %{ + period: 60_000, + limit: 100 + } + } + + conn = build_conn() + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 100 + assert period == 60_000 + end + + test "applies rate limit by static API key" do + static_api_key = "static_key" + original_config = Application.get_env(:block_scout_web, :api_rate_limit) + + Application.put_env( + :block_scout_web, + :api_rate_limit, + Keyword.put(original_config, :static_api_key_value, static_api_key) + ) + + config = %{ + temporary_token: %{ + period: 60_000, + limit: 100 + }, + static_api_key: %{ + period: 60_000, + limit: 500 + } + } + + conn = build_conn() |> Map.put(:query_params, %{"apikey" => static_api_key}) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count >= 0 + assert limit == 500 + assert period == 60_000 + end + + test "applies rate limit by whitelisted IP" do + ip = "192.168.1.1" + original_config = Application.get_env(:block_scout_web, :api_rate_limit) + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_config, :whitelisted_ips, ip)) + + config = %{ + temporary_token: %{ + period: 60_000, + limit: 100 + }, + static_api_key: %{ + period: 60_000, + limit: 200 + }, + account_api_key: %{ + period: 60_000, + limit: 300 + }, + whitelisted_ip: %{ + period: 60_000, + limit: 400 + }, + ip: %{ + period: 60_000, + limit: 500 + } + } + + conn = build_conn() |> Map.put(:remote_ip, {192, 168, 1, 1}) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 400 + assert period == 60_000 + end + + test "applies rate limit by temporary token" do + config = %{ + temporary_token: %{ + period: 60_000, + limit: 100 + }, + static_api_key: %{ + period: 60_000, + limit: 200 + }, + account_api_key: %{ + period: 60_000, + limit: 300 + }, + whitelisted_ip: %{ + period: 60_000, + limit: 400 + }, + ip: %{ + period: 60_000, + limit: 1 + } + } + + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.put(Application.get_env(:block_scout_web, :recaptcha), :bypass_token, "test_token") + ) + + conn = + build_conn() + |> Map.put(:remote_ip, {192, 168, 1, 1}) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 1 + assert period == 60_000 + + assert {:deny, time_to_reset, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert time_to_reset > 0 and time_to_reset < 60_000 + assert limit == 1 + assert period == 60_000 + + # First make request to get temporary token + conn = + build_conn() + |> Map.put(:remote_ip, {192, 168, 1, 1}) + |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + |> post("/api/v2/key", %{"recaptcha_bypass_token" => "test_token"}) + + # Extract token from response + token = conn.resp_cookies["api_v2_temp_token"].value + + # Now make request with the token + conn = + conn + |> Map.put(:remote_ip, {192, 168, 1, 1}) + |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + |> Map.put(:req_cookies, %{"api_v2_temp_token" => token}) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 100 + assert period == 60_000 + end + + test "handles recaptcha bypass" do + config = %{ + temporary_token: %{ + period: 60_000, + limit: 100 + }, + static_api_key: %{ + period: 60_000, + limit: 200 + }, + account_api_key: %{ + period: 60_000, + limit: 300 + }, + whitelisted_ip: %{ + period: 60_000, + limit: 400 + }, + ip: %{ + period: 70_000, + limit: 1 + }, + recaptcha_to_bypass_429: true + } + + # First request to hit the limit + conn = build_conn() |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + assert {:allow, 1, 1, _} = RateLimit.rate_limit_with_config(conn, config) + + # Second request that should be denied + conn = build_conn() |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + assert {:deny, _, _, _} = RateLimit.rate_limit_with_config(conn, config) + + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.put(Application.get_env(:block_scout_web, :recaptcha), :bypass_token, "test_token") + ) + + # Request with valid recaptcha + conn = + build_conn() + |> Map.put(:req_headers, [ + {"user-agent", "test-agent"}, + {"recaptcha-bypass-token", "test_token"} + ]) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 1 + assert period == 70_000 + end + end + + describe "get_user_agent/1" do + test "returns user agent from headers" do + conn = build_conn() |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + assert RateLimit.get_user_agent(conn) == "test-agent" + end + + test "returns nil when user agent is not present" do + conn = build_conn() + assert RateLimit.get_user_agent(conn) == nil + end + end + + describe "rate_limit/4" do + setup do + original_config = Application.get_env(:block_scout_web, :api_rate_limit) + Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_config, :disabled, false)) + + on_exit(fn -> + Application.put_env(:block_scout_web, :api_rate_limit, original_config) + :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) + end) + end + + test "returns {:allow, count, limit, period} when under limit" do + key = "test_key" + period = 60_000 + limit = 100 + multiplier = 33 + + assert {:allow, count, ^limit, ^period} = RateLimit.rate_limit(key, period, limit, multiplier) + assert count == 33 + end + + test "returns {:deny, time_to_reset, limit, period} when over limit" do + key = "test_key" + period = 60_000 + limit = 1 + multiplier = 1 + + # First request + RateLimit.rate_limit(key, period, limit, multiplier) + + # Second request that should be denied + assert {:deny, time_to_reset, ^limit, ^period} = RateLimit.rate_limit(key, period, limit, multiplier) + assert time_to_reset > 0 + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs b/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs new file mode 100644 index 000000000000..f806f4be7789 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs @@ -0,0 +1,294 @@ +defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do + use BlockScoutWeb.ConnCase, async: false + alias BlockScoutWeb.Utility.RateLimitConfigHelper + + describe "store_rate_limit_config/0" do + setup do + original_config_from_persistent_term = :persistent_term.get(:rate_limit_config) + + # Store original config URL + original_config = Application.get_env(:block_scout_web, :api_rate_limit) + + on_exit(fn -> + Application.put_env(:block_scout_web, :api_rate_limit, original_config) + :persistent_term.put(:rate_limit_config, original_config_from_persistent_term) + end) + end + + test "successfully fetches and parses config from URL" do + config = %{ + "api/v2/*" => %{ + "static_api_key" => true, + "account_api_key" => true + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + + assert :persistent_term.get(:rate_limit_config) == %{ + wildcard_match: %{ + {["api", "v2"], 2} => %{ + static_api_key: true, + account_api_key: true, + bucket_key_prefix: "" + } + }, + parametrized_match: %{}, + static_match: %{} + } + end + + test "falls back to local config when URL fetch fails" do + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 500, "Internal Server Error") + end) + + RateLimitConfigHelper.store_rate_limit_config() + # Verify that we got some config (from local file) + config = :persistent_term.get(:rate_limit_config) + assert is_map(config[:static_match]["default"]) + + assert config[:static_match]["api/account/v2/authenticate_via_wallet"][:bucket_key_prefix] == + "api/account/v2/authenticate_via_wallet_" + + assert config[:static_match]["api/account/v2/authenticate_via_wallet"][:isolate_rate_limit?] == true + end + + test "correctly categorizes different path types when fetching config" do + config = %{ + "api/v2/*" => %{"limit" => 100}, + "api/v2/tokens/:param" => %{"limit" => 50}, + "api/v2/static" => %{"limit" => 25} + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + result = :persistent_term.get(:rate_limit_config) + + assert result.wildcard_match == %{ + {["api", "v2"], 2} => %{limit: 100, bucket_key_prefix: ""} + } + + assert result.parametrized_match == %{ + ["api", "v2", "tokens", ":param"] => %{limit: 50, bucket_key_prefix: ""} + } + + assert result.static_match == %{ + "api/v2/static" => %{limit: 25, bucket_key_prefix: ""} + } + end + + test "falls back to local config when fetching config with invalid wildcard placement" do + config = %{ + "api/*/v2" => %{ + "ip" => %{ + "period" => "1h", + "limit" => 100 + } + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + assert is_map(:persistent_term.get(:rate_limit_config)[:static_match]["default"]) + end + + test "converts time strings to milliseconds when fetching config" do + config = %{ + "api/v2/endpoint" => %{ + "ip" => %{ + "period" => "5m", + "limit" => 100 + } + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + result = :persistent_term.get(:rate_limit_config) + + # 5 minutes in milliseconds + assert result.static_match["api/v2/endpoint"][:ip][:period] == 300_000 + # 100 requests per 5 minutes + assert result.static_match["api/v2/endpoint"][:ip][:limit] == 100 + end + + test "falls back to local config when fetching config with invalid time format" do + config = %{ + "api/v2/endpoint" => %{ + "ip" => %{ + "period" => "invalid" + } + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + assert is_map(:persistent_term.get(:rate_limit_config)[:static_match]["default"]) + end + + test "correctly processes all reserved keywords in configuration" do + config = %{ + "api/v2/endpoint" => %{ + "account_api_key" => true, + "bypass_token_scope" => "test_scope", + "cost" => 5, + "ip" => %{ + "period" => "1h", + "limit" => 100 + }, + "ignore" => true, + "recaptcha_to_bypass_429" => true, + "static_api_key" => true, + "temporary_token" => true, + "whitelisted_ip" => true, + "isolate_rate_limit?" => true + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + result = :persistent_term.get(:rate_limit_config) + + # Check that all keys are properly converted to atoms + processed_config = result.static_match["api/v2/endpoint"] + + assert processed_config[:account_api_key] == true + assert processed_config[:bypass_token_scope] == "test_scope" + assert processed_config[:cost] == 5 + # 1h in milliseconds + assert processed_config[:ip][:period] == 3_600_000 + assert processed_config[:ip][:limit] == 100 + assert processed_config[:ignore] == true + assert processed_config[:recaptcha_to_bypass_429] == true + assert processed_config[:static_api_key] == true + assert processed_config[:temporary_token] == true + assert processed_config[:whitelisted_ip] == true + assert processed_config[:isolate_rate_limit?] == true + assert processed_config[:bucket_key_prefix] == "api/v2/endpoint_" + end + + test "correctly processes nested structures with reserved keywords" do + config = %{ + "api/v2/tokens/:param" => %{ + "ip" => %{ + "period" => "5m", + "limit" => 50 + }, + "static_api_key" => %{ + "period" => "1h", + "limit" => 500 + }, + "whitelisted_ip" => %{ + "period" => "10m", + "limit" => 1000 + } + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + result = :persistent_term.get(:rate_limit_config) + + # Check processing of parametrized path with nested config + processed_config = result.parametrized_match[["api", "v2", "tokens", ":param"]] + + # Check nested IP config + # 5m in milliseconds + assert processed_config[:ip][:period] == 300_000 + assert processed_config[:ip][:limit] == 50 + + # Check nested static_api_key config + # 1h in milliseconds + assert processed_config[:static_api_key][:period] == 3_600_000 + assert processed_config[:static_api_key][:limit] == 500 + + # Check nested whitelisted_ip config + # 10m in milliseconds + assert processed_config[:whitelisted_ip][:period] == 600_000 + assert processed_config[:whitelisted_ip][:limit] == 1000 + end + + test "handles unsupported keywords gracefully" do + config = %{ + "api/v2/endpoint" => %{ + "unknown_keyword" => true, + "another_unknown" => "value", + "ip" => %{ + "period" => "1h", + "limit" => 100, + "unsupported_nested" => "test" + } + } + } + + bypass = Bypass.open() + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + + Bypass.expect_once(bypass, "GET", "/config", fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(config)) + end) + + RateLimitConfigHelper.store_rate_limit_config() + result = :persistent_term.get(:rate_limit_config) + + processed_config = result.static_match["api/v2/endpoint"] + + # It should process the supported keywords while ignoring unsupported ones + # 1h in milliseconds + assert processed_config[:ip][:period] == 3_600_000 + assert processed_config[:ip][:limit] == 100 + + # Verify unsupported keywords are not included or are handled gracefully + assert processed_config["unknown_keyword"] == true + assert processed_config["another_unknown"] == "value" + assert processed_config[:ip]["unsupported_nested"] == "test" + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs b/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs deleted file mode 100644 index c18cb955e1d2..000000000000 --- a/apps/block_scout_web/test/block_scout_web/views/access_helper_test.exs +++ /dev/null @@ -1,70 +0,0 @@ -defmodule BlockScoutWeb.AccessHelperTest do - alias BlockScoutWeb.AccessHelper - use BlockScoutWeb.ConnCase - import Mox - - setup :verify_on_exit! - - setup do - configuration = Application.get_env(:block_scout_web, :api_rate_limit) - - on_exit(fn -> - Application.put_env(:block_scout_web, :api_rate_limit, configuration) - end) - - :ok - end - - describe "check_rate_limit/1" do - test "rate_limit_disabled", %{conn: conn} do - Application.put_env(:block_scout_web, :api_rate_limit, - global_limit: 0, - limit_by_key: 0, - limit_by_whitelisted_ip: 0, - time_interval_limit: 1_000, - disabled: true - ) - - assert AccessHelper.check_rate_limit(conn) == :ok - end - - test "no_rate_limit_api_key", %{conn: conn} do - Application.put_env(:block_scout_web, :api_rate_limit, - global_limit: 0, - limit_by_key: 0, - limit_by_whitelisted_ip: 0, - time_interval_limit: 1_000, - no_rate_limit_api_key: "123" - ) - - conn = %{conn | query_params: %{"apikey" => "123"}} - assert AccessHelper.check_rate_limit(conn) == :ok - end - - test "rate limit, if no_rate_limit_api_key is nil", %{conn: conn} do - Application.put_env(:block_scout_web, :api_rate_limit, - global_limit: 0, - limit_by_key: 0, - limit_by_whitelisted_ip: 0, - time_interval_limit: 1_000, - no_rate_limit_api_key: nil - ) - - conn = %{conn | query_params: %{"apikey" => nil}} - assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached - end - - test "rate limit, if no_rate_limit_api_key is empty", %{conn: conn} do - Application.put_env(:block_scout_web, :api_rate_limit, - global_limit: 0, - limit_by_key: 0, - limit_by_whitelisted_ip: 0, - time_interval_limit: 1_000, - no_rate_limit_api_key: "" - ) - - conn = %{conn | query_params: %{"apikey" => " "}} - assert AccessHelper.check_rate_limit(conn) == :rate_limit_reached - end - end -end diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 4ee164b1cb0d..656a83aa0c9a 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -183,6 +183,9 @@ config :explorer, Explorer.SmartContract.CertifiedSmartContractCataloger, enable config :explorer, Explorer.Utility.RateLimiter, enabled: true +config :explorer, Explorer.Utility.Hammer.Redis, enabled: true +config :explorer, Explorer.Utility.Hammer.ETS, enabled: true + config :explorer, Explorer.Repo, migration_timestamps: [type: :utc_datetime_usec] config :explorer, Explorer.Tracer, diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index a2775855cfec..9c7d9115959f 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -44,6 +44,9 @@ config :explorer, Explorer.TokenInstanceOwnerAddressMigration.Supervisor, enable config :explorer, Explorer.Utility.RateLimiter, enabled: false +config :explorer, Explorer.Utility.Hammer.Redis, enabled: false +config :explorer, Explorer.Utility.Hammer.ETS, enabled: true + for migrator <- [ # Background migrations Explorer.Migrator.TransactionsDenormalization, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index e220c6c28022..e2b3b8e116dc 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -37,6 +37,7 @@ defmodule Explorer.Application do alias Explorer.Market.MarketHistoryCache alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.Repo.PrometheusLogger + alias Explorer.Utility.Hammer @impl Application def start(_type, _args) do @@ -304,7 +305,8 @@ defmodule Explorer.Application do Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, - configure_mode_dependent_process(Explorer.Utility.RateLimiter, :api) + configure_mode_dependent_process(Explorer.Utility.RateLimiter, :api), + Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api) ] |> List.flatten() @@ -353,6 +355,10 @@ defmodule Explorer.Application do end end + defp should_start?({process, _opts}) do + Application.get_env(:explorer, process, [])[:enabled] == true + end + defp should_start?(process) do Application.get_env(:explorer, process, [])[:enabled] == true end diff --git a/apps/explorer/lib/explorer/utility/hammer.ex b/apps/explorer/lib/explorer/utility/hammer.ex new file mode 100644 index 000000000000..9cb44367f7ee --- /dev/null +++ b/apps/explorer/lib/explorer/utility/hammer.ex @@ -0,0 +1,41 @@ +defmodule Explorer.Utility.Hammer.ETS do + @moduledoc false + use Hammer, backend: Hammer.ETS +end + +defmodule Explorer.Utility.Hammer.Redis do + @moduledoc false + use Hammer, backend: Hammer.Redis +end + +defmodule Explorer.Utility.Hammer do + @moduledoc """ + Wrapper for the rate limit functions. Defines union of all functions from `Explorer.Utility.Hammer.ETS` and `Explorer.Utility.Hammer.Redis`. Resolves the backend to use based on `Application.get_env(:explorer, Explorer.Utility.RateLimiter)[:hammer_backend_module]` in runtime. + """ + alias Explorer.Utility.Hammer.{ETS, Redis} + + functions = + (ETS.__info__(:functions) ++ Redis.__info__(:functions)) + |> Enum.uniq() + + for {name, arity} <- functions do + args = Macro.generate_arguments(arity, nil) + + def unquote(name)(unquote_splicing(args)) do + apply( + Application.get_env(:explorer, Explorer.Utility.RateLimiter)[:hammer_backend_module], + unquote(name), + unquote(args) + ) + end + end + + def child_for_supervisor do + config = Application.get_env(:explorer, Explorer.Utility.RateLimiter) + + case config[:storage] do + :redis -> {Explorer.Utility.Hammer.Redis, [url: config[:redis_url]]} + :ets -> {Explorer.Utility.Hammer.ETS, []} + end + end +end diff --git a/apps/explorer/lib/explorer/utility/rate_limiter.ex b/apps/explorer/lib/explorer/utility/rate_limiter.ex index 4945b2ca3f6b..4b683a507dc8 100644 --- a/apps/explorer/lib/explorer/utility/rate_limiter.ex +++ b/apps/explorer/lib/explorer/utility/rate_limiter.ex @@ -2,6 +2,7 @@ defmodule Explorer.Utility.RateLimiter do @moduledoc """ Rate limit logic with separation by action type and exponential backoff for bans. """ + alias Explorer.Utility.Hammer use GenServer @@ -74,7 +75,7 @@ defmodule Explorer.Utility.RateLimiter do end end - defp key(identifier, action), do: "#{identifier}_#{action}" + defp key(identifier, action), do: "#{Application.get_env(:block_scout_web, :chain_id)}_#{identifier}_#{action}" defp parse_ban_data(ban_data) do ban_data @@ -90,7 +91,7 @@ defmodule Explorer.Utility.RateLimiter do time_interval_limit = config[:time_interval_limit] limit_by_ip = config[:limit_by_ip] - case Hammer.check_rate(key, time_interval_limit, limit_by_ip) do + case Hammer.hit(key, time_interval_limit, limit_by_ip) do {:allow, _count} -> false diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 02055cedc8c0..4b676560de5e 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -119,7 +119,7 @@ defmodule Explorer.Mixfile do {:cbor, "~> 1.0"}, {:cloak_ecto, "~> 1.3.0"}, {:redix, "~> 1.1"}, - {:hammer_backend_redis, "~> 6.1"}, + {:hammer_backend_redis, "~> 7.0"}, {:logger_json, "~> 5.1"}, {:typed_ecto_schema, "~> 0.4.1"}, {:ueberauth, "~> 0.7"}, @@ -133,7 +133,7 @@ defmodule Explorer.Mixfile do {:utils, in_umbrella: true}, {:dns, "~> 2.4.0"}, {:inet_cidr, "~> 1.0.0"}, - {:hammer, "~> 6.0"}, + {:hammer, "~> 7.0"}, {:ton, "~> 0.5.0"} ] end diff --git a/apps/explorer/test/explorer/utility/rate_limiter_test.exs b/apps/explorer/test/explorer/utility/rate_limiter_test.exs index df5baba49116..5df982ec73f0 100644 --- a/apps/explorer/test/explorer/utility/rate_limiter_test.exs +++ b/apps/explorer/test/explorer/utility/rate_limiter_test.exs @@ -32,7 +32,8 @@ defmodule Explorer.Utility.RateLimiterTest do assert RateLimiter.check_rate("test", :on_demand) == :deny expected_ban_data = "#{now + 1}:1" - assert [{"test_on_demand", ^expected_ban_data}] = :ets.lookup(:rate_limiter, "test_on_demand") + key = add_chain_id_prefix("test_on_demand") + assert [{^key, ^expected_ban_data}] = :ets.lookup(:rate_limiter, key) Process.sleep(2000) @@ -42,6 +43,10 @@ defmodule Explorer.Utility.RateLimiterTest do assert RateLimiter.check_rate("test", :on_demand) == :deny expected_ban_data = "#{now + 2}:2" - assert [{"test_on_demand", ^expected_ban_data}] = :ets.lookup(:rate_limiter, "test_on_demand") + assert [{^key, ^expected_ban_data}] = :ets.lookup(:rate_limiter, key) + end + + defp add_chain_id_prefix(key) do + "#{Application.get_env(:block_scout_web, :chain_id)}_#{key}" end end diff --git a/apps/utils/lib/utils/config_helper.ex b/apps/utils/lib/utils/config_helper.ex new file mode 100644 index 000000000000..6541317f6bb1 --- /dev/null +++ b/apps/utils/lib/utils/config_helper.ex @@ -0,0 +1,19 @@ +defmodule Utils.ConfigHelper do + @moduledoc """ + Helper functions for parsing config values. + """ + + @doc """ + Parses a time value from a string. + """ + @spec parse_time_value(String.t()) :: non_neg_integer() | :error + def parse_time_value(value) do + case value |> String.downcase() |> Integer.parse() do + {milliseconds, "ms"} -> milliseconds + {hours, "h"} -> :timer.hours(hours) + {minutes, "m"} -> :timer.minutes(minutes) + {seconds, s} when s in ["s", ""] -> :timer.seconds(seconds) + _ -> :error + end + end +end diff --git a/config/config_helper.exs b/config/config_helper.exs index dc6524b6a441..a4fa60691c0f 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -4,6 +4,7 @@ defmodule ConfigHelper do import Bitwise alias Explorer.Market.Source alias Indexer.Transform.Blocks + alias Utils.ConfigHelper def repos do base_repos = [Explorer.Repo, Explorer.Repo.Account] @@ -105,12 +106,12 @@ defmodule ConfigHelper do nil value -> - case value |> String.downcase() |> Integer.parse() do - {milliseconds, "ms"} -> milliseconds - {hours, "h"} -> :timer.hours(hours) - {minutes, "m"} -> :timer.minutes(minutes) - {seconds, s} when s in ["s", ""] -> :timer.seconds(seconds) - _ -> raise "Invalid time format in environment variable #{env_var}: #{value}" + case ConfigHelper.parse_time_value(value) do + :error -> + raise "Invalid time format in environment variable #{env_var}: #{value}" + + time -> + time end end end diff --git a/config/runtime.exs b/config/runtime.exs index 52a26a54354b..a157ee82aa03 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -110,25 +110,44 @@ config :block_scout_web, :contract, certified_list: ConfigHelper.parse_list_env_var("CONTRACT_CERTIFIED_LIST", ""), partial_reverification_disabled: !ConfigHelper.parse_bool_env_var("CONTRACT_ENABLE_PARTIAL_REVERIFICATION") -default_global_api_rate_limit = 50 +default_global_api_rate_limit = 25 default_api_rate_limit_by_key = 10 +api_rate_limit_redis_url = ConfigHelper.safe_get_env("API_RATE_LIMIT_HAMMER_REDIS_URL", nil) config :block_scout_web, :api_rate_limit, disabled: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_DISABLED"), - global_limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT", default_global_api_rate_limit), - limit_by_key: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_KEY", default_api_rate_limit_by_key), - limit_by_whitelisted_ip: - ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_WHITELISTED_IP", default_global_api_rate_limit), - time_interval_limit: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_TIME_INTERVAL", "1s"), - limit_by_ip: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_IP", 3000), - time_interval_limit_by_ip: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_IP_TIME_INTERVAL", "5m"), - static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY"), - no_rate_limit_api_key: System.get_env("API_NO_RATE_LIMIT_API_KEY"), + static_api_key_value: System.get_env("API_RATE_LIMIT_STATIC_API_KEY"), + static_api_key: %{ + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_KEY", default_api_rate_limit_by_key), + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_KEY_TIME_INTERVAL", "1s") + }, + whitelisted_ip: %{ + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_WHITELISTED_IP", default_global_api_rate_limit), + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_WHITELISTED_IP_TIME_INTERVAL", "1s") + }, + ip: %{ + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_IP", 500), + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_IP_TIME_INTERVAL", "15m") + }, + temporary_token: %{ + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 4), + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN_TIME_INTERVAL", "1s") + }, + account_api_key: %{ + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_ACCOUNT_API_KEY_TIME_INTERVAL", "1s") + }, + no_rate_limit_api_key_value: System.get_env("API_NO_RATE_LIMIT_API_KEY"), whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS"), is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"), - api_v2_ui_limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 5), - api_v2_token_ttl_seconds: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL_IN_SECONDS", 18000), - eth_json_rpc_max_batch_size: ConfigHelper.parse_integer_env_var("ETH_JSON_RPC_MAX_BATCH_SIZE", 5) + api_v2_token_ttl: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL", "30m"), + eth_json_rpc_max_batch_size: ConfigHelper.parse_integer_env_var("ETH_JSON_RPC_MAX_BATCH_SIZE", 5), + redis_url: if(api_rate_limit_redis_url == "", do: nil, else: api_rate_limit_redis_url), + rate_limit_backend: + if(api_rate_limit_redis_url == "", + do: BlockScoutWeb.RateLimit.Hammer.ETS, + else: BlockScoutWeb.RateLimit.Hammer.Redis + ), + config_url: System.get_env("API_RATE_LIMIT_CONFIG_URL") default_graphql_rate_limit = 10 @@ -144,7 +163,7 @@ config :block_scout_web, Api.GraphQL, limit_by_key: ConfigHelper.parse_integer_env_var("API_GRAPHQL_RATE_LIMIT_BY_KEY", default_graphql_rate_limit), time_interval_limit: ConfigHelper.parse_time_env_var("API_GRAPHQL_RATE_LIMIT_TIME_INTERVAL", "1s"), limit_by_ip: ConfigHelper.parse_integer_env_var("API_GRAPHQL_RATE_LIMIT_BY_IP", 500), - time_interval_limit_by_ip: ConfigHelper.parse_time_env_var("API_GRAPHQL_RATE_LIMIT_BY_IP_TIME_INTERVAL", "5m"), + time_interval_limit_by_ip: ConfigHelper.parse_time_env_var("API_GRAPHQL_RATE_LIMIT_BY_IP_TIME_INTERVAL", "1h"), static_api_key: System.get_env("API_GRAPHQL_RATE_LIMIT_STATIC_API_KEY") # Configures History @@ -841,11 +860,13 @@ config :explorer, Explorer.Utility.RateLimiter, redis_url: rate_limiter_redis_url, on_demand: [ time_interval_limit: ConfigHelper.parse_time_env_var("RATE_LIMITER_ON_DEMAND_TIME_INTERVAL", "5s"), - limit_by_ip: ConfigHelper.parse_integer_env_var("RATE_LIMITER_ON_DEMAND_LIMIT_BY_IP", 100), + limit_by_ip: ConfigHelper.parse_integer_env_var("RATE_LIMITER_ON_DEMAND_LIMIT_BY_IP", 50), exp_timeout_coeff: ConfigHelper.parse_integer_env_var("RATE_LIMITER_ON_DEMAND_EXPONENTIAL_TIMEOUT_COEFF", 100), max_ban_interval: ConfigHelper.parse_time_env_var("RATE_LIMITER_ON_DEMAND_MAX_BAN_INTERVAL", "1h"), limitation_period: ConfigHelper.parse_time_env_var("RATE_LIMITER_ON_DEMAND_LIMITATION_PERIOD", "1h") - ] + ], + hammer_backend_module: + if(rate_limiter_redis_url, do: Explorer.Utility.Hammer.Redis, else: Explorer.Utility.Hammer.ETS) config :explorer, Explorer.ThirdPartyIntegrations.UniversalProxy, config_url: diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 41c4f5899159..65cdd6a817b1 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -143,7 +143,10 @@ API_V1_READ_METHODS_DISABLED=false API_V1_WRITE_METHODS_DISABLED=false # API_RATE_LIMIT_DISABLED=true # API_SENSITIVE_ENDPOINTS_KEY= -# API_RATE_LIMIT_TIME_INTERVAL=1s +# API_RATE_LIMIT_BY_KEY_TIME_INTERVAL=1s +# API_RATE_LIMIT_BY_WHITELISTED_IP_TIME_INTERVAL=1s +# API_RATE_LIMIT_UI_V2_WITH_TOKEN_TIME_INTERVAL=1s +# API_RATE_LIMIT_BY_ACCOUNT_API_KEY_TIME_INTERVAL=1s # API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m # API_RATE_LIMIT=50 # API_RATE_LIMIT_BY_KEY=10 @@ -152,6 +155,7 @@ API_V1_WRITE_METHODS_DISABLED=false # API_RATE_LIMIT_STATIC_API_KEY= # API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 # API_RATE_LIMIT_BY_IP=3000 +# API_RATE_LIMIT_CONFIG_URL= # API_NO_RATE_LIMIT_API_KEY= # API_GRAPHQL_ENABLED= # API_GRAPHQL_MAX_COMPLEXITY= diff --git a/mix.lock b/mix.lock index 3bcb7908837a..8abc913c83bd 100644 --- a/mix.lock +++ b/mix.lock @@ -81,8 +81,8 @@ "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"}, - "hammer": {:hex, :hammer, "6.2.1", "5ae9c33e3dceaeb42de0db46bf505bd9c35f259c8defb03390cd7556fea67ee2", [:mix], [{:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}], "hexpm", "b9476d0c13883d2dc0cc72e786bac6ac28911fba7cc2e04b70ce6a6d9c4b2bdc"}, - "hammer_backend_redis": {:hex, :hammer_backend_redis, "6.2.0", "f39a9c8491387cdf719a38593311537e3e0251ca54725b6ee9145406821f39d2", [:mix], [{:hammer, "~> 6.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.1", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "9965d55705d7ca7412bb0685f5cd44fc47d103bf388abc50438e71974c36c9fa"}, + "hammer": {:hex, :hammer, "7.0.1", "136edcd81af44becbe6b73a958c109e2364ab0dc026d7b19892037dc2632078c", [:mix], [], "hexpm", "796edf14ab2aa80df72080210fcf944ee5e8868d8ece7a7511264d802f58cc2d"}, + "hammer_backend_redis": {:hex, :hammer_backend_redis, "7.0.2", "b58a2c4b553cd809b4ff059b80fc146f89954f03760b9eca674e0e6db3ed1fa9", [:mix], [{:hammer, "~> 7.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.5", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "827a5ff418f99c7c4b17c1d59a4b0fef099591a75910daa3131005da3223106d"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, From 56677c3547d0dd57315c4c28b7fa1501ec84ec1b Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 9 Jul 2025 17:20:26 +0300 Subject: [PATCH 0151/1006] fix: Fix race condition for EventNotification (#12738) * fix: Fix race condition for EventNotification * Add envs to common-blockscout.env * Process review comments * Process review comments --- .../lib/explorer/chain/events/listener.ex | 12 +-- .../explorer/utility/event_notification.ex | 2 + ...4_add_timestamp_to_event_notifications.exs | 11 +++ apps/explorer/test/support/factory.ex | 9 +- apps/indexer/lib/indexer/supervisor.ex | 5 + .../utils/event_notifications_cleaner.ex | 50 ++++++++++ .../event_notifications_cleaner_test.exs | 98 +++++++++++++++++++ config/runtime.exs | 5 + docker-compose/envs/common-blockscout.env | 3 + 9 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs create mode 100644 apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex create mode 100644 apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs diff --git a/apps/explorer/lib/explorer/chain/events/listener.ex b/apps/explorer/lib/explorer/chain/events/listener.ex index 4a771d3afdd5..dad47911cd5f 100644 --- a/apps/explorer/lib/explorer/chain/events/listener.ex +++ b/apps/explorer/lib/explorer/chain/events/listener.ex @@ -40,7 +40,7 @@ defmodule Explorer.Chain.Events.Listener do defp expand_payload(payload) do case Integer.parse(payload) do - {event_notification_id, ""} -> fetch_and_delete_event_notification(event_notification_id) + {event_notification_id, ""} -> fetch_event_notification(event_notification_id) _ -> payload end end @@ -68,18 +68,12 @@ defmodule Explorer.Chain.Events.Listener do end) end - defp fetch_and_delete_event_notification(id) do + defp fetch_event_notification(id) do case Repo.get(EventNotification, id) do nil -> nil - %{data: data} = notification -> - try do - Repo.delete(notification) - rescue - Ecto.StaleEntryError -> nil - end - + %{data: data} -> data end end diff --git a/apps/explorer/lib/explorer/utility/event_notification.ex b/apps/explorer/lib/explorer/utility/event_notification.ex index ccb52ea5e9b0..21a2ba644211 100644 --- a/apps/explorer/lib/explorer/utility/event_notification.ex +++ b/apps/explorer/lib/explorer/utility/event_notification.ex @@ -7,6 +7,8 @@ defmodule Explorer.Utility.EventNotification do typed_schema "event_notifications" do field(:data, :string) + + timestamps() end @doc false diff --git a/apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs b/apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs new file mode 100644 index 000000000000..124fa572b7e2 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250704124014_add_timestamp_to_event_notifications.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.AddTimestampToEventNotifications do + use Ecto.Migration + + def change do + execute("TRUNCATE event_notifications;") + + alter table(:event_notifications) do + timestamps() + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index a98ddf245977..5109d17ea637 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -62,7 +62,7 @@ defmodule Explorer.Factory do alias Explorer.Market.MarketHistory alias Explorer.Repo - alias Explorer.Utility.{MissingBalanceOfToken, MissingBlockRange} + alias Explorer.Utility.{EventNotification, MissingBalanceOfToken, MissingBlockRange} alias Ueberauth.Strategy.Auth0 alias Ueberauth.Auth.{Extra, Info} @@ -1399,4 +1399,11 @@ defmodule Explorer.Factory do transaction: transaction } end + + def event_notification_factory do + %EventNotification{ + data: "test_data", + inserted_at: DateTime.utc_now() + } + end end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 09b3b2b43a22..9eeaa640bd26 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -64,6 +64,8 @@ defmodule Indexer.Supervisor do UnclesWithoutIndex } + alias Indexer.Utils.EventNotificationsCleaner + def child_spec([]) do child_spec([[]]) end @@ -256,6 +258,9 @@ defmodule Indexer.Supervisor do {PendingTransactionsSanitizer, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, {TokenTotalSupplyUpdater, [[]]}, + # Notifications cleaner + configure(EventNotificationsCleaner, [[]]), + # Temporary workers {UncatalogedTokenTransfers.Supervisor, [[]]}, {UnclesWithoutIndex.Supervisor, diff --git a/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex b/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex new file mode 100644 index 000000000000..f21792ac8a78 --- /dev/null +++ b/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex @@ -0,0 +1,50 @@ +defmodule Indexer.Utils.EventNotificationsCleaner do + @moduledoc """ + Module is responsible for cleaning up event notifications from the database. + """ + + alias Explorer.Repo + alias Explorer.Utility.EventNotification + + import Ecto.Query + + use GenServer + + require Logger + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(args) do + Process.send(self(), :clean_up_event_notifications, []) + {:ok, args} + end + + def handle_info(:clean_up_event_notifications, state) do + clean_up_event_notifications() + Process.send_after(self(), :clean_up_event_notifications, interval()) + {:noreply, state} + end + + defp clean_up_event_notifications do + {count, _} = + EventNotification + |> where([en], en.inserted_at < ago(^max_age(), "millisecond")) + |> Repo.delete_all() + + Logger.info("Deleted #{count} event notifications") + end + + defp max_age do + config()[:max_age] + end + + defp interval do + config()[:interval] + end + + defp config do + Application.get_env(:indexer, __MODULE__) + end +end diff --git a/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs b/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs new file mode 100644 index 000000000000..5fc17cf69114 --- /dev/null +++ b/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs @@ -0,0 +1,98 @@ +defmodule Indexer.Utils.EventNotificationsCleanerTest do + use Explorer.DataCase + + alias Explorer.Repo + alias Explorer.Utility.EventNotification + alias Indexer.Utils.EventNotificationsCleaner + + import Ecto.Query + + setup do + # Store original config + original_config = Application.get_env(:indexer, EventNotificationsCleaner) + + # Restore original config after each test + on_exit(fn -> + Application.put_env(:indexer, EventNotificationsCleaner, original_config) + end) + + :ok + end + + describe "start_link/1" do + test "starts the GenServer with the given name" do + assert {:ok, pid} = EventNotificationsCleaner.start_link([]) + assert Process.alive?(pid) + Process.exit(pid, :normal) + end + end + + describe "clean_up_event_notifications/0" do + test "deletes notifications older than max_age" do + # Create notifications with different timestamps + old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond) + new_time = DateTime.utc_now() |> DateTime.add(-500, :millisecond) + + insert(:event_notification, data: "old_data", inserted_at: old_time) + insert(:event_notification, data: "new_data", inserted_at: new_time) + + # Verify both notifications exist + assert Repo.aggregate(EventNotification, :count) == 2 + + # Set configuration for max_age of 1000ms + config = Application.get_env(:indexer, EventNotificationsCleaner) + Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000)) + + assert {:ok, _pid} = EventNotificationsCleaner.start_link([]) + Process.sleep(500) + + # Verify only the old notification was deleted + assert Repo.aggregate(EventNotification, :count) == 1 + remaining = Repo.one(from(n in EventNotification, select: n.data)) + assert remaining == "new_data" + end + + test "deletes multiple old notifications" do + old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond) + + # Insert multiple old notifications + insert_list(3, :event_notification, inserted_at: old_time) + + # Insert one new notification + insert(:event_notification, data: "new_data") + + # Verify all notifications exist + assert Repo.aggregate(EventNotification, :count) == 4 + + # Set configuration + config = Application.get_env(:indexer, EventNotificationsCleaner) + Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000)) + + assert {:ok, _pid} = EventNotificationsCleaner.start_link([]) + Process.sleep(500) + + # Verify only the new notification remains + assert Repo.aggregate(EventNotification, :count) == 1 + remaining = Repo.one(from(n in EventNotification, select: n.data)) + assert remaining == "new_data" + end + + test "does not delete notifications when none are old enough" do + # Insert only new notifications + insert_list(3, :event_notification) + + # Verify notifications exist + assert Repo.aggregate(EventNotification, :count) == 3 + + # Set configuration + config = Application.get_env(:indexer, EventNotificationsCleaner) + Application.put_env(:indexer, EventNotificationsCleaner, Keyword.put(config, :max_age, 1000)) + + assert {:ok, _pid} = EventNotificationsCleaner.start_link([]) + Process.sleep(500) + + # Verify all notifications still exist + assert Repo.aggregate(EventNotification, :count) == 3 + end + end +end diff --git a/config/runtime.exs b/config/runtime.exs index a157ee82aa03..b9ad2da2bd9b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1432,6 +1432,11 @@ config :indexer, Indexer.Fetcher.Scroll.BridgeL2.Supervisor, disabled?: ConfigHe config :indexer, Indexer.Fetcher.Scroll.Batch.Supervisor, disabled?: ConfigHelper.chain_type() != :scroll +config :indexer, Indexer.Utils.EventNotificationsCleaner, + interval: ConfigHelper.parse_time_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_INTERVAL", "2m"), + enabled: ConfigHelper.parse_bool_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED", "true"), + max_age: ConfigHelper.parse_time_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE", "5m") + config :ex_aws, json_codec: Jason, access_key_id: System.get_env("NFT_MEDIA_HANDLER_AWS_ACCESS_KEY_ID"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 65cdd6a817b1..ba1b2df6f9cc 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -221,6 +221,9 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TOKEN_BALANCES_CONCURRENCY= # INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL= # INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF= +# INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED=true +# INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_INTERVAL= +# INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE= # INDEXER_TX_ACTIONS_ENABLE= # INDEXER_TX_ACTIONS_MAX_TOKEN_CACHE_SIZE= # INDEXER_TX_ACTIONS_REINDEX_FIRST_BLOCK= From 8c01fbfa931e52a2971d84277ebe669124d7df6c Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 9 Jul 2025 18:27:14 +0300 Subject: [PATCH 0152/1006] fix: Fix CSV export tests (#12744) * fix: Fix CSV export tests * Fix apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs --- .../api/v2/csv_export_controller_test.exs | 47 +++++++++++++++++++ .../api/v2/token_controller_test.exs | 26 ++++++++-- 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index 327e8e531f25..e281f6d78007 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -487,6 +487,7 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do end defp csv_setup() do + original_config = :persistent_term.get(:rate_limit_config) old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) original_api_rate_limit = Application.get_env(:block_scout_web, :api_rate_limit) @@ -504,7 +505,53 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_api_rate_limit, :disabled, false)) + config = %{ + static_match: %{}, + wildcard_match: %{}, + parametrized_match: %{ + ["api", "v2", "addresses", ":param", "election-rewards", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/addresses/:param/election-rewards/csv_", + isolate_rate_limit?: true + }, + ["api", "v2", "addresses", ":param", "internal-transactions", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/addresses/:param/internal-transactions/csv_", + isolate_rate_limit?: true + }, + ["api", "v2", "addresses", ":param", "logs", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/addresses/:param/logs/csv_", + isolate_rate_limit?: true + }, + ["api", "v2", "addresses", ":param", "token-transfers", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/addresses/:param/token-transfers/csv_", + isolate_rate_limit?: true + }, + ["api", "v2", "addresses", ":param", "transactions", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/addresses/:param/transactions/csv_", + isolate_rate_limit?: true + }, + ["api", "v2", "tokens", ":param", "holders", "csv"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bucket_key_prefix: "api/v2/tokens/:param/holders/csv_", + isolate_rate_limit?: true + } + } + } + + :persistent_term.put(:rate_limit_config, config) + on_exit(fn -> + :persistent_term.put(:rate_limit_config, original_config) Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index ffba606adefe..fcc9a971c931 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1535,6 +1535,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do setup :verify_on_exit! setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + original_config = :persistent_term.get(:rate_limit_config) old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) old_explorer_http_adapter = Application.get_env(:explorer, :http_adapter) @@ -1567,7 +1568,24 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_api_rate_limit, :disabled, false)) + config = %{ + static_match: %{}, + wildcard_match: %{}, + parametrized_match: %{ + ["api", "v2", "tokens", ":param", "instances", ":param", "refetch-metadata"] => %{ + ip: %{period: 3_600_000, limit: 1}, + recaptcha_to_bypass_429: true, + bypass_token_scope: "token_instance_refetch_metadata", + bucket_key_prefix: "api/v2/tokens/:param/instances/:param/refetch-metadata_", + isolate_rate_limit?: true + } + } + } + + :persistent_term.put(:rate_limit_config, config) + on_exit(fn -> + :persistent_term.put(:rate_limit_config, original_config) Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) Application.put_env(:explorer, :http_adapter, old_explorer_http_adapter) @@ -1998,7 +2016,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> put_req_header("scoped-recaptcha-bypass-token", "wrong_scoped_token") |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) + assert %{"message" => "Too Many Requests"} = json_response(request, 429) # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" @@ -2109,7 +2127,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> put_req_header("scoped-recaptcha-bypass-token", "some_token_that_does_not_exist") |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) + assert %{"message" => "Too Many Requests"} = json_response(request, 429) request = Phoenix.ConnTest.build_conn() @@ -2117,7 +2135,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> put_req_header("scoped-recaptcha-bypass-token", "some_token_that_does_not_exist") |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) + assert %{"message" => "Too Many Requests"} = json_response(request, 429) request = Phoenix.ConnTest.build_conn() @@ -2125,7 +2143,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> put_req_header("scoped-recaptcha-bypass-token", "") |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - assert %{"message" => "429 Too Many Requests"} = json_response(request, 429) + assert %{"message" => "Too Many Requests"} = json_response(request, 429) # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" From 9b957f20ec326c1fbf026e669cca94cec892c237 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 10 Jul 2025 18:40:50 +0300 Subject: [PATCH 0153/1006] feat: Async multichain data export (#12490) * feat: Async multichain DB data export * Process review comments * Update config/config.exs Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --------- Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- apps/explorer/lib/explorer/chain.ex | 14 +- .../multichain_search_db/main_export_queue.ex | 96 ++++++ ...multichain_search_db_export_retry_queue.ex | 68 ----- .../multichain_search.ex | 260 ++++++++++------- ...ain_search_db_export_retry_queue_table.exs | 11 + ...ultichain_search_db_export_queue_table.exs | 10 + ...ultichain_search_db_export_queue_table.exs | 14 + .../multichain_search_test.exs | 32 +- apps/explorer/test/support/factory.ex | 6 +- .../multichain_search_db/main_export_queue.ex | 275 ++++++++++++++++++ .../multichain_search_db_export/retry.ex | 205 ------------- apps/indexer/lib/indexer/supervisor.ex | 4 +- .../main_export_queue_test.exs} | 137 ++++----- config/config.exs | 3 + config/runtime.exs | 20 +- docker-compose/envs/common-blockscout.env | 10 +- 16 files changed, 679 insertions(+), 486 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex delete mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250526111446_rename_multichain_search_db_export_retry_queue_table.exs create mode 100644 apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs create mode 100644 apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex rename apps/indexer/test/indexer/fetcher/{multichain_search_db_export/retry_test.exs => multichain_search_db/main_export_queue_test.exs} (57%) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 416f576cc825..1e10f172ef6b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1161,18 +1161,8 @@ defmodule Explorer.Chain do transactions: imported[:transactions] || [] } - if assets_to_import == %{ - addresses: [], - blocks: [], - transactions: [] - } do - result - else - # todo: consider robust async way of importing - MultichainSearch.batch_import(assets_to_import) - - result - end + MultichainSearch.send_data_to_queue(assets_to_import) + result other_result -> other_result diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex new file mode 100644 index 000000000000..1c5dea917e37 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex @@ -0,0 +1,96 @@ +defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do + @moduledoc """ + Tracks data pending for export to the Multichain Service database. + """ + + use Explorer.Schema + import Ecto.Query + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Block.Range + + @required_attrs ~w(hash hash_type)a + + @primary_key false + typed_schema "multichain_search_db_main_export_queue" do + field(:hash, :binary, null: false) + + field(:hash_type, Ecto.Enum, + values: [ + :block, + :transaction, + :address + ], + null: false + ) + + field(:retries_number, :integer) + field(:block_range, Range) + + timestamps() + end + + def changeset(%__MODULE__{} = pending_ops, attrs) do + pending_ops + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end + + @spec stream_multichain_db_data_batch( + initial :: accumulator, + reducer :: (entry :: map(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_multichain_db_data_batch(initial, reducer, limited? \\ false) + when is_function(reducer, 2) do + __MODULE__ + |> select([export], %{ + hash: export.hash, + hash_type: export.hash_type, + block_range: export.block_range + }) + |> order_by([export], fragment("upper(?) DESC", export.block_range)) + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Builds a query to retrieve records from the `Explorer.Chain.MultichainSearchDb.MainExportQueue` module + where the `hash` field matches any of the given `hashes`. + + ## Parameters + + - `hashes`: A list of hash values to filter the records by. + + ## Returns + + - An Ecto query that can be executed to fetch the matching records. + """ + @spec by_hashes_query([binary()]) :: Ecto.Query.t() + def by_hashes_query(hashes) do + __MODULE__ + |> where([export], export.hash in ^hashes) + end + + @doc """ + Returns an Ecto query that defines the default conflict resolution strategy for the + `multichain_search_db_main_export_queue` table. On conflict, it increments the `retries_number` + (by using the value from `EXCLUDED.retries_number` or 0 if not present) and updates the + `updated_at` field to the greatest value between the current and the new timestamp. + + This is typically used in upsert operations to ensure retry counts are tracked and + timestamps are properly updated. + """ + @spec default_on_conflict :: Ecto.Query.t() + def default_on_conflict do + from( + multichain_search_db_main_export_queue in __MODULE__, + update: [ + set: [ + retries_number: fragment("COALESCE(EXCLUDED.retries_number, 0) + 1"), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", multichain_search_db_main_export_queue.updated_at) + ] + ] + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex deleted file mode 100644 index a47beec70818..000000000000 --- a/apps/explorer/lib/explorer/chain/multichain_search_db_export_retry_queue.ex +++ /dev/null @@ -1,68 +0,0 @@ -defmodule Explorer.Chain.MultichainSearchDbExportRetryQueue do - @moduledoc """ - Tracks data pending retry for export to the Multichain Service database after an initial failure. - """ - - use Explorer.Schema - import Ecto.Query - alias Explorer.{Chain, Repo} - - @required_attrs ~w(hash hash_type)a - - @primary_key false - typed_schema "multichain_search_db_export_retry_queue" do - field(:hash, :binary, null: true) - - field(:hash_type, Ecto.Enum, - values: [ - :block, - :transaction, - :address - ], - null: true - ) - - timestamps() - end - - def changeset(%__MODULE__{} = pending_ops, attrs) do - pending_ops - |> cast(attrs, @required_attrs) - |> validate_required(@required_attrs) - end - - @spec stream_multichain_db_data_batch_to_retry_export( - initial :: accumulator, - reducer :: (entry :: map(), accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_multichain_db_data_batch_to_retry_export(initial, reducer, limited? \\ false) - when is_function(reducer, 2) do - __MODULE__ - |> select([export], %{ - hash: export.hash, - hash_type: export.hash_type - }) - |> Chain.add_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - - @doc """ - Builds a query to retrieve records from the `Explorer.Chain.MultichainSearchDbExportRetryQueue` module - where the `hash` field matches any of the given `hashes`. - - ## Parameters - - - `hashes`: A list of hash values to filter the records by. - - ## Returns - - - An Ecto query that can be executed to fetch the matching records. - """ - @spec by_hashes_query([binary()]) :: Ecto.Query.t() - def by_hashes_query(hashes) do - __MODULE__ - |> where([export], export.hash in ^hashes) - end -end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index bb49c02afcc4..8564bd87d086 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -2,11 +2,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @moduledoc """ Module to interact with Multichain search microservice """ - alias Ecto.Association.NotLoaded alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.{Address, Block, Hash, Transaction} - alias Explorer.Chain.MultichainSearchDbExportRetryQueue + alias Explorer.Chain.{Block, Hash, Transaction} + alias Explorer.Chain.Block.Range + alias Explorer.Chain.MultichainSearchDb.MainExportQueue alias Explorer.{Helper, Repo} alias Explorer.Utility.Microservice alias HTTPoison.Response @@ -18,35 +18,35 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @unspecified "UNSPECIFIED" @doc """ - Performs a batch import of addresses, blocks, and transactions to the Multichain Search microservice. + Processes a batch import of data by splitting the input parameters into chunks and sending each chunk as an HTTP POST request to a configured microservice endpoint. - ## Parameters - - `params`: A map containing: - - `addresses`: List of address structs. - - `blocks`: List of block structs. - - `transactions`: List of transaction structs. + If the microservice is enabled, the function: + - Splits the input `params` into manageable chunks. + - Sends each chunk concurrently using `Task.async_stream/3` with a maximum concurrency and timeout. + - Collects the results, merging any errors and accumulating data that needs to be retried. + - Returns `{:ok, {:chunks_processed, params_chunks}}` if all chunks are processed successfully. + - Returns `{:error, data_to_retry}` if any chunk fails, where `data_to_retry` contains the addresses, block ranges, and hashes that need to be retried. - ## Returns - - `{:ok, :service_disabled}`: If the integration with Multichain Search Service is disabled. - - `{:ok, result}`: If the import was successful. - - `{:error, reason}`: If an error occurred. + If the microservice is disabled, returns `{:ok, :service_disabled}`. + + ## Parameters + + - `params` (`map()`): The parameters to be imported, which will be split into chunks for processing. + + ## Returns + + - `{:ok, any()}`: If all chunks are processed successfully or the service is disabled. + - `{:error, map()}`: If one or more chunks fail, with details about the data that needs to be retried. """ - @spec batch_import( - %{ - addresses: [Address.t()], - blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], - transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] - }, - boolean() - ) :: {:error, map()} | {:ok, any()} - def batch_import(params, retry? \\ false) do + @spec batch_import(map()) :: {:error, map()} | {:ok, any()} + def batch_import(params) do if enabled?() do params_chunks = extract_batch_import_params_into_chunks(params) url = batch_import_url() params_chunks |> Task.async_stream( - fn export_body -> http_post_request(url, export_body, retry?) end, + fn export_body -> http_post_request(url, export_body) end, max_concurrency: @max_concurrency, timeout: @post_timeout, zip_input_on_exit: true @@ -56,7 +56,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do acc {:ok, {:error, error}}, acc -> - on_error(error, retry?) + on_error(error) case acc do {:ok, {:chunks_processed, _}} -> @@ -78,22 +78,25 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end {:exit, {export_body, reason}}, acc -> - on_error( - %{ - url: url, - data_to_retry: export_body, - reason: reason - }, - retry? - ) + on_error(%{ + url: url, + data_to_retry: export_body, + reason: reason + }) # credo:disable-for-next-line Credo.Check.Refactor.Nesting case acc do {:ok, {:chunks_processed, _}} -> {:error, export_body} - {:error, acc} -> - {:error, Map.merge(acc, export_body)} + {:error, data_to_retry} -> + merged_data_to_retry = %{ + addresses: data_to_retry.data_to_retry.addresses ++ export_body.addresses, + block_ranges: data_to_retry.data_to_retry.block_ranges ++ export_body.block_ranges, + hashes: data_to_retry.data_to_retry.hashes ++ export_body.hashes + } + + {:error, merged_data_to_retry} end end) else @@ -141,46 +144,107 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Logger.configure(truncate: old_truncate) end - @spec on_error( - map(), - boolean() - ) :: {non_neg_integer(), nil | [term()]} | :ok - # sobelow_skip ["DOS.StringToAtom"] + @spec on_error(map()) :: {non_neg_integer(), nil | [term()]} | :ok defp on_error( %{ - data_to_retry: %{addresses: addresses, hashes: hashes} - } = error, - false + data_to_retry: data_to_retry + } = error ) do log_error(error) - hashes_to_retry = + prepared_data = prepare_export_data_for_queue(data_to_retry) + + Repo.insert_all( + MainExportQueue, + Helper.add_timestamps(prepared_data), + on_conflict: MainExportQueue.default_on_conflict(), + conflict_target: [:hash, :hash_type] + ) + end + + defp on_error(_), do: :ignore + + @doc """ + Sends the provided data to the export queue if the feature is enabled. + + Accepts a map with `:addresses`, `:blocks`, and `:transactions` keys, each containing a list. + If all lists are empty, returns `:ignore`. + + Otherwise, splits the data into chunks, prepares each chunk for export, and inserts them into the `MainExportQueue` table. + If the feature is not enabled, returns `:ignore`. + + ## Parameters + + - data: a map with the following keys: + - `:addresses` - list of addresses + - `:blocks` - list of blocks + - `:transactions` - list of transactions + + ## Returns + + - `:ok` on successful insertion + - `:ignore` if the feature is disabled or the data is empty + """ + @spec send_data_to_queue(%{addresses: list(), blocks: list(), transactions: list()}) :: :ignore | :ok + def send_data_to_queue(%{addresses: [], blocks: [], transactions: []}), do: :ignore + + def send_data_to_queue(data) do + if enabled?() do + data + |> extract_batch_import_params_into_chunks() + |> Enum.each(fn data_chunk -> + prepared_data = prepare_export_data_for_queue(data_chunk) + + Repo.insert_all(MainExportQueue, Helper.add_timestamps(prepared_data), on_conflict: :nothing) + end) + + :ok + else + :ignore + end + end + + # sobelow_skip ["DOS.StringToAtom"] + defp prepare_export_data_for_queue(%{addresses: addresses, hashes: hashes, block_ranges: block_ranges}) do + block_range = + case block_ranges do + [%{min_block_number: min_str, max_block_number: max_str} | _] -> + with {min_num, ""} <- Integer.parse(min_str), + {max_num, ""} <- Integer.parse(max_str) do + %Range{from: min_num, to: max_num} + else + _ -> nil + end + + _ -> + nil + end + + hashes_to_queue = hashes |> Enum.map( &%{ hash: Helper.hash_to_binary(&1.hash), - hash_type: &1.hash_type |> String.downcase() |> String.to_atom() + hash_type: &1.hash_type |> String.downcase() |> String.to_atom(), + block_range: block_range } ) - addresses_to_retry = + addresses_to_queue = addresses |> Enum.map(fn %{hash: address_hash_string} -> %{ hash: Helper.hash_to_binary(address_hash_string), - hash_type: :address + hash_type: :address, + block_range: block_range } end) - prepared_data = hashes_to_retry ++ addresses_to_retry - - Repo.insert_all(MultichainSearchDbExportRetryQueue, Helper.add_timestamps(prepared_data), on_conflict: :nothing) + hashes_to_queue ++ addresses_to_queue end - defp on_error(_, _), do: :ignore - - @spec http_post_request(String.t(), map(), boolean()) :: {:ok, any()} | {:error, String.t()} - defp http_post_request(url, body, retry?) do + @spec http_post_request(String.t(), map()) :: {:ok, any()} | {:error, String.t()} + defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] case Application.get_env(:explorer, :http_adapter).post(url, Jason.encode!(body), headers, @@ -195,7 +259,6 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do %{ url: url, data_to_retry: body, - retry?: retry?, status_code: status_code, response_body: response_body }} @@ -205,50 +268,30 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do %{ url: url, data_to_retry: body, - retry?: retry?, reason: reason }} end end @doc """ - Extracts batch import parameters into chunks for processing. - - This function takes a map containing `addresses`, `blocks`, and `transactions`, - and processes them into chunks suitable for batch import. It performs the following steps: - - - Retrieves the chain ID. - - Computes block ranges from the given blocks. - - Preloads associated `:token` and `:smart_contract` data for the addresses, removes duplicates, and formats them. - - Formats the blocks and transactions into hashes. - - Combines block hashes and transaction hashes into a single list of `block_transaction_hashes`. - - Splits the formatted addresses into chunks of size `addresses_chunk_size()` and indexes them. - - Constructs a base data chunk containing the API key, chain ID, and block ranges. - - The function returns a list of data chunks. If there are no addresses, it returns a single chunk with only the `block_transaction_hashes`. Otherwise, it creates a chunk for each group of addresses, including the `block_transaction_hashes` only in the first chunk. - - ## Parameters - - - `%{addresses: raw_addresses, blocks: blocks, transactions: transactions}`: A map containing: - - `addresses`: A list of raw address data. - - `blocks`: A list of block data. - - `transactions`: A list of transaction data. - - ## Returns - - - A list of maps, where each map represents a chunk of data for batch import. Each chunk contains: - - `:api_key`: The API key for the request. - - `:chain_id`: The chain ID as a string. - - `:addresses`: A chunk of formatted addresses. - - `:block_ranges`: The computed block ranges. - - `:hashes`: A list of block and transaction hashes (only included in the first chunk). - + Extracts and organizes batch import parameters into chunks suitable for microservice requests. + + Given a map of parameters, this function: + - Uniquely formats and chunks addresses. + - Prepares block ranges and hashes (from blocks, block hashes, and transactions). + - Associates each chunk with the current API key and chain ID. + - Ensures that the first chunk includes all block ranges and hashes, while subsequent chunks only contain address data. + + Returns a list of maps, each representing a chunk of import parameters with the following structure: + - `:api_key` - The API key as a string. + - `:chain_id` - The chain ID as a string. + - `:addresses` - A list of formatted address strings. + - `:block_ranges` - A list of maps with `:min_block_number` and `:max_block_number` as strings. + - `:hashes` - A list of maps with `:hash` and `:hash_type` as strings. + + If there are no addresses, a single chunk is returned containing only the hashes and block ranges. """ - @spec extract_batch_import_params_into_chunks(%{ - addresses: [Address.t()], - blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], - transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] - }) :: [ + @spec extract_batch_import_params_into_chunks(map()) :: [ %{ api_key: String.t(), chain_id: String.t(), @@ -257,13 +300,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do hashes: [%{hash: String.t(), hash_type: String.t()}] } ] - def extract_batch_import_params_into_chunks(%{ - addresses: raw_addresses, - blocks: blocks, - transactions: transactions - }) do - chain_id = ChainId.get_id() - block_ranges = get_block_ranges(blocks) + def extract_batch_import_params_into_chunks( + %{ + addresses: raw_addresses, + transactions: transactions + } = params + ) do + block_ranges = + if Map.has_key?(params, :block_ranges), do: params.block_ranges, else: get_block_ranges(params.blocks) addresses = raw_addresses @@ -271,7 +315,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do |> Repo.preload([:token, :smart_contract]) |> Enum.map(&format_address(&1)) - block_hashes = format_blocks(blocks) + block_hashes = + if Map.has_key?(params, :block_hashes), + do: format_block_hashes(params.block_hashes), + else: format_blocks(params.blocks) transaction_hashes = format_transactions(transactions) @@ -283,6 +330,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do |> Enum.with_index() api_key = api_key() + chain_id = ChainId.get_id() chain_id_string = to_string(chain_id) base_data_chunk = %{ @@ -330,7 +378,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do ] defp format_blocks(blocks) do blocks - |> Enum.map(&format_block(to_string(&1.hash))) + |> Enum.map(&format_block_hash(to_string(&1.hash))) + end + + @spec format_block_hashes([Hash.t()]) :: [ + %{hash: String.t(), hash_type: String.t()} + ] + defp format_block_hashes(block_hashes) do + block_hashes + |> Enum.map(&format_block_hash(to_string(&1))) end @spec format_transactions([Transaction.t() | %{hash: String.t(), hash_type: String.t()}]) :: [ @@ -341,11 +397,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do |> Enum.map(&format_transaction(to_string(&1.hash))) end - @spec format_block(String.t()) :: %{ + @spec format_block_hash(String.t()) :: %{ hash: String.t(), hash_type: String.t() } - defp format_block(block_hash_string) do + defp format_block_hash(block_hash_string) do %{ hash: block_hash_string, hash_type: "BLOCK" diff --git a/apps/explorer/priv/repo/migrations/20250526111446_rename_multichain_search_db_export_retry_queue_table.exs b/apps/explorer/priv/repo/migrations/20250526111446_rename_multichain_search_db_export_retry_queue_table.exs new file mode 100644 index 000000000000..21aeb318b716 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250526111446_rename_multichain_search_db_export_retry_queue_table.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.RenameMultichainSearchDbExportRetryQueueTable do + use Ecto.Migration + + def change do + rename(table(:multichain_search_db_export_retry_queue), to: table(:multichain_search_db_main_export_queue)) + + execute( + "ALTER TABLE multichain_search_db_main_export_queue RENAME CONSTRAINT multichain_search_db_export_retry_queue_pkey TO multichain_search_db_main_export_queue_pkey" + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs new file mode 100644 index 000000000000..178795f1bced --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Migrations.AddBlockRangeAndRetriesNumberToMultichainSearchDbExportQueueTable do + use Ecto.Migration + + def change do + alter table(:multichain_search_db_main_export_queue) do + add(:block_range, :int8range, null: true) + add(:retries_number, :smallint, null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs new file mode 100644 index 000000000000..6dc27a8b9bba --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.AddBlockRangeIndexToMultichainSearchDbExportQueueTable do + use Ecto.Migration + + def change do + execute( + """ + CREATE INDEX multichain_search_db_main_export_queue_upper_block_range_index ON multichain_search_db_main_export_queue (upper(block_range) DESC); + """, + """ + DROP INDEX multichain_search_db_main_export_queue_upper_block_range_index; + """ + ) + end +end diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 8a5906779787..54da6c4a7aa5 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -4,15 +4,13 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do import Mox alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.MultichainSearchDbExportRetryQueue + alias Explorer.Chain.MultichainSearchDb.MainExportQueue alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.{Repo, TestHelper} alias Plug.Conn setup :verify_on_exit! - @error_msg "Error while sending request to Multichain Search DB Service" - describe "batch_import/2" do setup do Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) @@ -55,7 +53,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ) end) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 block_1 = insert(:block) block_2 = insert(:block) @@ -73,10 +71,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do } assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_import(params) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 end - test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_export_retry_queue' table is populated" do + test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_main_export_queue' table is populated" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -100,7 +98,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ) end) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 address_1 = insert(:address) address_2 = insert(:address) @@ -132,8 +130,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ] }} == MultichainSearch.batch_import(params) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 6 - records = Repo.all(MultichainSearchDbExportRetryQueue) + assert Repo.aggregate(MainExportQueue, :count, :hash) == 6 + records = Repo.all(MainExportQueue) assert Enum.all?(records, fn record -> (record.hash == address_1.hash.bytes && record.hash_type == :address) || @@ -161,7 +159,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do TestHelper.get_chain_id_mock() - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 block_1 = insert(:block) block_2 = insert(:block) @@ -199,10 +197,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert Enum.count(results.block_ranges) == 1 assert Enum.count(results.hashes) == 4 # 7000 addresses + 2 blocks + 2 transactions - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 7004 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 7004 end - test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_main_export_queue' table is populated with all the input data" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -226,7 +224,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ) end) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 addresses = 10 |> insert_list(:address) @@ -241,10 +239,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert Enum.count(results.block_ranges) == 0 assert Enum.count(results.hashes) == 0 - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 10 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 10 end - test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_retry_queue' table is populated with all the input data" do + test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_main_export_queue' table is populated with all the input data" do bypass = Bypass.open() Application.put_env(:explorer, MultichainSearch, @@ -268,7 +266,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ) end) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 0 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 addresses = 15 |> insert_list(:address) @@ -283,7 +281,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert Enum.count(results.block_ranges) == 0 assert Enum.count(results.hashes) == 0 - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 15 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 15 end end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 5109d17ea637..d7d03ac0c6a6 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -38,7 +38,7 @@ defmodule Explorer.Factory do Hash, InternalTransaction, Log, - MultichainSearchDbExportRetryQueue, + MultichainSearchDb, PendingBlockOperation, PendingTransactionOperation, SmartContract, @@ -744,8 +744,8 @@ defmodule Explorer.Factory do %PendingTransactionOperation{} end - def multichain_search_db_export_retry_queue_factory do - %MultichainSearchDbExportRetryQueue{} + def multichain_search_db_main_export_queue_factory do + %MultichainSearchDb.MainExportQueue{} end def internal_transaction_factory() do diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex new file mode 100644 index 000000000000..8c7d79210a42 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -0,0 +1,275 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do + @moduledoc """ + Exports blockchain data to Multichain Search DB service from the queue. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash, MultichainSearchDb.MainExportQueue, Transaction} + alias Explorer.MicroserviceInterfaces.MultichainSearch + + alias Indexer.BufferedTask + alias Indexer.Helper, as: IndexerHelper + + @behaviour BufferedTask + + @default_max_batch_size 1000 + @default_max_concurrency 10 + @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + MainExportQueue.stream_multichain_db_data_batch( + initial_acc, + fn data, acc -> + IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end, + true + ) + + acc + end + + @impl BufferedTask + def run( + %{ + addresses: _addresses, + transactions: _transactions, + block_ranges: _block_ranges, + block_hashes: _block_hashes + } = prepared_export_data, + _json_rpc_named_arguments + ) do + export_data_to_multichain(prepared_export_data) + end + + @impl BufferedTask + def run(data, _json_rpc_named_arguments) when is_list(data) do + prepared_export_data = prepare_export_data(data) + + export_data_to_multichain(prepared_export_data) + end + + defp export_data_to_multichain(prepared_export_data) do + case MultichainSearch.batch_import(prepared_export_data) do + {:ok, {:chunks_processed, result}} -> + all_hashes = + result + |> Enum.flat_map(fn params -> + hashes = prepare_hashes_for_db_query(params[:hashes], :full) + addresses = prepare_hashes_for_db_query(params[:addresses], :address) + + hashes ++ addresses + end) + + all_hashes + |> MainExportQueue.by_hashes_query() + |> Repo.delete_all() + + :ok + + {:error, data_to_retry} -> + Logger.error(fn -> + ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] + end) + + hashes = all_hashes(prepared_export_data) + failed_hashes = failed_hashes(data_to_retry) + + successful_hashes = hashes -- failed_hashes + + successful_hash_binaries = + successful_hashes + |> Enum.map(fn hash -> + "0x" <> hex = hash + Base.decode16!(hex, case: :mixed) + end) + + successful_hash_binaries + |> MainExportQueue.by_hashes_query() + |> Repo.delete_all() + + {:retry, data_to_retry} + end + end + + defp prepare_hashes_for_db_query(entities, entity_type) do + entities + |> Enum.map(fn entity -> + fun = if entity_type == :address, do: :string_to_address_hash, else: :string_to_full_hash + + case apply(Chain, fun, [Map.get(entity, :hash)]) do + {:ok, hash} -> hash.bytes + {:error, _} -> nil + end + end) + |> Enum.reject(&is_nil/1) + end + + defp all_hashes(prepared_export_data) do + transaction_hashes = + prepared_export_data[:transactions] |> Enum.map(&Map.get(&1, :hash)) + + block_hashes = prepared_export_data[:block_hashes] |> Enum.map(&to_string(&1)) + + address_hashes = prepared_export_data[:addresses] |> Enum.map(&to_string(Map.get(&1, :hash))) + + transaction_hashes ++ block_hashes ++ address_hashes + end + + defp failed_hashes(data_to_retry) do + block_transaction_hashes = + data_to_retry.hashes + |> Enum.map(&Map.get(&1, :hash)) + + address_hashes = + data_to_retry.addresses + |> Enum.map(&Map.get(&1, :hash)) + + block_transaction_hashes ++ address_hashes + end + + @doc """ + Prepares export data from a list of maps containing `:hash`, `:hash_type`, and `:block_range` keys. + + Processes each entry by its `:hash_type` (`:address`, `:block`, or `:transaction`), accumulating the corresponding hashes and block ranges. + Converts address hashes to address structs and returns a map with the following keys: + + - `:addresses` - a list of `Address.t()` structs derived from address hashes. + - `:transactions` - a list of `Transaction.t()` structs or maps with transaction hash and hash type. + - `:block_ranges` - a list of maps with `:min_block_number` and `:max_block_number` as strings. + - `:block_hashes` - a list of `Hash.t()` structs for block hashes. + + ## Parameters + + - `export_data`: a list of maps, each containing: + - `:hash` (binary): the hash value. + - `:hash_type` (atom): the type of hash (`:address`, `:block`, or `:transaction`). + - `:block_range` (any): the block range associated with the hash. + + ## Returns + + A map with prepared export data, including addresses, transactions, block ranges, and block hashes. + """ + @spec prepare_export_data([%{hash: binary, hash_type: atom, block_range: any()}]) :: %{ + addresses: [Address.t()], + transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}], + block_ranges: [%{min_block_number: String.t(), max_block_number: String.t()}], + block_hashes: [Hash.t()] + } + def prepare_export_data(export_data) do + pre_prepared_export_data = + export_data + |> Enum.reduce( + %{ + address_hashes: [], + block_hashes: [], + transactions: [], + block_ranges: [ + %{ + min_block_number: nil, + max_block_number: nil + } + ] + }, + fn res, acc -> + case res.hash_type do + :address -> + acc + |> Map.update( + :address_hashes, + [%Hash{byte_count: 20, bytes: res.hash}], + &[%Hash{byte_count: 20, bytes: res.hash} | &1] + ) + |> maybe_update_block_ranges_in_params_map(res.block_range) + + :block -> + acc + |> Map.update( + :block_hashes, + [%Hash{byte_count: 32, bytes: res.hash}], + &[%Hash{byte_count: 32, bytes: res.hash} | &1] + ) + |> maybe_update_block_ranges_in_params_map(res.block_range) + + :transaction -> + acc + |> Map.update( + :transactions, + [%{hash: to_string(%Hash{byte_count: 32, bytes: res.hash})}], + &[%{hash: to_string(%Hash{byte_count: 32, bytes: res.hash})} | &1] + ) + |> maybe_update_block_ranges_in_params_map(res.block_range) + end + end + ) + + addresses = + pre_prepared_export_data.address_hashes + |> Chain.hashes_to_addresses() + + pre_prepared_export_data + |> Map.put(:addresses, addresses) + |> Map.drop([:address_hashes]) + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end + + defp maybe_update_block_ranges_in_params_map(params_map, nil), do: params_map + + defp maybe_update_block_ranges_in_params_map(params_map, block_range) do + params_map + |> Map.update!( + :block_ranges, + &[ + %{ + min_block_number: to_string(min(block_range.from, parse_block_number(&1, :min_block_number))), + max_block_number: to_string(max(block_range.to, parse_block_number(&1, :max_block_number))) + } + ] + ) + end + + defp parse_block_number(nil, _), do: 0 + + defp parse_block_number([%{min_block_number: nil}], :min_block_number), do: nil + + defp parse_block_number([%{max_block_number: nil}], :max_block_number), do: 0 + + defp parse_block_number( + [ + %{ + min_block_number: _, + max_block_number: _ + } = block_range + ], + type + ) do + case Integer.parse(block_range[type]) do + {num, _} -> num + :error -> 0 + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex deleted file mode 100644 index bf0818062a6d..000000000000 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db_export/retry.ex +++ /dev/null @@ -1,205 +0,0 @@ -defmodule Indexer.Fetcher.MultichainSearchDbExport.Retry do - @moduledoc """ - Retries exporting blockchain data to Multichain Search DB service. - """ - - require Logger - - use Indexer.Fetcher, restart: :permanent - use Spandex.Decorators - - alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Address, Block, Hash, MultichainSearchDbExportRetryQueue, Transaction} - alias Explorer.MicroserviceInterfaces.MultichainSearch - - alias Indexer.BufferedTask - alias Indexer.Helper, as: IndexerHelper - - @behaviour BufferedTask - - @default_max_batch_size 10 - @default_max_concurrency 10 - @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" - - @doc false - def child_spec([init_options, gen_server_options]) do - merged_init_opts = - defaults() - |> Keyword.merge(init_options) - |> Keyword.merge(state: []) - - Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) - end - - @impl BufferedTask - def init(initial_acc, reducer, _) do - {:ok, acc} = - MultichainSearchDbExportRetryQueue.stream_multichain_db_data_batch_to_retry_export( - initial_acc, - fn data, acc -> - IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) - end, - true - ) - - acc - end - - @impl BufferedTask - def run(data, _json_rpc_named_arguments) when is_list(data) do - prepared_export_data = prepare_export_data(data) - - case MultichainSearch.batch_import(prepared_export_data) do - {:ok, result} -> - unless result == :service_disabled do - hashes = all_hashes(prepared_export_data) - - hashes - |> MultichainSearchDbExportRetryQueue.by_hashes_query() - |> Repo.delete_all() - end - - :ok - - {:error, data_to_retry} -> - Logger.error(fn -> - ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] - end) - - hashes = all_hashes(prepared_export_data) - failed_hashes = failed_hashes(data_to_retry) - - successful_hashes = hashes -- failed_hashes - - successful_hash_binaries = - successful_hashes - |> Enum.map(fn hash -> - "0x" <> hex = hash - Base.decode16!(hex, case: :mixed) - end) - - successful_hash_binaries - |> MultichainSearchDbExportRetryQueue.by_hashes_query() - |> Repo.delete_all() - - {:retry, [data_to_retry]} - end - end - - defp all_hashes(prepared_export_data) do - transaction_hashes = - prepared_export_data[:transactions] |> Enum.map(&Map.get(&1, :hash)) - - block_hashes = prepared_export_data[:blocks] |> Enum.map(&to_string(Map.get(&1, :hash))) - - address_hashes = prepared_export_data[:addresses] |> Enum.map(&to_string(Map.get(&1, :hash))) - - transaction_hashes ++ block_hashes ++ address_hashes - end - - defp failed_hashes(data_to_retry) do - block_transaction_hashes = - data_to_retry.hashes - |> Enum.map(&Map.get(&1, :hash)) - - address_hashes = - data_to_retry.addresses - |> Enum.map(&Map.get(&1, :hash)) - - block_transaction_hashes ++ address_hashes - end - - @doc """ - Prepares export data by categorizing input hashes into addresses, blocks, and transactions, - then enriches the result with resolved address and block data. - - ## Parameters - - - `export_data`: A list of maps, each containing a `:hash` (binary) and a `:hash_type` (`:address`, `:block`, or `:transaction`). - - ## Returns - - - A map containing: - - `:addresses` - a list of resolved address structs from the given address hashes. - - `:blocks` - a list of block structs fetched from the database using the block hashes or list of hash and hash_type maps. - - `:transactions` - a list Transaction.t() objects or list of hash and hash_type maps. - - ## Example - - iex> prepare_export_data([ - ...> %{hash: <<1,2,3>>, hash_type: :address}, - ...> %{hash: <<4,5,6>>, hash_type: :block}, - ...> %{hash: <<7,8,9>>, hash_type: :transaction} - ...> ]) - %{ - addresses: [...], - blocks: [...], - transactions: [...] - } - - """ - @spec prepare_export_data([%{hash: binary, hash_type: atom}]) :: %{ - addresses: [Address.t()], - blocks: [Block.t() | %{hash: String.t(), hash_type: String.t()}], - transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}] - } - def prepare_export_data(export_data) do - pre_prepared_export_data = - export_data - |> Enum.reduce(%{address_hashes: [], block_hashes: [], transactions: []}, fn %{ - hash: hash_bytes, - hash_type: hash_type - }, - acc -> - case hash_type do - :address -> - Map.update( - acc, - :address_hashes, - [%Hash{byte_count: 20, bytes: hash_bytes}], - &[%Hash{byte_count: 20, bytes: hash_bytes} | &1] - ) - - :block -> - Map.update( - acc, - :block_hashes, - [%Hash{byte_count: 32, bytes: hash_bytes}], - &[%Hash{byte_count: 32, bytes: hash_bytes} | &1] - ) - - :transaction -> - Map.update( - acc, - :transactions, - [%{hash: to_string(%Hash{byte_count: 32, bytes: hash_bytes})}], - &[%{hash: to_string(%Hash{byte_count: 32, bytes: hash_bytes})} | &1] - ) - end - end) - - addresses = - pre_prepared_export_data.address_hashes - |> Chain.hashes_to_addresses() - - blocks = - pre_prepared_export_data.block_hashes - |> Block.by_hashes_query() - |> Repo.all() - - pre_prepared_export_data - |> Map.put(:addresses, addresses) - |> Map.put(:blocks, blocks) - |> Map.drop([:address_hashes]) - |> Map.drop([:block_hashes]) - end - - defp defaults do - [ - flush_interval: :timer.seconds(10), - max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, - max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, - task_supervisor: __MODULE__.TaskSupervisor - ] - end -end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 9eeaa640bd26..f453c349d743 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -23,7 +23,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.Blackfort.Validator, as: ValidatorBlackfort alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime - alias Indexer.Fetcher.MultichainSearchDbExport.Retry, as: MultichainSearchDbExportRetry + alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry @@ -149,7 +149,7 @@ defmodule Indexer.Supervisor do {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, - {MultichainSearchDbExportRetry.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbMainExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, configure( Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs similarity index 57% rename from apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs rename to apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index 5fa56e09d16a..337f87191375 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db_export/retry_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -1,24 +1,25 @@ -defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do +defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do use ExUnit.Case use Explorer.DataCase import ExUnit.CaptureLog, only: [capture_log: 1] import Mox - alias Explorer.Chain.MultichainSearchDbExportRetryQueue + alias Explorer.Chain.MultichainSearchDb.MainExportQueue + alias Explorer.Chain.Block.Range alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.TestHelper - alias Indexer.Fetcher.MultichainSearchDbExport.Retry, as: MultichainSearchDbExportRetry + alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue alias Plug.Conn @moduletag :capture_log setup do start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) - Application.put_env(:indexer, MultichainSearchDbExportRetry.Supervisor, disabled?: false) + Application.put_env(:indexer, MultichainSearchDbMainExportQueue.Supervisor, disabled?: false) on_exit(fn -> - Application.put_env(:indexer, MultichainSearchDbExportRetry.Supervisor, disabled?: true) + Application.put_env(:indexer, MultichainSearchDbMainExportQueue.Supervisor, disabled?: true) end) :ok @@ -46,23 +47,23 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do transaction_hash_bytes = "aba197aa8a13871bdd53861f7b5108394000fc0f72893661ae39610e9cd94019" |> Base.decode16!(case: :mixed) - insert(:multichain_search_db_export_retry_queue, %{hash: address_hash_bytes, hash_type: :address}) - insert(:multichain_search_db_export_retry_queue, %{hash: block_hash_bytes, hash_type: :block}) - insert(:multichain_search_db_export_retry_queue, %{hash: transaction_hash_bytes, hash_type: :transaction}) + insert(:multichain_search_db_main_export_queue, %{hash: address_hash_bytes, hash_type: :address}) + insert(:multichain_search_db_main_export_queue, %{hash: block_hash_bytes, hash_type: :block}) + insert(:multichain_search_db_main_export_queue, %{hash: transaction_hash_bytes, hash_type: :transaction}) reducer = fn data, acc -> [data | acc] end pid = [] - |> MultichainSearchDbExportRetry.Supervisor.child_spec() + |> MultichainSearchDbMainExportQueue.Supervisor.child_spec() |> ExUnit.Callbacks.start_supervised!() - results = MultichainSearchDbExportRetry.init([], reducer, nil) + results = MultichainSearchDbMainExportQueue.init([], reducer, nil) assert Enum.count(results) == 3 - assert Enum.member?(results, %{hash: address_hash_bytes, hash_type: :address}) - assert Enum.member?(results, %{hash: block_hash_bytes, hash_type: :block}) - assert Enum.member?(results, %{hash: transaction_hash_bytes, hash_type: :transaction}) + assert Enum.member?(results, %{hash: address_hash_bytes, hash_type: :address, block_range: nil}) + assert Enum.member?(results, %{hash: block_hash_bytes, hash_type: :block, block_range: nil}) + assert Enum.member?(results, %{hash: transaction_hash_bytes, hash_type: :transaction, block_range: nil}) :timer.sleep(10) GenServer.stop(pid) end @@ -96,9 +97,9 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do "aba197aa8a13871bdd53861f7b5108394000fc0f72893661ae39610e9cd94019" |> Base.decode16!(case: :mixed) export_data = [ - %{hash: address_hash_bytes, hash_type: :address}, - %{hash: block_hash_bytes, hash_type: :block}, - %{hash: transaction_hash_bytes, hash_type: :transaction} + %{hash: address_hash_bytes, hash_type: :address, block_range: nil}, + %{hash: block_hash_bytes, hash_type: :block, block_range: nil}, + %{hash: transaction_hash_bytes, hash_type: :transaction, block_range: nil} ] TestHelper.get_chain_id_mock() @@ -111,7 +112,7 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do ) end) - assert :ok = MultichainSearchDbExportRetry.run(export_data, nil) + assert :ok = MultichainSearchDbMainExportQueue.run(export_data, nil) end test "returns {:retry, failed_data} on error where failed_data is only chunks that failed to export", %{ @@ -135,18 +136,22 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do address_2_hash_string = to_string(address_2) |> String.downcase() address_3 = insert(:address) address_3_hash_string = to_string(address_3) |> String.downcase() - block = insert(:block) + block = insert(:block, number: 1) + block_number_string = to_string(block.number) block_hash_string = to_string(block.hash) - block_number = to_string(block.number) transaction = insert(:transaction) |> with_block(block) transaction_hash_string = to_string(transaction.hash) export_data = [ - %{hash: address_1.hash.bytes, hash_type: :address}, - %{hash: address_2.hash.bytes, hash_type: :address}, - %{hash: address_3.hash.bytes, hash_type: :address}, - %{hash: block.hash.bytes, hash_type: :block}, - %{hash: transaction.hash.bytes, hash_type: :transaction} + %{hash: address_1.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: address_2.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: address_3.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: block.hash.bytes, hash_type: :block, block_range: %Range{from: block.number, to: block.number}}, + %{ + hash: transaction.hash.bytes, + hash_type: :transaction, + block_range: %Range{from: block.number, to: block.number} + } ] TestHelper.get_chain_id_mock() @@ -173,51 +178,53 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do log = capture_log(fn -> assert {:retry, - [ - %{ - addresses: [ - %{ - hash: ^address_2_hash_string, - token_type: "UNSPECIFIED", - is_contract: false, - token_name: nil, - contract_name: nil, - ens_name: nil, - is_token: false, - is_verified_contract: false - }, - %{ - hash: ^address_3_hash_string, - token_type: "UNSPECIFIED", - is_contract: false, - token_name: nil, - contract_name: nil, - ens_name: nil, - is_token: false, - is_verified_contract: false - } - ], - block_ranges: [%{max_block_number: ^block_number, min_block_number: ^block_number}], - hashes: [ - %{hash: ^block_hash_string, hash_type: "BLOCK"}, - %{ - hash: ^transaction_hash_string, - hash_type: "TRANSACTION" - } - ] - } - ]} = MultichainSearchDbExportRetry.run(export_data, nil) + %{ + addresses: [ + %{ + hash: ^address_2_hash_string, + token_type: "UNSPECIFIED", + is_contract: false, + token_name: nil, + contract_name: nil, + ens_name: nil, + is_token: false, + is_verified_contract: false + }, + %{ + hash: ^address_3_hash_string, + token_type: "UNSPECIFIED", + is_contract: false, + token_name: nil, + contract_name: nil, + ens_name: nil, + is_token: false, + is_verified_contract: false + } + ], + block_ranges: [%{max_block_number: ^block_number_string, min_block_number: ^block_number_string}], + hashes: [ + %{hash: ^block_hash_string, hash_type: "BLOCK"}, + %{ + hash: ^transaction_hash_string, + hash_type: "TRANSACTION" + } + ] + }} = MultichainSearchDbMainExportQueue.run(export_data, nil) end) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 4 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 4 assert log =~ "Batch export retry to the Multichain Search DB failed" export_data_2 = [ - %{hash: address_2.hash.bytes, hash_type: :address}, - %{hash: address_3.hash.bytes, hash_type: :address}, - %{hash: block.hash.bytes, hash_type: :block}, - %{hash: transaction.hash.bytes, hash_type: :transaction} + %{hash: address_2.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: address_3.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: block.hash.bytes, hash_type: :block, block_range: %Range{from: block.number, to: block.number}}, + %{ + hash: transaction.hash.bytes, + hash_type: :transaction, + block_range: %Range{from: block.number, to: block.number} + } ] TestHelper.get_chain_id_mock() @@ -238,9 +245,9 @@ defmodule Indexer.Fetcher.MultichainSearchDbExport.RetryTest do end) end - MultichainSearchDbExportRetry.run(export_data_2, nil) + MultichainSearchDbMainExportQueue.run(export_data_2, nil) - assert Repo.aggregate(MultichainSearchDbExportRetryQueue, :count, :hash) == 3 + assert Repo.aggregate(MainExportQueue, :count, :hash) == 3 end end end diff --git a/config/config.exs b/config/config.exs index f09d21a3f065..c7adb85d85c4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -72,6 +72,9 @@ config :logger, :error, ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count block_number step count error_count shrunk import_id transaction_id)a +# todo: migrate from deprecated usages +config :tesla, disable_deprecated_builder_warning: true + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/config/runtime.exs b/config/runtime.exs index b9ad2da2bd9b..609bf4cb2ba6 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1029,9 +1029,9 @@ config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC1155, config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_TOKEN_INSTANCE_ERC_721_SANITIZE_FETCHER", "false") -config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry.Supervisor, +config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue.Supervisor, disabled?: - ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_FETCHER") || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_FETCHER") || is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, @@ -1115,12 +1115,18 @@ config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, ConfigHelper.parse_integer_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BLOCKS_BATCH_SIZE", 100_000), high_verbosity: ConfigHelper.parse_bool_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY", "true") -config :indexer, Indexer.Fetcher.MultichainSearchDbExport.Retry, - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY", 10), - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE", 10), +config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, + concurrency: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_CONCURRENCY", 10), + batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_BATCH_SIZE", 1000), enqueue_busy_waiting_timeout: - ConfigHelper.parse_time_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s"), - max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE", 1000) + ConfigHelper.parse_time_env_var( + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_ENQUEUE_BUSY_WAITING_TIMEOUT", + "1s" + ), + max_queue_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE", 1000) config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index ba1b2df6f9cc..f95ffe41bbb1 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -184,7 +184,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER= # INDEXER_DISABLE_WITHDRAWALS_FETCHER= # INDEXER_DISABLE_REPLACED_TRANSACTION_FETCHER= -# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_FETCHER= # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -203,10 +203,10 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY= # INDEXER_TOKEN_INSTANCE_REFETCH_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_REFETCH_CONCURRENCY= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_BATCH_SIZE= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_CONCURRENCY= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_ENQUEUE_BUSY_WAITING_TIMEOUT= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_RETRY_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= From b2d46b296614c30371feb593715c590f14f1cf42 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 11 Jul 2025 09:52:18 +0300 Subject: [PATCH 0154/1006] fix: error on too big block numbers in APIv1 `txlist` method (#12727) * fix: error on too big block numbers in APIv1 `txlist` method * fix: update `safe_parse_non_negative_integer/2` spec --- .../controllers/api/rpc/address_controller.ex | 9 +++++- .../api/rpc/address_controller_test.exs | 28 +++++++++++++++++++ apps/explorer/lib/explorer/helper.ex | 6 ++-- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index bcaf7b543b26..c759be31df18 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -6,6 +6,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do alias Explorer.{Chain, Etherscan} alias Explorer.Chain.{Address, Wei} alias Explorer.Etherscan.{Addresses, Blocks} + alias Explorer.Helper, as: ExplorerHelper alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand @api_true [api?: true] @@ -14,6 +15,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do @invalid_contract_address_message "Invalid contract address format" @no_token_transfers_message "No token transfers found" + @max_safe_block_number round(:math.pow(2, 31)) - 1 + def listaccounts(conn, params) do options = params @@ -479,7 +482,11 @@ defmodule BlockScoutWeb.API.RPC.AddressController do # sobelow_skip ["DOS.StringToAtom"] defp put_block(options, params, key) do with %{^key => block_param} <- params, - {block_number, ""} <- Integer.parse(block_param) do + {:ok, block_number} <- + ExplorerHelper.safe_parse_non_negative_integer( + block_param, + @max_safe_block_number + ) do Map.put(options, String.to_atom(key), block_number) else _ -> diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index 7fe69832b077..00cdd01a44f6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -1243,6 +1243,34 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) end + test "ignores too big endblock", %{conn: conn} do + blocks = [_, _, _, _] = insert_list(4, :block) + address = insert(:address) + + for block <- blocks do + 2 + |> insert_list(:transaction, from_address: address) + |> with_block(block) + end + + params = %{ + "module" => "account", + "action" => "txlist", + "address" => "#{address.hash}", + "endblock" => "99999999999" + } + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert length(response["result"]) == 8 + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(txlist_schema(), response) + end + test "with start_timestamp and end_timestamp params", %{conn: conn} do now = Timex.now() timestamp1 = Timex.shift(now, hours: -6) diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 9757e33eccdc..bccbba1779e4 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -133,11 +133,13 @@ defmodule Explorer.Helper do iex> safe_parse_non_negative_integer("27606393966689717254124294199939478533331961967491413693980084341759630764504") {:error, :too_big_integer} """ - def safe_parse_non_negative_integer(string) do + @spec safe_parse_non_negative_integer(String.t(), integer()) :: + {:ok, integer()} | {:error, :negative_integer | :too_big_integer | :invalid_integer} + def safe_parse_non_negative_integer(string, max_safe_integer \\ @max_safe_integer) do case Integer.parse(string) do {num, ""} -> case num do - _ when num > @max_safe_integer -> {:error, :too_big_integer} + _ when num > max_safe_integer -> {:error, :too_big_integer} _ when num < 0 -> {:error, :negative_integer} _ -> {:ok, num} end From 3aad7090cf1303a06f2ef35270650ecfa9bb0d67 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 11 Jul 2025 12:45:55 +0300 Subject: [PATCH 0155/1006] chore: hold parity with Etherscan APIv1 for `getcontractcreation` and `getblocknobytime` endpoints (#12721) * chore: modify `getcontractcreation` to hold parity with etherscan * chore: modify `getblocknobytime` to hold parity with etherscan * fix: error when excessively large timestamp param is provided * chore: address review comments --- .../lib/block_scout_web/chain.ex | 25 ++++--- .../api/rpc/contract_controller.ex | 12 +++- .../views/api/rpc/block_view.ex | 6 +- .../views/api/rpc/contract_view.ex | 59 ++++++++++++---- .../api/rpc/block_controller_test.exs | 65 ++++++++++++------ .../api/rpc/contract_controller_test.exs | 68 ++++++++++++++++++- 6 files changed, 183 insertions(+), 52 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index fa0d97b9e0e5..f9b14893e375 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -679,17 +679,24 @@ defmodule BlockScoutWeb.Chain do end end - def param_to_block_timestamp(timestamp_string) when is_binary(timestamp_string) do - case Integer.parse(timestamp_string) do - {timestamp_int, ""} -> - timestamp = - timestamp_int - |> DateTime.from_unix!(:second) + @doc """ + Converts a timestamp string to a `DateTime.t()` struct for block timestamp + queries. - {:ok, timestamp} + ## Parameters + - `timestamp_string`: A string containing a Unix timestamp in seconds - _ -> - {:error, :invalid_timestamp} + ## Returns + - `{:ok, DateTime.t()}` if the timestamp is valid and can be converted + - `{:error, :invalid_timestamp}` if the timestamp is invalid or out of range + """ + @spec param_to_block_timestamp(String.t()) :: {:ok, DateTime.t()} | {:error, :invalid_timestamp} + def param_to_block_timestamp(timestamp_string) when is_binary(timestamp_string) do + with {timestamp_int, ""} <- Integer.parse(timestamp_string), + {:ok, timestamp} <- DateTime.from_unix(timestamp_int, :second) do + {:ok, timestamp} + else + _ -> {:error, :invalid_timestamp} end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index c7a1074ac6b0..05a9ccfcecc9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -49,9 +49,19 @@ defmodule BlockScoutWeb.API.RPC.ContractController do |> Enum.map(fn address_hash_string -> case validate_address(address_hash_string, params) do {:ok, _address_hash, address} -> + contract_creation_internal_transaction_with_transaction_association = [ + contract_creation_internal_transaction: { + Address.contract_creation_internal_transaction_preload_query(), + :transaction + } + ] + Address.maybe_preload_smart_contract_associations( address, - Address.contract_creation_transaction_associations(), + [ + Address.contract_creation_transaction_association(), + contract_creation_internal_transaction_with_transaction_association + ], @api_true ) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex index b0262aaf937c..ac840679bbe5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex @@ -78,11 +78,7 @@ defmodule BlockScoutWeb.API.RPC.BlockView do end def render("getblocknobytime.json", %{block_number: block_number}) do - data = %{ - "blockNumber" => to_string(block_number) - } - - RPCView.render("show.json", data: data) + RPCView.render("show.json", data: to_string(block_number)) end def render("eth_block_number.json", %{number: number, id: id}) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex index 02cbbe6bc96c..57326a58972a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex @@ -1,15 +1,20 @@ defmodule BlockScoutWeb.API.RPC.ContractView do use BlockScoutWeb, :view - alias BlockScoutWeb.AddressView alias BlockScoutWeb.API.RPC.RPCView alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper - alias Explorer.Chain.{Address, SmartContract} + + alias Explorer.Chain.{ + Address, + InternalTransaction, + SmartContract, + Transaction + } defguardp is_empty_string(input) when input == "" or input == nil def render("getcontractcreation.json", %{addresses: addresses}) do - contracts = addresses |> Enum.map(&address_to_response/1) |> Enum.reject(&is_nil/1) + contracts = addresses |> Enum.map(&prepare_contract_creation_info/1) |> Enum.reject(&is_nil/1) RPCView.render("show.json", data: contracts) end @@ -238,15 +243,45 @@ defmodule BlockScoutWeb.API.RPC.ContractView do end end - defp address_to_response(address) do - creator_hash = AddressView.from_address_hash(address) - creation_transaction = creator_hash && AddressView.transaction_hash(address) + @spec prepare_contract_creation_info(Address.t()) :: %{binary() => binary()} | nil + defp prepare_contract_creation_info(%Address{ + contract_creation_internal_transaction: + %InternalTransaction{ + transaction: %Transaction{} = transaction + } = internal_transaction + }) do + %{ + "contractAddress" => to_string(internal_transaction.created_contract_address_hash), + "contractFactory" => to_string(internal_transaction.from_address_hash), + "creationBytecode" => to_string(internal_transaction.init) + } + |> with_creation_transaction_info(transaction) + end - creation_transaction && - %{ - "contractAddress" => to_string(address.hash), - "contractCreator" => to_string(creator_hash), - "txHash" => to_string(creation_transaction) - } + defp prepare_contract_creation_info(%Address{ + contract_creation_transaction: %Transaction{} = transaction + }) do + %{ + "contractAddress" => to_string(transaction.created_contract_address_hash), + "contractFactory" => "", + "creationBytecode" => to_string(transaction.input) + } + |> with_creation_transaction_info(transaction) + end + + defp prepare_contract_creation_info(_), do: nil + + @spec with_creation_transaction_info(%{binary() => binary()}, Transaction.t()) :: + %{binary() => binary()} + defp with_creation_transaction_info(info, transaction) do + unix_timestamp = DateTime.to_unix(transaction.block_timestamp, :second) + + %{ + "contractCreator" => to_string(transaction.from_address_hash), + "txHash" => to_string(transaction.hash), + "blockNumber" => to_string(transaction.block_number), + "timestamp" => to_string(unix_timestamp) + } + |> Map.merge(info) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs index 9ee058f30e72..ce5c4e1bd713 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs @@ -225,7 +225,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -239,7 +239,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -258,7 +258,26 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() + assert :ok = ExJsonSchema.Validator.validate(schema, response) + end + + test "with an excessively large timestamp param", %{conn: conn} do + response = + conn + |> get("/api", %{ + "module" => "block", + "action" => "getblocknobytime", + "timestamp" => "1000000000000000000000000", + "closest" => "before" + }) + |> json_response(200) + + assert response["message"] =~ "Invalid `timestamp` param" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -277,7 +296,7 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do assert response["status"] == "0" assert Map.has_key?(response, "result") refute response["result"] - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -292,10 +311,6 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do (timestamp_int + 1) |> to_string() - expected_result = %{ - "blockNumber" => "#{block.number}" - } - assert response = conn |> get("/api", %{ @@ -306,10 +321,10 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == expected_result + assert response["result"] == "#{block.number}" assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -324,10 +339,6 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do (timestamp_int - 1) |> to_string() - expected_result = %{ - "blockNumber" => "#{block.number}" - } - assert response = conn |> get("/api", %{ @@ -338,10 +349,10 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == expected_result + assert response["result"] == "#{block.number}" assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end @@ -356,10 +367,6 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do (timestamp_int - 2 * 60) |> to_string() - expected_result = %{ - "blockNumber" => "#{block.number}" - } - assert response = conn |> get("/api", %{ @@ -370,10 +377,10 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == expected_result + assert response["result"] == "#{block.number}" assert response["status"] == "1" assert response["message"] == "OK" - schema = resolve_getblockreward_schema() + schema = resolve_getblocknobytime_schema() assert :ok = ExJsonSchema.Validator.validate(schema, response) end end @@ -427,4 +434,18 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do } }) end + + defp resolve_getblocknobytime_schema do + ExJsonSchema.Schema.resolve(%{ + "type" => "object", + "properties" => %{ + "message" => %{"type" => "string"}, + "status" => %{"type" => "string"}, + "result" => %{ + "type" => ["string", "null"], + "description" => "Block number as a string or null if not found" + } + } + }) + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 04e2eb956c02..eb44da8ea6fa 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -1054,11 +1054,17 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do |> json_response(200) end - test "get not empty list", %{conn: conn, params: params} do + test "get contract creation info from a transaction", %{conn: conn, params: params} do address_1 = build(:address) address = insert(:contract_address) + {:ok, block_timestamp, _} = DateTime.from_iso8601("2021-05-05T21:42:11.000000Z") + unix_timestamp = DateTime.to_unix(block_timestamp, :second) - transaction = insert(:transaction, created_contract_address: address) + transaction = + insert(:transaction, + created_contract_address: address, + block_timestamp: block_timestamp + ) %{ "status" => "1", @@ -1067,7 +1073,11 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do %{ "contractAddress" => contract_address, "contractCreator" => contract_creator, - "txHash" => transaction_hash + "txHash" => transaction_hash, + "blockNumber" => block_number, + "timestamp" => timestamp, + "contractFactory" => "", + "creationBytecode" => creation_bytecode } ] } = @@ -1078,6 +1088,58 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert contract_address == to_string(address.hash) assert contract_creator == to_string(transaction.from_address_hash) assert transaction_hash == to_string(transaction.hash) + assert block_number == to_string(transaction.block_number) + assert timestamp == to_string(unix_timestamp) + assert creation_bytecode == to_string(transaction.input) + end + + test "get contract creation info via internal transaction", %{conn: conn, params: params} do + {:ok, block_timestamp, _} = DateTime.from_iso8601("2021-05-05T21:42:11.000000Z") + unix_timestamp = DateTime.to_unix(block_timestamp, :second) + + block = insert(:block, timestamp: block_timestamp) + + transaction = + :transaction + |> insert() + |> with_block(block) + + internal_transaction = + insert(:internal_transaction_create, + transaction: transaction, + index: 1, + block_hash: transaction.block_hash, + block_index: transaction.index + ) + + address = internal_transaction.created_contract_address + + %{ + "status" => "1", + "message" => "OK", + "result" => [ + %{ + "contractAddress" => contract_address, + "contractCreator" => contract_creator, + "txHash" => transaction_hash, + "blockNumber" => block_number, + "timestamp" => timestamp, + "contractFactory" => contract_factory, + "creationBytecode" => creation_bytecode + } + ] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", to_string(address))) + |> json_response(200) + + assert contract_address == to_string(internal_transaction.created_contract_address_hash) + assert contract_creator == to_string(internal_transaction.transaction.from_address_hash) + assert transaction_hash == to_string(internal_transaction.transaction.hash) + assert block_number == to_string(internal_transaction.transaction.block_number) + assert timestamp == to_string(unix_timestamp) + assert contract_factory == to_string(internal_transaction.from_address_hash) + assert creation_bytecode == to_string(internal_transaction.init) end end From 7dd5b282a45cfa2d67ed8e166740fd4db30945fd Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 11 Jul 2025 14:54:33 +0400 Subject: [PATCH 0156/1006] chore: Optimize missing block ranges operations (#12705) * chore: Optimize missing block ranges operations * Handle processed missing blocks by batch * Fix warnings --- .../explorer/utility/missing_block_range.ex | 13 +++++------- .../lib/indexer/block/catchup/fetcher.ex | 21 +++++++++++++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/explorer/lib/explorer/utility/missing_block_range.ex b/apps/explorer/lib/explorer/utility/missing_block_range.ex index 976580605cea..8b8cc9e7e6d1 100644 --- a/apps/explorer/lib/explorer/utility/missing_block_range.ex +++ b/apps/explorer/lib/explorer/utility/missing_block_range.ex @@ -408,8 +408,7 @@ defmodule Explorer.Utility.MissingBlockRange do @spec fill_ranges_between(Block.block_number(), Block.block_number(), integer() | nil) :: :ok defp fill_ranges_between(from, to, priority) when from >= to do __MODULE__ - |> where([r], r.from_number <= ^from) - |> where([r], r.to_number >= ^to) + |> where([r], fragment("int4range(?, ?, '[]') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) |> priority_filter(priority) |> Repo.delete_all() @@ -475,8 +474,7 @@ defmodule Explorer.Utility.MissingBlockRange do defp select_all_ranges_within_the_range(from, to) do __MODULE__ - |> where([r], r.from_number <= ^from) - |> where([r], r.to_number >= ^to) + |> where([r], fragment("int4range(?, ?, '[]') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) |> order_by([r], desc: r.from_number) |> Repo.all() end @@ -492,9 +490,8 @@ defmodule Explorer.Utility.MissingBlockRange do # Deletes all missing block ranges that overlap with the interval [from, to] @spec delete_ranges_between(Block.block_number(), Block.block_number()) :: :ok defp delete_ranges_between(from, to) do - from - |> from_number_below_query() - |> to_number_above_query(to) + __MODULE__ + |> where([r], fragment("int4range(?, ?, '()') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) |> Repo.delete_all() end @@ -604,7 +601,7 @@ defmodule Explorer.Utility.MissingBlockRange do """ @spec include_bound_query(Block.block_number()) :: Ecto.Query.t() def include_bound_query(bound) do - from(r in __MODULE__, where: r.from_number >= ^bound, where: r.to_number <= ^bound) + from(r in __MODULE__, where: fragment("int4range(?, ?, '[]') @> ?::int", r.to_number, r.from_number, ^bound)) end defp numbers_to_ranges([]), do: [] diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 3edc286ad9f8..ea730a74cab6 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -157,7 +157,7 @@ defmodule Indexer.Block.Catchup.Fetcher do timeout: :infinity, shutdown: Application.get_env(:indexer, :graceful_shutdown_period) ) - |> Stream.run() + |> handle_fetch_and_import_results() end # Run at state.blocks_concurrency max_concurrency when called by `stream_import/1` @@ -178,11 +178,10 @@ defmodule Indexer.Block.Catchup.Fetcher do Prometheus.Instrumenter.block_full_process(fetch_duration, __MODULE__) case result do - {:ok, %{inserted: inserted, errors: errors}} -> + {:ok, %{errors: errors}} -> valid_errors = handle_null_rounds(errors) - clear_missing_ranges(range, valid_errors) - {:ok, inserted: inserted} + {:ok, %{range: range, errors: valid_errors}} {:error, {:import = step, [%Changeset{} | _] = changesets}} = error -> Prometheus.Instrumenter.import_errors() @@ -224,6 +223,20 @@ defmodule Indexer.Block.Catchup.Fetcher do {:error, exception} end + defp handle_fetch_and_import_results(results) do + results + |> Enum.reduce([], fn + {:ok, {:ok, %{range: range, errors: errors}}}, acc -> + success_numbers = Enum.to_list(range) -- Enum.map(errors, &block_error_to_number/1) + success_numbers ++ acc + + _result, acc -> + acc + end) + |> numbers_to_ranges() + |> MissingRangesManipulator.clear_batch() + end + defp handle_null_rounds(errors) do {null_rounds, other_errors} = Enum.split_with(errors, fn From 4f5ed5fd25bb5a942bb54fa453b173a531b22758 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 11 Jul 2025 15:31:13 +0300 Subject: [PATCH 0157/1006] feat: add token1155tx token404tx api v1 endpoints (#12720) Co-authored-by: BitWonka <79578887+BitWonka@users.noreply.github.com> --- .../controllers/api/rpc/address_controller.ex | 68 +- .../views/api/rpc/address_view.ex | 126 +-- .../api/rpc/address_controller_test.exs | 966 +++++++++++++++++- .../lib/explorer/chain/token_transfer.ex | 44 +- apps/explorer/lib/explorer/etherscan.ex | 301 ++---- .../explorer/test/explorer/etherscan_test.exs | 97 +- 6 files changed, 1182 insertions(+), 420 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index c759be31df18..b4241fbd42d8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -190,31 +190,22 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end def tokentx(conn, params) do - options = optional_params(params) - - with {:address_param, {:ok, address_param}} <- fetch_address(params), - {:format, {:ok, address_hash}} <- to_address_hash(address_param), - {:contract_address, {:ok, contract_address_hash}} <- - {:contract_address, to_address_hash_optional(params["contractaddress"])}, - {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, - {:ok, token_transfers} <- list_token_transfers(address_hash, contract_address_hash, options) do - render(conn, :tokentx, %{token_transfers: token_transfers}) - else - {:address_param, :error} -> - render(conn, :error, error: "Query parameter address is required") + do_tokentx(conn, params, :erc20) + end - {:format, :error} -> - render(conn, :error, error: @invalid_address_message) + def tokennfttx(conn, params) do + do_tokentx(conn, params, :erc721) + end - {:contract_address, :error} -> - render(conn, :error, error: @invalid_contract_address_message) + def token1155tx(conn, params) do + do_tokentx(conn, params, :erc1155) + end - {_, :not_found} -> - render(conn, :error, error: @no_token_transfers_message, data: []) - end + def token404tx(conn, params) do + do_tokentx(conn, params, :erc404) end - def tokennfttx(conn, params) do + defp do_tokentx(conn, params, transfers_type) do options = optional_params(params) with {:address, {:ok, address_hash}} <- {:address, to_address_hash_optional(params["address"])}, @@ -222,8 +213,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:contract_address, to_address_hash_optional(params["contractaddress"])}, true <- !is_nil(address_hash) or !is_nil(contract_address_hash), {:ok, token_transfers, max_block_number} <- - list_nft_transfers(address_hash, contract_address_hash, options) do - render(conn, :tokennfttx, %{token_transfers: token_transfers, max_block_number: max_block_number}) + list_token_transfers(transfers_type, address_hash, contract_address_hash, options) do + render(conn, :tokentx, %{token_transfers: token_transfers, max_block_number: max_block_number}) else false -> render(conn, :error, error: "Query parameter address or contractaddress is required") @@ -543,33 +534,18 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end - defp list_token_transfers(address_hash, contract_address_hash, options) do - case Etherscan.list_token_transfers(address_hash, contract_address_hash, options) do - [] -> {:error, :not_found} - token_transfers -> {:ok, token_transfers} - end - end - - defp list_nft_transfers(nil, contract_address_hash, options) do + defp list_token_transfers(transfers_type, address_hash, contract_address_hash, options) do with {:ok, max_block_number} <- Chain.max_consensus_block_number(), - token_transfers when token_transfers != [] <- - Etherscan.list_nft_transfers_by_token(contract_address_hash, options) do - {:ok, token_transfers, max_block_number} - else - _ -> - {:error, :not_found} - end - end - - defp list_nft_transfers(address_hash, contract_address_hash, options) do - with {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, - {:ok, max_block_number} <- Chain.max_consensus_block_number(), - token_transfers when token_transfers != [] <- - Etherscan.list_nft_transfers(address_hash, contract_address_hash, options) do + [_ | _] = token_transfers <- + Etherscan.list_token_transfers( + transfers_type, + address_hash, + contract_address_hash, + options + ) do {:ok, token_transfers, max_block_number} else - _ -> - {:error, :not_found} + _ -> {:error, :not_found} end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index dc7f3947df19..c2c4cf3ce3bc 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView alias BlockScoutWeb.API.RPC.RPCView - alias Explorer.Chain.DenormalizationHelper + alias Explorer.Chain.{DenormalizationHelper, Transaction} def render("listaccounts.json", %{accounts: accounts}) do accounts = Enum.map(accounts, &prepare_account/1) @@ -39,13 +39,16 @@ defmodule BlockScoutWeb.API.RPC.AddressView do RPCView.render("show.json", data: data) end - def render("tokentx.json", %{token_transfers: token_transfers}) do - data = Enum.map(token_transfers, &prepare_token_transfer/1) - RPCView.render("show.json", data: data) - end + def render("tokentx.json", %{token_transfers: token_transfers, max_block_number: max_block_number}) do + transactions = token_transfers |> Enum.map(& &1.transaction) |> Transaction.decode_transactions(true, api?: true) + + data = + token_transfers + |> Enum.zip(transactions) + |> Enum.map(fn {token_transfer, decoded_input} -> + prepare_token_transfer(token_transfer, max_block_number, decoded_input) + end) - def render("tokennfttx.json", %{token_transfers: token_transfers, max_block_number: max_block_number}) do - data = Enum.map(token_transfers, &prepare_nft_transfer(&1, max_block_number)) RPCView.render("show.json", data: data) end @@ -150,99 +153,78 @@ defmodule BlockScoutWeb.API.RPC.AddressView do } end - defp prepare_common_token_transfer(token_transfer) do + defp prepare_common_token_transfer(token_transfer, max_block_number, decoded_input) do + tt_denormalization_fields = + if DenormalizationHelper.tt_denormalization_finished?() do + %{"timeStamp" => to_string(DateTime.to_unix(token_transfer.transaction.block_timestamp))} + else + %{"timeStamp" => to_string(DateTime.to_unix(token_transfer.block.timestamp))} + end + %{ "blockNumber" => to_string(token_transfer.block_number), - "timeStamp" => to_string(DateTime.to_unix(token_transfer.block_timestamp)), "hash" => to_string(token_transfer.transaction_hash), - "nonce" => to_string(token_transfer.transaction_nonce), + "nonce" => to_string(token_transfer.transaction.nonce), "blockHash" => to_string(token_transfer.block_hash), "from" => to_string(token_transfer.from_address_hash), "contractAddress" => to_string(token_transfer.token_contract_address_hash), "to" => to_string(token_transfer.to_address_hash), - "logIndex" => to_string(token_transfer.token_log_index), - "tokenName" => token_transfer.token_name, - "tokenSymbol" => token_transfer.token_symbol, - "tokenDecimal" => to_string(token_transfer.token_decimals), - "transactionIndex" => to_string(token_transfer.transaction_index), - "gas" => to_string(token_transfer.transaction_gas), - "gasPrice" => to_string(token_transfer.transaction_gas_price && token_transfer.transaction_gas_price.value), - "gasUsed" => to_string(token_transfer.transaction_gas_used), - "cumulativeGasUsed" => to_string(token_transfer.transaction_cumulative_gas_used), - "input" => to_string(token_transfer.transaction_input), - "confirmations" => to_string(token_transfer.confirmations) + "tokenName" => token_transfer.token.name, + "tokenSymbol" => token_transfer.token.symbol, + "tokenDecimal" => to_string(token_transfer.token.decimals), + "transactionIndex" => to_string(token_transfer.transaction.index), + "gas" => to_string(token_transfer.transaction.gas), + "gasPrice" => to_string(token_transfer.transaction.gas_price && token_transfer.transaction.gas_price.value), + "gasUsed" => to_string(token_transfer.transaction.gas_used), + "cumulativeGasUsed" => to_string(token_transfer.transaction.cumulative_gas_used), + "input" => to_string(token_transfer.transaction.input), + "confirmations" => to_string(max_block_number - token_transfer.block_number) } + |> Map.merge(tt_denormalization_fields) + |> Map.merge(prepare_decoded_input(decoded_input)) end - defp prepare_token_transfer(%{token_type: "ERC-721"} = token_transfer) do + defp prepare_token_transfer(%{token_type: "ERC-721"} = token_transfer, max_block_number, decoded_input) do token_transfer - |> prepare_common_token_transfer() + |> prepare_common_token_transfer(max_block_number, decoded_input) |> Map.put_new(:tokenID, List.first(token_transfer.token_ids)) end - # todo: Mark tokenID field as deprecated in release notes, and delete it in the next release - # when tokenID will be deleted, merge this, and next `prepare_token_transfer/1` clauses - defp prepare_token_transfer(%{token_type: "ERC-1155", token_ids: [token_id]} = token_transfer) do + defp prepare_token_transfer(%{token_type: "ERC-1155"} = token_transfer, max_block_number, decoded_input) do token_transfer - |> prepare_common_token_transfer() - |> Map.put_new(:tokenID, token_id) - |> Map.put_new(:tokenIDs, token_transfer.token_ids) - |> Map.put_new(:values, token_transfer.amounts) + |> prepare_common_token_transfer(max_block_number, decoded_input) + |> Map.put_new(:tokenID, token_transfer.token_id) + |> Map.put_new(:tokenValue, token_transfer.amount) end - defp prepare_token_transfer(%{token_type: "ERC-1155"} = token_transfer) do + defp prepare_token_transfer(%{token_type: "ERC-404"} = token_transfer, max_block_number, decoded_input) do token_transfer - |> prepare_common_token_transfer() - |> Map.put_new(:tokenIDs, token_transfer.token_ids) - |> Map.put_new(:values, token_transfer.amounts) + |> prepare_common_token_transfer(max_block_number, decoded_input) + |> Map.put_new(:tokenID, to_string(List.first(token_transfer.token_ids))) + |> Map.put_new(:value, to_string(List.first(token_transfer.amounts))) end - defp prepare_token_transfer(%{token_type: "ERC-404"} = token_transfer) do + defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer, max_block_number, decoded_input) do token_transfer - |> prepare_common_token_transfer() - |> Map.put_new(:tokenIDs, token_transfer.token_ids) - |> Map.put_new(:values, token_transfer.amounts) - end - - defp prepare_token_transfer(%{token_type: "ERC-20"} = token_transfer) do - token_transfer - |> prepare_common_token_transfer() + |> prepare_common_token_transfer(max_block_number, decoded_input) |> Map.put_new(:value, to_string(token_transfer.amount)) end - defp prepare_token_transfer(token_transfer) do - prepare_common_token_transfer(token_transfer) + defp prepare_token_transfer(token_transfer, max_block_number, decoded_input) do + prepare_common_token_transfer(token_transfer, max_block_number, decoded_input) end - defp prepare_nft_transfer(token_transfer, max_block_number) do - timestamp = - if DenormalizationHelper.tt_denormalization_finished?() do - to_string(DateTime.to_unix(token_transfer.transaction.block_timestamp)) - else - to_string(DateTime.to_unix(token_transfer.block.timestamp)) - end + defp prepare_decoded_input({:ok, method_id, text, _mapping}) do + %{ + "methodId" => method_id, + "functionName" => text + } + end + defp prepare_decoded_input(_) do %{ - "blockNumber" => to_string(token_transfer.block_number), - "timeStamp" => timestamp, - "hash" => to_string(token_transfer.transaction_hash), - "nonce" => to_string(token_transfer.transaction.nonce), - "blockHash" => to_string(token_transfer.block_hash), - "from" => to_string(token_transfer.from_address_hash), - "contractAddress" => to_string(token_transfer.token_contract_address_hash), - "to" => to_string(token_transfer.to_address_hash), - "tokenID" => to_string(List.first(token_transfer.token_ids)), - "logIndex" => to_string(token_transfer.log_index), - "tokenName" => token_transfer.token.name, - "tokenSymbol" => token_transfer.token.symbol, - "tokenDecimal" => to_string(token_transfer.token.decimals || 0), - "transactionIndex" => to_string(token_transfer.transaction.index), - "gas" => to_string(token_transfer.transaction.gas), - "gasPrice" => to_string(token_transfer.transaction.gas_price && token_transfer.transaction.gas_price.value), - "gasUsed" => to_string(token_transfer.transaction.gas_used), - "cumulativeGasUsed" => to_string(token_transfer.transaction.cumulative_gas_used), - "input" => "deprecated", - "confirmations" => to_string(max_block_number - token_transfer.block_number) + "methodId" => "", + "functionName" => "" } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index 00cdd01a44f6..b91708bf716b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do alias BlockScoutWeb.API.RPC.AddressController alias Explorer.Chain alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.{Events.Subscriber, Transaction, Wei} + alias Explorer.Chain.{Events.Subscriber, Transaction, Wei, Repo} alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Explorer.Repo @@ -2851,41 +2851,63 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) end - test "has correct value for ERC-721", %{conn: conn} do + test "returns only ERC-20 transfers", %{conn: conn} do transaction = :transaction |> insert() |> with_block() - token_address = insert(:contract_address) - insert(:token, %{contract_address: token_address, type: "ERC-721"}) + erc721_token = insert(:token, %{type: "ERC-721"}) - token_transfer = + erc721_token_transfer = insert(:token_transfer, %{ - token_contract_address: token_address, + token_contract_address: erc721_token.contract_address, token_ids: [666], + token_type: "ERC-721", transaction: transaction, block: transaction.block, block_number: transaction.block_number }) - {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + erc1155_token = insert(:token, %{type: "ERC-1155"}) + + erc1155_token_transfer = + insert(:token_transfer, %{ + token_contract_address: erc1155_token.contract_address, + token_ids: [666], + amounts: [1], + token_type: "ERC-1155", + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) params = %{ "module" => "account", "action" => "tokentx", - "address" => to_string(token_transfer.from_address.hash) + "address" => to_string(erc721_token_transfer.from_address.hash) } assert response = - %{"result" => [result]} = + %{"message" => "No token transfers found", "result" => [], "status" => "0"} = + conn + |> get("/api", params) + |> json_response(200) + + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + + params = %{ + "module" => "account", + "action" => "tokentx", + "address" => to_string(erc1155_token_transfer.from_address.hash) + } + + assert response = + %{"message" => "No token transfers found", "result" => [], "status" => "0"} = conn |> get("/api", params) |> json_response(200) - assert result["tokenID"] == to_string(List.first(token_transfer.token_ids)) - assert response["status"] == "1" - assert response["message"] == "OK" assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) end @@ -2926,9 +2948,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "gasPrice" => to_string(transaction.gas_price.value), "gasUsed" => to_string(transaction.gas_used), "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), - "logIndex" => to_string(token_transfer.log_index), "input" => to_string(transaction.input), - "confirmations" => "0" + "confirmations" => "0", + "functionName" => "", + "methodId" => "" } ] @@ -3103,6 +3126,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do insert(:token_transfer, %{ token_contract_address: token_address, token_ids: [666], + token_type: "ERC-721", transaction: transaction, block: transaction.block, block_number: transaction.block_number @@ -3140,6 +3164,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do transaction: transaction, block_number: block.number, token_ids: [1010], + token_type: token.type, token_contract_address: token_address ) @@ -3163,10 +3188,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "gasPrice" => to_string(transaction.gas_price.value), "gasUsed" => to_string(transaction.gas_used), "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), - "logIndex" => to_string(token_transfer.log_index), - "input" => "deprecated", + "input" => to_string(transaction.input), "confirmations" => "0", - "tokenID" => "1010" + "tokenID" => "1010", + "functionName" => "", + "methodId" => "" } ] @@ -3321,6 +3347,912 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do end end + describe "token1155tx" do + setup do + %{params: %{"module" => "account", "action" => "token1155tx"}} + end + + test "API endpoint works after `transactions` table denormalization finished", %{conn: conn, params: params} do + old_tt_denormalization_finished = BackgroundMigrations.get_tt_denormalization_finished() + BackgroundMigrations.set_tt_denormalization_finished(true) + + on_exit(fn -> + BackgroundMigrations.set_tt_denormalization_finished(old_tt_denormalization_finished) + end) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + token = insert(:token, name: "NFT", type: "ERC-1155") + + insert(:token_transfer, + transaction: transaction, + from_address: address, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_type: token.type, + token_ids: [10, 20, 30], + amounts: [100, 200, 300] + ) + + new_params = + params + |> Map.put("address", Explorer.Chain.Hash.to_string(address.hash)) + + response = + conn + |> get("/api", new_params) + + assert response.status == 200 + end + + test "with missing address and contract address hash", %{conn: conn, params: params} do + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "Query parameter address or contractaddress is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid address hash", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "badhash"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No token transfers found" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "has correct value for single ERC-1155", %{conn: conn, params: params} do + transaction = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + insert(:token, %{contract_address: token_address, type: "ERC-1155"}) + + token_transfer = + insert(:token_transfer, %{ + token_contract_address: token_address, + token_ids: [666], + token_type: "ERC-1155", + amount: 1, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) + + {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + assert response = + %{"result" => results} = + conn + |> get("/api", params) + |> json_response(200) + + assert length(results) == 1 + + [result] = results + + assert result["tokenID"] == to_string(List.first(token_transfer.token_ids)) + assert result["tokenValue"] == to_string(token_transfer.amount) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "has correct value for multiple ERC-1155", %{conn: conn, params: params} do + transaction = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + insert(:token, %{contract_address: token_address, type: "ERC-1155"}) + + token_transfer = + insert(:token_transfer, %{ + token_contract_address: token_address, + token_ids: [669, 999, 1337], + amounts: [1, 2, 10], + token_type: "ERC-1155", + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + }) + + {:ok, _} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + assert response = + %{"result" => results} = + conn + |> get("/api", params) + |> json_response(200) + + assert length(results) == 3 + + [result1, result2, result3] = results + assert result1["tokenID"] == to_string(Enum.at(token_transfer.token_ids, 2)) + assert result1["tokenValue"] == to_string(Enum.at(token_transfer.amounts, 2)) + + assert result2["tokenID"] == to_string(Enum.at(token_transfer.token_ids, 1)) + assert result2["tokenValue"] == to_string(Enum.at(token_transfer.amounts, 1)) + + assert result3["tokenID"] == to_string(Enum.at(token_transfer.token_ids, 0)) + assert result3["tokenValue"] == to_string(Enum.at(token_transfer.amounts, 0)) + + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "returns all the required fields", %{conn: conn, params: params} do + transaction = + %{block: block} = + :transaction + |> insert() + |> with_block() + + token_address = insert(:contract_address) + token = insert(:token, contract_address: token_address, type: "ERC-1155") + + token_transfer = + insert(:token_transfer, + block: transaction.block, + token_type: token.type, + transaction: transaction, + block_number: block.number, + token_ids: [1010], + token_contract_address: token_address, + amount: 5 + ) + + params = Map.merge(params, %{"address" => to_string(token_transfer.from_address.hash)}) + + expected_result = [ + %{ + "blockNumber" => to_string(transaction.block_number), + "timeStamp" => to_string(DateTime.to_unix(block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(transaction.nonce), + "blockHash" => to_string(block.hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "tokenName" => token.name, + "tokenSymbol" => token.symbol, + "tokenDecimal" => to_string(token.decimals), + "transactionIndex" => to_string(transaction.index), + "gas" => to_string(transaction.gas), + "gasPrice" => to_string(transaction.gas_price.value), + "gasUsed" => to_string(transaction.gas_used), + "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), + "input" => to_string(transaction.input), + "confirmations" => "0", + "tokenID" => "1010", + "tokenValue" => "5", + "functionName" => "", + "methodId" => "" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid contract address", %{conn: conn, params: params} do + params = + Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", "contractaddress" => "invalid"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "filters results by contract address", %{conn: conn, params: params} do + address = insert(:address) + + contract_address = insert(:contract_address) + + insert(:token, contract_address: contract_address, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number + ) + + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [123], + amount: 1 + ) + + params = + Map.merge(params, %{"address" => to_string(address.hash), "contractaddress" => to_string(contract_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["contractAddress"] == to_string(contract_address.hash) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "Check pagination and ordering (page, offset, sort parameters)", %{conn: conn, params: params} do + address = insert(:address) + + erc_1155_token = insert(:token, type: "ERC-1155") + + erc_1155_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_type: erc_1155_token.type, + token_contract_address: erc_1155_token.contract_address, + token_ids: [x, x + 100], + amounts: [1, 2] + ) + end + + # sort: asc + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 1, "sort" => "asc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 2, "sort" => "asc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 3, "sort" => "asc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_1155_tt, 0).transaction_hash) + assert Enum.at(token_transfers_1, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 0).token_ids, 0)) + assert Enum.at(token_transfers_1, 0)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 0).amounts, 0)) + assert Enum.at(token_transfers_1, 1)["hash"] == to_string(Enum.at(erc_1155_tt, 0).transaction_hash) + assert Enum.at(token_transfers_1, 1)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 0).token_ids, 1)) + assert Enum.at(token_transfers_1, 1)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 0).amounts, 1)) + + assert Enum.at(token_transfers_1, 48)["hash"] == to_string(Enum.at(erc_1155_tt, 24).transaction_hash) + assert Enum.at(token_transfers_1, 48)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 24).token_ids, 0)) + assert Enum.at(token_transfers_1, 48)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 24).amounts, 0)) + assert Enum.at(token_transfers_1, 49)["hash"] == to_string(Enum.at(erc_1155_tt, 24).transaction_hash) + assert Enum.at(token_transfers_1, 49)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 24).token_ids, 1)) + assert Enum.at(token_transfers_1, 49)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 24).amounts, 1)) + + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_1155_tt, 25).transaction_hash) + assert Enum.at(token_transfers_2, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 25).token_ids, 0)) + assert Enum.at(token_transfers_2, 0)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 25).amounts, 0)) + assert Enum.at(token_transfers_2, 1)["hash"] == to_string(Enum.at(erc_1155_tt, 25).transaction_hash) + assert Enum.at(token_transfers_2, 1)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 25).token_ids, 1)) + assert Enum.at(token_transfers_2, 1)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 25).amounts, 1)) + + assert Enum.at(token_transfers_2, 48)["hash"] == to_string(Enum.at(erc_1155_tt, 49).transaction_hash) + assert Enum.at(token_transfers_2, 48)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 49).token_ids, 0)) + assert Enum.at(token_transfers_2, 48)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 49).amounts, 0)) + assert Enum.at(token_transfers_2, 49)["hash"] == to_string(Enum.at(erc_1155_tt, 49).transaction_hash) + assert Enum.at(token_transfers_2, 49)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 49).token_ids, 1)) + assert Enum.at(token_transfers_2, 49)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 49).amounts, 1)) + + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_1155_tt, 50).transaction_hash) + assert Enum.at(token_transfers_3, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 50).token_ids, 0)) + assert Enum.at(token_transfers_3, 0)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 50).amounts, 0)) + assert Enum.at(token_transfers_3, 1)["hash"] == to_string(Enum.at(erc_1155_tt, 50).transaction_hash) + assert Enum.at(token_transfers_3, 1)["tokenID"] == to_string(Enum.at(Enum.at(erc_1155_tt, 50).token_ids, 1)) + assert Enum.at(token_transfers_3, 1)["tokenValue"] == to_string(Enum.at(Enum.at(erc_1155_tt, 50).amounts, 1)) + + assert Enum.count(token_transfers_3) == 2 + + # sort: desc + erc_1155_tt_reversed = Enum.reverse(erc_1155_tt) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 1, "sort" => "desc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 2, "sort" => "desc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 3, "sort" => "desc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 0).transaction_hash) + + assert Enum.at(token_transfers_1, 0)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 0).token_ids, 1)) + + assert Enum.at(token_transfers_1, 0)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 0).amounts, 1)) + + assert Enum.at(token_transfers_1, 1)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 0).transaction_hash) + + assert Enum.at(token_transfers_1, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 0).token_ids, 0)) + + assert Enum.at(token_transfers_1, 1)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 0).amounts, 0)) + + assert Enum.at(token_transfers_1, 48)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 24).transaction_hash) + + assert Enum.at(token_transfers_1, 48)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 24).token_ids, 1)) + + assert Enum.at(token_transfers_1, 48)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 24).amounts, 1)) + + assert Enum.at(token_transfers_1, 49)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 24).transaction_hash) + + assert Enum.at(token_transfers_1, 49)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 24).token_ids, 0)) + + assert Enum.at(token_transfers_1, 49)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 24).amounts, 0)) + + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 25).transaction_hash) + + assert Enum.at(token_transfers_2, 0)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 25).token_ids, 1)) + + assert Enum.at(token_transfers_2, 0)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 25).amounts, 1)) + + assert Enum.at(token_transfers_2, 1)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 25).transaction_hash) + + assert Enum.at(token_transfers_2, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 25).token_ids, 0)) + + assert Enum.at(token_transfers_2, 1)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 25).amounts, 0)) + + assert Enum.at(token_transfers_2, 48)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 49).transaction_hash) + + assert Enum.at(token_transfers_2, 48)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 49).token_ids, 1)) + + assert Enum.at(token_transfers_2, 48)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 49).amounts, 1)) + + assert Enum.at(token_transfers_2, 49)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 49).transaction_hash) + + assert Enum.at(token_transfers_2, 49)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 49).token_ids, 0)) + + assert Enum.at(token_transfers_2, 49)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 49).amounts, 0)) + + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 50).transaction_hash) + + assert Enum.at(token_transfers_3, 0)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 50).token_ids, 1)) + + assert Enum.at(token_transfers_3, 0)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 50).amounts, 1)) + + assert Enum.at(token_transfers_3, 1)["hash"] == to_string(Enum.at(erc_1155_tt_reversed, 50).transaction_hash) + + assert Enum.at(token_transfers_3, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 50).token_ids, 0)) + + assert Enum.at(token_transfers_3, 1)["tokenValue"] == + to_string(Enum.at(Enum.at(erc_1155_tt_reversed, 50).amounts, 0)) + + assert Enum.count(token_transfers_3) == 2 + end + end + + describe "token404tx" do + setup do + %{params: %{"module" => "account", "action" => "token404tx"}} + end + + test "works before denormalization finished and after", %{conn: conn, params: params} do + old_tt_denormalization_finished = BackgroundMigrations.get_tt_denormalization_finished() + + on_exit(fn -> + BackgroundMigrations.set_tt_denormalization_finished(old_tt_denormalization_finished) + end) + + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + token = insert(:token, name: "NFT", type: "ERC-404") + + insert(:token_transfer, + transaction: transaction, + token_type: token.type, + from_address: address, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_type: token.type, + token_ids: [10], + amounts: [] + ) + + insert(:token_transfer, + transaction: transaction, + token_type: token.type, + from_address: address, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_type: token.type, + token_ids: [], + amounts: [20] + ) + + new_params = + params + |> Map.put("address", Explorer.Chain.Hash.to_string(address.hash)) + + BackgroundMigrations.set_tt_denormalization_finished(false) + + response = + conn + |> get("/api", new_params) + + assert response.status == 200 + + BackgroundMigrations.set_tt_denormalization_finished(true) + + response = + conn + |> get("/api", new_params) + + assert response.status == 200 + end + + test "with missing address and contract address hash", %{conn: conn, params: params} do + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] == "Query parameter address or contractaddress is required" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid address hash", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "badhash"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an address that doesn't exist", %{conn: conn, params: params} do + params = Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == [] + assert response["status"] == "0" + assert response["message"] == "No token transfers found" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "with an invalid contract address", %{conn: conn, params: params} do + params = + Map.merge(params, %{"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", "contractaddress" => "invalid"}) + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["message"] =~ "Invalid contract address format" + assert response["status"] == "0" + assert Map.has_key?(response, "result") + refute response["result"] + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "filters results by contract address", %{conn: conn, params: params} do + address = insert(:address) + + contract_address = insert(:contract_address) + + insert(:token, contract_address: contract_address, type: "ERC-404") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + from_address: address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_type: "ERC-404", + token_ids: [1], + amounts: [] + ) + + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [], + amounts: [1], + token_type: "ERC-404" + ) + + params = + Map.merge(params, %{"address" => to_string(address.hash), "contractaddress" => to_string(contract_address.hash)}) + + assert response = + %{"result" => [result]} = + conn + |> get("/api", params) + |> json_response(200) + + assert result["contractAddress"] == to_string(contract_address.hash) + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "returns all the required fields", %{conn: conn, params: params} do + address = insert(:address) + + contract_address = insert(:contract_address) + + token = insert(:token, contract_address: contract_address, type: "ERC-404") + + transaction = + :transaction + |> insert() + |> with_block() + + token_transfer = + insert(:token_transfer, + from_address: address, + token_contract_address: contract_address, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_ids: [], + amounts: [1], + token_type: token.type + ) + + params = + Map.merge(params, %{"address" => to_string(address.hash), "contractaddress" => to_string(contract_address.hash)}) + + expected_result = [ + %{ + "blockNumber" => to_string(transaction.block_number), + "timeStamp" => to_string(DateTime.to_unix(transaction.block.timestamp)), + "hash" => to_string(token_transfer.transaction_hash), + "nonce" => to_string(transaction.nonce), + "blockHash" => to_string(transaction.block.hash), + "from" => to_string(token_transfer.from_address_hash), + "contractAddress" => to_string(token_transfer.token_contract_address_hash), + "to" => to_string(token_transfer.to_address_hash), + "tokenName" => token.name, + "tokenSymbol" => token.symbol, + "tokenDecimal" => to_string(token.decimals), + "transactionIndex" => to_string(transaction.index), + "gas" => to_string(transaction.gas), + "gasPrice" => to_string(transaction.gas_price.value), + "gasUsed" => to_string(transaction.gas_used), + "cumulativeGasUsed" => to_string(transaction.cumulative_gas_used), + "input" => to_string(transaction.input), + "confirmations" => "0", + "tokenID" => "", + "value" => "1", + "functionName" => "", + "methodId" => "" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(tokentx_schema(), response) + end + + test "check pagination and ordering (page, offset, sort parameters)", %{conn: conn, params: params} do + address = insert(:address) + + erc_404_token = insert(:token, type: "ERC-404") + + erc_404_tt = + for x <- 0..50 do + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + [ + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_type: erc_404_token.type, + token_contract_address: erc_404_token.contract_address, + token_ids: [x], + amounts: [] + ), + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_type: erc_404_token.type, + token_contract_address: erc_404_token.contract_address, + token_ids: [], + amounts: [x + 100] + ) + ] + end + |> List.flatten() + + # sort: asc + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 1, "sort" => "asc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 2, "sort" => "asc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 3, "sort" => "asc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_404_tt, 0).transaction_hash) + assert Enum.at(token_transfers_1, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_404_tt, 0).token_ids, 0)) + assert Enum.at(token_transfers_1, 0)["value"] == "" + assert Enum.at(token_transfers_1, 1)["hash"] == to_string(Enum.at(erc_404_tt, 1).transaction_hash) + assert Enum.at(token_transfers_1, 1)["tokenID"] == "" + assert Enum.at(token_transfers_1, 1)["value"] == to_string(Enum.at(Enum.at(erc_404_tt, 1).amounts, 0)) + + assert Enum.at(token_transfers_1, 48)["hash"] == to_string(Enum.at(erc_404_tt, 48).transaction_hash) + assert Enum.at(token_transfers_1, 48)["tokenID"] == to_string(Enum.at(Enum.at(erc_404_tt, 48).token_ids, 0)) + assert Enum.at(token_transfers_1, 48)["value"] == "" + assert Enum.at(token_transfers_1, 49)["hash"] == to_string(Enum.at(erc_404_tt, 49).transaction_hash) + assert Enum.at(token_transfers_1, 49)["tokenID"] == "" + assert Enum.at(token_transfers_1, 49)["value"] == to_string(Enum.at(Enum.at(erc_404_tt, 49).amounts, 0)) + + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_404_tt, 50).transaction_hash) + assert Enum.at(token_transfers_2, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_404_tt, 50).token_ids, 0)) + assert Enum.at(token_transfers_2, 0)["value"] == "" + assert Enum.at(token_transfers_2, 1)["hash"] == to_string(Enum.at(erc_404_tt, 51).transaction_hash) + assert Enum.at(token_transfers_2, 1)["tokenID"] == "" + assert Enum.at(token_transfers_2, 1)["value"] == to_string(Enum.at(Enum.at(erc_404_tt, 51).amounts, 0)) + + assert Enum.at(token_transfers_2, 48)["hash"] == to_string(Enum.at(erc_404_tt, 98).transaction_hash) + assert Enum.at(token_transfers_2, 48)["tokenID"] == to_string(Enum.at(Enum.at(erc_404_tt, 98).token_ids, 0)) + assert Enum.at(token_transfers_2, 48)["value"] == "" + assert Enum.at(token_transfers_2, 49)["hash"] == to_string(Enum.at(erc_404_tt, 99).transaction_hash) + assert Enum.at(token_transfers_2, 49)["tokenID"] == "" + assert Enum.at(token_transfers_2, 49)["value"] == to_string(Enum.at(Enum.at(erc_404_tt, 99).amounts, 0)) + + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_404_tt, 100).transaction_hash) + assert Enum.at(token_transfers_3, 0)["tokenID"] == to_string(Enum.at(Enum.at(erc_404_tt, 100).token_ids, 0)) + assert Enum.at(token_transfers_3, 0)["value"] == "" + assert Enum.at(token_transfers_3, 1)["hash"] == to_string(Enum.at(erc_404_tt, 101).transaction_hash) + assert Enum.at(token_transfers_3, 1)["tokenID"] == "" + assert Enum.at(token_transfers_3, 1)["value"] == to_string(Enum.at(Enum.at(erc_404_tt, 101).amounts, 0)) + + assert Enum.count(token_transfers_3) == 2 + + # sort: desc + erc_404_tt_reversed = Enum.reverse(erc_404_tt) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 1, "sort" => "desc"}) + + assert %{"result" => token_transfers_1} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 2, "sort" => "desc"}) + + assert %{"result" => token_transfers_2} = + conn + |> get("/api", params) + |> json_response(200) + + params = Map.merge(params, %{"address" => to_string(address.hash), "offset" => 50, "page" => 3, "sort" => "desc"}) + + assert %{"result" => token_transfers_3} = + conn + |> get("/api", params) + |> json_response(200) + + assert Enum.at(token_transfers_1, 0)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 0).transaction_hash) + + assert Enum.at(token_transfers_1, 0)["tokenID"] == "" + + assert Enum.at(token_transfers_1, 0)["value"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 0).amounts, 0)) + + assert Enum.at(token_transfers_1, 1)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 1).transaction_hash) + + assert Enum.at(token_transfers_1, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 1).token_ids, 0)) + + assert Enum.at(token_transfers_1, 1)["value"] == "" + + assert Enum.at(token_transfers_1, 48)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 48).transaction_hash) + + assert Enum.at(token_transfers_1, 48)["tokenID"] == "" + + assert Enum.at(token_transfers_1, 48)["value"] == to_string(Enum.at(Enum.at(erc_404_tt_reversed, 48).amounts, 0)) + + assert Enum.at(token_transfers_1, 49)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 49).transaction_hash) + + assert Enum.at(token_transfers_1, 49)["tokenID"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 49).token_ids, 0)) + + assert Enum.at(token_transfers_1, 49)["value"] == "" + + assert Enum.at(token_transfers_2, 0)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 50).transaction_hash) + + assert Enum.at(token_transfers_2, 0)["tokenID"] == "" + + assert Enum.at(token_transfers_2, 0)["value"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 50).amounts, 0)) + + assert Enum.at(token_transfers_2, 1)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 51).transaction_hash) + + assert Enum.at(token_transfers_2, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 51).token_ids, 0)) + + assert Enum.at(token_transfers_2, 1)["value"] == "" + + assert Enum.at(token_transfers_2, 48)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 98).transaction_hash) + + assert Enum.at(token_transfers_2, 48)["tokenID"] == "" + + assert Enum.at(token_transfers_2, 48)["value"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 98).amounts, 0)) + + assert Enum.at(token_transfers_2, 49)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 99).transaction_hash) + + assert Enum.at(token_transfers_2, 49)["tokenID"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 99).token_ids, 0)) + + assert Enum.at(token_transfers_2, 49)["value"] == "" + + assert Enum.at(token_transfers_3, 0)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 100).transaction_hash) + + assert Enum.at(token_transfers_3, 0)["tokenID"] == "" + + assert Enum.at(token_transfers_3, 0)["value"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 100).amounts, 0)) + + assert Enum.at(token_transfers_3, 1)["hash"] == to_string(Enum.at(erc_404_tt_reversed, 101).transaction_hash) + + assert Enum.at(token_transfers_3, 1)["tokenID"] == + to_string(Enum.at(Enum.at(erc_404_tt_reversed, 101).token_ids, 0)) + + assert Enum.at(token_transfers_3, 1)["value"] == "" + assert(Enum.count(token_transfers_3) == 2) + end + end + describe "tokenbalance" do test "without required params", %{conn: conn} do params = %{ diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 36c8876a787f..e06d8c8bf2eb 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -320,7 +320,38 @@ defmodule Explorer.Chain.TokenTransfer do end end - defp maybe_filter_by_token_type(query, token_types) do + @doc """ + Conditionally filters token transfers by token type based on denormalization status. + + This function applies token type filtering to the query using either the + denormalized `token_type` field or by joining with the tokens table, + depending on whether the token transfer denormalization process has been + completed. When denormalization is finished, it filters directly on + `tt.token_type`. Otherwise, it joins with the associated token and filters + on `token.type`. + + ## Parameters + - `query`: An Ecto query for token transfers + - `token_type`: Either a binary token type (e.g., "ERC-20") or a list of + token types to filter by + + ## Returns + - The modified query with token type filtering applied + - For empty token type lists, returns the original query unchanged + """ + @spec maybe_filter_by_token_type(Ecto.Query.t(), binary() | [binary()]) :: Ecto.Query.t() + def maybe_filter_by_token_type(query, token_type) when is_binary(token_type) do + if DenormalizationHelper.tt_denormalization_finished?() do + query + |> where([tt], tt.token_type == ^token_type) + else + query + |> join(:inner, [tt], token in assoc(tt, :token), as: :token) + |> where([tt, block, token], token.type == ^token_type) + end + end + + def maybe_filter_by_token_type(query, token_types) do if Enum.empty?(token_types) do query else @@ -675,17 +706,6 @@ defmodule Explorer.Chain.TokenTransfer do end end - @doc """ - Returns ecto query to fetch consensus token transfers with ERC-721 token type - """ - @spec erc_721_token_transfers_query() :: Ecto.Query.t() - def erc_721_token_transfers_query do - only_consensus_transfers_query() - |> join(:inner, [tt], token in assoc(tt, :token), as: :token) - |> where([tt, token: token], token.type == "ERC-721") - |> preload([tt, token: token], [{:token, token}]) - end - @doc """ Fetches token transfers from logs. """ diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 6ca55ae3f3da..627523c69a68 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -3,19 +3,7 @@ defmodule Explorer.Etherscan do The etherscan context. """ - import Ecto.Query, - only: [ - from: 2, - where: 3, - union: 2, - subquery: 1, - order_by: 3, - limit: 2, - offset: 2, - preload: 2, - preload: 3, - select_merge: 3 - ] + import Ecto.Query import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @@ -295,76 +283,55 @@ defmodule Explorer.Etherscan do end @doc """ - Gets a list of token transfers for a given `t:Explorer.Chain.Hash.Address.t/0`. - + Retrieves token transfers filtered by token standard type with optional address and contract filtering. + + This function queries token transfers based on the specified token standard + (ERC-20, ERC-721, ERC-1155, or ERC-404) and applies optional filtering by + address and contract address. The function merges provided options with + default settings for pagination, ordering, and block range filtering. + + For ERC-1155 transfers, the function performs additional processing to unnest + arrays of token IDs and amounts into individual transfer records, with each + record containing the specific token ID, amount, and index within the batch. + + ## Parameters + - `token_transfers_type`: The token standard type (`:erc20`, `:erc721`, + `:erc1155`, or `:erc404`) + - `address_hash`: Optional address hash to filter transfers involving this + address as sender or recipient (filters by `from_address_hash` or + `to_address_hash`) + - `contract_address_hash`: Optional contract address hash to filter transfers + for a specific token contract + - `options`: Map of query options that gets merged with default options + including pagination (`page_number`, `page_size`), ordering + (`order_by_direction`), and block range filtering (`startblock`, `endblock`) + + ## Returns + - A list of `TokenTransfer` structs matching the specified criteria + - For ERC-1155 transfers, each struct includes unnested `token_id`, `amount`, + and `index_in_batch` fields """ - @spec list_token_transfers(Hash.Address.t(), Hash.Address.t() | nil, map()) :: [map()] - def list_token_transfers( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, - contract_address_hash, - options \\ @default_options - ) do - case Chain.max_consensus_block_number() do - {:ok, block_height} -> - merged_options = Map.merge(@default_options, options) - list_token_transfers(address_hash, contract_address_hash, block_height, merged_options) + @spec list_token_transfers( + :erc20 | :erc721 | :erc1155 | :erc404, + Hash.Address.t() | nil, + Hash.Address.t() | nil, + map() + ) :: [TokenTransfer.t()] + def list_token_transfers(token_transfers_type, address_hash, contract_address_hash, options) do + options = Map.merge(@default_options, options) - _ -> - [] - end - end + case token_transfers_type do + :erc20 -> + list_erc20_token_transfers(address_hash, contract_address_hash, options) - @doc """ - Gets a list of ERC-721 token transfers for a given address_hash. If contract_address_hash is not nil, transfers will be filtered by contract. - """ - @spec list_nft_transfers(Hash.Address.t(), Hash.Address.t() | nil, map()) :: [TokenTransfer.t()] - def list_nft_transfers( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, - contract_address_hash, - options \\ @default_options - ) do - options - |> base_nft_transfers_query(contract_address_hash) - |> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) - |> Repo.replica().all() - end + :erc721 -> + list_nft_transfers(address_hash, contract_address_hash, options) - @doc """ - Gets a list of ERC-721 token transfers for a given token contract_address_hash. - """ - @spec list_nft_transfers_by_token(Hash.Address.t(), map()) :: [TokenTransfer.t()] - def list_nft_transfers_by_token( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = contract_address_hash, - options \\ @default_options - ) do - options - |> base_nft_transfers_query(contract_address_hash) - |> Repo.replica().all() - end + :erc1155 -> + list_erc1155_token_transfers(address_hash, contract_address_hash, options) - defp base_nft_transfers_query(options, contract_address_hash) do - options = Map.merge(@default_options, options) - - TokenTransfer.erc_721_token_transfers_query() - |> where_contract_address_match(contract_address_hash) - |> order_by([tt], [ - {^options.order_by_direction, tt.block_number}, - {^options.order_by_direction, tt.log_index} - ]) - |> where_start_block_match_tt(options) - |> where_end_block_match_tt(options) - |> limit(^options.page_size) - |> offset(^offset(options)) - |> maybe_preload_block() - end - - defp maybe_preload_block(query) do - if DenormalizationHelper.tt_denormalization_finished?() do - query - |> preload(:transaction) - else - query - |> preload([block: block], [{:block, block}, :transaction]) + :erc404 -> + list_erc404_token_transfers(address_hash, contract_address_hash, options) end end @@ -561,124 +528,70 @@ defmodule Explorer.Etherscan do ) end - @token_transfer_fields ~w( - block_number - block_hash - block_consensus - token_contract_address_hash - transaction_hash - from_address_hash - to_address_hash - amount - amounts - )a + defp list_erc20_token_transfers(address_hash, contract_address_hash, options) do + "ERC-20" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + end - defp list_token_transfers(address_hash, contract_address_hash, block_height, options) do - tt_query = - from( - tt in TokenTransfer, - inner_join: tkn in assoc(tt, :token), - where: tt.from_address_hash == ^address_hash, - or_where: tt.to_address_hash == ^address_hash, - order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.log_index}], - limit: ^options.page_size, - offset: ^offset(options), - select: - merge(map(tt, ^@token_transfer_fields), %{ - token_ids: tt.token_ids, - token_name: tkn.name, - token_symbol: tkn.symbol, - token_decimals: tkn.decimals, - token_log_index: tt.log_index - }) - ) + defp list_nft_transfers(address_hash, contract_address_hash, options) do + "ERC-721" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + end - tt_query_with_token_type = - if DenormalizationHelper.tt_denormalization_finished?() do - select_merge(tt_query, [tt, _tkn], %{token_type: tt.token_type}) - else - select_merge(tt_query, [_tt, tkn], %{token_type: tkn.type}) - end + defp list_erc1155_token_transfers(address_hash, contract_address_hash, options) do + "ERC-1155" + |> base_token_transfers_query(address_hash, contract_address_hash, options) + |> join( + :inner, + [token_transfer], + unnest in fragment( + "LATERAL (SELECT unnest(?) AS token_id, unnest(COALESCE(?, ARRAY[?])) AS amount, GENERATE_SERIES(0, COALESCE(ARRAY_LENGTH(?, 1), 0) - 1) as index_in_batch)", + token_transfer.token_ids, + token_transfer.amounts, + token_transfer.amount, + token_transfer.amounts + ), + as: :unnest, + on: true + ) + |> select_merge([unnest: unnest], %{ + token_id: fragment("?::numeric", unnest.token_id), + amount: fragment("?::numeric", unnest.amount), + index_in_batch: fragment("?::integer", unnest.index_in_batch) + }) + |> order_by( + [unnest: unnest], + {^options.order_by_direction, unnest.index_in_batch} + ) + |> Repo.all() + end - tt_specific_token_query = - tt_query_with_token_type - |> where_start_block_match_tt(options) - |> where_end_block_match_tt(options) - |> where_contract_address_match(contract_address_hash) + defp list_erc404_token_transfers(address_hash, contract_address_hash, options) do + "ERC-404" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + end - wrapped_query = - if DenormalizationHelper.transactions_denormalization_finished?() do - from( - tt in subquery(tt_specific_token_query), - inner_join: t in Transaction, - on: - tt.transaction_hash == t.hash and tt.block_number == t.block_number and tt.block_hash == t.block_hash and - t.block_consensus == true, - order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.token_log_index}], - select: %{ - token_contract_address_hash: tt.token_contract_address_hash, - transaction_hash: tt.transaction_hash, - from_address_hash: tt.from_address_hash, - to_address_hash: tt.to_address_hash, - amount: tt.amount, - amounts: tt.amounts, - transaction_nonce: t.nonce, - transaction_index: t.index, - transaction_gas: t.gas, - transaction_gas_price: t.gas_price, - transaction_gas_used: t.gas_used, - transaction_cumulative_gas_used: t.cumulative_gas_used, - transaction_input: t.input, - block_hash: t.block_hash, - block_number: t.block_number, - block_timestamp: t.block_timestamp, - confirmations: fragment("? - ?", ^block_height, t.block_number), - token_ids: tt.token_ids, - token_name: tt.token_name, - token_symbol: tt.token_symbol, - token_decimals: tt.token_decimals, - token_type: tt.token_type, - token_log_index: tt.token_log_index - } - ) - else - from( - tt in subquery(tt_specific_token_query), - inner_join: t in Transaction, - on: tt.transaction_hash == t.hash and tt.block_number == t.block_number and tt.block_hash == t.block_hash, - inner_join: b in assoc(t, :block), - where: b.consensus == true, - order_by: [{^options.order_by_direction, tt.block_number}, {^options.order_by_direction, tt.token_log_index}], - select: %{ - token_contract_address_hash: tt.token_contract_address_hash, - transaction_hash: tt.transaction_hash, - from_address_hash: tt.from_address_hash, - to_address_hash: tt.to_address_hash, - amount: tt.amount, - amounts: tt.amounts, - transaction_nonce: t.nonce, - transaction_index: t.index, - transaction_gas: t.gas, - transaction_gas_price: t.gas_price, - transaction_gas_used: t.gas_used, - transaction_cumulative_gas_used: t.cumulative_gas_used, - transaction_input: t.input, - block_hash: b.hash, - block_number: b.number, - block_timestamp: b.timestamp, - confirmations: fragment("? - ?", ^block_height, t.block_number), - token_ids: tt.token_ids, - token_name: tt.token_name, - token_symbol: tt.token_symbol, - token_decimals: tt.token_decimals, - token_type: tt.token_type, - token_log_index: tt.token_log_index - } - ) - end + defp base_token_transfers_query(transfers_type, address_hash, contract_address_hash, options) do + TokenTransfer.only_consensus_transfers_query() + |> TokenTransfer.maybe_filter_by_token_type(transfers_type) + |> where_contract_address_match(contract_address_hash) + |> where_address_match_token_transfer(address_hash) + |> order_by([tt], [ + {^options.order_by_direction, tt.block_number}, + {^options.order_by_direction, tt.log_index} + ]) + |> where_start_block_match_tt(options) + |> where_end_block_match_tt(options) + |> limit(^options.page_size) + |> offset(^offset(options)) + |> maybe_preload_entities() + end - wrapped_query - |> Repo.replica().all() + defp maybe_preload_entities(query) do + if DenormalizationHelper.tt_denormalization_finished?() do + query + |> preload([:transaction, :token]) + else + query + |> preload([:block, :token, :transaction]) + end end defp where_start_block_match(query, %{startblock: nil}), do: query @@ -751,6 +664,12 @@ defmodule Explorer.Etherscan do where(query, [tt], tt.token_contract_address_hash == ^contract_address_hash) end + defp where_address_match_token_transfer(query, nil), do: query + + defp where_address_match_token_transfer(query, address_hash) do + where(query, [tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) + end + defp offset(options), do: (options.page_number - 1) * options.page_size @doc """ diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index 4cbee4ff49c7..2cf8768c64ec 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -1261,7 +1261,7 @@ defmodule Explorer.EtherscanTest do test "with empty db" do address = build(:address) - assert Etherscan.list_token_transfers(address.hash, nil) == [] + assert Etherscan.list_token_transfers(:erc20, address.hash, nil, %{}) == [] end test "with from address" do @@ -1277,7 +1277,7 @@ defmodule Explorer.EtherscanTest do block_number: transaction.block_number ) - [found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil) + [found_token_transfer] = Etherscan.list_token_transfers(:erc20, token_transfer.from_address_hash, nil, %{}) assert token_transfer.from_address_hash == found_token_transfer.from_address_hash end @@ -1295,7 +1295,7 @@ defmodule Explorer.EtherscanTest do block_number: transaction.block_number ) - [found_token_transfer] = Etherscan.list_token_transfers(token_transfer.to_address_hash, nil) + [found_token_transfer] = Etherscan.list_token_transfers(:erc20, token_transfer.to_address_hash, nil, %{}) assert token_transfer.to_address_hash == found_token_transfer.to_address_hash end @@ -1303,7 +1303,7 @@ defmodule Explorer.EtherscanTest do test "with address with 0 token transfers" do address = insert(:address) - assert Etherscan.list_token_transfers(address.hash, nil) == [] + assert Etherscan.list_token_transfers(:erc20, address.hash, nil, %{}) == [] end test "with address with multiple token transfers" do @@ -1336,7 +1336,7 @@ defmodule Explorer.EtherscanTest do block_number: transaction.block_number ) - found_token_transfers = Etherscan.list_token_transfers(address1.hash, nil) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address1.hash, nil, %{}) assert length(found_token_transfers) == 2 @@ -1345,73 +1345,6 @@ defmodule Explorer.EtherscanTest do end end - test "confirmations value is calculated correctly" do - insert(:block) - - transaction = - :transaction - |> insert() - |> with_block() - - token_transfer = - insert(:token_transfer, - transaction: transaction, - block: transaction.block, - block_number: transaction.block_number - ) - - insert(:block) - - [found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil) - - block_height = Chain.block_height() - expected_confirmations = block_height - transaction.block_number - - assert found_token_transfer.confirmations == expected_confirmations - end - - test "returns all required fields" do - block = insert(:block) - - transaction = - %{block: block} = - :transaction - |> insert(block_timestamp: block.timestamp) - |> with_block(block) - - token_transfer = - insert(:token_transfer, - transaction: transaction, - block: transaction.block, - block_number: transaction.block_number - ) - - {:ok, token} = Chain.token_from_address_hash(token_transfer.token_contract_address_hash) - - [found_token_transfer] = Etherscan.list_token_transfers(token_transfer.from_address_hash, nil) - - assert found_token_transfer.block_number == transaction.block_number - assert found_token_transfer.block_timestamp == block.timestamp - assert found_token_transfer.transaction_hash == token_transfer.transaction_hash - assert found_token_transfer.transaction_nonce == transaction.nonce - assert found_token_transfer.block_hash == block.hash - assert found_token_transfer.from_address_hash == token_transfer.from_address_hash - assert found_token_transfer.token_contract_address_hash == token_transfer.token_contract_address_hash - assert found_token_transfer.to_address_hash == token_transfer.to_address_hash - assert found_token_transfer.amount == token_transfer.amount - assert found_token_transfer.token_name == token.name - assert found_token_transfer.token_symbol == token.symbol - assert found_token_transfer.token_decimals == token.decimals - assert found_token_transfer.transaction_index == transaction.index - assert found_token_transfer.transaction_gas == transaction.gas - assert found_token_transfer.transaction_gas_price == transaction.gas_price - assert found_token_transfer.transaction_gas_used == transaction.gas_used - assert found_token_transfer.transaction_cumulative_gas_used == transaction.cumulative_gas_used - assert found_token_transfer.transaction_input == transaction.input - # There is a separate test to ensure confirmations are calculated correctly. - assert found_token_transfer.confirmations - end - test "orders token transfers by block, in ascending order (default)" do address = insert(:address) @@ -1454,7 +1387,7 @@ defmodule Explorer.EtherscanTest do block_number: transaction3.block_number ) - found_token_transfers = Etherscan.list_token_transfers(address.hash, nil) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, %{}) block_numbers_order = Enum.map(found_token_transfers, & &1.block_number) @@ -1506,7 +1439,7 @@ defmodule Explorer.EtherscanTest do options = %{order_by_direction: :desc} - found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options) block_numbers_order = Enum.map(found_token_transfers, & &1.block_number) @@ -1562,7 +1495,7 @@ defmodule Explorer.EtherscanTest do options1 = %{page_number: 1, page_size: 2} - page1_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options1) + page1_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options1) page1_hashes = Enum.map(page1_token_transfers, & &1.transaction_hash) @@ -1574,7 +1507,7 @@ defmodule Explorer.EtherscanTest do options2 = %{page_number: 2, page_size: 2} - page2_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options2) + page2_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options2) page2_hashes = Enum.map(page2_token_transfers, & &1.transaction_hash) @@ -1586,7 +1519,7 @@ defmodule Explorer.EtherscanTest do options3 = %{page_number: 3, page_size: 2} - page3_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options3) + page3_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options3) page3_hashes = Enum.map(page3_token_transfers, & &1.transaction_hash) @@ -1598,7 +1531,7 @@ defmodule Explorer.EtherscanTest do options4 = %{page_number: 4, page_size: 2} - assert Etherscan.list_token_transfers(address.hash, nil, options4) == [] + assert Etherscan.list_token_transfers(:erc20, address.hash, nil, options4) == [] end test "with start and end block options" do @@ -1624,7 +1557,7 @@ defmodule Explorer.EtherscanTest do endblock: third_block.number } - found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options) expected_block_numbers = [second_block.number, third_block.number] @@ -1655,7 +1588,7 @@ defmodule Explorer.EtherscanTest do options = %{startblock: third_block.number} - found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options) expected_block_numbers = [third_block.number, fourth_block.number] @@ -1686,7 +1619,7 @@ defmodule Explorer.EtherscanTest do options = %{endblock: second_block.number} - found_token_transfers = Etherscan.list_token_transfers(address.hash, nil, options) + found_token_transfers = Etherscan.list_token_transfers(:erc20, address.hash, nil, options) expected_block_numbers = [first_block.number, second_block.number] @@ -1719,7 +1652,7 @@ defmodule Explorer.EtherscanTest do block_number: transaction.block_number ) - [found_token_transfer] = Etherscan.list_token_transfers(address.hash, contract_address.hash) + [found_token_transfer] = Etherscan.list_token_transfers(:erc20, address.hash, contract_address.hash, %{}) assert found_token_transfer.token_contract_address_hash == contract_address.hash end From f54a8d315d47b6cb6875602ac0eb238a5786bd87 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 11 Jul 2025 17:59:06 +0300 Subject: [PATCH 0158/1006] Actualize old UI Dockerfile and make GA workflow to manually publish docker image with the old UI --- .../workflows/publish-docker-image-old-ui.yml | 47 +++++++++++++++++++ docker/oldUI.Dockerfile | 8 ++-- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/publish-docker-image-old-ui.yml diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml new file mode 100644 index 000000000000..58e6e74a349c --- /dev/null +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -0,0 +1,47 @@ +name: Publish Docker image with an old UI + +on: + workflow_dispatch: + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 8.1.1 + steps: + - uses: actions/checkout@v4 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build & Push Docker image with an old UI (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/oldUI.Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-with-old-ui-postrelease-${{ env.SHORT_SHA }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} diff --git a/docker/oldUI.Dockerfile b/docker/oldUI.Dockerfile index 94c3418ef869..a2552f144449 100644 --- a/docker/oldUI.Dockerfile +++ b/docker/oldUI.Dockerfile @@ -3,7 +3,7 @@ FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 AS builder-deps WORKDIR /app RUN apk --no-cache --update add \ - alpine-sdk gmp-dev automake libtool inotify-tools autoconf python3 file gcompat libstdc++ curl ca-certificates git make + alpine-sdk gmp-dev automake libtool inotify-tools autoconf python3 file gcompat libstdc++ curl ca-certificates git make bash # Cache elixir deps COPY mix.exs mix.lock ./ @@ -12,6 +12,7 @@ COPY apps/explorer/mix.exs ./apps/explorer/ COPY apps/ethereum_jsonrpc/mix.exs ./apps/ethereum_jsonrpc/ COPY apps/indexer/mix.exs ./apps/indexer/ COPY apps/utils/mix.exs ./apps/utils/ +COPY apps/nft_media_handler/mix.exs ./apps/nft_media_handler/ ENV MIX_ENV="prod" ENV MIX_HOME=/opt/mix @@ -78,8 +79,7 @@ RUN apk --no-cache --update add jq curl && \ adduser --system --uid ${BLOCKSCOUT_UID} --ingroup ${BLOCKSCOUT_GROUP} --disabled-password ${BLOCKSCOUT_USER} ENV DISABLE_WEBAPP=false -ARG ADMIN_PANEL_ENABLED -ENV ADMIN_PANEL_ENABLED=${ADMIN_PANEL_ENABLED} +ENV ADMIN_PANEL_ENABLED=false ARG DISABLE_API ENV DISABLE_API=${DISABLE_API} ARG API_V1_READ_METHODS_DISABLED @@ -104,6 +104,6 @@ COPY --from=builder --chown=${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} /app/config/c COPY --from=builder --chown=${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} /app/config/config_helper.exs /app/releases/${RELEASE_VERSION}/config_helper.exs COPY --from=builder --chown=${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} /app/config/assets/precompiles-arbitrum.json ./config/assets/precompiles-arbitrum.json -RUN chown -R ${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} /app +RUN mkdir dets && mkdir temp && chown -R ${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} /app USER ${BLOCKSCOUT_USER}:${BLOCKSCOUT_GROUP} From 5f45c17e35ae9760f0ee5811201748a89e722ab2 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 14 Jul 2025 20:10:24 +0300 Subject: [PATCH 0159/1006] fix: Fix timeout on cache update (#12773) --- apps/explorer/lib/explorer/chain/ordered_cache.ex | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/ordered_cache.ex b/apps/explorer/lib/explorer/chain/ordered_cache.ex index 2371a7a4e873..62704e95244e 100644 --- a/apps/explorer/lib/explorer/chain/ordered_cache.ex +++ b/apps/explorer/lib/explorer/chain/ordered_cache.ex @@ -255,13 +255,16 @@ defmodule Explorer.Chain.OrderedCache do def update(elements) when is_nil(elements), do: :ok def update(elements) when is_list(elements) do + prepared_elements = + elements + |> Enum.sort_by(&element_to_id(&1), &prevails?(&1, &2)) + |> Enum.take(max_size()) + |> do_preloads() + |> Enum.map(&{element_to_id(&1), sanitize_before_update(&1)}) + ConCache.update(cache_name(), ids_list_key(), fn ids -> updated_list = - elements - |> Enum.sort_by(&element_to_id(&1), &prevails?(&1, &2)) - |> Enum.take(max_size()) - |> do_preloads() - |> Enum.map(&{element_to_id(&1), sanitize_before_update(&1)}) + prepared_elements |> merge_and_update(ids || [], max_size()) # ids_list is set to never expire From 2b6b543d143bac3e5c43f35ffab701458841a012 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:27:51 +0300 Subject: [PATCH 0160/1006] chore(deps): bump ex_secp256k1 from 0.7.4 to 0.7.5 (#12781) --- updated-dependencies: - dependency-name: ex_secp256k1 dependency-version: 0.7.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 8abc913c83bd..96087e33f1e4 100644 --- a/mix.lock +++ b/mix.lock @@ -66,7 +66,7 @@ "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.3", "a2877e7028807a494fec3e5286e82872e10d632ddc885c4e26e64eac9ece5651", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "cbb80889f0bb1154fc694ddb66807ae9b7c96cb6a59ea70021d7beaf3a4a9d08"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, - "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.4", "d771a74917eb21e9f02e52bb1d0ac9aba4e2b5118d7f515014d0a311c990e323", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "465fd788c83c24d2df47f302e8fb1011054c81a905345e377c957b159a783bfc"}, + "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.5", "46e2de19374585da06d99d39de9c6ef9bd666fdf90930d7963cb7dbf1141af46", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "046207ab26908de8b99fbd799d35b1edc6318c2ccf891a8d58f90b7d2072e6bd"}, "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, From fa993209303170f3fa2bafc3445dc4406286499a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:28:21 +0300 Subject: [PATCH 0161/1006] chore(deps): bump open_api_spex from 3.21.4 to 3.21.5 (#12782) --- updated-dependencies: - dependency-name: open_api_spex dependency-version: 3.21.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 96087e33f1e4..98c1f8c7884f 100644 --- a/mix.lock +++ b/mix.lock @@ -122,7 +122,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, - "open_api_spex": {:hex, :open_api_spex, "3.21.4", "f066dab3257a2804cdb54efd84ed98bc46612dc60bf1d6313de373b776b83639", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "6a4e692222eef9472e1c6ce6aeccc89fa8d585aa1729fb4ececba06012743aa3"}, + "open_api_spex": {:hex, :open_api_spex, "3.21.5", "ff0c7fe5ceff9a56b9b0bb5a6dcfb7bc96e8afc563a3bef6ae91927de4d38b8e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "bd83c8f462222236fa85044098ba3bf57f7b7d7fd5286e6bc0060c7916f7c0d8"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, From 6d534f97b9458682b9ac97223f8f77f855cee48f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:30:44 +0300 Subject: [PATCH 0162/1006] chore(deps): bump ezstd from 1.2.1 to 1.2.2 (#12784) --- updated-dependencies: - dependency-name: ezstd dependency-version: 1.2.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 98c1f8c7884f..b44144346efc 100644 --- a/mix.lock +++ b/mix.lock @@ -72,7 +72,7 @@ "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, "exvcr": {:hex, :exvcr, "0.16.0", "11579f43c88ae81f57c82ce4f09e3ebda4c40117c859ed39e61a653c3a0b4ff4", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "8f576af22369942f7a1482baff1f31e2f45983cf6fac45d49d2bd2e84b4d5be8"}, - "ezstd": {:hex, :ezstd, "1.2.1", "3093e7b48687471ae58da415cc707b87ad5e0e3bd115f91d6154cd89b5393d4c", [:rebar3], [], "hexpm", "ddce7be69ea524ad02643449608d9d8557e2ecec0198284f0315c4e5fc7225ad"}, + "ezstd": {:hex, :ezstd, "1.2.2", "9d286f6727409260970bc08582cffec9a80a61d51cd9effa2b52fde5a333a2c6", [:rebar3], [], "hexpm", "6fcadd8bf64ab40e345c1e23f3c2aac12592813364c517f35a2139fab94e145e"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, From 4b537df5a26939139e9ace6e0cd67bbc3715de3a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:37:39 +0300 Subject: [PATCH 0163/1006] chore(deps): bump image from 0.61.0 to 0.61.1 (#12785) --- updated-dependencies: - dependency-name: image dependency-version: 0.61.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index b44144346efc..c706d04c00ba 100644 --- a/mix.lock +++ b/mix.lock @@ -87,7 +87,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.61.0", "3797e9486aded811a58d49027cbb8a6484971351456abd4db1c6d18afe4760fa", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "be6d82b1fb169413c5f37f3e88d7e8f795c321079b51418e13e10f03a2c32e66"}, + "image": {:hex, :image, "0.61.1", "029d9e2070288dab4a9c50938b1a7809298b61c23d43cd617d6e92ac9ceebb24", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "88f1fdeaeec36696acb9b261f035bbe53198cd2830f841c3e9cbf9010c16a5f9"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, @@ -174,7 +174,7 @@ "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"}, "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, - "vix": {:hex, :vix, "0.33.1", "72717f9b7a2ccd314c0b320a91d8cae67f100c0df378251acea19151fbf6e4b6", [:make, :mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "d9143e03fcd7f2f0e4db9a86d4b64aca0e39e99c303059d5841450cc4481b29c"}, + "vix": {:hex, :vix, "0.35.0", "f6319b715e3b072e53eba456a21af5f2ff010a7a7b19b884600ea98a0609b18c", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "a3e80067a89d0631b6cf2b93594e03c1b303a2c7cddbbdd28040750d521984e5"}, "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, From 762dfe264f850b058f7fec63ca874dcd9d3d9cae Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 15 Jul 2025 14:04:26 +0300 Subject: [PATCH 0164/1006] fix: filter scilla transactions by status (#12756) --- apps/explorer/lib/explorer/chain/zilliqa/reader.ex | 2 +- apps/indexer/lib/indexer/fetcher/contract_code.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/zilliqa/reader.ex b/apps/explorer/lib/explorer/chain/zilliqa/reader.ex index 8a65532ee069..726439ff551e 100644 --- a/apps/explorer/lib/explorer/chain/zilliqa/reader.ex +++ b/apps/explorer/lib/explorer/chain/zilliqa/reader.ex @@ -39,7 +39,7 @@ defmodule Explorer.Chain.Zilliqa.Reader do a in Address, join: t in Transaction, on: a.hash == t.created_contract_address_hash, - where: t.v == 0 and not is_nil(a.contract_code) and a.verified == false, + where: t.status == :ok and t.v == 0 and not is_nil(a.contract_code) and a.verified == false, order_by: [desc: t.block_number], select: a ) diff --git a/apps/indexer/lib/indexer/fetcher/contract_code.ex b/apps/indexer/lib/indexer/fetcher/contract_code.ex index 657ae5285bb2..6abb7f16f912 100644 --- a/apps/indexer/lib/indexer/fetcher/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/contract_code.ex @@ -273,7 +273,7 @@ defmodule Indexer.Fetcher.ContractCode do defp zilliqa_verify_scilla_contracts(entries, addresses) do zilliqa_contract_address_hashes = entries - |> Enum.filter(&ZilliqaHelper.scilla_transaction?(&1.type)) + |> Enum.filter(&(ZilliqaHelper.scilla_transaction?(&1.type) and &1.status == :ok)) |> MapSet.new(& &1.created_contract_address_hash) addresses From e63ebc1459bb587d167f88dd6cc45a86b42b9b58 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Tue, 15 Jul 2025 15:56:38 +0400 Subject: [PATCH 0165/1006] feat: add eip7702 authorization status fetcher (#12451) * feat: add eip7702 authorization status fetcher * chore: cleanup * chore: fix indexer test * chore: suggestions * fix: auth status fetcher * fix: test block numbers * chore: apply suggestions * chore: typo * chore: fix block_scout_web tests * chore: reorder checks for eip7702 compliance * chore: apply suggestions * fix: don't save ok statuses on import * fix: limit concurrency to 1 * fix: exclude refetch_needed --- .../views/api/v2/transaction_view.ex | 3 +- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 39 ++ .../lib/ethereum_jsonrpc/nonce.ex | 3 +- .../lib/ethereum_jsonrpc/nonces.ex | 84 ++++ .../import/runner/signed_authorizations.ex | 6 +- .../explorer/chain/signed_authorization.ex | 83 ++- .../chain/signed_authorization/reader.ex | 73 +++ .../lib/explorer/chain/smart_contract.ex | 3 +- .../proxy/models/implementation.ex | 87 +++- ...0250512083932_add_authorization_status.exs | 14 + apps/explorer/test/support/factory.ex | 16 + .../lib/indexer/block/catchup/fetcher.ex | 2 + apps/indexer/lib/indexer/block/fetcher.ex | 10 + .../lib/indexer/block/realtime/fetcher.ex | 2 + .../fetcher/on_demand/contract_code.ex | 17 +- .../fetcher/signed_authorization_status.ex | 471 ++++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 2 + .../transform/signed_authorizations.ex | 24 +- .../signed_authorization_status_test.exs | 313 ++++++++++++ ...ed_authorization_status_supervisor_case.ex | 17 + config/runtime.exs | 3 + docker-compose/envs/common-blockscout.env | 1 + 22 files changed, 1240 insertions(+), 33 deletions(-) create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonces.ex create mode 100644 apps/explorer/lib/explorer/chain/signed_authorization/reader.ex create mode 100644 apps/explorer/priv/repo/migrations/20250512083932_add_authorization_status.exs create mode 100644 apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex create mode 100644 apps/indexer/test/indexer/fetcher/signed_authorization_status_test.exs create mode 100644 apps/indexer/test/support/indexer/fetcher/signed_authorization_status_supervisor_case.ex diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 98629c38d30b..4243eec757a7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -387,7 +387,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "r" => signed_authorization.r, "s" => signed_authorization.s, "v" => signed_authorization.v, - "authority" => Address.checksum(signed_authorization.authority) + "authority" => Address.checksum(signed_authorization.authority), + "status" => signed_authorization.status } end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 89ccef0e08ec..0d1a15fb30e0 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -34,6 +34,7 @@ defmodule EthereumJSONRPC do FetchedBalances, FetchedBeneficiaries, FetchedCodes, + Nonces, Receipts, RequestCoordinator, Subscription, @@ -317,6 +318,44 @@ defmodule EthereumJSONRPC do end end + @doc """ + Fetches address nonces for multiple addresses at specified block numbers. + + This function takes a list of parameters, each containing an address and a + block number, and retrieves the nonce for each address at the specified + block. + + ## Parameters + - `params_list`: A list of maps, each containing: + - `:block_quantity`: The block number (as a quantity string) at which to fetch the nonce. + - `:address`: The address of the contract to fetch the nonce for. + - `json_rpc_named_arguments`: A keyword list of JSON-RPC configuration options. + + ## Returns + - `{:ok, fetched_nonces}`, where `fetched_nonces` is a `Nonces.t()` struct containing: + - `params_list`: A list of successfully fetched code parameters, each containing: + - `address`: The contract address. + - `block_number`: The block number at which the nonce was fetched. + - `nonce`: The fetched nonce. + - `errors`: A list of errors encountered during the fetch operation. + - `{:error, reason}`: An error occurred during the fetch operation. + """ + @spec fetch_nonces( + [%{required(:block_quantity) => quantity, required(:address) => address()}], + json_rpc_named_arguments + ) :: {:ok, Nonces.t()} | {:error, reason :: term} + def fetch_nonces(params_list, json_rpc_named_arguments) + when is_list(params_list) and is_list(json_rpc_named_arguments) do + id_to_params = id_to_params(params_list) + + with {:ok, responses} <- + id_to_params + |> Nonces.requests() + |> json_rpc(json_rpc_named_arguments) do + {:ok, Nonces.from_responses(responses, id_to_params)} + end + end + @doc """ Fetches block reward contract beneficiaries from variant API. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonce.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonce.ex index 41dfa2de2646..46eccdac302e 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonce.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonce.ex @@ -7,6 +7,7 @@ defmodule EthereumJSONRPC.Nonce do alias EthereumJSONRPC.FetchedCode @type params :: %{address: EthereumJSONRPC.address(), block_number: non_neg_integer(), nonce: non_neg_integer()} + @type error :: FetchedCode.error() @doc """ Converts a JSON-RPC response of `eth_getTransactionCount` to code params or an annotated error. @@ -53,7 +54,7 @@ defmodule EthereumJSONRPC.Nonce do @spec from_response(%{id: EthereumJSONRPC.request_id(), error: %{code: integer(), message: String.t()}}, %{ non_neg_integer() => %{block_quantity: String.t(), address: String.t()} - }) :: {:error, FetchedCode.error()} + }) :: {:error, error()} def from_response(%{id: _id, error: %{code: code, message: message}} = response, id_to_params) when is_integer(code) and is_binary(message) and is_map(id_to_params) do FetchedCode.from_response(response, id_to_params) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonces.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonces.ex new file mode 100644 index 000000000000..2a2bf6ba5997 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nonces.ex @@ -0,0 +1,84 @@ +defmodule EthereumJSONRPC.Nonces do + @moduledoc """ + Nonce params and errors from a batch request from `eth_getTransactionCount`. + """ + + alias EthereumJSONRPC.Nonce + + defstruct params_list: [], + errors: [] + + @typedoc """ + * `params_list` - all the nonce params from requests that succeeded in the batch. + * `errors` - all the errors from requests that failed in the batch. + """ + @type t :: %__MODULE__{params_list: [Nonce.params()], errors: [Nonce.error()]} + + @doc """ + Generates a list of `eth_getTransactionCount` JSON-RPC requests for the given parameters. + + This function takes a map of request IDs to parameter maps and transforms them + into a list of JSON-RPC request structures for fetching nonces. + + ## Parameters + - `id_to_params`: A map where keys are request IDs and values are maps + containing `block_quantity` and `address` for each request. + + ## Returns + A list of maps, each representing a JSON-RPC request with the following + structure: + - `jsonrpc`: The JSON-RPC version (always "2.0"). + - `id`: The request identifier. + - `method`: The RPC method name (always "eth_getTransactionCount"). + - `params`: A list containing the address and block identifier. + """ + @spec requests(%{id => %{block_quantity: block_quantity, address: address}}) :: [ + %{jsonrpc: String.t(), id: id, method: String.t(), params: [address | block_quantity]} + ] + when id: EthereumJSONRPC.request_id(), + block_quantity: EthereumJSONRPC.quantity(), + address: EthereumJSONRPC.address() + def requests(id_to_params) when is_map(id_to_params) do + Enum.map(id_to_params, fn {id, %{block_quantity: block_quantity, address: address}} -> + Nonce.request(%{id: id, block_quantity: block_quantity, address: address}) + end) + end + + @doc """ + Processes responses from `eth_getTransactionCount` JSON-RPC calls and converts them into a structured format. + + This function takes a list of responses from `eth_getTransactionCount` calls and a map of + request IDs to their corresponding parameters. It sanitizes the responses, + processes each one, and accumulates the results into a `Nonces` struct. + + ## Parameters + - `responses`: A list of response maps from `eth_getTransactionCount` calls. Each map + contains an `:id` key and either a `:result` or `:error` key. + - `id_to_params`: A map where keys are request IDs and values are maps + containing `block_quantity` and `address` for each request. + + ## Returns + A `Nonces` struct containing: + - `params_list`: A list of successfully fetched nonce parameters. + - `errors`: A list of errors encountered during the process. + """ + @spec from_responses( + [%{:id => EthereumJSONRPC.request_id(), optional(:error) => map(), optional(:result) => String.t()}], + %{non_neg_integer() => %{block_quantity: String.t(), address: String.t()}} + ) :: t() + def from_responses(responses, id_to_params) do + responses + |> EthereumJSONRPC.sanitize_responses(id_to_params) + |> Enum.map(&Nonce.from_response(&1, id_to_params)) + |> Enum.reduce( + %__MODULE__{}, + fn + {:ok, params}, %__MODULE__{params_list: params_list} = acc -> + %__MODULE__{acc | params_list: [params | params_list]} + + {:error, reason}, %__MODULE__{errors: errors} = acc -> + %__MODULE__{acc | errors: [reason | errors]} + end + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex index 2683c87bdcbd..5f71e9a697fb 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/signed_authorizations.ex @@ -91,20 +91,22 @@ defmodule Explorer.Chain.Import.Runner.SignedAuthorizations do s: fragment("EXCLUDED.s"), v: fragment("EXCLUDED.v"), authority: fragment("EXCLUDED.authority"), + status: fragment("EXCLUDED.status"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", authorization.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", authorization.updated_at) ] ], where: fragment( - "(EXCLUDED.chain_id, EXCLUDED.address, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.v, EXCLUDED.authority) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.chain_id, EXCLUDED.address, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.v, EXCLUDED.authority, EXCLUDED.status) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)", authorization.chain_id, authorization.address, authorization.nonce, authorization.r, authorization.s, authorization.v, - authorization.authority + authorization.authority, + authorization.status ) ) end diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex index b8ca690bed91..0b6665099158 100644 --- a/apps/explorer/lib/explorer/chain/signed_authorization.ex +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -3,9 +3,9 @@ defmodule Explorer.Chain.SignedAuthorization do use Explorer.Schema - alias Explorer.Chain.{Hash, Transaction} + alias Explorer.Chain.{Cache.ChainId, Data, Hash, Transaction} - @optional_attrs ~w(authority)a + @optional_attrs ~w(authority status)a @required_attrs ~w(transaction_hash index chain_id address nonce r s v)a @typedoc """ @@ -19,8 +19,9 @@ defmodule Explorer.Chain.SignedAuthorization do * `r` - the 'r' component of the signature. * `s` - the 's' component of the signature. * `authority` - the signer of the authorization. + * `status` - the status of the authorization. """ - @type to_import :: %__MODULE__{ + @type to_import :: %{ transaction_hash: binary(), index: non_neg_integer(), chain_id: non_neg_integer(), @@ -29,7 +30,8 @@ defmodule Explorer.Chain.SignedAuthorization do r: non_neg_integer(), s: non_neg_integer(), v: non_neg_integer(), - authority: binary() | nil + authority: binary() | nil, + status: :ok | :invalid_chain_id | :invalid_signature | :invalid_nonce | nil } @typedoc """ @@ -42,6 +44,7 @@ defmodule Explorer.Chain.SignedAuthorization do * `r` - the 'r' component of the signature. * `s` - the 's' component of the signature. * `authority` - the signer of the authorization. + * `status` - the validity status of the authorization. * `inserted_at` - timestamp indicating when the signed authorization was created. * `updated_at` - timestamp indicating when the signed authorization was last updated. * `transaction` - an instance of `Explorer.Chain.Transaction` referenced by `transaction_hash`. @@ -56,6 +59,7 @@ defmodule Explorer.Chain.SignedAuthorization do field(:s, :decimal, null: false) field(:v, :integer, null: false) field(:authority, Hash.Address, null: true) + field(:status, Ecto.Enum, values: [:ok, :invalid_chain_id, :invalid_signature, :invalid_nonce], null: true) belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, @@ -67,6 +71,19 @@ defmodule Explorer.Chain.SignedAuthorization do timestamps() end + @local_fields [:__meta__, :inserted_at, :updated_at] + + @doc """ + Returns a map representation of the signed authorization. + """ + @spec to_map(__MODULE__.t()) :: map() + def to_map(%__MODULE__{} = struct) do + association_fields = struct.__struct__.__schema__(:associations) + waste_fields = association_fields ++ @local_fields + + struct |> Map.from_struct() |> Map.drop(waste_fields) + end + @doc """ Validates that the `attrs` are valid. """ @@ -77,4 +94,62 @@ defmodule Explorer.Chain.SignedAuthorization do |> validate_required(@required_attrs) |> foreign_key_constraint(:transaction_hash) end + + @doc """ + Converts a `SignedAuthorization.t()` into a map of import params that can be + fed into `Chain.import/1`. It sets synthetic contract code for EIP-7702 proxies + and updates the nonce. + """ + @spec to_address_params(Ecto.Schema.t()) :: %{ + hash: Hash.Address.t(), + contract_code: Data.t() | nil, + nonce: non_neg_integer() + } + def to_address_params(%__MODULE__{} = struct) do + code = + if struct.address.bytes == <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>> do + nil + else + %Data{bytes: <<239, 1, 0>> <> struct.address.bytes} + end + + %{hash: struct.authority, contract_code: code, nonce: struct.nonce |> Decimal.to_integer()} + end + + @doc """ + Does basic validation on a `SignedAuthorization.t()` according to EIP-7702, with + the exception of verifying current authority nonce, which requires calling + `eth_getTransactionCount` JSON-RPC method. + + Authority nonce validity is verified in async `Indexer.Fetcher.SignedAuthorizationStatus` fetcher. + + ## Returns + - `:ok` if the signed authorization is valid and we should proceed with nonce validation. + - `:invalid_chain_id` if the signed authorization is for another chain ID. + - `:invalid_signature` if the signed authorization has an invalid signature. + - `:invalid_nonce` if the signed authorization has an invalid nonce. + - `nil` if the signed authorization status is unknown due to unknown chain ID. + """ + @spec basic_validate(Ecto.Schema.t() | to_import()) :: + :ok | :invalid_chain_id | :invalid_signature | :invalid_nonce | nil + def basic_validate(%{} = struct) do + chain_id = ChainId.get_id() + + cond do + struct.chain_id != 0 and !is_nil(chain_id) and struct.chain_id != chain_id -> + :invalid_chain_id + + struct.chain_id != 0 and is_nil(chain_id) -> + nil + + struct.nonce |> Decimal.gte?(2 ** 64 - 1) -> + :invalid_nonce + + is_nil(struct.authority) -> + :invalid_signature + + true -> + :ok + end + end end diff --git a/apps/explorer/lib/explorer/chain/signed_authorization/reader.ex b/apps/explorer/lib/explorer/chain/signed_authorization/reader.ex new file mode 100644 index 000000000000..dd2be17ffb0b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/signed_authorization/reader.ex @@ -0,0 +1,73 @@ +defmodule Explorer.Chain.SignedAuthorization.Reader do + @moduledoc """ + Reads signed authorization data from the database. + """ + + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.{Block, Hash, SignedAuthorization} + alias Explorer.Repo + + @doc """ + Returns a stream of `Block.t()` for signed authorizations with missing statuses. + + ## Parameters + + - `initial`: The initial accumulator value for the stream. + - `reducer`: A function that processes each entry in the stream, receiving + the entry and the current accumulator, and returning a new accumulator. + + ## Returns + + - `{:ok, accumulator}`: The final accumulator value after streaming through + the block numbers. + """ + @spec stream_blocks_to_refetch_signed_authorizations_statuses( + initial :: accumulator, + reducer :: (entry :: term(), accumulator -> accumulator) + ) :: {:ok, accumulator} + when accumulator: term() + def stream_blocks_to_refetch_signed_authorizations_statuses(initial, reducer) when is_function(reducer, 2) do + query = + from( + b in Block, + join: t in assoc(b, :transactions), + join: s in assoc(t, :signed_authorizations), + where: is_nil(s.status) and b.consensus == true and b.refetch_needed == false, + distinct: true, + select: %{ + block_hash: b.hash, + block_number: b.number + } + ) + + query |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Returns latest successful authorizations for a list of addresses. + + ## Parameters + + - `address_hashes`: The list of `authority` addresses to fetch authorizations for. + + ## Returns + + - `[%{authority: Hash.Address.t(), nonce: non_neg_integer()}]`: The list of latest + successful authorizations and their nonces for the given addresses, if there are any. + """ + @spec address_hashes_to_latest_authorizations([Hash.Address.t()]) :: [ + %{authority: Hash.Address.t(), nonce: non_neg_integer()} + ] + def address_hashes_to_latest_authorizations(address_hashes) do + query = + from( + s in SignedAuthorization, + where: s.authority in ^address_hashes and s.status == :ok, + select: %{authority: s.authority, nonce: max(s.nonce)}, + group_by: s.authority + ) + + Repo.all(query) + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 2aa753914f13..8e081523610c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -1126,7 +1126,8 @@ defmodule Explorer.Chain.SmartContract do @doc """ Converts address hash to smart-contract object with metadata_from_verified_bytecode_twin=true """ - @spec address_hash_to_smart_contract_with_bytecode_twin(Hash.Address.t(), [api?]) :: {__MODULE__.t() | nil, boolean()} + @spec address_hash_to_smart_contract_with_bytecode_twin(Hash.Address.t(), [api?], boolean()) :: + {__MODULE__.t() | nil, boolean()} def address_hash_to_smart_contract_with_bytecode_twin(address_hash, options \\ [], fetch_implementation? \\ true) do current_smart_contract = address_hash_to_smart_contract(address_hash, options) diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index 98e4fa4f971e..5c5cf97eabe8 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do alias Explorer.Chain.{Address, Hash, SmartContract} alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.EIP7702 alias Timex.Duration @burn_address_hash_string "0x0000000000000000000000000000000000000000" @@ -347,7 +348,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do with {:ok, implementation_address_hash} <- Chain.string_to_address_hash(implementation_address_hash_string), {:implementation, {%SmartContract{name: name}, _}} <- { :implementation, - SmartContract.address_hash_to_smart_contract_with_bytecode_twin(implementation_address_hash, options) + SmartContract.address_hash_to_smart_contract_with_bytecode_twin( + implementation_address_hash, + options, + false + ) } do {implementation_address_hash_string, name} else @@ -426,11 +431,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do end @doc """ - Deletes all proxy implementations associated with the given proxy address hash. + Deletes all proxy implementations associated with the given proxy address hashes. ## Parameters - - `address_hash` (binary): The hash of the proxy address whose implementations + - `address_hashes` (binary): The list of proxy address hashes whose implementations should be deleted. ## Returns @@ -438,15 +443,83 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do - `{count, nil}`: A tuple where `count` is the number of records deleted. This function uses a query to find all proxy implementations matching the - provided `address_hash` and deletes them from the database. + provided `address_hashes` and deletes them from the database. """ - @spec delete_implementations(Hash.Address.t()) :: {non_neg_integer(), nil} - def delete_implementations(address_hash) do + @spec delete_implementations([Hash.Address.t()]) :: {non_neg_integer(), nil} + def delete_implementations(address_hashes) do __MODULE__ - |> where([proxy_implementations], proxy_implementations.proxy_address_hash == ^address_hash) + |> where([proxy_implementations], proxy_implementations.proxy_address_hash in ^address_hashes) |> Repo.delete_all() end + @doc """ + Upserts multiple EIP-7702 proxy records for given addresses. + + ## Parameters + + - `addresses`: The list of addresses to upsert EIP-7702 proxy records for. + + ## Returns + + - `{count, nil}`: A tuple where `count` is the number of records updated. + """ + @spec upsert_eip7702_implementations([Address.t()]) :: {non_neg_integer(), nil | []} + def upsert_eip7702_implementations(addresses) do + now = DateTime.utc_now() + + records = + addresses + |> Repo.preload(:smart_contract) + |> Enum.flat_map(fn address -> + with true <- Address.smart_contract?(address), + delegate_string when not is_nil(delegate_string) <- + EIP7702.get_delegate_address(address.contract_code.bytes), + {:ok, delegate_address_hash} <- Hash.Address.cast(delegate_string) do + name = + case address do + %{smart_contract: %{name: name}} -> name + _ -> nil + end + + [ + %{ + proxy_address_hash: address.hash, + proxy_type: :eip7702, + address_hashes: [delegate_address_hash], + names: [name], + inserted_at: now, + updated_at: now + } + ] + else + _ -> [] + end + end) + + Repo.insert_all(__MODULE__, records, + on_conflict: eip7702_on_conflict(), + conflict_target: [:proxy_address_hash] + ) + end + + defp eip7702_on_conflict do + from( + proxy_implementations in __MODULE__, + update: [ + set: [ + address_hashes: fragment("EXCLUDED.address_hashes"), + names: fragment("EXCLUDED.names"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", proxy_implementations.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", proxy_implementations.updated_at) + ] + ], + where: + fragment("EXCLUDED.proxy_type = ?", proxy_implementations.proxy_type) and + (fragment("EXCLUDED.address_hashes <> ?", proxy_implementations.address_hashes) or + fragment("EXCLUDED.names <> ?", proxy_implementations.names)) + ) + end + # Cut off implementations per proxy up to @max_implementations_number_per_proxy number # before insert into the DB to prevent DoS via the verification endpoint of Diamond smart contracts. @spec sanitize_implementation_address_hash_strings([EthereumJSONRPC.hash()]) :: [EthereumJSONRPC.hash()] diff --git a/apps/explorer/priv/repo/migrations/20250512083932_add_authorization_status.exs b/apps/explorer/priv/repo/migrations/20250512083932_add_authorization_status.exs new file mode 100644 index 000000000000..6c07291dbe55 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250512083932_add_authorization_status.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.AddAuthorizationStatus do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE signed_authorization_status AS ENUM ('ok', 'invalid_chain_id', 'invalid_signature', 'invalid_nonce')", + "DROP TYPE signed_authorization_status" + ) + + alter table(:signed_authorizations) do + add(:status, :signed_authorization_status, null: true) + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index d7d03ac0c6a6..ec6a00f3f210 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -41,6 +41,7 @@ defmodule Explorer.Factory do MultichainSearchDb, PendingBlockOperation, PendingTransactionOperation, + SignedAuthorization, SmartContract, SmartContractAdditionalSource, Token, @@ -1010,6 +1011,21 @@ defmodule Explorer.Factory do } end + def signed_authorization_factory do + %SignedAuthorization{ + transaction: build(:transaction), + index: 0, + chain_id: 0, + address: address_hash(), + nonce: 0, + r: 0, + s: 0, + v: 0, + authority: address_hash(), + status: nil + } + end + def smart_contract_factory do contract_code_info = contract_code_info() diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index ea730a74cab6..69d7fe184cb9 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -17,6 +17,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_filecoin_addresses_info: 2, async_import_internal_transactions: 2, async_import_replaced_transactions: 2, + async_import_signed_authorizations_statuses: 2, async_import_token_balances: 2, async_import_token_instances: 1, async_import_tokens: 2, @@ -146,6 +147,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_blobs(imported, realtime?) async_import_celo_epoch_block_operations(imported, realtime?) async_import_filecoin_addresses_info(imported, realtime?) + async_import_signed_authorizations_statuses(imported, realtime?) end defp stream_fetch_and_import(state, ranges) do diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 9a5328466154..061c20ce407b 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -34,6 +34,7 @@ defmodule Indexer.Block.Fetcher do ContractCode, InternalTransaction, ReplacedTransaction, + SignedAuthorizationStatus, Token, TokenBalance, UncleBlock @@ -568,6 +569,15 @@ defmodule Indexer.Block.Fetcher do def async_import_filecoin_addresses_info(_, _), do: :ok + def async_import_signed_authorizations_statuses( + %{transactions: transactions, signed_authorizations: signed_authorizations}, + realtime? + ) do + SignedAuthorizationStatus.async_fetch(transactions, signed_authorizations, realtime?) + end + + def async_import_signed_authorizations_statuses(_, _), do: :ok + defp block_reward_errors_to_block_numbers(block_reward_errors) when is_list(block_reward_errors) do Enum.map(block_reward_errors, &block_reward_error_to_block_number/1) end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 496f7cec1489..6e2c638e0890 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -22,6 +22,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_polygon_zkevm_bridge_l1_tokens: 1, async_import_realtime_coin_balances: 1, async_import_replaced_transactions: 2, + async_import_signed_authorizations_statuses: 2, async_import_token_balances: 2, async_import_token_instances: 1, async_import_tokens: 2, @@ -559,5 +560,6 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_polygon_zkevm_bridge_l1_tokens(imported) async_import_celo_epoch_block_operations(imported, realtime?) async_import_filecoin_addresses_info(imported, realtime?) + async_import_signed_authorizations_statuses(imported, realtime?) end end diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex index 6e43593ad604..e77324ade626 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex @@ -12,9 +12,9 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do alias Explorer.Chain alias Explorer.Chain.{Address, Data} + alias Explorer.Chain.Cache.Accounts alias Explorer.Chain.Cache.Counters.Helper alias Explorer.Chain.Events.Publisher - alias Explorer.Chain.SmartContract.Proxy.EIP7702 alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Utility.{AddressContractCodeFetchAttempt, RateLimiter} alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @@ -118,7 +118,9 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do fields_to_update: [:contract_code] } }) do - {:ok, _} -> + {:ok, %{addresses: addresses}} -> + Accounts.drop(addresses) + # Update EIP7702 proxy addresses to avoid inconsistencies between addresses and proxy_implementations tables. # Other proxy types are not handled here, since their bytecode doesn't change the way EIP7702 bytecode does. cond do @@ -126,17 +128,10 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do :ok is_nil(fetched_code) -> - Implementation.delete_implementations(address.hash) - - # TODO: it's better to use a generic code like this, but it does unnecessary minimal proxy checks and DB lookups - # Address.eoa_with_code?(new_address) -> - # Proxy.fetch_implementation_address_hash(address.hash, [], []) - - delegate = EIP7702.get_delegate_address(fetched_code.bytes) -> - Implementation.save_implementation_data([delegate |> to_string()], address.hash, :eip7702, []) + Implementation.delete_implementations([address.hash]) true -> - :ok + Implementation.upsert_eip7702_implementations(addresses) end Publisher.broadcast(%{fetched_bytecode: [address.hash, contract_code_object.code]}, :on_demand) diff --git a/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex new file mode 100644 index 000000000000..cbb1306b880a --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex @@ -0,0 +1,471 @@ +defmodule Indexer.Fetcher.SignedAuthorizationStatus do + @moduledoc """ + Fetches `status` `t:Explorer.Chain.SignedAuthorization.t/0`. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + require Logger + + import Ecto.Query, only: [from: 2] + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + import Explorer.Chain.SignedAuthorization.Reader, + only: [ + stream_blocks_to_refetch_signed_authorizations_statuses: 2, + address_hashes_to_latest_authorizations: 1 + ] + + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Block, BlockNumberHelper, Hash, SignedAuthorization, Transaction} + alias Explorer.Chain.Cache.Accounts + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + alias Indexer.{BufferedTask, Tracer} + + @failed_to_import "failed to import signed_authorization status for transactions: " + + @type inner_entry :: + {:transaction, %{authority: Hash.Address.t(), nonce: non_neg_integer()}} + | {:authorization, SignedAuthorization.t()} + + @typedoc """ + Each entry is a list of all transactions and signed authorizations from the same block. + + - `:block_number` - The block number of given batch. + - `:block_hash` - The block hash of given batch. + - `:entries` - The signed authorizations merged with transaction nonces. + """ + @type entry :: %{ + required(:block_number) => Block.block_number(), + required(:block_hash) => Hash.Full.t(), + optional(:entries) => [inner_entry()] + } + + @behaviour BufferedTask + + @default_max_batch_size 10 + @default_max_concurrency 1 + + @doc """ + Enqueues a batch of transactions to fetch and handle signed authorization statuses. + Only works correctly if all transactions and signed authorizations from the particular block + are present in the list at the same time. + """ + @spec async_fetch([Transaction.t()], [SignedAuthorization.t()], boolean(), integer()) :: :ok + def async_fetch( + transactions, + signed_authorizations, + realtime?, + timeout \\ 5000 + ) do + grouped_signed_authorizations = signed_authorizations |> Enum.group_by(& &1.transaction_hash) + + BufferedTask.buffer( + __MODULE__, + transactions + |> RangesHelper.filter_traceable_block_numbers() + |> Enum.map(&Map.put(&1, :signed_authorizations, Map.get(grouped_signed_authorizations, &1.hash, []))) + |> entries_from_transactions(), + realtime?, + timeout + ) + end + + # Chunks a list of transactions into entries, with all transactions from the same block grouped together. + @spec entries_from_transactions([Transaction.t()]) :: [entry()] + defp entries_from_transactions(transactions) do + transactions + |> Enum.group_by(& &1.block_hash) + |> Enum.map(fn {block_hash, block_transactions} -> + %{ + block_number: block_transactions |> List.first() |> Map.get(:block_number), + block_hash: block_hash, + entries: + block_transactions + |> Enum.sort_by(& &1.index) + |> Enum.flat_map(&transaction_to_inner_entries/1) + } + end) + |> Enum.sort_by(& &1.block_number) + end + + # Extract all nonce changes from transaction with signed authorizations. + # + # Any transaction change the nonce of the transaction sender. + # + # Additionally, EIP7702 transactions may change the nonce of successful EIP7702 tuple authorities. + @spec transaction_to_inner_entries(Transaction.t()) :: [inner_entry()] + defp transaction_to_inner_entries(transaction) do + [ + {:transaction, %{authority: transaction.from_address_hash, nonce: transaction.nonce}} + | transaction + |> Map.get(:signed_authorizations, []) + |> Enum.sort_by(& &1.index) + |> Enum.map(&{:authorization, &1}) + ] + end + + @doc false + def child_spec([init_options, gen_server_options]) do + {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) + + unless state do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> + "to allow for json_rpc calls when running." + end + + merged_init_opts = + defaults() + |> Keyword.merge(mergeable_init_options) + |> Keyword.put(:state, state) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + stream_reducer = RangesHelper.stream_reducer_traceable(reducer) + + # here we stream only block hashes, transactions are preloaded later by `preload_entries/1` + {:ok, final} = + stream_blocks_to_refetch_signed_authorizations_statuses( + initial, + stream_reducer + ) + + final + end + + @doc """ + Processes a batch of entries to fetch and handle signed authorization statuses. + This function is executed as part of the `BufferedTask` behavior. + + ## Parameters + + - `entries`: A list of entries to process. + - `json_rpc_named_arguments`: A list of options for JSON-RPC communication. + + ## Returns + + - `:ok`: Indicates successful processing of the signed authorization statuses. + - `{:retry, any()}`: Returns the entries for retry if an error occurs during + the fetch operation. + """ + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.SignedAuthorizationStatus.run/2", + service: :indexer, + tracer: Tracer + ) + @spec run([entry()], [ + {:throttle_timeout, non_neg_integer()} + | {:transport, atom()} + | {:transport_options, any()} + | {:variant, atom()} + ]) :: :ok | {:retry, any()} + def run(entries, json_rpc_named_arguments) do + Logger.debug("fetching signed authorization statuses") + + # preload transactions for init-generated entries, in case of retry, preloaded transactions are preserved + entries = entries |> Enum.map(&preload_entries/1) + + # compute pairs of block numbers and addresses for which don't know nonce at the start of the block + missing_nonces_for = entries |> Enum.flat_map(&compute_missing_nonces/1) + + with {:fetch, {:ok, nonces_map}} <- {:fetch, fetch_nonces(missing_nonces_for, json_rpc_named_arguments)}, + {new_entries, updated_authorizations} = + entries + |> Enum.map(&compute_statuses(&1, Map.get(nonces_map, &1.block_number, %{}))) + |> Enum.unzip(), + {:import, :ok} <- {:import, import_authorizations(List.flatten(updated_authorizations))} do + entries_to_retry = + new_entries + |> Enum.filter( + &Enum.any?(&1, fn + {:authorization, %{status: nil}} -> true + _ -> false + end) + ) + + if Enum.empty?(entries_to_retry) do + :ok + else + {:retry, entries_to_retry} + end + else + {:fetch, {:error, reason}} -> + Logger.error(fn -> ["failed to fetch address nonces: ", inspect(reason)] end, + error_count: Enum.count(missing_nonces_for) + ) + + {:retry, entries} + + {:import, {:error, reason}} -> + Logger.error(fn -> ["failed to import signed authorizations: ", inspect(reason)] end) + + {:retry, entries} + end + end + + @spec preload_entries(entry()) :: entry() + defp preload_entries(%{entries: entries} = entry) when not is_nil(entries) do + entry + end + + defp preload_entries(%{block_hash: block_hash} = entry) do + block = + block_hash + |> Chain.fetch_block_by_hash() + |> Repo.preload([:transactions, [transactions: :signed_authorizations]]) + + entry + |> Map.put( + :entries, + block + |> Map.get(:transactions, []) + |> Enum.sort_by(& &1.index) + |> Enum.flat_map(&transaction_to_inner_entries/1) + ) + end + + # Compute pairs of block numbers and addresses for which don't know nonce at the start of the block. + # + # Checks all authorizations signers and returns those without any prior transactions with known nonce. + @spec compute_missing_nonces(entry()) :: [%{block_number: Block.block_number(), address_hash: Hash.Address.t()}] + defp compute_missing_nonces(%{block_number: block_number, entries: entries}) do + entries + |> Enum.reduce({[], MapSet.new()}, fn element, {missing_nonces, known_nonces} -> + case element do + # once we have seen a transaction in the block, we can be certain in address nonce for later transactions in the same block + {:transaction, %{authority: authority}} -> + {missing_nonces, known_nonces |> MapSet.put(authority)} + + {:authorization, %{authority: authority, status: nil}} when not is_nil(authority) -> + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if MapSet.member?(known_nonces, authority) do + {missing_nonces, known_nonces} + else + {[%{block_number: block_number, address_hash: authority} | missing_nonces], + known_nonces |> MapSet.put(authority)} + end + + _ -> + {missing_nonces, known_nonces} + end + end) + |> elem(0) + end + + # Compute authorization statuses by iteratively going through all transactions and authorizations in the block, + # while keeping track of expected nonces. + @spec compute_statuses(entry(), %{Hash.Address.t() => non_neg_integer()}) :: {entry(), [SignedAuthorization.t()]} + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + defp compute_statuses(entry, known_nonces) do + {new_entries, {updated_authorizations, _}} = + entry.entries + |> Enum.map_reduce({[], known_nonces}, fn + {:transaction, transaction} = entry, {updated_authorizations, known_nonces} -> + {entry, {updated_authorizations, known_nonces |> Map.put(transaction.authority, transaction.nonce + 1)}} + + {:authorization, authorization} = entry, {updated_authorizations, known_nonces} -> + nonce = Decimal.to_integer(authorization.nonce) + status = SignedAuthorization.basic_validate(authorization) + + cond do + # if authorization is valid, update known nonce and proceed + authorization.status == :ok -> + {entry, {updated_authorizations, known_nonces |> Map.put(authorization.authority, nonce + 1)}} + + # if authorization is invalid, nonce is not incremented + authorization.status in [:invalid_signature, :invalid_chain_id, :invalid_nonce] -> + {entry, {updated_authorizations, known_nonces}} + + # remaining cases handle authorization.status == nil + + status in [:invalid_signature, :invalid_chain_id, :invalid_nonce] -> + new_authorization = %{authorization | status: status} + + {{:authorization, new_authorization}, {[new_authorization | updated_authorizations], known_nonces}} + + # we still can't get chain_id from the json rpc, so we can't validate authorization and don't know up-to-date nonce anymore + is_nil(status) -> + {entry, {updated_authorizations, known_nonces |> Map.delete(authorization.authority)}} + + Map.has_key?(known_nonces, authorization.authority) and + nonce != Map.get(known_nonces, authorization.authority) -> + new_authorization = %{authorization | status: :invalid_nonce} + + {{:authorization, new_authorization}, {[new_authorization | updated_authorizations], known_nonces}} + + Map.has_key?(known_nonces, authorization.authority) and + nonce == Map.get(known_nonces, authorization.authority) -> + new_authorization = %{authorization | status: :ok} + + { + {:authorization, new_authorization}, + {[new_authorization | updated_authorizations], + known_nonces |> Map.put(authorization.authority, nonce + 1)} + } + + true -> + # we couldn't validate authorization due to unknown nonce, we don't know up-to-date nonce anymore + {entry, {updated_authorizations, known_nonces |> Map.delete(authorization.authority)}} + end + end) + + {entry |> Map.put(:entries, new_entries), updated_authorizations |> Enum.reverse()} + end + + @spec fetch_nonces([%{block_number: Block.block_number(), address_hash: Hash.Address.t()}], keyword()) :: + {:ok, %{Block.block_number() => %{Hash.Address.t() => non_neg_integer()}}} | {:error, any()} + defp fetch_nonces([], _json_rpc_named_arguments), do: {:ok, %{}} + + defp fetch_nonces(entries, json_rpc_named_arguments) do + # fetch nonces for at the end of the previous block, to know starting nonces for the current block + entries + |> Enum.map( + &%{ + block_quantity: integer_to_quantity(BlockNumberHelper.previous_block_number(&1.block_number)), + address: to_string(&1.address_hash) + } + ) + |> EthereumJSONRPC.fetch_nonces(json_rpc_named_arguments) + |> case do + {:ok, %{params_list: params}} -> + {:ok, nonces_map_from_params(params)} + + error -> + error + end + end + + defp nonces_map_from_params(params) do + Enum.reduce(params, %{}, fn %{address: address, block_number: block_number, nonce: nonce}, acc -> + case Hash.Address.cast(address) do + {:ok, address_hash} -> + acc + |> Map.update( + BlockNumberHelper.next_block_number(block_number), + %{address_hash => nonce}, + &Map.put(&1, address_hash, nonce) + ) + + _ -> + acc + end + end) + end + + # Imports all updated signed authorizations, updates relevant addresses and proxy implementations. + @spec import_authorizations([SignedAuthorization.t()]) :: :ok | {:error, any()} + defp import_authorizations(signed_authorizations) do + address_params = + signed_authorizations + |> Enum.filter(&(&1.status == :ok)) + # keeps only the latest record for each authority address + |> Enum.into(%{}, &{&1.authority, SignedAuthorization.to_address_params(&1)}) + |> Map.values() + + # Fetch latest successful authorizations for each authority address + # and skip importing addresses for which newer authorization already exists. + # Will only work correctly with concurrency of the fetcher set to 1. + # Alternative concurrent approach may be considered in the future by moving + # this check to the DB level inside an "on conflict" clause and introduction + # of the `last_code_change_nonce` column. + latest_authorization_nonces = + address_params + |> Enum.map(& &1.hash) + |> address_hashes_to_latest_authorizations() + |> Enum.into(%{}, &{&1.authority, &1.nonce}) + + addresses = + address_params + |> Enum.filter(fn %{hash: hash, nonce: nonce} -> + Decimal.gt?(nonce, Map.get(latest_authorization_nonces, hash, -1)) + end) + + case Chain.import(%{ + addresses: %{ + params: addresses, + on_conflict: address_on_conflict(), + fields_to_update: [:contract_code, :nonce] + }, + signed_authorizations: %{ + params: signed_authorizations |> Enum.map(&SignedAuthorization.to_map/1), + on_conflict: {:replace, [:status, :updated_at]} + } + }) do + {:ok, %{addresses: addresses}} -> + Accounts.drop(addresses) + + # Update EIP7702 proxy addresses to avoid inconsistencies between addresses and proxy_implementations tables. + {contract_addresses, eoa_addresses} = addresses |> Enum.split_with(&Address.smart_contract?/1) + + if !Enum.empty?(eoa_addresses) do + eoa_addresses + |> Enum.map(& &1.hash) + |> Implementation.delete_implementations() + end + + if !Enum.empty?(contract_addresses) do + contract_addresses + |> Implementation.upsert_eip7702_implementations() + end + + :ok + + {:ok, %{}} -> + :ok + + {:error, step, reason, _changes_so_far} -> + Logger.error( + fn -> + [ + @failed_to_import, + inspect(reason) + ] + end, + step: step + ) + + {:error, reason} + + {:error, reason} -> + Logger.error(fn -> + [ + @failed_to_import, + inspect(reason) + ] + end) + + {:error, reason} + end + end + + defp address_on_conflict do + from(address in Address, + update: [ + set: [ + contract_code: fragment("EXCLUDED.contract_code"), + nonce: fragment("GREATEST(EXCLUDED.nonce, ?)", address.nonce), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", address.updated_at) + ] + ] + ) + end + + defp defaults do + [ + poll: false, + flush_interval: :timer.seconds(3), + max_concurrency: @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor, + metadata: [fetcher: :signed_authorization_status] + ] + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index f453c349d743..0dd8ab3f0403 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -252,6 +252,8 @@ defmodule Indexer.Supervisor do ]}, {Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.Beacon.Blob.Supervisor, [[memory_monitor: memory_monitor]]}, + {Indexer.Fetcher.SignedAuthorizationStatus.Supervisor, + [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, # Out-of-band fetchers {EmptyBlocksSanitizer.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, diff --git a/apps/indexer/lib/indexer/transform/signed_authorizations.ex b/apps/indexer/lib/indexer/transform/signed_authorizations.ex index b7e5f8e50980..be85fbc3dcff 100644 --- a/apps/indexer/lib/indexer/transform/signed_authorizations.ex +++ b/apps/indexer/lib/indexer/transform/signed_authorizations.ex @@ -30,12 +30,24 @@ defmodule Indexer.Transform.SignedAuthorizations do &(&1.authorization_list |> Enum.with_index() |> Enum.map(fn {authorization, index} -> - authorization - |> Map.merge(%{ - transaction_hash: &1.hash, - index: index, - authority: recover_authority(authorization) - }) + new_authorization = + authorization + |> Map.merge(%{ + transaction_hash: &1.hash, + index: index, + authority: recover_authority(authorization) + }) + + # we can immediately do some basic validation that doesn't require any extra JSON-RPC requests + # full validation for :invalid_nonce is deferred to async fetcher (so :ok is replaced with nil) + status = + case SignedAuthorization.basic_validate(new_authorization) do + :ok -> nil + status -> status + end + + new_authorization + |> Map.put(:status, status) end)) ) end diff --git a/apps/indexer/test/indexer/fetcher/signed_authorization_status_test.exs b/apps/indexer/test/indexer/fetcher/signed_authorization_status_test.exs new file mode 100644 index 000000000000..06721805cdb0 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/signed_authorization_status_test.exs @@ -0,0 +1,313 @@ +defmodule Indexer.Fetcher.SignedAuthorizationStatusTest do + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + import Mox + + alias Explorer.Chain.Cache.ChainId + alias Explorer.Chain.{Data, SignedAuthorization, SmartContract.Proxy.Models.Implementation} + alias Explorer.TestHelper + alias Indexer.BufferedTask + alias Indexer.Fetcher.SignedAuthorizationStatus + + @moduletag :capture_log + + # MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to + # use expectations and stubs from test's pid. + setup :set_mox_global + + setup :verify_on_exit! + + setup do + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + + :ok + end + + describe "async_fetch/1" do + test "fetched authorization statuses, proxy implementations and nonces", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + SignedAuthorizationStatus.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + block1 = insert(:block, number: 1010) + block2 = insert(:block, number: 1020) + + delegate1 = insert(:address) + delegate2 = insert(:address) + + address1 = insert(:address) + address2 = insert(:address) + address3 = insert(:address) + address4 = insert(:address) + address5 = insert(:address) + + transaction11 = insert(:transaction, from_address: address1, nonce: 100, type: 2) |> with_block(block1) + transaction12 = insert(:transaction, from_address: address1, nonce: 101, type: 2) |> with_block(block1) + transaction13 = insert(:transaction, from_address: address2, nonce: 200, type: 4) |> with_block(block1) + transaction14 = insert(:transaction, from_address: address1, nonce: 103, type: 4) |> with_block(block1) + transaction15 = insert(:transaction, from_address: address5, nonce: 500, type: 4) |> with_block(block1) + + transaction21 = insert(:transaction, from_address: address3, nonce: 300, type: 4) |> with_block(block2) + transaction22 = insert(:transaction, from_address: address1, nonce: 105, type: 2) |> with_block(block2) + transaction23 = insert(:transaction, from_address: address5, nonce: 502, type: 4) |> with_block(block2) + + # invalid signature + auth1 = + insert(:signed_authorization, + transaction: transaction13, + index: 0, + address: delegate1.hash, + authority: nil, + nonce: 102 + ) + + # invalid chain id + auth2 = + insert(:signed_authorization, + transaction: transaction13, + index: 1, + address: delegate1.hash, + authority: address1.hash, + nonce: 102, + chain_id: 123 + ) + + # known nonce, invalid nonce + auth3 = + insert(:signed_authorization, + transaction: transaction13, + index: 2, + address: delegate1.hash, + authority: address1.hash, + nonce: 77 + ) + + # known nonce, all ok + auth4 = + insert(:signed_authorization, + transaction: transaction13, + index: 3, + address: delegate1.hash, + authority: address1.hash, + nonce: 102 + ) + + # known nonce, all ok + auth5 = + insert(:signed_authorization, + transaction: transaction13, + index: 4, + address: delegate1.hash, + authority: address2.hash, + nonce: 201 + ) + + # known nonce, all ok + auth6 = + insert(:signed_authorization, + transaction: transaction13, + index: 5, + address: delegate2.hash, + authority: address2.hash, + nonce: 202 + ) + + # unknown nonce, invalid nonce + auth7 = + insert(:signed_authorization, + transaction: transaction13, + index: 6, + address: delegate1.hash, + authority: address3.hash, + nonce: 77 + ) + + # unknown nonce, all ok + auth8 = + insert(:signed_authorization, + transaction: transaction13, + index: 7, + address: delegate1.hash, + authority: address4.hash, + nonce: 400 + ) + + # unknown nonce, all ok + auth9 = + insert(:signed_authorization, + transaction: transaction14, + index: 0, + address: delegate2.hash, + authority: address4.hash, + nonce: 401 + ) + + # known nonce, all ok + auth10 = + insert(:signed_authorization, + transaction: transaction15, + index: 0, + address: delegate1.hash, + authority: address5.hash, + nonce: 501 + ) + + # unknown nonce, all ok + auth11 = + insert(:signed_authorization, + transaction: transaction21, + index: 0, + address: delegate2.hash, + authority: address1.hash, + nonce: 104 + ) + + # known nonce, all ok + auth12 = + insert(:signed_authorization, + transaction: transaction23, + index: 0, + address: "0x0000000000000000000000000000000000000000", + authority: address5.hash, + nonce: 503 + ) + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + block1_quantity = integer_to_quantity(block1.number - 1) + block2_quantity = integer_to_quantity(block2.number - 1) + address1_string = to_string(address1.hash) + address3_string = to_string(address3.hash) + address4_string = to_string(address4.hash) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id2, + method: "eth_getTransactionCount", + params: [^address4_string, ^block1_quantity] + }, + %{ + id: id1, + method: "eth_getTransactionCount", + params: [^address3_string, ^block1_quantity] + }, + %{ + id: id3, + method: "eth_getTransactionCount", + params: [^address1_string, ^block2_quantity] + } + ], + _options -> + {:ok, + [ + %{id: id1, result: integer_to_quantity(300)}, + %{id: id2, result: integer_to_quantity(400)}, + %{id: id3, result: integer_to_quantity(104)} + ]} + end) + + TestHelper.get_chain_id_mock() + end + + addresses = [address1, address2, address3, address4, address5] + + transactions = [ + transaction11, + transaction12, + transaction13, + transaction14, + transaction15, + transaction21, + transaction22, + transaction23 + ] + + auths = [auth1, auth2, auth3, auth4, auth5, auth6, auth7, auth8, auth9, auth10, auth11, auth12] + + assert :ok = SignedAuthorizationStatus.async_fetch(transactions, auths, false) + + wait_for_tasks(SignedAuthorizationStatus) + + auths = from(auth in SignedAuthorization, order_by: [asc: :transaction_hash, asc: :index]) |> Repo.all() + + assert auths |> Enum.map(& &1.status) == [ + :invalid_signature, + :invalid_chain_id, + :invalid_nonce, + :ok, + :ok, + :ok, + :invalid_nonce, + :ok, + :ok, + :ok, + :ok, + :ok + ] + + addresses = + addresses + |> Repo.reload() + |> Repo.preload(Implementation.proxy_implementations_association()) + + assert addresses |> Enum.map(& &1.nonce) == [ + 104, + 202, + nil, + 401, + 503 + ] + + assert addresses |> Enum.map(& &1.contract_code) == [ + %Data{bytes: <<239, 1, 0>> <> delegate2.hash.bytes}, + %Data{bytes: <<239, 1, 0>> <> delegate2.hash.bytes}, + nil, + %Data{bytes: <<239, 1, 0>> <> delegate2.hash.bytes}, + nil + ] + + assert addresses |> Enum.map(&((&1.proxy_implementations || %{}) |> Map.get(:address_hashes))) == [ + [delegate2.hash], + [delegate2.hash], + nil, + [delegate2.hash], + nil + ] + end + end + + defp wait_for_tasks(buffered_task) do + wait_until(:timer.seconds(10), fn -> + counts = BufferedTask.debug_count(buffered_task) + counts.buffer == 0 and counts.tasks == 0 + end) + end + + defp wait_until(timeout, producer) do + parent = self() + ref = make_ref() + + spawn(fn -> do_wait_until(parent, ref, producer) end) + + receive do + {^ref, :ok} -> :ok + after + timeout -> exit(:timeout) + end + end + + defp do_wait_until(parent, ref, producer) do + if producer.() do + send(parent, {ref, :ok}) + else + :timer.sleep(100) + do_wait_until(parent, ref, producer) + end + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/signed_authorization_status_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/signed_authorization_status_supervisor_case.ex new file mode 100644 index 000000000000..371f9e49b32d --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/signed_authorization_status_supervisor_case.ex @@ -0,0 +1,17 @@ +defmodule Indexer.Fetcher.SignedAuthorizationStatus.Supervisor.Case do + alias Indexer.Fetcher.SignedAuthorizationStatus + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + fetcher_arguments, + flush_interval: 50, + max_batch_size: 2, + max_concurrency: 2 + ) + + [merged_fetcher_arguments] + |> SignedAuthorizationStatus.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 609bf4cb2ba6..c6fff02146af 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1128,6 +1128,9 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE", 1000) +config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) + config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.DisputeGame.Supervisor, enabled: ConfigHelper.chain_type() == :optimism diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index f95ffe41bbb1..bd5bb6b1cf22 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -207,6 +207,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_CONCURRENCY= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE= +# INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= From 6997f3ab7790b89b52f974a36baa904ce9cf5d8a Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 15 Jul 2025 14:59:15 +0300 Subject: [PATCH 0166/1006] fix: change default ordering in `/api/v2/smart-contracts` (#12767) * fix: change default ordering in `/api/v2/smart-contracts` * fix: APIv1 verified contracts controller test * refactor: clean up imports in VerifiedContractsController --- .../api/v2/smart_contract_controller.ex | 84 ++++++++++++++----- .../verified_contracts_controller.ex | 10 +-- .../api/v2/smart_contract_controller_test.exs | 4 +- .../verified_contracts_controller_test.exs | 11 +-- .../lib/explorer/chain/smart_contract.ex | 22 +++-- .../chain/smart_contract/legacy_helper.ex | 1 + 6 files changed, 93 insertions(+), 39 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 5d3fc3609bec..3fa2e9d968c8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -23,7 +23,11 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do default_paging_options: 0 ] - import Explorer.Helper, only: [parse_integer: 1] + import Explorer.Helper, + only: [ + parse_integer: 1, + safe_parse_non_negative_integer: 1 + ] alias BlockScoutWeb.AccessHelper alias Explorer.Chain @@ -124,12 +128,24 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do addresses_plus_one = SmartContract.verified_contract_addresses(full_options) {addresses, next_page} = split_list_by_page(addresses_plus_one) + # If no sorting options are provided, we sort by `id` descending only. If + # there are some sorting options supplied, we sort by `:hash` ascending as a + # secondary key. + pager = + full_options + |> Keyword.get(:sorting) + |> if do + &smart_contract_addresses_paging_params/1 + else + &%{smart_contract_id: &1.smart_contract.id} + end + next_page_params = next_page |> next_page_params( addresses, delete_parameters_from_next_page_params(params), - &smart_contract_addresses_paging_params/1 + pager ) conn @@ -146,18 +162,33 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do ## Returns If 'hash', 'transaction_count', and 'coin_balance' parameters are provided, - uses them as pagination keys. Otherwise, returns default paging options. + uses them as pagination keys for address-based sorting. If 'smart_contract_id' + parameter is provided, uses it as pagination key for smart contract ID-based + sorting. Otherwise, returns default paging options. ## Examples iex> smart_contract_addresses_paging_options(%{"hash" => "0x123...", "transaction_count" => "100", "coin_balance" => "1000"}) [paging_options: %{key: %{hash: ..., transactions_count: 100, fetched_coin_balance: 1000}}] + + iex> smart_contract_addresses_paging_options(%{"smart_contract_id" => "42"}) + [paging_options: %{key: %{id: 42}}] + + iex> smart_contract_addresses_paging_options(%{}) + [paging_options: %{}] """ @spec smart_contract_addresses_paging_options(%{required(String.t()) => String.t()}) :: [paging_options: map()] def smart_contract_addresses_paging_options(params) do - options = - with %{"hash" => hash_string} <- params, - {:ok, address_hash} <- Chain.string_to_address_hash(hash_string) do + options = do_smart_contract_addresses_paging_options(params) + [paging_options: default_paging_options() |> Map.merge(options)] + end + + @spec do_smart_contract_addresses_paging_options(%{required(String.t()) => String.t()}) :: map() + defp do_smart_contract_addresses_paging_options(%{"hash" => hash_string} = params) do + hash_string + |> Chain.string_to_address_hash() + |> case do + {:ok, address_hash} -> transactions_count = parse_integer(params["transaction_count"]) coin_balance = parse_integer(params["coin_balance"]) @@ -168,13 +199,23 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do fetched_coin_balance: coin_balance } } - else - _ -> %{} - end - [paging_options: default_paging_options() |> Map.merge(options)] + _ -> + %{} + end + end + + defp do_smart_contract_addresses_paging_options(%{"smart_contract_id" => smart_contract_id}) do + smart_contract_id + |> safe_parse_non_negative_integer() + |> case do + {:ok, id} -> %{key: %{id: id}} + _ -> %{} + end end + defp do_smart_contract_addresses_paging_options(_params), do: %{} + @doc """ Extracts pagination parameters from an Address struct for use in the next page URL. @@ -208,16 +249,21 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do {:sorting, list()} ] defp smart_contract_addresses_sorting(%{"sort" => sort_field, "order" => order}) do - sorting = - case {sort_field, order} do - {"balance", "asc"} -> [{:asc_nulls_first, :fetched_coin_balance}] - {"balance", "desc"} -> [{:desc_nulls_last, :fetched_coin_balance}] - {"transactions_count", "asc"} -> [{:asc_nulls_first, :transactions_count}] - {"transactions_count", "desc"} -> [{:desc_nulls_last, :transactions_count}] - _ -> [] - end + {sort_field, order} + |> case do + {"balance", "asc"} -> {:ok, [{:asc_nulls_first, :fetched_coin_balance}]} + {"balance", "desc"} -> {:ok, [{:desc_nulls_last, :fetched_coin_balance}]} + {"transactions_count", "asc"} -> {:ok, [{:asc_nulls_first, :transactions_count}]} + {"transactions_count", "desc"} -> {:ok, [{:desc_nulls_last, :transactions_count}]} + _ -> :error + end + |> case do + {:ok, sorting_params} -> + [sorting: sorting_params] - [sorting: sorting] + :error -> + [] + end end defp smart_contract_addresses_sorting(_), do: [] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex index efaf6a85dbd2..1e3235600c08 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex @@ -7,10 +7,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do import BlockScoutWeb.PagingHelper, only: [current_filter: 1, search_query: 1] import BlockScoutWeb.API.V2.SmartContractController, - only: [ - smart_contract_addresses_paging_options: 1, - smart_contract_addresses_paging_params: 1 - ] + only: [smart_contract_addresses_paging_options: 1] alias BlockScoutWeb.{Controller, VerifiedContractsView} alias Explorer.Chain @@ -18,7 +15,8 @@ defmodule BlockScoutWeb.VerifiedContractsController do alias Phoenix.View @necessity_by_association %{ - :token => :optional + :token => :optional, + :smart_contract => :optional } def index(conn, %{"type" => "JSON"} = params) do @@ -48,7 +46,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do next_page, verified_contracts, params, - &smart_contract_addresses_paging_params(&1.address) + &%{id: &1.id} ) do nil -> nil next_page_params -> verified_contracts_path(conn, :index, Map.delete(next_page_params, "type")) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index d486ce0ad3c5..40d1b59c06ea 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -1617,7 +1617,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do for _ <- 0..50 do insert(:smart_contract) end - |> Enum.sort_by(& &1.address_hash.bytes, :desc) + |> Enum.sort_by(& &1.id) request = get(conn, "/api/v2/smart-contracts") assert response = json_response(request, 200) @@ -1634,7 +1634,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do for _ <- 0..50 do insert(:smart_contract) end - |> Enum.sort_by(& &1.address_hash.bytes, :desc) + |> Enum.sort_by(& &1.id) ordering_params = %{"sort" => "foo", "order" => "bar"} diff --git a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs index e0ca5bd1292b..3c9d077903ad 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/verified_contracts_controller_test.exs @@ -55,21 +55,18 @@ defmodule BlockScoutWeb.VerifiedContractsControllerTest do end test "next_page_path exist if not on last page", %{conn: conn} do - %SmartContract{address_hash: address_hash} = + %SmartContract{id: id} = 60 |> insert_list(:smart_contract) - |> Enum.reverse() + |> Enum.sort_by(& &1.id, :asc) |> Enum.fetch!(10) conn = get(conn, verified_contracts_path(conn, :index), %{"type" => "JSON"}) expected_path = verified_contracts_path(conn, :index, - coin_balance: nil, - hash: address_hash, - items_count: "50", - transaction_count: nil, - transactions_count: nil + id: id, + items_count: "50" ) assert Map.get(json_response(conn, 200), "next_page_path") == expected_path diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 8e081523610c..0b4db2173569 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -249,8 +249,6 @@ defmodule Explorer.Chain.SmartContract do @dead_address_hash_string end - @default_sorting [asc: :hash] - @typedoc """ The name of a parameter to a function or event. """ @@ -1444,7 +1442,20 @@ defmodule Explorer.Chain.SmartContract do def verified_contract_addresses(options \\ []) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) - sorting_options = Keyword.get(options, :sorting, []) + + # If no sorting options are provided, we sort by `:id` descending only. If + # there are some sorting options supplied, we sort by `:hash` ascending as a + # secondary key. + {sorting_options, default_sorting_options} = + options + |> Keyword.get(:sorting) + |> case do + nil -> + {[], [{:desc, :id, :smart_contract}]} + + options -> + {options, [asc: :hash]} + end addresses_query = if background_migrations_finished?() do @@ -1456,8 +1467,8 @@ defmodule Explorer.Chain.SmartContract do addresses_query |> ExplorerHelper.maybe_hide_scam_addresses(:hash, options) - |> SortingHelper.apply_sorting(sorting_options, @default_sorting) - |> SortingHelper.page_with_sorting(paging_options, sorting_options, @default_sorting) + |> SortingHelper.apply_sorting(sorting_options, default_sorting_options) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting_options) |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() end @@ -1501,6 +1512,7 @@ defmodule Explorer.Chain.SmartContract do as: :address, where: address.verified == true, inner_lateral_join: contract in ^smart_contracts_subquery, + as: :smart_contract, on: true, select: address, preload: [smart_contract: contract] diff --git a/apps/explorer/lib/explorer/chain/smart_contract/legacy_helper.ex b/apps/explorer/lib/explorer/chain/smart_contract/legacy_helper.ex index 98baffb2d5f5..26e54602bf7e 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/legacy_helper.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/legacy_helper.ex @@ -36,6 +36,7 @@ defmodule Explorer.Chain.SmartContract.LegacyHelper do from( address in Address, join: contract in SmartContract, + as: :smart_contract, on: address.hash == contract.address_hash, preload: [:smart_contract] ) From 41f6db26b1319be0f8be43f3b8806dd84d60eee3 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:27:54 +0400 Subject: [PATCH 0167/1006] chore: Merge adjacent missing block ranges (#12778) * chore: Merge adjacent missing block ranges * Add missing clause to MissingBlockRange --- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 1 + .../merge_adjacent_missing_block_ranges.ex | 71 +++++++++ .../explorer/utility/missing_block_range.ex | 140 ++++++++++++------ ...rge_adjacent_missing_block_ranges_test.exs | 50 +++++++ .../utility/missing_block_range_test.exs | 70 ++------- config/runtime.exs | 3 + docker-compose/envs/common-blockscout.env | 1 + 9 files changed, 238 insertions(+), 102 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/merge_adjacent_missing_block_ranges.ex create mode 100644 apps/explorer/test/explorer/migrator/merge_adjacent_missing_block_ranges_test.exs diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 656a83aa0c9a..b99c37d2c383 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -133,7 +133,8 @@ for migrator <- [ Explorer.Migrator.SanitizeEmptyContractCodeAddresses, Explorer.Migrator.BackfillMetadataURL, Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, - Explorer.Migrator.ReindexDuplicatedInternalTransactions + Explorer.Migrator.ReindexDuplicatedInternalTransactions, + Explorer.Migrator.MergeAdjacentMissingBlockRanges ] do config :explorer, migrator, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 9c7d9115959f..f0ffa6c19f4f 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -72,6 +72,7 @@ for migrator <- [ Explorer.Migrator.BackfillMetadataURL, Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, Explorer.Migrator.ReindexDuplicatedInternalTransactions, + Explorer.Migrator.MergeAdjacentMissingBlockRanges, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index e2b3b8e116dc..b9dab2491e26 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -191,6 +191,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.SanitizeEmptyContractCodeAddresses, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexDuplicatedInternalTransactions, :indexer), + configure_mode_dependent_process(Explorer.Migrator.MergeAdjacentMissingBlockRanges, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, diff --git a/apps/explorer/lib/explorer/migrator/merge_adjacent_missing_block_ranges.ex b/apps/explorer/lib/explorer/migrator/merge_adjacent_missing_block_ranges.ex new file mode 100644 index 000000000000..30332b7afa57 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/merge_adjacent_missing_block_ranges.ex @@ -0,0 +1,71 @@ +defmodule Explorer.Migrator.MergeAdjacentMissingBlockRanges do + @moduledoc """ + Merges adjacent missing block ranges (like 10..5, 4..3) into one (10..3). + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + alias Explorer.Utility.MissingBlockRange + + @migration_name "merge_adjacent_missing_block_ranges" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() + + data = + unprocessed_data_query() + |> select([m1, _m2], m1) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {data, state} + end + + @impl FillingMigration + def unprocessed_data_query do + from(m1 in MissingBlockRange, + inner_join: m2 in MissingBlockRange, + on: + m1.from_number + 1 == m2.to_number and + ((is_nil(m1.priority) and is_nil(m2.priority)) or m1.priority == m2.priority) + ) + end + + @impl FillingMigration + def update_batch(ranges_batch) do + {priority_ranges, non_priority_ranges, delete_ids} = + Enum.reduce(ranges_batch, {[], [], []}, fn range, {priority_acc, non_priority_acc, delete_acc} -> + if is_nil(range.priority) do + {priority_acc, [range.from_number..range.to_number | non_priority_acc], [range.id | delete_acc]} + else + {[range.from_number..range.to_number | priority_acc], non_priority_acc, [range.id | delete_acc]} + end + end) + + Repo.transaction(fn -> + MissingBlockRange + |> where([m], m.id in ^Enum.uniq(delete_ids)) + |> Repo.delete_all(timeout: :infinity) + + priority_ranges + |> RangesHelper.sanitize_ranges() + |> MissingBlockRange.save_batch(1) + + non_priority_ranges + |> RangesHelper.sanitize_ranges() + |> MissingBlockRange.save_batch(nil) + end) + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/lib/explorer/utility/missing_block_range.ex b/apps/explorer/lib/explorer/utility/missing_block_range.ex index 8b8cc9e7e6d1..87d8b59dd9ee 100644 --- a/apps/explorer/lib/explorer/utility/missing_block_range.ex +++ b/apps/explorer/lib/explorer/utility/missing_block_range.ex @@ -160,7 +160,7 @@ defmodule Explorer.Utility.MissingBlockRange do case {lower_range, higher_range} do {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> if is_nil(same_range.priority) && not is_nil(priority) do - delete_range(same_range.from_number..same_range.to_number) + Repo.delete(same_range) inside_range_params = %{from_number: max_number, to_number: min_number, priority: priority} insert_range(inside_range_params) @@ -171,25 +171,23 @@ defmodule Explorer.Utility.MissingBlockRange do {%__MODULE__{} = range, nil} -> delete_less_priority_range(range, priority) - fill_ranges_between(max_number, range.from_number + 1, priority) - split_right_range_priorities(range, priority, min_number) + fill_ranges_between(max_number, range.from_number + 1, priority) {nil, %__MODULE__{} = range} -> delete_less_priority_range(range, priority) - fill_ranges_between(range.to_number - 1, min_number, priority) - split_left_range_priorities(range, priority, max_number) + fill_ranges_between(range.to_number - 1, min_number, priority) {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> delete_less_priority_range(range_2, priority) delete_less_priority_range(range_1, priority) - fill_ranges_between(range_2.to_number - 1, range_1.from_number + 1, priority) - split_left_range_priorities(range_2, priority, max_number) split_right_range_priorities(range_1, priority, min_number) + fill_ranges_between(range_2.to_number - 1, range_1.from_number + 1, priority) + {nil, nil} -> fill_ranges_between(max_number, min_number, priority) end @@ -397,10 +395,11 @@ defmodule Explorer.Utility.MissingBlockRange do - A single range record of `Explorer.Utility.MissingBlockRange` that includes the given block number, or `nil` if no such range is found. """ - @spec get_range_by_block_number(Block.block_number()) :: nil | __MODULE__.t() - def get_range_by_block_number(number) do + @spec get_range_by_block_number(Block.block_number(), integer() | nil | :not_specified) :: nil | __MODULE__.t() + def get_range_by_block_number(number, priority \\ :not_specified) do number |> include_bound_query() + |> priority_query(priority) |> Repo.one() end @@ -417,8 +416,7 @@ defmodule Explorer.Utility.MissingBlockRange do if Enum.empty?(priority_ranges) do # if no priority ranges inside the requested interval, fill the full range - range = %{from_number: from, to_number: to, priority: priority} - insert_range(range) + insert_or_update_adjacent_ranges(from, to, priority, :both) else full_range_map_set = from @@ -426,44 +424,56 @@ defmodule Explorer.Utility.MissingBlockRange do |> Enum.to_list() |> MapSet.new() - ranges_to_fill = - priority_ranges - |> Enum.reduce(full_range_map_set, fn range, acc -> - map_set = - range.from_number - |> Range.new(range.to_number) - |> Enum.to_list() - |> MapSet.new() - - acc - |> MapSet.difference(map_set) - end) - |> MapSet.to_list() - |> Enum.sort_by(& &1, :desc) - |> Enum.reduce({[], {nil, nil}}, fn num, {ranges, {start_range, end_range}} -> - if is_nil(start_range) do - {ranges, {num, num}} - else - # credo:disable-for-next-line Credo.Check.Refactor.Nesting - if end_range - num > 1 do - {[Range.new(start_range, end_range) | ranges], {num, num}} - else - {ranges, {start_range, num}} - end - end - end) - |> then(fn {ranges, {start_range, end_range}} -> - if not is_nil(start_range) && not is_nil(end_range) do - [Range.new(start_range, end_range) | ranges] + priority_ranges + |> Enum.reduce(full_range_map_set, fn range, acc -> + map_set = + range.from_number + |> Range.new(range.to_number) + |> Enum.to_list() + |> MapSet.new() + + acc + |> MapSet.difference(map_set) + end) + |> MapSet.to_list() + |> Enum.sort_by(& &1, :desc) + |> Enum.reduce({[], {nil, nil}}, fn num, {ranges, {start_range, end_range}} -> + if is_nil(start_range) do + {ranges, {num, num}} + else + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if end_range - num > 1 do + {[Range.new(start_range, end_range) | ranges], {num, num}} else - ranges + {ranges, {start_range, num}} end - end) + end + end) + |> then(fn {ranges, {start_range, end_range}} -> + if not is_nil(start_range) && not is_nil(end_range) do + [Range.new(start_range, end_range) | ranges] + else + ranges + end + end) + |> then(fn + [range | []] -> + insert_or_update_adjacent_ranges(range.first, range.last, priority, :both) - ranges_to_fill - |> Enum.each(fn %Range{first: first, last: last} -> - range_params = %{from_number: first, to_number: last, priority: priority} - insert_range(range_params) + [lowest_range | rest_ranges] -> + [highest_range | middle_ranges] = Enum.reverse(rest_ranges) + + insert_or_update_adjacent_ranges(lowest_range.first, lowest_range.last, priority, :down) + insert_or_update_adjacent_ranges(highest_range.first, highest_range.last, priority, :up) + + middle_ranges + |> Enum.each(fn %Range{first: first, last: last} -> + range_params = %{from_number: first, to_number: last, priority: priority} + insert_range(range_params) + end) + + [] -> + :ok end) end @@ -472,6 +482,40 @@ defmodule Explorer.Utility.MissingBlockRange do defp fill_ranges_between(_from, _to, _priority), do: :ok + defp insert_or_update_adjacent_ranges(from, to, priority, :both) do + upper_range = get_range_by_block_number(from + 1, priority) + lower_range = get_range_by_block_number(to - 1, priority) + + case {lower_range, upper_range} do + {nil, nil} -> + insert_range(%{from_number: from, to_number: to, priority: priority}) + + {_, nil} -> + update_range(lower_range, %{from_number: from}) + + {nil, _} -> + update_range(upper_range, %{to_number: to}) + + {_, _} -> + Repo.delete(lower_range) + update_range(upper_range, %{to_number: lower_range.to_number}) + end + end + + defp insert_or_update_adjacent_ranges(from, to, priority, direction) do + {range, update_params} = + case direction do + :up -> {get_range_by_block_number(from + 1, priority), %{to_number: to}} + :down -> {get_range_by_block_number(to - 1, priority), %{from_number: from}} + end + + if is_nil(range) do + insert_range(%{from_number: from, to_number: to, priority: priority}) + else + update_range(range, update_params) + end + end + defp select_all_ranges_within_the_range(from, to) do __MODULE__ |> where([r], fragment("int4range(?, ?, '[]') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) @@ -604,6 +648,10 @@ defmodule Explorer.Utility.MissingBlockRange do from(r in __MODULE__, where: fragment("int4range(?, ?, '[]') @> ?::int", r.to_number, r.from_number, ^bound)) end + defp priority_query(query, :not_specified), do: query + defp priority_query(query, nil), do: where(query, [m], is_nil(m.priority)) + defp priority_query(query, _priority), do: where(query, [m], not is_nil(m.priority)) + defp numbers_to_ranges([]), do: [] defp numbers_to_ranges(numbers) when is_list(numbers) do diff --git a/apps/explorer/test/explorer/migrator/merge_adjacent_missing_block_ranges_test.exs b/apps/explorer/test/explorer/migrator/merge_adjacent_missing_block_ranges_test.exs new file mode 100644 index 000000000000..2f8ac7afa7f3 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/merge_adjacent_missing_block_ranges_test.exs @@ -0,0 +1,50 @@ +defmodule Explorer.Migrator.MergeAdjacentMissingBlockRangesTest do + use Explorer.DataCase, async: false + + alias Explorer.Migrator.MergeAdjacentMissingBlockRanges + alias Explorer.Repo + alias Explorer.Utility.MissingBlockRange + + test "Merges adjacent ranges" do + Repo.delete_all(MissingBlockRange) + + insert(:missing_block_range, from_number: 100, to_number: 70, priority: nil) + insert(:missing_block_range, from_number: 69, to_number: 60, priority: nil) + insert(:missing_block_range, from_number: 59, to_number: 50, priority: 1) + insert(:missing_block_range, from_number: 45, to_number: 45, priority: nil) + insert(:missing_block_range, from_number: 40, to_number: 30, priority: 1) + insert(:missing_block_range, from_number: 29, to_number: 20, priority: 1) + insert(:missing_block_range, from_number: 19, to_number: 10, priority: nil) + insert(:missing_block_range, from_number: 9, to_number: 5, priority: nil) + insert(:missing_block_range, from_number: 4, to_number: 0, priority: nil) + + MergeAdjacentMissingBlockRanges.start_link([]) + Process.sleep(100) + + ranges = Repo.all(MissingBlockRange) + + assert length(ranges) == 5 + + assert Enum.any?(ranges, fn range -> + range.from_number == 100 and range.to_number == 60 and range.priority == nil + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 59 and range.to_number == 50 and range.priority == 1 + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 45 and range.to_number == 45 and range.priority == nil + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 40 and range.to_number == 20 and range.priority == 1 + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 19 and range.to_number == 0 and range.priority == nil + end) + + Repo.delete_all(MissingBlockRange) + end +end diff --git a/apps/explorer/test/explorer/utility/missing_block_range_test.exs b/apps/explorer/test/explorer/utility/missing_block_range_test.exs index bde341055c78..e2dbaee77d2c 100644 --- a/apps/explorer/test/explorer/utility/missing_block_range_test.exs +++ b/apps/explorer/test/explorer/utility/missing_block_range_test.exs @@ -1,4 +1,4 @@ -defmodule Explorer.Utility.TestMissingBlockRange do +defmodule Explorer.Utility.MissingBlockRangeTest do use ExUnit.Case, async: true alias Explorer.Utility.MissingBlockRange @@ -82,14 +82,10 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 5 - - assert Enum.any?(ranges, fn range -> - range.from_number == 15 and range.to_number == 12 and range.priority == nil - end) + assert length(ranges) == 4 assert Enum.any?(ranges, fn range -> - range.from_number == 11 and range.to_number == 11 and range.priority == nil + range.from_number == 15 and range.to_number == 11 and range.priority == nil end) assert Enum.any?(ranges, fn range -> @@ -118,22 +114,14 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 4 + assert length(ranges) == 2 assert Enum.any?(ranges, fn range -> range.from_number == 15 and range.to_number == 14 and range.priority == nil end) assert Enum.any?(ranges, fn range -> - range.from_number == 13 and range.to_number == 12 and range.priority == 1 - end) - - assert Enum.any?(ranges, fn range -> - range.from_number == 11 and range.to_number == 7 and range.priority == 1 - end) - - assert Enum.any?(ranges, fn range -> - range.from_number == 6 and range.to_number == 3 and range.priority == 1 + range.from_number == 13 and range.to_number == 3 and range.priority == 1 end) end @@ -269,14 +257,10 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 4 - - assert Enum.any?(ranges, fn range -> - range.from_number == 112 and range.to_number == 86 and range.priority == nil - end) + assert length(ranges) == 3 assert Enum.any?(ranges, fn range -> - range.from_number == 85 and range.to_number == 46 and range.priority == nil + range.from_number == 112 and range.to_number == 46 and range.priority == nil end) assert Enum.any?(ranges, fn range -> @@ -331,18 +315,14 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 3 + assert length(ranges) == 2 assert Enum.any?(ranges, fn range -> range.from_number == 112 and range.to_number == 111 and range.priority == nil end) assert Enum.any?(ranges, fn range -> - range.from_number == 110 and range.to_number == 86 and range.priority == 1 - end) - - assert Enum.any?(ranges, fn range -> - range.from_number == 85 and range.to_number == 7 and range.priority == 1 + range.from_number == 110 and range.to_number == 7 and range.priority == 1 end) end @@ -358,14 +338,10 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 2 - - assert Enum.any?(ranges, fn range -> - range.from_number == 112 and range.to_number == 86 and range.priority == 1 - end) + assert length(ranges) == 1 assert Enum.any?(ranges, fn range -> - range.from_number == 85 and range.to_number == 7 and range.priority == 1 + range.from_number == 112 and range.to_number == 7 and range.priority == 1 end) end @@ -435,18 +411,10 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 4 - - assert Enum.any?(ranges, fn range -> - range.from_number == 130 and range.to_number == 46 and range.priority == 1 - end) - - assert Enum.any?(ranges, fn range -> - range.from_number == 45 and range.to_number == 30 and range.priority == 1 - end) + assert length(ranges) == 2 assert Enum.any?(ranges, fn range -> - range.from_number == 29 and range.to_number == 23 and range.priority == 1 + range.from_number == 130 and range.to_number == 23 and range.priority == 1 end) assert Enum.any?(ranges, fn range -> @@ -466,18 +434,10 @@ defmodule Explorer.Utility.TestMissingBlockRange do ranges = Repo.all(MissingBlockRange) - assert length(ranges) == 3 - - assert Enum.any?(ranges, fn range -> - range.from_number == 130 and range.to_number == 46 and range.priority == 1 - end) - - assert Enum.any?(ranges, fn range -> - range.from_number == 45 and range.to_number == 30 and range.priority == 1 - end) + assert length(ranges) == 1 assert Enum.any?(ranges, fn range -> - range.from_number == 29 and range.to_number == 20 and range.priority == 1 + range.from_number == 130 and range.to_number == 20 and range.priority == 1 end) end end diff --git a/config/runtime.exs b/config/runtime.exs index c6fff02146af..5cd30c6480fe 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -821,6 +821,9 @@ config :explorer, Explorer.Migrator.BackfillMetadataURL, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_BACKFILL_METADATA_URL_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_BACKFILL_METADATA_URL_CONCURRENCY", 5) +config :explorer, Explorer.Migrator.MergeAdjacentMissingBlockRanges, + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE", 100) + config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index bd5bb6b1cf22..5e75a0b55315 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -518,6 +518,7 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_TIMEOUT= # MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BLOCKS_BATCH_SIZE= # MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY= +# MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE= # HEALTH_MONITOR_CHECK_INTERVAL=1m # HEALTH_MONITOR_BLOCKS_PERIOD=5m # HEALTH_MONITOR_BATCHES_PERIOD=4h From f0ffaf4753a92c3eea6bf6370e4c6ee616db82e0 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 15 Jul 2025 19:30:08 +0300 Subject: [PATCH 0168/1006] perf: api v1 `txlsit`& `txlistinternal` endpoints (#12774) * perf: api v1 `txlsit`& `txlistinternal` endpoints * Process review and add limit for page_size * page --- .../controllers/api/rpc/address_controller.ex | 97 ++++---- .../api/rpc/address_controller_test.exs | 29 +-- .../explorer/chain/internal_transaction.ex | 6 +- apps/explorer/lib/explorer/etherscan.ex | 221 ++++++++++-------- .../explorer/test/explorer/etherscan_test.exs | 28 ++- 5 files changed, 216 insertions(+), 165 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index b4241fbd42d8..fb786e155419 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -14,21 +14,27 @@ defmodule BlockScoutWeb.API.RPC.AddressController do @invalid_address_message "Invalid address format" @invalid_contract_address_message "Invalid contract address format" @no_token_transfers_message "No token transfers found" - + @results_window 10000 + @results_window_too_large_message "Result window is too large, PageNo x Offset size must be less than or equal to #{@results_window}" @max_safe_block_number round(:math.pow(2, 31)) - 1 def listaccounts(conn, params) do - options = - params - |> optional_params() - |> Map.put_new(:page_number, 0) - |> Map.put_new(:page_size, 10) + case optional_params(params) do + {:ok, options} -> + options = + options + |> Map.put_new(:page_number, 0) + |> Map.put_new(:page_size, 10) + + accounts = list_accounts(options, AccessHelper.conn_to_ip_string(conn)) - accounts = list_accounts(options, AccessHelper.conn_to_ip_string(conn)) + conn + |> put_status(200) + |> render(:listaccounts, %{accounts: accounts}) - conn - |> put_status(200) - |> render(:listaccounts, %{accounts: accounts}) + {:error, :results_window_too_large} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) + end end def eth_get_balance(conn, params) do @@ -83,9 +89,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end def pendingtxlist(conn, params) do - options = optional_params(params) - - with {:address_param, {:ok, address_param}} <- fetch_address(params), + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), {:ok, transactions} <- list_pending_transactions(address_hash, options) do render(conn, :pendingtxlist, %{transactions: transactions}) @@ -102,13 +107,15 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:error, :not_found} -> render(conn, :error, error: "No transactions found", data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end def txlist(conn, params) do - options = optional_params(params) - - with {:address_param, {:ok, address_param}} <- fetch_address(params), + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hash}} <- to_address_hash(address_param), {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, transactions} <- list_transactions(address_hash, options) do @@ -126,6 +133,9 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {_, :not_found} -> render(conn, :error, error: "No transactions found", data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end @@ -143,11 +153,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end def txlistinternal(conn, params, transaction_param, :transaction) do - options = - params - |> optional_params() - - with {:format, {:ok, transaction_hash}} <- to_transaction_hash(transaction_param), + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:format, {:ok, transaction_hash}} <- to_transaction_hash(transaction_param), {:ok, internal_transactions} <- list_internal_transactions(transaction_hash, options) do render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) else @@ -156,13 +163,15 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:error, :not_found} -> render(conn, :error, error: "No internal transactions found", data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end def txlistinternal(conn, params, address_param, :address) do - options = optional_params(params) - - with {:format, {:ok, address_hash}} <- to_address_hash(address_param), + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:format, {:ok, address_hash}} <- to_address_hash(address_param), {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, {:ok, internal_transactions} <- list_internal_transactions(address_hash, options) do render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) @@ -172,20 +181,22 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {_, :not_found} -> render(conn, :error, error: "No internal transactions found", data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end def txlistinternal(conn, params, :no_param) do - options = - params - |> optional_params() - - case list_internal_transactions(:all, options) do - {:ok, internal_transactions} -> - render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) - + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:ok, internal_transactions} <- list_internal_transactions(:all, options) do + render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + else {:error, :not_found} -> render(conn, :error, error: "No internal transactions found", data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end @@ -206,9 +217,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end defp do_tokentx(conn, params, transfers_type) do - options = optional_params(params) - - with {:address, {:ok, address_hash}} <- {:address, to_address_hash_optional(params["address"])}, + with {:params, {:ok, options}} <- {:params, optional_params(params)}, + {:address, {:ok, address_hash}} <- {:address, to_address_hash_optional(params["address"])}, {:contract_address, {:ok, contract_address_hash}} <- {:contract_address, to_address_hash_optional(params["contractaddress"])}, true <- !is_nil(address_hash) or !is_nil(contract_address_hash), @@ -227,6 +237,9 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {_, :not_found} -> render(conn, :error, error: @no_token_transfers_message, data: []) + + {:params, {:error, :results_window_too_large}} -> + render(conn, :error, error: @results_window_too_large_message, data: nil) end end @@ -285,11 +298,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end end - @doc """ - Sanitizes optional params. - - """ - @spec optional_params(map()) :: map() + @doc false + @spec optional_params(map()) :: {:ok, map()} | {:error, :results_window_too_large} def optional_params(params) do %{} |> put_boolean(params, "include_zero_value", :include_zero_value) @@ -300,6 +310,13 @@ defmodule BlockScoutWeb.API.RPC.AddressController do |> put_filter_by(params) |> put_timestamp(params, "start_timestamp") |> put_timestamp(params, "end_timestamp") + |> case do + %{page_number: page_number, page_size: page_size} when page_number * page_size > @results_window -> + {:error, :results_window_too_large} + + params -> + {:ok, params} + end end @doc """ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index b91708bf716b..a57bc991634f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -2589,7 +2589,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do index: index, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index + block_index: index, + value: 1 } insert(:internal_transaction_create, internal_transaction_details) @@ -4727,7 +4728,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "end_timestamp" => "1539186474" } - optional_params = AddressController.optional_params(params) + {:ok, optional_params} = AddressController.optional_params(params) # 1539186474 equals "2018-10-10 15:47:54Z" {:ok, expected_timestamp, _} = DateTime.from_iso8601("2018-10-10 15:47:54Z") @@ -4745,41 +4746,41 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do test "'sort' values can be 'asc' or 'desc'" do params1 = %{"sort" => "asc"} - optional_params = AddressController.optional_params(params1) + {:ok, optional_params} = AddressController.optional_params(params1) assert optional_params.order_by_direction == :asc params2 = %{"sort" => "desc"} - optional_params = AddressController.optional_params(params2) + {:ok, optional_params} = AddressController.optional_params(params2) assert optional_params.order_by_direction == :desc params3 = %{"sort" => "invalid"} - assert AddressController.optional_params(params3) == %{} + assert AddressController.optional_params(params3) == {:ok, %{}} end test "'filter_by' value can be 'to' or 'from'" do params1 = %{"filter_by" => "to"} - optional_params1 = AddressController.optional_params(params1) + {:ok, optional_params1} = AddressController.optional_params(params1) assert optional_params1.filter_by == "to" params2 = %{"filter_by" => "from"} - optional_params2 = AddressController.optional_params(params2) + {:ok, optional_params2} = AddressController.optional_params(params2) assert optional_params2.filter_by == "from" params3 = %{"filter_by" => "invalid"} - assert AddressController.optional_params(params3) == %{} + assert AddressController.optional_params(params3) == {:ok, %{}} end test "only includes optional params when they're given" do - assert AddressController.optional_params(%{}) == %{} + assert AddressController.optional_params(%{}) == {:ok, %{}} end test "ignores invalid optional params, keeps valid ones" do @@ -4793,7 +4794,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "end_timestamp" => "invalid" } - assert AddressController.optional_params(params1) == %{} + assert AddressController.optional_params(params1) == {:ok, %{}} params2 = %{ "startblock" => "4", @@ -4805,7 +4806,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "end_timestamp" => "invalid" } - optional_params = AddressController.optional_params(params2) + {:ok, optional_params} = AddressController.optional_params(params2) assert optional_params.startblock == 4 assert optional_params.endblock == 10 @@ -4814,19 +4815,19 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do test "ignores 'page' if less than 1" do params = %{"page" => "0"} - assert AddressController.optional_params(params) == %{} + assert AddressController.optional_params(params) == {:ok, %{}} end test "ignores 'offset' if less than 1" do params = %{"offset" => "0"} - assert AddressController.optional_params(params) == %{} + assert AddressController.optional_params(params) == {:ok, %{}} end test "ignores 'offset' if more than 10,000" do params = %{"offset" => "10001"} - assert AddressController.optional_params(params) == %{} + assert AddressController.optional_params(params) == {:ok, %{}} end end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index ebaa3e7e6b81..8385d3b920b2 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -1018,6 +1018,10 @@ defmodule Explorer.Chain.InternalTransaction do def include_zero_value(query, true), do: query def include_zero_value(query, false) do - where(query, [internal_transaction], internal_transaction.value > ^0) + where( + query, + [internal_transaction], + (internal_transaction.type == :call and internal_transaction.value > ^0) or internal_transaction.type != :call + ) end end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 627523c69a68..5cf7fdfcb867 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -76,6 +76,7 @@ defmodule Explorer.Etherscan do from_address_hash to_address_hash transaction_hash + transaction_index index value created_contract_address_hash @@ -149,68 +150,34 @@ defmodule Explorer.Etherscan do ) do options = Map.merge(@default_options, raw_options) - direction = - case options do - %{filter_by: "to"} -> :to - %{filter_by: "from"} -> :from - _ -> nil - end - - if direction do + options + |> options_to_directions() + |> Enum.map(fn direction -> options - |> internal_transactions_with_transactions_and_blocks_query() - |> InternalTransaction.where_transaction_has_multiple_internal_transactions() + |> consensus_internal_transactions_with_transactions_and_blocks_query() |> InternalTransaction.where_address_fields_match(address_hash, direction) |> InternalTransaction.where_is_different_from_parent_transaction() |> InternalTransaction.include_zero_value(options.include_zero_value) - |> where_start_block_match(options) - |> where_end_block_match(options) + |> where_start_block_match_internal_transaction(options) + |> where_end_block_match_internal_transaction(options) |> InternalTransaction.where_nonpending_block() - |> Repo.replica().all() - else - consensus_blocks = Block.consensus_blocks_query() - query = internal_transactions_query(options, consensus_blocks) - - query_to_address_hash_wrapped = - query - |> InternalTransaction.where_address_fields_match(address_hash, :to_address_hash) - |> InternalTransaction.where_is_different_from_parent_transaction() - |> InternalTransaction.include_zero_value(options.include_zero_value) - |> where_start_block_match(options) - |> where_end_block_match(options) - |> Chain.wrapped_union_subquery() - - query_from_address_hash_wrapped = - query - |> InternalTransaction.where_address_fields_match(address_hash, :from_address_hash) - |> InternalTransaction.where_is_different_from_parent_transaction() - |> InternalTransaction.include_zero_value(options.include_zero_value) - |> where_start_block_match(options) - |> where_end_block_match(options) - |> Chain.wrapped_union_subquery() - - query_created_contract_address_hash_wrapped = - query - |> InternalTransaction.where_address_fields_match(address_hash, :created_contract_address_hash) - |> InternalTransaction.where_is_different_from_parent_transaction() - |> InternalTransaction.include_zero_value(options.include_zero_value) - |> where_start_block_match(options) - |> where_end_block_match(options) - |> Chain.wrapped_union_subquery() - - query_to_address_hash_wrapped - |> union(^query_from_address_hash_wrapped) - |> union(^query_created_contract_address_hash_wrapped) |> Chain.wrapped_union_subquery() - |> order_by( - [q], - [ - {^options.order_by_direction, q.block_number}, - desc: q.index - ] - ) - |> Repo.replica().all() - end + end) + |> Enum.reduce(fn query, acc -> + union(acc, ^query) + end) + |> Chain.wrapped_union_subquery() + |> order_by( + [q], + [ + {^options.order_by_direction, q.block_number}, + {^options.order_by_direction, q.transaction_index}, + {^options.order_by_direction, q.index} + ] + ) + |> offset(^options_to_offset(options)) + |> limit(^options.page_size) + |> Repo.replica().all() end def list_internal_transactions( @@ -225,20 +192,25 @@ defmodule Explorer.Etherscan do |> internal_transactions_query(consensus_blocks) |> InternalTransaction.where_is_different_from_parent_transaction() |> InternalTransaction.include_zero_value(options.include_zero_value) - |> where_start_block_match(options) - |> where_end_block_match(options) + |> where_start_block_match_internal_transaction(options) + |> where_end_block_match_internal_transaction(options) |> Repo.replica().all() end - defp internal_transactions_with_transactions_and_blocks_query(options) do + defp consensus_internal_transactions_with_transactions_and_blocks_query(options) do if DenormalizationHelper.transactions_denormalization_finished?() do from( it in InternalTransaction, + as: :internal_transaction, inner_join: transaction in assoc(it, :transaction), where: not is_nil(transaction.block_hash), - order_by: [{^options.order_by_direction, transaction.block_number}], - limit: ^options.page_size, - offset: ^offset(options), + where: transaction.block_consensus == true, + order_by: [ + {^options.order_by_direction, it.block_number}, + {^options.order_by_direction, it.transaction_index}, + {^options.order_by_direction, it.index} + ], + limit: ^options_to_limit_for_inner_query(options), select: merge(map(it, ^@internal_transaction_fields), %{ block_timestamp: transaction.block_timestamp, @@ -248,11 +220,16 @@ defmodule Explorer.Etherscan do else from( it in InternalTransaction, + as: :internal_transaction, inner_join: t in assoc(it, :transaction), inner_join: b in assoc(t, :block), - order_by: [{^options.order_by_direction, t.block_number}], - limit: ^options.page_size, - offset: ^offset(options), + where: b.consensus == true, + order_by: [ + {^options.order_by_direction, it.block_number}, + {^options.order_by_direction, it.transaction_index}, + {^options.order_by_direction, it.index} + ], + limit: ^options_to_limit_for_inner_query(options), select: merge(map(it, ^@internal_transaction_fields), %{ block_timestamp: b.timestamp, @@ -269,11 +246,11 @@ defmodule Explorer.Etherscan do on: it.block_number == block.number, order_by: [ {^options.order_by_direction, it.block_number}, - {:desc, it.transaction_index}, - {:desc, it.index} + {^options.order_by_direction, it.transaction_index}, + {^options.order_by_direction, it.index} ], limit: ^options.page_size, - offset: ^offset(options), + offset: ^options_to_offset(options), select: merge(map(it, ^@internal_transaction_fields), %{ block_timestamp: block.timestamp, @@ -361,7 +338,7 @@ defmodule Explorer.Etherscan do where: block.miner_hash == ^address_hash, order_by: [desc: block.number], limit: ^merged_options.page_size, - offset: ^offset(merged_options), + offset: ^options_to_offset(merged_options), select: %{ number: block.number, timestamp: block.timestamp @@ -459,15 +436,30 @@ defmodule Explorer.Etherscan do query = from( t in Transaction, - limit: ^options.page_size, - offset: ^offset(options), + limit: ^options_to_limit_for_inner_query(options), select: map(t, ^@pending_transaction_fields) ) - query - |> where_address_match(address_hash, options) - |> Chain.pending_transactions_query() - |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) + options + |> options_to_directions() + |> Enum.map(fn direction -> + query + |> where_address_match(address_hash, direction) + |> Chain.pending_transactions_query() + |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) + |> Chain.wrapped_union_subquery() + end) + |> Enum.reduce(fn query, acc -> + union(acc, ^query) + end) + |> Chain.wrapped_union_subquery() + |> order_by( + [transaction], + desc: transaction.inserted_at, + desc: transaction.hash + ) + |> offset(^options_to_offset(options)) + |> limit(^options.page_size) |> Repo.replica().all() end @@ -478,9 +470,8 @@ defmodule Explorer.Etherscan do t in Transaction, where: not is_nil(t.block_hash), where: t.block_consensus == true, - order_by: [{^options.order_by_direction, t.block_number}], - limit: ^options.page_size, - offset: ^offset(options), + order_by: [{^options.order_by_direction, t.block_number}, {^options.order_by_direction, t.index}], + limit: ^options_to_limit_for_inner_query(options), select: merge(map(t, ^@transaction_fields), %{ confirmations: fragment("? - ?", ^max_block_number, t.block_number) @@ -491,9 +482,8 @@ defmodule Explorer.Etherscan do t in Transaction, inner_join: b in assoc(t, :block), where: b.consensus == true, - order_by: [{^options.order_by_direction, t.block_number}], - limit: ^options.page_size, - offset: ^offset(options), + order_by: [{^options.order_by_direction, t.block_number}, {^options.order_by_direction, t.index}], + limit: ^options_to_limit_for_inner_query(options), select: merge(map(t, ^@transaction_fields), %{ block_timestamp: b.timestamp, @@ -502,30 +492,43 @@ defmodule Explorer.Etherscan do ) end - query - |> where_address_match(address_hash, options) - |> where_start_transaction_block_match(options) - |> where_end_transaction_block_match(options) - |> where_start_timestamp_match(options) - |> where_end_timestamp_match(options) + options + |> options_to_directions() + |> Enum.map(fn direction -> + query + |> where_address_match(address_hash, direction) + |> where_start_transaction_block_match(options) + |> where_end_transaction_block_match(options) + |> where_start_timestamp_match(options) + |> where_end_timestamp_match(options) + |> Chain.wrapped_union_subquery() + end) + |> Enum.reduce(fn query, acc -> + union(acc, ^query) + end) + |> Chain.wrapped_union_subquery() + |> order_by( + [q], + [ + {^options.order_by_direction, q.block_number}, + {^options.order_by_direction, q.index} + ] + ) + |> offset(^options_to_offset(options)) + |> limit(^options.page_size) |> Repo.replica().all() end - defp where_address_match(query, address_hash, %{filter_by: "to"}) do + defp where_address_match(query, address_hash, :to_address_hash) do where(query, [t], t.to_address_hash == ^address_hash) end - defp where_address_match(query, address_hash, %{filter_by: "from"}) do + defp where_address_match(query, address_hash, :from_address_hash) do where(query, [t], t.from_address_hash == ^address_hash) end - defp where_address_match(query, address_hash, _) do - where( - query, - [t], - t.to_address_hash == ^address_hash or t.from_address_hash == ^address_hash or - t.created_contract_address_hash == ^address_hash - ) + defp where_address_match(query, address_hash, :created_contract_address_hash) do + where(query, [t], t.created_contract_address_hash == ^address_hash) end defp list_erc20_token_transfers(address_hash, contract_address_hash, options) do @@ -580,7 +583,7 @@ defmodule Explorer.Etherscan do |> where_start_block_match_tt(options) |> where_end_block_match_tt(options) |> limit(^options.page_size) - |> offset(^offset(options)) + |> offset(^options_to_offset(options)) |> maybe_preload_entities() end @@ -638,6 +641,18 @@ defmodule Explorer.Etherscan do where(query, [tt], tt.block_number <= ^end_block) end + defp where_start_block_match_internal_transaction(query, %{startblock: nil}), do: query + + defp where_start_block_match_internal_transaction(query, %{startblock: start_block}) do + where(query, [internal_transaction: internal_transaction], internal_transaction.block_number >= ^start_block) + end + + defp where_end_block_match_internal_transaction(query, %{endblock: nil}), do: query + + defp where_end_block_match_internal_transaction(query, %{endblock: end_block}) do + where(query, [internal_transaction: internal_transaction], internal_transaction.block_number <= ^end_block) + end + defp where_start_timestamp_match(query, %{start_timestamp: nil}), do: query defp where_start_timestamp_match(query, %{start_timestamp: start_timestamp}) do @@ -670,7 +685,17 @@ defmodule Explorer.Etherscan do where(query, [tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash) end - defp offset(options), do: (options.page_number - 1) * options.page_size + defp options_to_offset(options), do: (options.page_number - 1) * options.page_size + + defp options_to_limit_for_inner_query(options), do: options.page_number * options.page_size + + defp options_to_directions(options) do + case options do + %{filter_by: "to"} -> [:to_address_hash, :created_contract_address_hash] + %{filter_by: "from"} -> [:from_address_hash] + _ -> [:to_address_hash, :from_address_hash, :created_contract_address_hash] + end + end @doc """ Gets a list of logs that meet the criteria in a given filter map. diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index 2cf8768c64ec..a6c83f33508b 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -880,6 +880,7 @@ defmodule Explorer.EtherscanTest do input: internal_transaction.input, index: internal_transaction.index, transaction_hash: internal_transaction.transaction_hash, + transaction_index: internal_transaction.transaction_index, type: internal_transaction.type, call_type: internal_transaction.call_type, gas: internal_transaction.gas, @@ -1030,23 +1031,26 @@ defmodule Explorer.EtherscanTest do blocks = [_, second_block, third_block, _] = insert_list(4, :block) address = insert(:address) - for block <- blocks, index <- 0..1 do + for block <- blocks do transaction = :transaction |> insert() |> with_block(block) - internal_transaction_details = %{ - transaction: transaction, - index: index, - from_address: address, - block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index - } - - insert(:internal_transaction, internal_transaction_details) + for index <- 0..1 do + internal_transaction_details = %{ + transaction: transaction, + index: index, + from_address: address, + block_number: block.number, + block_hash: block.hash, + block_index: index, + transaction_index: transaction.index, + value: 1 + } + + insert(:internal_transaction, internal_transaction_details) + end end options = %{ From ab002ac0c507a3775944c702474e5c12e80ca02c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 15 Jul 2025 23:23:54 +0300 Subject: [PATCH 0169/1006] v9.0.0 --- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- ...lish-docker-image-for-optimism-interop.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-suave.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 105 ++++++++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 +- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 64 files changed, 169 insertions(+), 64 deletions(-) diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 68fbaaf3b4ca..74e2854e68d9 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index d63c64081cf4..a81ffa8ac494 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index d773345831b8..38469927d753 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 5f2e04dfbf1d..019cc25cd563 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 7b1ff0e355e2..26b227203284 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 0b4de86a4c4e..eadaf588916d 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 6c6f4a550b9e..72b5c44a4e1f 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 8248639410c2..c40b8687420e 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 89d944047e3a..5740a7fff571 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 4287007611d6..88c6914dcd04 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index c8565c2276a8..5c88e2180f99 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 9b72a2493f23..6e6fe7960686 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 14c35fb81f0d..964a6ae0d0f8 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 19b44a3138ed..c3b665807408 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 677d1985daa4..e12b76391202 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 70873b756193..b7b5e1b5cf1f 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 61175ca3cf33..a3ee47350f5a 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 01ec6f9f073a..373db1adf81f 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 4470c3d6e1c4..df8daa2e1a15 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 43a36d3a498f..aaad795e0c9c 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 0be53a189d87..3112c2de1a2a 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index dbda667a22bc..8ebca265b809 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index f59660eb9a89..2d7da3ae0072 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 9897e3474815..1f47b8f5f2e8 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/publish-docker-image-for-optimism-interop.yml index a0bb756b6608..7b1f1c0807bf 100644 --- a/.github/workflows/publish-docker-image-for-optimism-interop.yml +++ b/.github/workflows/publish-docker-image-for-optimism-interop.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 400d21141914..fee395147737 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index a610990a42cd..bad9afb96f8e 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index e74c8dcc5ce8..85eef533da0d 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index a62bdf1b3bdf..2422e75f0739 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index cda7292ad77b..aabc23cc8e3c 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 5354aa4870ff..e9fdd8352a95 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 7bb9140123d9..72c70c073416 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index e663a18bdadc..3b8cf65b6730 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 58e6e74a349c..5cbc10c0ee97 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -12,7 +12,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 6e37e9206303..5cefcabb5eef 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 jobs: push_to_registry: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 4fb18189ba25..b4878b64bace 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 jobs: push_to_registry: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 110eab734496..03149dbe4bce 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 30de3e0ebe4b..46648481d74e 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 505f1bba4d51..26171805ef44 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 1ebafe653406..8534f19f60a6 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 1a13ecc51cb5..971365857c8e 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index a3add02d8b01..e6d92963a38f 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 2c5797b38e81..2697824b035d 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index e0853990c675..2fafe3eed18b 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index f61da399d229..4ae903b37e04 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 4c91a8b7ded1..f7d2763af104 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index 0e7f99bf16ac..70490bcad29d 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index bfb962c0b8cc..5161890b6fb7 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 815a1a48ba93..416e19b8dd29 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 178599bcfb89..d1d3845ace5e 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f1a48f8afe96..c9a0f15eb1a4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 49b6ae3d5b05..93b6f9ff5ff6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,110 @@ # Changelog +## 9.0.0 + +### 🚀 Features + +- Add eip7702 authorization status fetcher ([#12451](https://github.com/blockscout/blockscout/issues/12451)) +- Add token1155tx token404tx api v1 endpoints ([#12720](https://github.com/blockscout/blockscout/issues/12720)) +- Async multichain data export ([#12490](https://github.com/blockscout/blockscout/issues/12490)) +- Rate limits refactoring ([#12386](https://github.com/blockscout/blockscout/issues/12386)) +- Integrate Open API Spex lib ([#11886](https://github.com/blockscout/blockscout/issues/11886)) +- Update CodeQL action to v3 ([#12697](https://github.com/blockscout/blockscout/issues/12697)) ([#12703](https://github.com/blockscout/blockscout/issues/12703)) +- Support merged tenants ([#12109](https://github.com/blockscout/blockscout/issues/12109)) +- Support ethereum pre-deploy contracts ([#12579](https://github.com/blockscout/blockscout/issues/12579)) +- Add `creation_status` field to address response ([#12660](https://github.com/blockscout/blockscout/issues/12660)) +- Decode OP interop message payload, store cross-chain transfer data, display message page, send messages to Multichain ([#12387](https://github.com/blockscout/blockscout/issues/12387)) +- Celo l2 epochs ([#12373](https://github.com/blockscout/blockscout/issues/12373)) +- Add `/api/v2/config/celo` convenience endpoint ([#12238](https://github.com/blockscout/blockscout/issues/12238)) + +### 🐛 Bug Fixes + +- Change default ordering in `/api/v2/smart-contracts` ([#12767](https://github.com/blockscout/blockscout/issues/12767)) +- Filter scilla transactions by status ([#12756](https://github.com/blockscout/blockscout/issues/12756)) +- Fix timeout on cache update ([#12773](https://github.com/blockscout/blockscout/issues/12773)) +- Error on too big block numbers in APIv1 `txlist` method ([#12727](https://github.com/blockscout/blockscout/issues/12727)) +- Fix CSV export tests ([#12744](https://github.com/blockscout/blockscout/issues/12744)) +- Fix race condition for EventNotification ([#12738](https://github.com/blockscout/blockscout/issues/12738)) +- Multichain retry hex decoding ([#12742](https://github.com/blockscout/blockscout/issues/12742)) +- Internal transactions balance extraction ([#12654](https://github.com/blockscout/blockscout/issues/12654)) +- Multichain search export: retry only on failed chunks ([#12459](https://github.com/blockscout/blockscout/issues/12459)) +- Display correct OP Deposit origin address ([#12672](https://github.com/blockscout/blockscout/issues/12672)) +- Store blocks_validated in DB for Stability Validators ([#12540](https://github.com/blockscout/blockscout/issues/12540)) +- `MarketHistory` on conflict clause ([#12541](https://github.com/blockscout/blockscout/issues/12541)) +- Flaky 404 in `/api/v2/internal-transactions` ([#12701](https://github.com/blockscout/blockscout/issues/12701)) +- CryptoRank integration ([#12523](https://github.com/blockscout/blockscout/issues/12523)) +- Fix timeout on fetching address internal transactions ([#12570](https://github.com/blockscout/blockscout/issues/12570)) +- Coin balance history with internal tx changes ([#12631](https://github.com/blockscout/blockscout/issues/12631)) +- Update all block fields on conflict ([#12418](https://github.com/blockscout/blockscout/issues/12418)) +- Fix pending transactions sanitizer ([#12559](https://github.com/blockscout/blockscout/issues/12559)) +- Don't send logs without topic to sig provider ([#12620](https://github.com/blockscout/blockscout/issues/12620)) +- Add missing fields to Celo Epochs-related endpoints ([#12589](https://github.com/blockscout/blockscout/issues/12589)) +- Correctly use Geth importer for Besu genesis file. ([#12466](https://github.com/blockscout/blockscout/issues/12466)) ([#12686](https://github.com/blockscout/blockscout/issues/12686)) +- Ignore unknown type txs in gas price oracle ([#12613](https://github.com/blockscout/blockscout/issues/12613)) +- Resolve timeouts on Celo epoch reward contract reads ([#12229](https://github.com/blockscout/blockscout/issues/12229)) +- Prevent constant refetching of celo epoch blocks ([#12498](https://github.com/blockscout/blockscout/issues/12498)) +- Fix typo in ondemand token balance request ([#12495](https://github.com/blockscout/blockscout/issues/12495)) +- Fix for `add_0x_prefix` function ([#12514](https://github.com/blockscout/blockscout/issues/12514)) + +### ⚡ Performance + +- Api v1 `txlsit`& `txlistinternal` endpoints ([#12774](https://github.com/blockscout/blockscout/issues/12774)) +- Optimize Explorer.Chain.Cache.Blocks ([#12402](https://github.com/blockscout/blockscout/issues/12402)) + +### ⚙️ Miscellaneous Tasks + +- Merge adjacent missing block ranges ([#12778](https://github.com/blockscout/blockscout/issues/12778)) +- Optimize missing block ranges operations ([#12705](https://github.com/blockscout/blockscout/issues/12705)) +- Hold parity with Etherscan APIv1 for `getcontractcreation` and `getblocknobytime` endpoints ([#12721](https://github.com/blockscout/blockscout/issues/12721)) +- Allow resending reindexed OP interop messages to Multichain service ([#12626](https://github.com/blockscout/blockscout/issues/12626)) +- Duplicate block countdown endpoint in API v2 ([#12704](https://github.com/blockscout/blockscout/issues/12704)) +- Revise Explorer.Helper.add_0x_prefix usage ([#12543](https://github.com/blockscout/blockscout/issues/12543)) +- New tac microservice endpoint for search ([#12448](https://github.com/blockscout/blockscout/issues/12448)) +- Add filter for value > 0 to txlistinternal ([#12679](https://github.com/blockscout/blockscout/issues/12679)) +- Optimize realtime events notifier ([#12494](https://github.com/blockscout/blockscout/issues/12494)) +- Drop address_coin_balances value_fetched_at index ([#12598](https://github.com/blockscout/blockscout/issues/12598)) +- Update deprecated address to address_hash in tx summary response ([#12617](https://github.com/blockscout/blockscout/issues/12617)) +- Remove redundant word in comment ([#12603](https://github.com/blockscout/blockscout/issues/12603)) +- Move background migrations under indexer mode ([#12480](https://github.com/blockscout/blockscout/issues/12480)) +- Support multiple interop messages view on transaction page ([#12455](https://github.com/blockscout/blockscout/issues/12455)) +- Remove `is_self_destructed` field in `/api/v2/smart-contracts/{address_hash}` response ([#12239](https://github.com/blockscout/blockscout/issues/12239)) +- Set home directory for blockscout user ([#12337](https://github.com/blockscout/blockscout/issues/12337)) + + +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED` | If `true`, `Indexer.Utils.EventNotificationsCleaner` process starts. Implemented in [#12738](https://github.com/blockscout/blockscout/pull/12738) |

Version: v9.0.0+
Default: true
Applications: Indexer

| +| `INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_INTERVAL` | Interval between DB event notifications cleanup. [Time format](backend-env-variables.md#time-format). Implemented in [#12738](https://github.com/blockscout/blockscout/pull/12738) |

Version: v9.0.0+
Default: 2m
Applications: Indexer

| +| `INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE` | Max age of DB event notifications before they are cleaned up. [Time format](backend-env-variables.md#time-format). Implemented in [#12738](https://github.com/blockscout/blockscout/pull/12738) |

Version: v9.0.0+
Default: 5m
Applications: Indexer

| +| `INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE` | Batch size (number of blocks) for EIP7702 authorizations status fetcher. Implemented in [#12451](https://github.com/blockscout/blockscout/pull/12451). |

Version: v9.0.0+
Default: 10
Applications: Indexer

| +| `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_BATCH_SIZE` | Number of blocks to reindex in the batch. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: 10
Applications: Indexer

| +| `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_CONCURRENCY` | Number of parallel reindexing block batches processing. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: 1
Applications: Indexer

| +| `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_TIMEOUT` | Timeout between reindexing block batches processing. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: 0
Applications: Indexer

| +| `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_ENABLED` | Enable reindex blocks with missing transactions migration. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: false
Applications: Indexer

| +| `MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_BATCH_SIZE` | Number of logs to process in the batch. Implemented in [#10466](https://github.com/blockscout/blockscout/pull/10466) |

Version: v6.8.0+
Default: 50
Applications: API, Indexer

| +| `MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE` | Specifies the missing block range batch size selected for the merge migration. Implemented in [#12778](https://github.com/blockscout/blockscout/pull/12778). |

Version: v9.0.0+
Default: 100
Applications: Indexer

| +| `API_RATE_LIMIT_CONFIG_URL` | URL to fetch API rate limit configuration from external source. Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: (empty)
Applications: API

| +| `API_RATE_LIMIT_BY_KEY_TIME_INTERVAL` | Time interval for API rate limit by key. [Time format](backend-env-variables.md#time-format). Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: 1s
Applications: API

| +| `API_RATE_LIMIT_BY_WHITELISTED_IP_TIME_INTERVAL` | Time interval for API rate limit by whitelisted IP. [Time format](backend-env-variables.md#time-format). Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: 1s
Applications: API

| +| `API_RATE_LIMIT_UI_V2_WITH_TOKEN_TIME_INTERVAL` | Time interval for API rate limit for UI v2 with token. [Time format](backend-env-variables.md#time-format). Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: 1s
Applications: API

| +| `API_RATE_LIMIT_BY_ACCOUNT_API_KEY_TIME_INTERVAL` | Time interval for API rate limit by account API key. [Time format](backend-env-variables.md#time-format). Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: 1s
Applications: API

| +| `INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER` | If `true`, multichain DB main (blocks, transactions, addresses) export fetcher doesn't run. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v9.0.0+
Default: false
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE` | Batch size for multichain DB main (blocks, transactions, addresses) export fetcher. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v9.0.0+
Default: 1000
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY` | Concurrency for multichain DB main (blocks, transactions, addresses) export fetcher. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v9.0.0+
Default: 10
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to multichain DB main (blocks, transactions, addresses) export queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v9.0.0+
Default: 1s
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE` | Maximum size of multichain DB main (blocks, transactions, addresses) export queue. Implemented in [#12377](https://github.com/blockscout/blockscout/pull/12377). |

Version: v9.0.0+
Default: 1000
Applications: Indexer

| +| `INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER` | If `true`, multichain DB balances export fetcher doesn't run. Implemented in [#12580](https://github.com/blockscout/blockscout/pull/12580). |

Version: v9.0.0+
Default: false
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE` | Batch size for multichain DB balances export fetcher. Implemented in [#12580](https://github.com/blockscout/blockscout/pull/12580). |

Version: v9.0.0+
Default: 1000
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY` | Concurrency for multichain DB balances export fetcher. Implemented in [#12580](https://github.com/blockscout/blockscout/pull/12580). |

Version: v9.0.0+
Default: 10
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to multichain DB balances export queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#12580](https://github.com/blockscout/blockscout/pull/12580). |

Version: v9.0.0+
Default: 1s
Applications: Indexer

| +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE` | Maximum size of multichain DB balances export queue. Implemented in [#12580](https://github.com/blockscout/blockscout/pull/12580). |

Version: v9.0.0+
Default: 1000
Applications: Indexer

| +| `INDEXER_POLYGON_ZKEVM_BATCHES_IGNORE` | Comma-separated list of batch numbers that should be ignored by the fetcher. Implemented in [#12387](https://github.com/blockscout/blockscout/pull/12387). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| +| `INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE` | Max number of items sent to the Multichain service in one batch. Implemented in [#12387](https://github.com/blockscout/blockscout/pull/12387). |

Version: v9.0.0+
Default: 100
Applications: Indexer

| +| `CELO_UNRELEASED_TREASURY_CONTRACT` | The address[^1] of the `CeloUnreleasedTreasury` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| +| `CELO_VALIDATORS_CONTRACT` | The address[^1] of the `Validators` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| +| `CELO_EPOCH_MANAGER_CONTRACT` | The address[^1] of the `EpochManager` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| + + ## 8.1.1 ### 🐛 Bug Fixes diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 5e865d3d01ee..66c8b193c681 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.1", + version: "9.0.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 0eff40a6a817..9a8062b01120 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.1" + version: "9.0.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 18c619da3019..980b8df07a9b 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -14,7 +14,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-8.1.1"}] + @default_headers [{"User-Agent", "blockscout-9.0.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 4b676560de5e..f4cc46babf59 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "8.1.1", + version: "9.0.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 76e6638f64ca..5a3ba6ca953b 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "8.1.1", + version: "9.0.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index bb9419abd8b8..d66a28e5e42a 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "8.1.1", + version: "9.0.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 57abfd1884f9..a8d2315e4575 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "8.1.1", + version: "9.0.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 95bf984eeee1..5e66c209e11e 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index 9795021cfb5c..186a735ba204 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 8.1.1 + RELEASE_VERSION: 9.0.0 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index b4e3bbfaad7b..83daac83b889 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '8.1.1' +RELEASE_VERSION ?= '9.0.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 35626ec936bc..5db3c1bc80c3 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "8.1.1", + version: "9.0.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 13b9d0281300..0575a39fa86b 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "8.1.1" + set version: "9.0.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From 4f7e05ee5862d545464ef9528a2a195672ce4784 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Wed, 16 Jul 2025 14:28:36 +0300 Subject: [PATCH 0170/1006] fix: filter out scilla transactions in internal transactions fetcher (#12793) * fix: filter scilla transactions in internal transactions fetcher * fix: add changelog entry --- CHANGELOG.md | 1 + apps/indexer/lib/indexer/fetcher/internal_transaction.ex | 3 +++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b6f9ff5ff6..196dd702bd85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Filter out scilla transactions in internal transactions fetcher ([#12793](https://github.com/blockscout/blockscout/issues/12793)) - Change default ordering in `/api/v2/smart-contracts` ([#12767](https://github.com/blockscout/blockscout/issues/12767)) - Filter scilla transactions by status ([#12756](https://github.com/blockscout/blockscout/issues/12756)) - Fix timeout on cache update ([#12773](https://github.com/blockscout/blockscout/issues/12773)) diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index 2c6d490f6571..a3f0027ec522 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -22,6 +22,7 @@ defmodule Indexer.Fetcher.InternalTransaction do alias Explorer.Chain alias Explorer.Chain.{Block, Hash, PendingBlockOperation, PendingTransactionOperation, Transaction} alias Explorer.Chain.Cache.{Accounts, Blocks} + alias Explorer.Chain.Zilliqa.Helper, as: ZilliqaHelper alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.InternalTransaction.Supervisor, as: InternalTransactionSupervisor alias Indexer.Transform.Celo.TransactionTokenTransfers, as: CeloTransactionTokenTransfers @@ -320,10 +321,12 @@ defmodule Indexer.Fetcher.InternalTransaction do end end + # TODO: should we cover this with tests? @zetachain_non_traceable_type 88 defp filter_non_traceable_transactions(transactions) do case Application.get_env(:explorer, :chain_type) do :zetachain -> Enum.reject(transactions, &(&1.type == @zetachain_non_traceable_type)) + :zilliqa -> Enum.reject(transactions, &ZilliqaHelper.scilla_transaction?/1) _ -> transactions end end From 36a2ac4b4d9153a137342862af9c1f85afd318ba Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 16 Jul 2025 16:54:00 +0400 Subject: [PATCH 0171/1006] chore: Migrate from HTTPoison to Tesla.Mint (#12699) * chore: Migrate from HTTPoison to Tesla.Mint * Adapt http options for http clients * Adapt tests for tesla * Adapt all request parameters for http clients * Explicitly set tesla mock on exit * Add http clients docs * Process review * Update CHANGELOG --- CHANGELOG.md | 1 + apps/block_scout_web/config/config.exs | 4 +- apps/block_scout_web/config/test.exs | 2 + .../lib/block_scout_web/captcha_helper.ex | 9 +- .../account/api/v2/email_controller.ex | 6 +- .../transaction_interpretation.ex | 11 +- .../utility/rate_limit_config_helper.ex | 9 +- .../account/custom_abi_controller_test.exs | 6 + .../address_contract_controller_test.exs | 8 + .../api/rpc/contract_controller_test.exs | 8 + .../api/v2/address_controller_test.exs | 6 + .../api/v2/csv_export_controller_test.exs | 122 +++++---- .../api/v2/search_controller_test.exs | 45 ++++ .../api/v2/smart_contract_controller_test.exs | 15 ++ .../api/v2/token_controller_test.exs | 243 +++++++++++------- .../api/v2/verification_controller_test.exs | 5 + .../address_contract_verification_test.exs | 3 + .../utility/rate_limit_config_helper_test.exs | 12 + apps/block_scout_web/test/test_helper.exs | 2 +- apps/ethereum_jsonrpc/README.md | 4 +- apps/ethereum_jsonrpc/config/config.exs | 2 + apps/ethereum_jsonrpc/config/test.exs | 2 + apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 6 +- .../lib/ethereum_jsonrpc/http.ex | 11 +- .../lib/ethereum_jsonrpc/http/helper.ex | 85 ++++++ .../lib/ethereum_jsonrpc/http/httpoison.ex | 66 +---- .../lib/ethereum_jsonrpc/http/tesla.ex | 36 +++ apps/ethereum_jsonrpc/mix.exs | 3 +- .../test/support/ethereum_jsonrpc/case.ex | 3 +- .../case/geth/http_websocket.ex | 4 +- .../case/nethermind/http_websocket.ex | 4 +- .../support/ethereum_jsonrpc/http/case.ex | 2 +- apps/explorer/config/config.exs | 4 +- apps/explorer/config/dev/anvil.exs | 7 +- apps/explorer/config/dev/besu.exs | 7 +- apps/explorer/config/dev/erigon.exs | 7 +- apps/explorer/config/dev/filecoin.exs | 7 +- apps/explorer/config/dev/geth.exs | 7 +- apps/explorer/config/dev/nethermind.exs | 7 +- apps/explorer/config/dev/rsk.exs | 7 +- apps/explorer/config/prod/anvil.exs | 7 +- apps/explorer/config/prod/besu.exs | 7 +- apps/explorer/config/prod/erigon.exs | 7 +- apps/explorer/config/prod/filecoin.exs | 7 +- apps/explorer/config/prod/geth.exs | 7 +- apps/explorer/config/prod/nethermind.exs | 7 +- apps/explorer/config/prod/rsk.exs | 7 +- .../lib/explorer/chain/blackfort/validator.ex | 5 +- .../cache/optimism_finalization_period.ex | 4 +- .../fetcher/addresses_blacklist/blockaid.ex | 6 +- .../lib/explorer/chain_spec/genesis_data.ex | 9 +- apps/explorer/lib/explorer/http_client.ex | 27 ++ .../lib/explorer/http_client/httpoison.ex | 46 ++++ .../lib/explorer/http_client/tesla.ex | 50 ++++ apps/explorer/lib/explorer/market/source.ex | 18 +- .../account_abstraction.ex | 10 +- .../explorer/microservice_interfaces/bens.ex | 11 +- .../microservice_interfaces/metadata.ex | 15 +- .../multichain_search.ex | 13 +- .../tac_operation_lifecycle.ex | 8 +- .../smart_contract/compiler_version.ex | 7 +- .../rust_verifier_interface_behaviour.ex | 12 +- .../smart_contract/sig_provider_interface.ex | 12 +- .../smart_contract/solc_downloader.ex | 3 +- .../smart_contract/solidity/code_compiler.ex | 1 + .../stylus_verifier_interface.ex | 12 +- .../smart_contract/vyper_downloader.ex | 9 +- .../third_party_integrations/airtable.ex | 7 +- .../third_party_integrations/auth0.ex | 6 +- .../third_party_integrations/noves_fi.ex | 10 +- .../third_party_integrations/solidityscan.ex | 6 +- .../third_party_integrations/sourcify.ex | 19 +- .../universal_proxy.ex | 21 +- .../third_party_integrations/xname.ex | 6 +- .../lib/explorer/token/metadata_retriever.ex | 13 +- .../lib/explorer/visualize/sol2uml.ex | 8 +- apps/explorer/mix.exs | 5 +- .../explorer/market/fetcher/history_test.exs | 4 + .../explorer/market/fetcher/token_test.exs | 3 + .../market/source/coin_gecko_test.exs | 3 + .../market/source/coin_market_cap_test.exs | 3 + .../market/source/crypto_compare_test.exs | 3 + .../market/source/crypto_rank_test.exs | 3 + .../multichain_search_test.exs | 47 +++- .../smart_contract/compiler_version_test.exs | 7 +- .../solidity/code_compiler_test.exs | 2 + .../solidity/publisher_test.exs | 2 + .../smart_contract/solidity/verifier_test.exs | 2 + .../smart_contract/vyper/publisher_test.exs | 2 + .../universal_proxy_test.exs | 2 +- .../token/metadata_retriever_test.exs | 130 ++++++---- apps/explorer/test/test_helper.exs | 1 - apps/indexer/config/dev/anvil.exs | 7 +- apps/indexer/config/dev/besu.exs | 7 +- apps/indexer/config/dev/erigon.exs | 7 +- apps/indexer/config/dev/filecoin.exs | 7 +- apps/indexer/config/dev/geth.exs | 7 +- apps/indexer/config/dev/nethermind.exs | 7 +- apps/indexer/config/dev/rsk.exs | 7 +- apps/indexer/config/prod/anvil.exs | 7 +- apps/indexer/config/prod/besu.exs | 7 +- apps/indexer/config/prod/erigon.exs | 7 +- apps/indexer/config/prod/filecoin.exs | 7 +- apps/indexer/config/prod/geth.exs | 7 +- apps/indexer/config/prod/nethermind.exs | 7 +- apps/indexer/config/prod/rsk.exs | 7 +- .../lib/indexer/fetcher/beacon/client.ex | 9 +- .../lib/indexer/fetcher/filecoin/beryx_api.ex | 15 +- .../indexer/fetcher/filecoin/filfox_api.ex | 15 +- .../lib/indexer/fetcher/polygon_edge.ex | 4 +- .../lib/indexer/fetcher/shibarium/l1.ex | 4 +- .../fetcher/zksync/batches_status_tracker.ex | 4 +- apps/indexer/lib/indexer/helper.ex | 4 +- .../test/indexer/fetcher/beacon/blob_test.exs | 40 +-- .../main_export_queue_test.exs | 42 ++- .../token_instance_metadata_refetch_test.exs | 48 ++-- .../fetcher/token_instance/helper_test.exs | 52 ++-- .../fetcher/token_instance/realtime_test.exs | 3 + .../fetcher/token_instance/refetch_test.exs | 3 + apps/indexer/test/test_helper.exs | 2 +- .../lib/utils/http_client/httpoison_helper.ex | 62 +++++ .../lib/utils/http_client/tesla_helper.ex | 85 ++++++ apps/utils/mix.exs | 5 +- config/config_helper.exs | 9 +- mix.exs | 2 +- 125 files changed, 1240 insertions(+), 706 deletions(-) create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/helper.ex create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/tesla.ex create mode 100644 apps/explorer/lib/explorer/http_client.ex create mode 100644 apps/explorer/lib/explorer/http_client/httpoison.ex create mode 100644 apps/explorer/lib/explorer/http_client/tesla.ex create mode 100644 apps/utils/lib/utils/http_client/httpoison_helper.ex create mode 100644 apps/utils/lib/utils/http_client/tesla_helper.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 196dd702bd85..5e97d6ec1275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ - Support multiple interop messages view on transaction page ([#12455](https://github.com/blockscout/blockscout/issues/12455)) - Remove `is_self_destructed` field in `/api/v2/smart-contracts/{address_hash}` response ([#12239](https://github.com/blockscout/blockscout/issues/12239)) - Set home directory for blockscout user ([#12337](https://github.com/blockscout/blockscout/issues/12337)) +- Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) | Variable | Description | Parameters | diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 705ee05abbff..80cbef2152b8 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -18,7 +18,7 @@ config :block_scout_web, session_cookie_ttl: 60 * 60 * 24 * 7, invalid_session_key: "invalid_session", api_v2_temp_token_key: "api_v2_temp_token", - http_adapter: HTTPoison + http_client: Explorer.HttpClient.Tesla config :block_scout_web, admin_panel_enabled: ConfigHelper.parse_bool_env_var("ADMIN_PANEL_ENABLED") @@ -118,6 +118,8 @@ config :ueberauth, Ueberauth, } ] +config :tesla, adapter: Tesla.Adapter.Mint + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/apps/block_scout_web/config/test.exs b/apps/block_scout_web/config/test.exs index 033fc74d0444..f7327da90a86 100644 --- a/apps/block_scout_web/config/test.exs +++ b/apps/block_scout_web/config/test.exs @@ -24,6 +24,8 @@ config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: f config :block_scout_web, BlockScoutWeb.Counters.InternalTransactionsIndexedCounter, enabled: false +config :tesla, adapter: Explorer.Mock.TeslaAdapter + config :ueberauth, Ueberauth, providers: [ auth0: { diff --git a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex index 80df6c8128ca..816aa9bcdd6c 100644 --- a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.CaptchaHelper do """ require Logger - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} @type token_scope() :: :token_instance_refetch_metadata @@ -103,13 +103,12 @@ defmodule BlockScoutWeb.CaptchaHelper do headers = [{"Content-type", "application/x-www-form-urlencoded"}] case !Application.get_env(:block_scout_web, :recaptcha)[:is_disabled] && - Application.get_env(:block_scout_web, :http_adapter).post( + HttpClient.post( "https://www.google.com/recaptcha/api/siteverify", body, - headers, - [] + headers ) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, %{status_code: 200, body: body}} -> body |> Jason.decode!() |> success?() false -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex index 4bec5561cece..8b4af23a2a06 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.Account.API.V2.EmailController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.Account.API.V2.AuthenticateController alias Explorer.Account.Identity - alias Explorer.{Helper, Repo} + alias Explorer.{Helper, HttpClient, Repo} alias Explorer.ThirdPartyIntegrations.Auth0 require Logger @@ -36,8 +36,8 @@ defmodule BlockScoutWeb.Account.API.V2.EmailController do "user_id" => user.uid } - case HTTPoison.post(url, Jason.encode!(body), headers, []) do - {:ok, %HTTPoison.Response{body: _body, status_code: 201}} -> + case HttpClient.post(url, Jason.encode!(body), headers) do + {:ok, %{body: _body, status_code: 201}} -> identity |> Identity.changeset(%{verification_email_sent_at: DateTime.utc_now()}) |> Repo.account_repo().update() diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 2685f5ec523c..2acbd97f8884 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -5,10 +5,9 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do alias BlockScoutWeb.API.V2.{Helper, InternalTransactionView, TokenTransferView, TokenView, TransactionView} alias Ecto.Association.NotLoaded - alias Explorer.Chain + alias Explorer.{Chain, HttpClient} alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Data, InternalTransaction, Log, TokenTransfer, Transaction} - alias HTTPoison.Response import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0, proxy_implementations_smart_contracts_association: 0] @@ -85,8 +84,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %{body: body, status_code: 200}} -> body |> Jason.decode() |> preload_template_variables() error -> @@ -106,7 +105,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do end defp try_get_cached_value(hash) do - with {:ok, %Response{body: body, status_code: 200}} <- HTTPoison.get(cache_url(hash)), + with {:ok, %{body: body, status_code: 200}} <- HttpClient.get(cache_url(hash)), {:ok, json} <- body |> Jason.decode() do {:ok, json} |> preload_template_variables() else @@ -115,7 +114,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do end end - defp http_response_code({:ok, %Response{status_code: status_code}}), do: status_code + defp http_response_code({:ok, %{status_code: status_code}}), do: status_code defp http_response_code(_), do: 500 def enabled?, do: check_enabled(:block_scout_web, __MODULE__) == :ok diff --git a/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex b/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex index 3a8a32de9ca8..32a77553ec3d 100644 --- a/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex @@ -4,6 +4,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelper do """ require Logger + alias Explorer.HttpClient alias Utils.ConfigHelper @doc """ @@ -48,16 +49,16 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelper do defp download_config(url) when is_binary(url) do url - |> HTTPoison.get([], follow_redirect: true) + |> HttpClient.get([], follow_redirect: true) |> case do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + {:ok, %{status_code: 200, body: body}} -> decode_config(body) - {:ok, %HTTPoison.Response{status_code: status}} -> + {:ok, %{status_code: status}} -> Logger.error("Failed to fetch config from #{url}: #{status}") {:error, status} - {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} -> {:error, reason} end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs index 6f0608f128d3..5f1d55ea2f81 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs @@ -9,6 +9,12 @@ defmodule BlockScoutWeb.Account.CustomABIControllerTest do setup %{conn: conn} do auth = build(:auth) + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + {:ok, user} = Identity.find_or_create(auth) {:ok, conn: Plug.Test.init_test_session(conn, current_user: user)} diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs index 62e23e20139c..6d936655b588 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs @@ -7,6 +7,14 @@ defmodule BlockScoutWeb.AddressContractControllerTest do alias Explorer.Market.Token alias Explorer.{Factory, TestHelper} + setup do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + end + describe "GET index/3" do test "returns not found for nonexistent address", %{conn: conn} do nonexistent_address_hash = Hash.to_string(Factory.address_hash()) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index eb44da8ea6fa..08d194d4ded3 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -76,6 +76,14 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } end + setup do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + end + describe "listcontracts" do setup do %{params: %{"module" => "contract", "action" => "listcontracts"}} diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 525d981663e7..6b93c0f4d575 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2668,6 +2668,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + old_chain_id = Application.get_env(:block_scout_web, :chain_id) chain_id = 1 Application.put_env(:block_scout_web, :chain_id, chain_id) @@ -2747,6 +2749,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do Application.put_env(:block_scout_web, :chain_id, old_chain_id) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, old_env_bens) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, old_env_metadata) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end @@ -2938,6 +2941,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do test "ignore logs without topics when trying to decode with sig provider", %{conn: conn} do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + old_env_sig_provider = Application.get_env(:explorer, Explorer.SmartContract.SigProviderInterface) Application.put_env(:explorer, Explorer.SmartContract.SigProviderInterface, @@ -2947,6 +2952,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.SigProviderInterface, old_env_sig_provider) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) address = insert(:contract_address) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index e281f6d78007..77b938dcb183 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -57,10 +57,16 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do } do expected_body = "secret=#{recaptcha_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"success" => false})}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"success" => false}) + }} + end + ) address = insert(:address) @@ -148,18 +154,20 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do test "exports token transfers to csv", %{conn: conn, v2_secret_key: recaptcha_secret_key} do expected_body = "secret=#{recaptcha_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) address = insert(:address) @@ -209,18 +217,20 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do test "download csv file with transactions", %{conn: conn, v2_secret_key: recaptcha_secret_key} do expected_body = "secret=#{recaptcha_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) address = insert(:address) @@ -270,18 +280,20 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do test "download csv file with internal transactions", %{conn: conn, v2_secret_key: recaptcha_secret_key} do expected_body = "secret=#{recaptcha_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) address = insert(:address) @@ -368,18 +380,20 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do test "download csv file with logs", %{conn: conn, v2_secret_key: recaptcha_secret_key} do expected_body = "secret=#{recaptcha_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) address = insert(:address) @@ -489,7 +503,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do defp csv_setup() do original_config = :persistent_term.get(:rate_limit_config) old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) - old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) original_api_rate_limit = Application.get_env(:block_scout_web, :api_rate_limit) v2_secret_key = "v2_secret_key" @@ -501,8 +514,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do is_disabled: false ) - Application.put_env(:block_scout_web, :http_adapter, Explorer.Mox.HTTPoison) - Application.put_env(:block_scout_web, :api_rate_limit, Keyword.put(original_api_rate_limit, :disabled, false)) config = %{ @@ -553,7 +564,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do on_exit(fn -> :persistent_term.put(:rate_limit_config, original_config) Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) - Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) Application.put_env(:block_scout_web, :api_rate_limit, original_api_rate_limit) end) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 2e759956a252..a153b45531fd 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -459,6 +459,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do metadata_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) bens_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) old_chain_id = Application.get_env(:block_scout_web, :chain_id) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) chain_id = 1 Application.put_env(:block_scout_web, :chain_id, chain_id) old_hide_scam_addresses = Application.get_env(:block_scout_web, :hide_scam_addresses) @@ -470,6 +471,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) Application.put_env(:block_scout_web, :chain_id, old_chain_id) Application.put_env(:block_scout_web, :hide_scam_addresses, old_hide_scam_addresses) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, @@ -773,12 +775,14 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do old_chain_id = Application.get_env(:block_scout_web, :chain_id) chain_id = 1 Application.put_env(:block_scout_web, :chain_id, chain_id) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, metadata_envs) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, @@ -1127,9 +1131,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" @@ -1186,9 +1193,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" @@ -1227,9 +1237,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) transaction = insert(:transaction) |> with_block() @@ -1293,9 +1306,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) transaction = insert(:transaction) |> with_block() @@ -1361,9 +1377,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) address_hash = Address.checksum(insert(:address).hash) @@ -1472,9 +1491,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) tac_response = """ @@ -1646,9 +1668,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" @@ -1870,12 +1895,14 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do old_chain_id = Application.get_env(:block_scout_web, :chain_id) chain_id = 1 Application.put_env(:block_scout_web, :chain_id, chain_id) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, metadata_envs) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, bens_envs) Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, @@ -2023,9 +2050,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" @@ -2079,9 +2109,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) transaction = insert(:transaction) |> with_block() @@ -2145,9 +2178,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) transaction = insert(:transaction) |> with_block() @@ -2213,9 +2249,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) address_hash = Address.checksum(insert(:address).hash) @@ -2294,9 +2333,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) tac_response = """ @@ -2478,9 +2520,12 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do enabled: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Bypass.down(bypass) Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) transaction = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 40d1b59c06ea..f22bbbe637f3 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -34,6 +34,14 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end describe "/smart-contracts/{address_hash}" do + setup do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + end + test "get 404 on non existing SC", %{conn: conn} do address = build(:address) @@ -591,6 +599,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end test "doesn't get smart-contract implementation for 'Clones with immutable arguments' pattern", %{conn: conn} do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + implementation_contract = insert(:smart_contract, external_libraries: [], @@ -671,6 +681,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") response = json_response(request, 200) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + result_props = correct_response |> Map.keys() for prop <- result_props do @@ -691,6 +703,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:block_scout_web, :chain_id, 5) Application.put_env( @@ -709,6 +723,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Application.put_env(:explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, old_fetcher_env) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_verifier_env) Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index fcc9a971c931..08d0dd83a37c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1537,8 +1537,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do original_config = :persistent_term.get(:rate_limit_config) old_recaptcha_env = Application.get_env(:block_scout_web, :recaptcha) - old_http_adapter = Application.get_env(:block_scout_web, :http_adapter) - old_explorer_http_adapter = Application.get_env(:explorer, :http_adapter) original_api_rate_limit = Application.get_env(:block_scout_web, :api_rate_limit) v2_secret_key = "v2_secret_key" @@ -1550,8 +1548,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do is_disabled: false ) - Application.put_env(:block_scout_web, :http_adapter, Explorer.Mox.HTTPoison) - mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) @@ -1587,8 +1583,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do on_exit(fn -> :persistent_term.put(:rate_limit_config, original_config) Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) - Application.put_env(:block_scout_web, :http_adapter, old_http_adapter) - Application.put_env(:explorer, :http_adapter, old_explorer_http_adapter) Application.put_env(:block_scout_web, :api_rate_limit, original_api_rate_limit) :ets.delete_all_objects(BlockScoutWeb.RateLimit.Hammer.ETS) end) @@ -1613,12 +1607,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) topic = "token_instances:#{token_contract_address_hash_string}" @@ -1654,25 +1652,33 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do expected_body = "secret=#{v2_secret_key}&response=123" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) request = Phoenix.ConnTest.build_conn() @@ -1743,12 +1749,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) topic = "token_instances:#{token_contract_address_hash_string}" @@ -1800,12 +1810,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 500, body: "Error"}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 500, + body: "Error" + }} + end + ) topic = "token_instances:#{token_contract_address_hash_string}" @@ -1857,11 +1871,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do ) ) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) - Application.put_env(:explorer, :http_adapter, HTTPoison) end) token = insert(:token, type: "ERC-721") @@ -1879,10 +1890,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) topic = "token_instances:#{token_contract_address_hash_string}" @@ -1925,10 +1942,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) request = Phoenix.ConnTest.build_conn() @@ -1977,11 +2000,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do ) ) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) - Application.put_env(:explorer, :http_adapter, HTTPoison) end) token = insert(:token, type: "ERC-721") @@ -1999,11 +2019,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"name" => "test"})}} - end) - request = patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) @@ -2021,25 +2036,44 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"name" => "test"}) + }} + end + ) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) topic = "token_instances:#{token_contract_address_hash_string}" @@ -2089,11 +2123,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do ) ) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - on_exit(fn -> Application.put_env(:block_scout_web, :recaptcha, old_recaptcha_env) - Application.put_env(:explorer, :http_adapter, HTTPoison) end) token = insert(:token, type: "ERC-721") @@ -2110,11 +2141,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do token_contract_address_hash_string = to_string(token.contract_address_hash) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"name" => "test"})}} - end) - request = patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) @@ -2148,25 +2174,44 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" - Explorer.Mox.HTTPoison - |> expect(:post, fn _url, ^expected_body, _headers, _options -> - {:ok, - %HTTPoison.Response{ - status_code: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"name" => "test"}) + }} + end + ) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) # Second request with correct reCAPTCHA token - should work request = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs index 2a83fcfbc87e..de46a0b10b0b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -13,9 +13,11 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do setup do configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) end @@ -291,6 +293,8 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do eth_bytecode_db?: true ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect_once(bypass, "POST", "/api/v2//verifier/vyper/sources%3Averify-multi-part", fn conn -> Conn.resp(conn, 200, sc_verifier_response) end) @@ -344,6 +348,7 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do assert response["is_blueprint"] == true Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, old_env) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end end diff --git a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs index bfad4cf065f8..f8451c62ffaf 100644 --- a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs @@ -13,8 +13,11 @@ defmodule BlockScoutWeb.AddressContractVerificationTest do Application.put_env(:explorer, :solc_bin_api_url, "http://localhost:#{bypass.port}") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, bypass: bypass} diff --git a/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs b/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs index f806f4be7789..58c39d5f458d 100644 --- a/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs +++ b/apps/block_scout_web/test/block_scout_web/utility/rate_limit_config_helper_test.exs @@ -11,6 +11,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do on_exit(fn -> Application.put_env(:block_scout_web, :api_rate_limit, original_config) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) :persistent_term.put(:rate_limit_config, original_config_from_persistent_term) end) end @@ -24,6 +25,9 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do } bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") Bypass.expect_once(bypass, "GET", "/config", fn conn -> @@ -48,6 +52,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do test "falls back to local config when URL fetch fails" do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 500, "Internal Server Error") @@ -73,6 +78,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -106,6 +112,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -127,6 +134,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -152,6 +160,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -182,6 +191,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -228,6 +238,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) @@ -270,6 +281,7 @@ defmodule BlockScoutWeb.Utility.RateLimitConfigHelperTest do bypass = Bypass.open() Application.put_env(:block_scout_web, :api_rate_limit, config_url: "http://localhost:#{bypass.port}/config") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Bypass.expect_once(bypass, "GET", "/config", fn conn -> Plug.Conn.resp(conn, 200, Jason.encode!(config)) diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index a40cf54b4702..13d0c3016555 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -42,4 +42,4 @@ Absinthe.Test.prime(BlockScoutWeb.GraphQL.Schema) Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport) -Mox.defmock(Explorer.Mox.HTTPoison, for: HTTPoison.Base) +Mox.defmock(Explorer.Mock.TeslaAdapter, for: Tesla.Adapter) diff --git a/apps/ethereum_jsonrpc/README.md b/apps/ethereum_jsonrpc/README.md index 7dfe70dd55d3..d84bcfaa1a7f 100644 --- a/apps/ethereum_jsonrpc/README.md +++ b/apps/ethereum_jsonrpc/README.md @@ -17,8 +17,8 @@ config :ethereum_jsonrpc, Note: the tracing node URL is provided separately from `:url`, via `:trace_url`. The trace URL is used for `fetch_internal_transactions`, which is only a supported method on -tracing nodes. The `:http` option is passed directly to the HTTP -library (`HTTPoison`), which forwards the options down to `:hackney`. +tracing nodes. The `:http` option is adapted +to the HTTP library (`HTTPoison` or `Tesla.Mint`). ## Testing diff --git a/apps/ethereum_jsonrpc/config/config.exs b/apps/ethereum_jsonrpc/config/config.exs index 503b7a3828ed..7df95245ee45 100644 --- a/apps/ethereum_jsonrpc/config/config.exs +++ b/apps/ethereum_jsonrpc/config/config.exs @@ -29,6 +29,8 @@ config :logger, :ethereum_jsonrpc, block_number step count error_count shrunk import_id transaction_id)a, metadata_filter: [application: :ethereum_jsonrpc] +config :tesla, adapter: Tesla.Adapter.Mint + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/apps/ethereum_jsonrpc/config/test.exs b/apps/ethereum_jsonrpc/config/test.exs index 61e5f67398a0..f04b52869c67 100644 --- a/apps/ethereum_jsonrpc/config/test.exs +++ b/apps/ethereum_jsonrpc/config/test.exs @@ -17,6 +17,8 @@ config :ethereum_jsonrpc, EthereumJSONRPC.RequestCoordinator, config :ethereum_jsonrpc, EthereumJSONRPC.Tracer, disabled?: false +config :tesla, adapter: Explorer.Mock.TeslaAdapter + config :logger, :ethereum_jsonrpc, level: :warn, path: Path.absname("logs/test/ethereum_jsonrpc.log") diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 0d1a15fb30e0..53e0c29c9ef5 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -9,12 +9,12 @@ defmodule EthereumJSONRPC do config :ethereum_jsonrpc, url: "http://localhost:8545", trace_url: "http://localhost:8545", - http: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] + http: [recv_timeout: 60_000, timeout: 60_000, pool: :ethereum_jsonrpc] Note: the tracing node URL is provided separately from `:url`, via `:trace_url`. The trace URL and is used for - `fetch_internal_transactions`, which is only a supported method on tracing nodes. The `:http` option is passed - directly to the HTTP library (`HTTPoison`), which forwards the options down to `:hackney`. + `fetch_internal_transactions`, which is only a supported method on tracing nodes. The `:http` option is adapted + to the HTTP library (`HTTPoison` or `Tesla.Mint`). ## Throttling diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex index 1d791aa73dfc..92fc0753c3f4 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex @@ -266,6 +266,15 @@ defmodule EthereumJSONRPC.HTTP do end defp headers do - Application.get_env(:ethereum_jsonrpc, __MODULE__)[:headers] + gzip_enabled? = Application.get_env(:ethereum_jsonrpc, __MODULE__)[:gzip_enabled?] + + additional_headers = + if gzip_enabled? do + [{"Accept-Encoding", "gzip"}] + else + [] + end + + Application.get_env(:ethereum_jsonrpc, __MODULE__)[:headers] ++ additional_headers end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/helper.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/helper.ex new file mode 100644 index 000000000000..7817b679ffa6 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/helper.ex @@ -0,0 +1,85 @@ +defmodule EthereumJSONRPC.HTTP.Helper do + @moduledoc """ + Helper functions for `EthereumJSONRPC.HTTP` implementations. + """ + + @doc """ + Extracts the JSON-RPC method from a JSON string payload. + + Supports both single objects and batch requests (arrays). + + ## Parameters + - `json_string`: The JSON string to parse + + ## Returns + - The method name as a binary, or `{:error, Jason.DecodeError.t()}` if extraction fails + """ + @spec get_method_from_json_string(binary()) :: binary() | {:error, Jason.DecodeError.t()} + def get_method_from_json_string(json_string) do + with {:ok, decoded_json} <- Jason.decode(json_string) do + if is_map(decoded_json) do + Map.get(decoded_json, "method") + else + decoded_json |> Enum.at(0) |> Map.get("method") + end + end + end + + @spec response_body_has_error?(map() | [map()]) :: boolean() + def response_body_has_error?(decoded_body) when is_map(decoded_body) do + Map.has_key?(decoded_body, "error") + end + + def response_body_has_error?(decoded_body) when is_list(decoded_body) do + Enum.any?(decoded_body, &response_body_has_error?/1) + end + + def response_body_has_error?(_decoded_body), do: false + + @doc """ + Conditionally decompresses gzip-encoded HTTP response bodies. + + Checks application configuration and HTTP headers to determine if decompression + should be attempted. + + ## Parameters + - `body`: The response body to potentially decompress + - `headers`: List of HTTP response headers as {key, value} tuples + + ## Returns + - Decompressed body if gzip-enabled and content is gzipped, otherwise original body + """ + @spec try_unzip(binary(), [{binary(), binary()}]) :: binary() + def try_unzip(body, headers) do + gzip_enabled? = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.HTTP)[:gzip_enabled?] + + if gzip_enabled? do + do_unzip(body, headers) + else + body + end + end + + defp do_unzip(body, headers) do + gzipped = + Enum.any?( + headers + |> Enum.map(fn {k, v} -> + {String.downcase(k), String.downcase(v)} + end), + fn kv -> + case kv do + {"content-encoding", "gzip"} -> true + {"content-encoding", "x-gzip"} -> true + _ -> false + end + end + ) + + if gzipped do + :zlib.gunzip(body) + else + body + end + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/httpoison.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/httpoison.ex index 60ef7d8047fc..abc04c7bde7d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/httpoison.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/httpoison.ex @@ -4,33 +4,26 @@ defmodule EthereumJSONRPC.HTTP.HTTPoison do """ alias EthereumJSONRPC.HTTP + alias EthereumJSONRPC.HTTP.Helper alias EthereumJSONRPC.Prometheus.Instrumenter + alias Utils.HttpClient.HTTPoisonHelper @behaviour HTTP @impl HTTP def json_rpc(url, json, headers, options) when is_binary(url) and is_list(options) do - gzip_enabled? = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.HTTP)[:gzip_enabled?] - - headers = - if gzip_enabled? do - [{"Accept-Encoding", "gzip"} | headers] - else - headers - end - - method = get_method_from_json_string(json) + method = Helper.get_method_from_json_string(json) Instrumenter.json_rpc_requests(method) - case HTTPoison.post(url, json, headers, options) do + case HTTPoison.post(url, json, headers, HTTPoisonHelper.request_opts(options)) do {:ok, %HTTPoison.Response{body: body, status_code: status_code, headers: headers}} -> with {:ok, decoded_body} <- Jason.decode(body), - true <- response_body_has_error?(decoded_body) do + true <- Helper.response_body_has_error?(decoded_body) do Instrumenter.json_rpc_errors(method) end - {:ok, %{body: try_unzip(gzip_enabled?, body, headers), status_code: status_code}} + {:ok, %{body: Helper.try_unzip(body, headers), status_code: status_code}} {:error, %HTTPoison.Error{reason: reason}} -> Instrumenter.json_rpc_errors(method) @@ -40,51 +33,4 @@ defmodule EthereumJSONRPC.HTTP.HTTPoison do end def json_rpc(url, _json, _headers, _options) when is_nil(url), do: {:error, "URL is nil"} - - defp get_method_from_json_string(json_string) do - with {:ok, decoded_json} <- Jason.decode(json_string) do - if is_map(decoded_json) do - Map.get(decoded_json, "method") - else - decoded_json |> Enum.at(0) |> Map.get("method") - end - end - end - - defp response_body_has_error?(decoded_body) when is_map(decoded_body) do - Map.has_key?(decoded_body, "error") - end - - defp response_body_has_error?(decoded_body) when is_list(decoded_body) do - Enum.any?(decoded_body, &response_body_has_error?/1) - end - - defp response_body_has_error?(_decoded_body), do: false - - defp try_unzip(true, body, headers) do - gzipped = - Enum.any?( - headers - |> Enum.map(fn {k, v} -> - {String.downcase(k), String.downcase(v)} - end), - fn kv -> - case kv do - {"content-encoding", "gzip"} -> true - {"content-encoding", "x-gzip"} -> true - _ -> false - end - end - ) - - if gzipped do - :zlib.gunzip(body) - else - body - end - end - - defp try_unzip(_gzip_enabled?, body, _headers) do - body - end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/tesla.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/tesla.ex new file mode 100644 index 000000000000..4db01c3df862 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http/tesla.ex @@ -0,0 +1,36 @@ +defmodule EthereumJSONRPC.HTTP.Tesla do + @moduledoc """ + Uses `Tesla.Mint` for `EthereumJSONRPC.HTTP` + """ + + alias EthereumJSONRPC.HTTP + alias EthereumJSONRPC.HTTP.Helper + alias EthereumJSONRPC.Prometheus.Instrumenter + alias Utils.HttpClient.TeslaHelper + + @behaviour HTTP + + @impl HTTP + def json_rpc(url, json, headers, options) when is_binary(url) and is_list(options) do + method = Helper.get_method_from_json_string(json) + + Instrumenter.json_rpc_requests(method) + + case Tesla.post(TeslaHelper.client(options), url, json, headers: headers, opts: TeslaHelper.request_opts(options)) do + {:ok, %Tesla.Env{body: body, status: status_code, headers: headers}} -> + with {:ok, decoded_body} <- Jason.decode(body), + true <- Helper.response_body_has_error?(decoded_body) do + Instrumenter.json_rpc_errors(method) + end + + {:ok, %{body: Helper.try_unzip(body, headers), status_code: status_code}} + + {:error, error} -> + Instrumenter.json_rpc_errors(method) + + {:error, error} + end + end + + def json_rpc(url, _json, _headers, _options) when is_nil(url), do: {:error, "URL is nil"} +end diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 9a8062b01120..439e50198566 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -31,7 +31,7 @@ defmodule EthereumJSONRPC.MixProject do def application do [ mod: {EthereumJSONRPC.Application, []}, - extra_applications: [:logger] + extra_applications: [:logger, :tesla] ] end @@ -74,6 +74,7 @@ defmodule EthereumJSONRPC.MixProject do {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog {:spandex_datadog, "~> 1.0"}, + {:tesla, "~> 1.14.2"}, # Convert unix timestamps in JSONRPC to DateTimes {:timex, "~> 3.7.1"}, # Encode/decode function names and arguments diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex index fbbee61574da..194680321d09 100644 --- a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case.ex @@ -15,7 +15,8 @@ defmodule EthereumJSONRPC.Case do environment variable to determine `:json_rpc_named_arguments` `:transport_options` `:url`. Failure to set `ETHEREUM_JSONRPC_HTTP_URL` in this case will raise an `ArgumentError`. - * `EthereumJSONRPC.HTTP.HTTPoison` - HTTP responses from calls to real chain URLs + * `EthereumJSONRPC.HTTP.HTTPoison` - HTTP responses from calls to real chain URLs (using HTTPoison client) + * `EthereumJSONRPC.HTTP.Tesla` - HTTP responses from calls to real chain URLs (using Tesla.Mint client) * `EthereumJSONRPC.HTTP.Mox` - mock HTTP responses, so can be used for HTTP-only behavior like status codes. ## `subscribe_named_arguments` diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/http_websocket.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/http_websocket.ex index 403daf407b1f..5845518cf367 100644 --- a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/http_websocket.ex +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/geth/http_websocket.ex @@ -10,8 +10,8 @@ defmodule EthereumJSONRPC.Case.Geth.HTTPWebSocket do :json_rpc_named_arguments, transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, - http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]], + http: EthereumJSONRPC.HTTP.Tesla, + http_options: [recv_timeout: 60_000, timeout: 60_000, pool: :ethereum_jsonrpc], urls: ["https://mainnet.infura.io/8lTvJTKmHPCHazkneJsY"] ], variant: EthereumJSONRPC.Geth diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex index 4f516d59613f..ef087b5b11d8 100644 --- a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/case/nethermind/http_websocket.ex @@ -10,8 +10,8 @@ defmodule EthereumJSONRPC.Case.Nethermind.HTTPWebSocket do :json_rpc_named_arguments, transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, - http_options: [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]], + http: EthereumJSONRPC.HTTP.Tesla, + http_options: [recv_timeout: 60_000, timeout: 60_000, pool: :ethereum_jsonrpc], urls: ["http://3.85.253.242:8545"] ], variant: EthereumJSONRPC.Nethermind diff --git a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/http/case.ex b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/http/case.ex index 767f41e4d58f..7488dc96b8cc 100644 --- a/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/http/case.ex +++ b/apps/ethereum_jsonrpc/test/support/ethereum_jsonrpc/http/case.ex @@ -21,7 +21,7 @@ defmodule EthereumJSONRPC.HTTP.Case do end def http_options do - [recv_timeout: 60_000, timeout: 60_000, hackney: [pool: :ethereum_jsonrpc]] + [recv_timeout: 60_000, timeout: 60_000, pool: :ethereum_jsonrpc] end def url do diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index b99c37d2c383..6eadf5ef5dfd 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -197,7 +197,7 @@ config :explorer, Explorer.Tracer, config :explorer, solc_bin_api_url: "https://solc-bin.ethereum.org" -config :explorer, :http_adapter, HTTPoison +config :explorer, :http_client, Explorer.HttpClient.Tesla config :explorer, Explorer.Chain.BridgedToken, enabled: ConfigHelper.parse_bool_env_var("BRIDGED_TOKENS_ENABLED") @@ -214,6 +214,8 @@ config :spandex_ecto, SpandexEcto.EctoLogger, tracer: Explorer.Tracer, otp_app: :explorer +config :tesla, adapter: Tesla.Adapter.Mint + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{config_env()}.exs" diff --git a/apps/explorer/config/dev/anvil.exs b/apps/explorer/config/dev/anvil.exs index be2764b3704f..58e65a7b126c 100644 --- a/apps/explorer/config/dev/anvil.exs +++ b/apps/explorer/config/dev/anvil.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), fallback_urls: ConfigHelper.parse_urls_list(:fallback_http), @@ -19,7 +16,7 @@ config :explorer, method_to_url: [ eth_call: :eth_call ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Anvil ], diff --git a/apps/explorer/config/dev/besu.exs b/apps/explorer/config/dev/besu.exs index 2493d3021123..b761b45abbf5 100644 --- a/apps/explorer/config/dev/besu.exs +++ b/apps/explorer/config/dev/besu.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Besu ], diff --git a/apps/explorer/config/dev/erigon.exs b/apps/explorer/config/dev/erigon.exs index 0275a0a1067e..a32d59ca293e 100644 --- a/apps/explorer/config/dev/erigon.exs +++ b/apps/explorer/config/dev/erigon.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Erigon ], diff --git a/apps/explorer/config/dev/filecoin.exs b/apps/explorer/config/dev/filecoin.exs index 9fcf631e1b29..12882aa3d139 100644 --- a/apps/explorer/config/dev/filecoin.exs +++ b/apps/explorer/config/dev/filecoin.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http, "http://localhost:1234/rpc/v1"), trace_urls: ConfigHelper.parse_urls_list(:trace, "http://localhost:1234/rpc/v1"), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call, "http://localhost:1234/rpc/v1"), @@ -22,7 +19,7 @@ config :explorer, eth_call: :eth_call, trace_block: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Filecoin ], diff --git a/apps/explorer/config/dev/geth.exs b/apps/explorer/config/dev/geth.exs index 745a74453dc6..0537f0d4d436 100644 --- a/apps/explorer/config/dev/geth.exs +++ b/apps/explorer/config/dev/geth.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, debug_traceTransaction: :trace, debug_traceBlockByNumber: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Geth ], diff --git a/apps/explorer/config/dev/nethermind.exs b/apps/explorer/config/dev/nethermind.exs index 3147355e90e3..995621848fe0 100644 --- a/apps/explorer/config/dev/nethermind.exs +++ b/apps/explorer/config/dev/nethermind.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Nethermind ], diff --git a/apps/explorer/config/dev/rsk.exs b/apps/explorer/config/dev/rsk.exs index 71782fd05901..c18344ec8abb 100644 --- a/apps/explorer/config/dev/rsk.exs +++ b/apps/explorer/config/dev/rsk.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.RSK ], diff --git a/apps/explorer/config/prod/anvil.exs b/apps/explorer/config/prod/anvil.exs index be2764b3704f..58e65a7b126c 100644 --- a/apps/explorer/config/prod/anvil.exs +++ b/apps/explorer/config/prod/anvil.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), fallback_urls: ConfigHelper.parse_urls_list(:fallback_http), @@ -19,7 +16,7 @@ config :explorer, method_to_url: [ eth_call: :eth_call ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Anvil ], diff --git a/apps/explorer/config/prod/besu.exs b/apps/explorer/config/prod/besu.exs index 2493d3021123..b761b45abbf5 100644 --- a/apps/explorer/config/prod/besu.exs +++ b/apps/explorer/config/prod/besu.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Besu ], diff --git a/apps/explorer/config/prod/erigon.exs b/apps/explorer/config/prod/erigon.exs index 0275a0a1067e..a32d59ca293e 100644 --- a/apps/explorer/config/prod/erigon.exs +++ b/apps/explorer/config/prod/erigon.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Erigon ], diff --git a/apps/explorer/config/prod/filecoin.exs b/apps/explorer/config/prod/filecoin.exs index 22c3862f7482..c006935abd16 100644 --- a/apps/explorer/config/prod/filecoin.exs +++ b/apps/explorer/config/prod/filecoin.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -22,7 +19,7 @@ config :explorer, eth_call: :eth_call, trace_block: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Filecoin ], diff --git a/apps/explorer/config/prod/geth.exs b/apps/explorer/config/prod/geth.exs index 745a74453dc6..0537f0d4d436 100644 --- a/apps/explorer/config/prod/geth.exs +++ b/apps/explorer/config/prod/geth.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, debug_traceTransaction: :trace, debug_traceBlockByNumber: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Geth ], diff --git a/apps/explorer/config/prod/nethermind.exs b/apps/explorer/config/prod/nethermind.exs index 3147355e90e3..995621848fe0 100644 --- a/apps/explorer/config/prod/nethermind.exs +++ b/apps/explorer/config/prod/nethermind.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Nethermind ], diff --git a/apps/explorer/config/prod/rsk.exs b/apps/explorer/config/prod/rsk.exs index 71782fd05901..c18344ec8abb 100644 --- a/apps/explorer/config/prod/rsk.exs +++ b/apps/explorer/config/prod/rsk.exs @@ -4,14 +4,11 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :explorer, json_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -23,7 +20,7 @@ config :explorer, eth_getBalance: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.RSK ], diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index 3152b9620b1a..c82fa0c57f92 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Blackfort.Validator do alias Explorer.Chain.{Address, Import} alias Explorer.Chain.Hash.Address, as: HashAddress - alias Explorer.{Chain, Helper, Repo, SortingHelper} + alias Explorer.{Chain, Helper, HttpClient, Repo, SortingHelper} require Logger @@ -148,8 +148,7 @@ defmodule Explorer.Chain.Blackfort.Validator do url = validator_url() with {:url, true} <- {:url, Helper.valid_url?(url)}, - {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- - HTTPoison.get(validator_url(), [], follow_redirect: true) do + {:ok, %{status_code: 200, body: body}} <- HttpClient.get(validator_url(), [], follow_redirect: true) do body |> Jason.decode() |> parse_validators_info() else {:url, false} -> diff --git a/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex b/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex index 7ce8d834368f..4de14b059e8d 100644 --- a/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex +++ b/apps/explorer/lib/explorer/chain/cache/optimism_finalization_period.ex @@ -41,12 +41,12 @@ defmodule Explorer.Chain.Cache.OptimismFinalizationPeriod do [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: [optimism_l1_rpc], http_options: [ recv_timeout: :timer.minutes(10), timeout: :timer.minutes(10), - hackney: [pool: :ethereum_jsonrpc] + pool: :ethereum_jsonrpc ] ] ] diff --git a/apps/explorer/lib/explorer/chain/fetcher/addresses_blacklist/blockaid.ex b/apps/explorer/lib/explorer/chain/fetcher/addresses_blacklist/blockaid.ex index 140f37e3c8b8..736340f9fa4a 100644 --- a/apps/explorer/lib/explorer/chain/fetcher/addresses_blacklist/blockaid.ex +++ b/apps/explorer/lib/explorer/chain/fetcher/addresses_blacklist/blockaid.ex @@ -2,7 +2,7 @@ defmodule Explorer.Chain.Fetcher.AddressesBlacklist.Blockaid do @moduledoc """ Fetcher for addresses blacklist from blockaid provider """ - alias Explorer.Chain + alias Explorer.{Chain, HttpClient} alias Explorer.Chain.Fetcher.AddressesBlacklist @behaviour AddressesBlacklist @@ -12,8 +12,8 @@ defmodule Explorer.Chain.Fetcher.AddressesBlacklist.Blockaid do @impl AddressesBlacklist def fetch_addresses_blacklist do - case HTTPoison.get(AddressesBlacklist.url(), [], recv_timeout: @timeout, timeout: @timeout) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + case HttpClient.get(AddressesBlacklist.url(), [], recv_timeout: @timeout, timeout: @timeout) do + {:ok, %{status_code: 200, body: body}} -> body |> Jason.decode() |> parse_blacklist() diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index 62323e2f45f5..f3a6f431d6b0 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -14,8 +14,7 @@ defmodule Explorer.ChainSpec.GenesisData do alias Explorer.Chain.SmartContract alias Explorer.ChainSpec.Geth.Importer, as: GethImporter alias Explorer.ChainSpec.Parity.Importer - alias Explorer.Helper - alias HTTPoison.Response + alias Explorer.{Helper, HttpClient} def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) @@ -172,10 +171,10 @@ defmodule Explorer.ChainSpec.GenesisData do end # Fetches JSON data from a provided URL. - @spec fetch_from_url(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t() | HTTPoison.Error.t()} + @spec fetch_from_url(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t() | any()} defp fetch_from_url(url) do - case HTTPoison.get(url) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url) do + {:ok, %{body: body, status_code: 200}} -> {:ok, Jason.decode!(body)} reason -> diff --git a/apps/explorer/lib/explorer/http_client.ex b/apps/explorer/lib/explorer/http_client.ex new file mode 100644 index 000000000000..5b634c0cc404 --- /dev/null +++ b/apps/explorer/lib/explorer/http_client.ex @@ -0,0 +1,27 @@ +defmodule Explorer.HttpClient do + @moduledoc false + + def get(url, headers \\ [], options \\ []) do + adapter().get(url, headers, options) + end + + def get!(url, headers \\ [], options \\ []) do + adapter().get!(url, headers, options) + end + + def post(url, body, headers \\ [], options \\ []) do + adapter().post(url, body, headers, options) + end + + def head(url, headers \\ [], options \\ []) do + adapter().head(url, headers, options) + end + + def request(method, url, headers, body, options \\ []) do + adapter().request(method, url, headers, body, options) + end + + defp adapter do + Application.get_env(:explorer, :http_client) + end +end diff --git a/apps/explorer/lib/explorer/http_client/httpoison.ex b/apps/explorer/lib/explorer/http_client/httpoison.ex new file mode 100644 index 000000000000..4ef9c66aa667 --- /dev/null +++ b/apps/explorer/lib/explorer/http_client/httpoison.ex @@ -0,0 +1,46 @@ +defmodule Explorer.HttpClient.HTTPoison do + @moduledoc false + + alias Utils.HttpClient.HTTPoisonHelper + + def get(url, headers, options) do + url + |> HTTPoison.get(headers, HTTPoisonHelper.request_opts(options)) + |> parse_response() + end + + def get!(url, headers, options) do + url + |> HTTPoison.get!(headers, HTTPoisonHelper.request_opts(options)) + |> parse_response() + end + + def post(url, body, headers, options) do + url + |> HTTPoison.post(body, headers, HTTPoisonHelper.request_opts(options)) + |> parse_response() + end + + def head(url, headers, options) do + url + |> HTTPoison.head(headers, HTTPoisonHelper.request_opts(options)) + |> parse_response() + end + + def request(method, url, headers, body, options) do + method + |> HTTPoison.request(url, body, headers, HTTPoisonHelper.request_opts(options)) + |> parse_response() + end + + defp parse_response({:ok, %{body: body, status_code: status_code, headers: response_headers}}) do + {:ok, %{body: body, status_code: status_code, headers: response_headers}} + end + + defp parse_response(%{body: body, status_code: status_code, headers: response_headers}) do + %{body: body, status_code: status_code, headers: response_headers} + end + + defp parse_response({:error, %{reason: reason}}), do: {:error, reason} + defp parse_response(error), do: error +end diff --git a/apps/explorer/lib/explorer/http_client/tesla.ex b/apps/explorer/lib/explorer/http_client/tesla.ex new file mode 100644 index 000000000000..9988dd313d07 --- /dev/null +++ b/apps/explorer/lib/explorer/http_client/tesla.ex @@ -0,0 +1,50 @@ +defmodule Explorer.HttpClient.Tesla do + @moduledoc false + + alias Utils.HttpClient.TeslaHelper + + def get(url, headers, options) do + options + |> TeslaHelper.client() + |> Tesla.get(url, headers: headers, query: options[:params] || [], opts: TeslaHelper.request_opts(options)) + |> parse_response() + end + + def get!(url, headers, options) do + options + |> TeslaHelper.client() + |> Tesla.get!(url, headers: headers, query: options[:params] || [], opts: TeslaHelper.request_opts(options)) + |> parse_response() + end + + def post(url, body, headers, options) do + options + |> TeslaHelper.client() + |> Tesla.post(url, body, headers: headers, opts: TeslaHelper.request_opts(options)) + |> parse_response() + end + + def head(url, headers, options) do + options + |> TeslaHelper.client() + |> Tesla.head(url, headers: headers, opts: TeslaHelper.request_opts(options)) + |> parse_response() + end + + def request(method, url, headers, body, options) do + options + |> TeslaHelper.client() + |> Tesla.request(method: method, url: url, headers: headers, body: body, opts: TeslaHelper.request_opts(options)) + |> parse_response() + end + + defp parse_response({:ok, %{body: body, status: status_code, headers: response_headers}}) do + {:ok, %{body: body, status_code: status_code, headers: response_headers}} + end + + defp parse_response(%{body: body, status: status_code, headers: response_headers}) do + %{body: body, status_code: status_code, headers: response_headers} + end + + defp parse_response(error), do: error +end diff --git a/apps/explorer/lib/explorer/market/source.ex b/apps/explorer/lib/explorer/market/source.ex index 3842368bd944..13b80d0d336a 100644 --- a/apps/explorer/lib/explorer/market/source.ex +++ b/apps/explorer/lib/explorer/market/source.ex @@ -34,7 +34,7 @@ defmodule Explorer.Market.Source do """ alias Explorer.Chain.Hash - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} alias Explorer.Market.Source.{ CoinGecko, @@ -47,8 +47,6 @@ defmodule Explorer.Market.Source do alias Explorer.Market.Token - alias HTTPoison.{Error, Response} - # Native coin processing @callback native_coin_fetching_enabled?() :: boolean() | :ignore @callback fetch_native_coin() :: {:ok, Token.t()} | {:error, any()} | :ignore @@ -128,22 +126,22 @@ defmodule Explorer.Market.Source do - HTTP client errors: reason will be the underlying error - JSON decoding errors: reason will be the raw response body """ - @spec http_request(String.t(), HTTPoison.headers()) :: {:ok, any()} | {:error, any()} + @spec http_request(String.t(), [{atom() | binary(), binary()}]) :: {:ok, any()} | {:error, any()} def http_request(source_url, additional_headers) do - case HTTPoison.get(source_url, headers() ++ additional_headers) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(source_url, headers() ++ additional_headers) do + {:ok, %{body: body, status_code: 200}} -> parse_http_success_response(body) - {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..526 -> + {:ok, %{body: body, status_code: status_code}} when status_code in 400..526 -> {:error, "#{status_code}: #{body}"} - {:ok, %Response{status_code: status_code}} when status_code in 300..308 -> + {:ok, %{status_code: status_code}} when status_code in 300..308 -> {:error, "Source redirected"} - {:ok, %Response{status_code: _status_code}} -> + {:ok, %{status_code: _status_code}} -> {:error, "Source unexpected status code"} - {:error, %Error{reason: reason}} -> + {:error, reason} -> {:error, reason} end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex b/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex index a525f42b8a92..9d471447ae0d 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/account_abstraction.ex @@ -3,8 +3,8 @@ defmodule Explorer.MicroserviceInterfaces.AccountAbstraction do Interface to interact with Blockscout Account Abstraction (EIP-4337) microservice """ + alias Explorer.HttpClient alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @doc """ @@ -138,13 +138,13 @@ defmodule Explorer.MicroserviceInterfaces.AccountAbstraction do end defp http_get_request(url, query_params) do - case HTTPoison.get(url, [], params: query_params) do - {:ok, %Response{body: body, status_code: status_code}} + case HttpClient.get(url, [], params: query_params) do + {:ok, %{body: body, status_code: status_code}} when status_code in [200, 404] -> {:ok, response_json} = Jason.decode(body) {status_code, response_json} - {_, %Response{body: body, status_code: status_code} = error} -> + {_, %{body: body, status_code: status_code} = error} -> old_truncate = Application.get_env(:logger, :truncate) Logger.configure(truncate: :infinity) @@ -159,7 +159,7 @@ defmodule Explorer.MicroserviceInterfaces.AccountAbstraction do {:ok, response_json} = Jason.decode(body) {status_code, response_json} - {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} -> {500, %{error: reason}} end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex index fe2c8792ba0f..14dbc8829e57 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex @@ -3,13 +3,12 @@ defmodule Explorer.MicroserviceInterfaces.BENS do Interface to interact with Blockscout ENS microservice """ - alias Explorer.Chain + alias Explorer.{Chain, HttpClient} alias Explorer.Chain.Address.MetadataPreloader alias Explorer.Chain.{Address, Transaction} alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @@ -92,8 +91,8 @@ defmodule Explorer.MicroserviceInterfaces.BENS do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %{body: body, status_code: 200}} -> Jason.decode(body) {_, error} -> @@ -113,8 +112,8 @@ defmodule Explorer.MicroserviceInterfaces.BENS do end defp http_get_request(url, query_params) do - case HTTPoison.get(url, [], params: query_params) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, [], params: query_params) do + {:ok, %{body: body, status_code: 200}} -> Jason.decode(body) {_, error} -> diff --git a/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex b/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex index 48a2e4342463..34ac1ea20873 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/metadata.ex @@ -3,10 +3,9 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do Module to interact with Metadata microservice """ - alias Explorer.Chain + alias Explorer.{Chain, HttpClient} alias Explorer.Chain.{Address.MetadataPreloader, Transaction} alias Explorer.Utility.Microservice - alias HTTPoison.Response import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.Chain.Address.MetadataPreloader, only: [maybe_preload_meta: 3] @@ -103,8 +102,8 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do defp http_get_request(url, params, parsing_function \\ &decode_meta/1) do headers = [] - case HTTPoison.get(url, headers, params: params, recv_timeout: @request_timeout) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, headers, params: params, recv_timeout: @request_timeout) do + {:ok, %{body: body, status_code: 200}} -> body |> Jason.decode() |> parsing_function.() {_, error} -> @@ -120,11 +119,11 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do end defp http_get_request_for_proxy_method(url, params, parsing_function) do - case HTTPoison.get(url, [], params: params, recv_timeout: config()[:proxy_requests_timeout]) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, [], params: params, recv_timeout: config()[:proxy_requests_timeout]) do + {:ok, %{body: body, status_code: 200}} -> {200, body |> Jason.decode() |> parsing_function.()} - {_, %Response{body: body, status_code: status_code} = error} -> + {_, %{body: body, status_code: status_code} = error} -> old_truncate = Application.get_env(:logger, :truncate) Logger.configure(truncate: :infinity) @@ -139,7 +138,7 @@ defmodule Explorer.MicroserviceInterfaces.Metadata do {:ok, response_json} = Jason.decode(body) {status_code, response_json} - {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} -> {500, %{error: reason}} end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 8564bd87d086..263ec3702c02 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -7,9 +7,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do alias Explorer.Chain.{Block, Hash, Transaction} alias Explorer.Chain.Block.Range alias Explorer.Chain.MultichainSearchDb.MainExportQueue - alias Explorer.{Helper, Repo} + alias Explorer.{Helper, HttpClient, Repo} alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @@ -247,14 +246,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case Application.get_env(:explorer, :http_adapter).post(url, Jason.encode!(body), headers, + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout, - hackney: [pool: false] + pool: false ) do - {:ok, %Response{body: response_body, status_code: 200}} -> + {:ok, %{body: response_body, status_code: 200}} -> response_body |> Jason.decode() - {:ok, %Response{body: response_body, status_code: status_code}} -> + {:ok, %{body: response_body, status_code: status_code}} -> {:error, %{ url: url, @@ -263,7 +262,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do response_body: response_body }} - {:error, %HTTPoison.Error{reason: reason}} -> + {:error, reason} -> {:error, %{ url: url, diff --git a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex index 88e95aff4067..fd8ee211fc0d 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/tac_operation_lifecycle.ex @@ -3,8 +3,8 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do Interface to interact with Tac Operation Lifecycle Service (https://github.com/blockscout/blockscout-rs/tree/main/tac-operation-lifecycle) """ + alias Explorer.HttpClient alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @request_error_msg "Error while sending request to Tac Operation Lifecycle Service" @@ -33,8 +33,8 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do end defp http_get_request(url, query_params) do - case HTTPoison.get(url, [], params: query_params) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, [], params: query_params) do + {:ok, %{body: body, status_code: 200}} -> case Jason.decode(body) do {:ok, decoded_body} -> {:ok, decoded_body} @@ -44,7 +44,7 @@ defmodule Explorer.MicroserviceInterfaces.TACOperationLifecycle do {:error, error} end - {:ok, %Response{body: _body, status_code: 404}} -> + {:ok, %{body: _body, status_code: 404}} -> {:error, :not_found} {_, error} -> diff --git a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex index 99f164c43e40..412c2eb1b752 100644 --- a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex +++ b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex @@ -3,7 +3,7 @@ defmodule Explorer.SmartContract.CompilerVersion do Adapter for fetching compiler versions from https://solc-bin.ethereum.org/bin/list.json. """ - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} alias Explorer.SmartContract.{RustVerifierInterface, StylusVerifierInterface} @unsupported_solc_versions ~w(0.1.1 0.1.2) @@ -76,15 +76,14 @@ defmodule Explorer.SmartContract.CompilerVersion do else headers = [{"Content-Type", "application/json"}] - # credo:disable-for-next-line - case HTTPoison.get(source_url(compiler_type), headers) do + case HttpClient.get(source_url(compiler_type), headers) do {:ok, %{status_code: 200, body: body}} -> {:ok, format_data(body, compiler_type)} {:ok, %{status_code: _status_code, body: body}} -> {:error, Helper.decode_json(body)["error"]} - {:error, %{reason: reason}} -> + {:error, reason} -> {:error, reason} end end diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex index 0de3d6639fd7..ff9774911a28 100644 --- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex +++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex @@ -5,8 +5,8 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do defmacro __using__(_) do # credo:disable-for-next-line quote([]) do + alias Explorer.HttpClient alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @post_timeout :timer.minutes(5) @@ -78,10 +78,10 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do def http_post_request(url, body, is_verification_request?, options \\ []) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), maybe_put_api_key_header(headers, is_verification_request?), + case HttpClient.post(url, Jason.encode!(body), maybe_put_api_key_header(headers, is_verification_request?), recv_timeout: @post_timeout ) do - {:ok, %Response{body: body, status_code: _}} -> + {:ok, %{body: body, status_code: _}} -> process_verifier_response(body, options) {:error, error} -> @@ -113,11 +113,11 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do end def http_get_request(url) do - case HTTPoison.get(url) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url) do + {:ok, %{body: body, status_code: 200}} -> process_verifier_response(body, []) - {:ok, %Response{body: body, status_code: _}} -> + {:ok, %{body: body, status_code: _}} -> {:error, body} {:error, error} -> diff --git a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex index 50dffccb61f6..635763e66b97 100644 --- a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex @@ -3,8 +3,8 @@ defmodule Explorer.SmartContract.SigProviderInterface do Adapter for decoding events and function calls with https://github.com/blockscout/blockscout-rs/tree/main/sig-provider """ + alias Explorer.HttpClient alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @request_error_msg "Error while sending request to sig-provider" @@ -78,11 +78,11 @@ defmodule Explorer.SmartContract.SigProviderInterface do end defp http_get_request(url) do - case HTTPoison.get(url) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url) do + {:ok, %{body: body, status_code: 200}} -> process_sig_provider_response(body) - {:ok, %Response{body: body, status_code: _}} -> + {:ok, %{body: body, status_code: _}} -> {:error, body} {:error, error} -> @@ -104,8 +104,8 @@ defmodule Explorer.SmartContract.SigProviderInterface do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %{body: body, status_code: 200}} -> body |> Jason.decode() error -> diff --git a/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex index 5f63b3189dbc..f85b2a67b34a 100644 --- a/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex +++ b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex @@ -5,6 +5,7 @@ defmodule Explorer.SmartContract.SolcDownloader do """ use GenServer + alias Explorer.HttpClient alias Explorer.SmartContract.CompilerVersion @latest_compiler_refetch_time :timer.minutes(30) @@ -93,7 +94,7 @@ defmodule Explorer.SmartContract.SolcDownloader do download_path = "https://solc-bin.ethereum.org/bin/soljson-#{version}.js" download_path - |> HTTPoison.get!([], timeout: 60_000, recv_timeout: 60_000) + |> HttpClient.get!([], timeout: 60_000, recv_timeout: 60_000) |> Map.get(:body) end end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex b/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex index 0e9489b067c6..ba1cca203908 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex @@ -20,6 +20,7 @@ defmodule Explorer.SmartContract.Solidity.CodeCompiler do ## Examples + iex(1)> Tesla.Test.expect_tesla_call(times: 1, returns: %Tesla.Env{status: 200, body: ""}) iex(1)> Explorer.SmartContract.Solidity.CodeCompiler.run([ ...> name: "SimpleStorage", ...> compiler_version: "v0.4.24+commit.e67f0147", diff --git a/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex b/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex index 596ebc6b2ce8..d5ab2ea9db4d 100644 --- a/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/stylus_verifier_interface.ex @@ -6,7 +6,7 @@ defmodule Explorer.SmartContract.StylusVerifierInterface do Handles verification requests for Stylus contracts deployed from GitHub repositories by communicating with an external verification service. """ - alias HTTPoison.Response + alias Explorer.HttpClient require Logger @post_timeout :timer.minutes(5) @@ -55,8 +55,8 @@ defmodule Explorer.SmartContract.StylusVerifierInterface do defp http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do - {:ok, %Response{body: body, status_code: _}} -> + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %{body: body, status_code: _}} -> process_verifier_response(body) {:error, error} -> @@ -73,11 +73,11 @@ defmodule Explorer.SmartContract.StylusVerifierInterface do @spec http_get_request(String.t()) :: {:ok, [String.t()]} | {:error, any()} defp http_get_request(url) do - case HTTPoison.get(url) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url) do + {:ok, %{body: body, status_code: 200}} -> process_verifier_response(body) - {:ok, %Response{body: body, status_code: _}} -> + {:ok, %{body: body, status_code: _}} -> {:error, body} {:error, error} -> diff --git a/apps/explorer/lib/explorer/smart_contract/vyper_downloader.ex b/apps/explorer/lib/explorer/smart_contract/vyper_downloader.ex index dfb873276c7a..2f6ca24e0a07 100644 --- a/apps/explorer/lib/explorer/smart_contract/vyper_downloader.ex +++ b/apps/explorer/lib/explorer/smart_contract/vyper_downloader.ex @@ -5,6 +5,7 @@ defmodule Explorer.SmartContract.VyperDownloader do """ use GenServer + alias Explorer.HttpClient alias Explorer.SmartContract.CompilerVersion @latest_compiler_refetch_time :timer.minutes(30) @@ -97,7 +98,7 @@ defmodule Explorer.SmartContract.VyperDownloader do releases_body = releases_path - |> HTTPoison.get!([], timeout: 60_000, recv_timeout: 60_000) + |> HttpClient.get!([], timeout: 60_000, recv_timeout: 60_000) |> Map.get(:body) |> Jason.decode!() @@ -121,7 +122,11 @@ defmodule Explorer.SmartContract.VyperDownloader do end) download_path - |> HTTPoison.get!([], timeout: 60_000, recv_timeout: 60_000, follow_redirect: true, hackney: [force_redirect: true]) + |> HttpClient.get!([], + timeout: 60_000, + recv_timeout: 60_000, + follow_redirect: true + ) |> Map.get(:body) end end diff --git a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex index f7e178002e86..401e9674ac88 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex @@ -7,8 +7,7 @@ defmodule Explorer.ThirdPartyIntegrations.AirTable do alias Ecto.Changeset alias Explorer.Account.PublicTagsRequest alias Explorer.Chain.SmartContract.AuditReport - alias Explorer.Repo - alias HTTPoison.Response + alias Explorer.{HttpClient, Repo} @doc """ Submits a public tags request or audit report to AirTable @@ -76,10 +75,10 @@ defmodule Explorer.ThirdPartyIntegrations.AirTable do "records" => [%{"fields" => map}] } - request = HTTPoison.post(url, Jason.encode!(body), headers, []) + request = HttpClient.post(url, Jason.encode!(body), headers) case request do - {:ok, %Response{body: body, status_code: 200}} -> + {:ok, %{body: body, status_code: 200}} -> request_id = Enum.at(Jason.decode!(body)["records"], 0)["fields"]["request_id"] success_callback.(request_id) diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex index e8629288824f..e48fdd44e6f7 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex @@ -5,7 +5,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do require Logger alias Explorer.Account.Identity - alias Explorer.{Account, Helper} + alias Explorer.{Account, Helper, HttpClient} alias Explorer.ThirdPartyIntegrations.Auth0.Internal alias Ueberauth.Auth alias Ueberauth.Strategy.Auth0.OAuth @@ -42,8 +42,8 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do "grant_type" => "client_credentials" } - case HTTPoison.post("https://#{config[:domain]}/oauth/token", Jason.encode!(body), @json_content_type, []) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + case HttpClient.post("https://#{config[:domain]}/oauth/token", Jason.encode!(body), @json_content_type) do + {:ok, %{status_code: 200, body: body}} -> case Jason.decode!(body) do %{"access_token" => token, "expires_in" => ttl} -> cache_token(token, ttl - 1) diff --git a/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex index bede506841d5..d9def2b47f32 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/noves_fi.ex @@ -5,7 +5,7 @@ defmodule Explorer.ThirdPartyIntegrations.NovesFi do require Logger - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} alias Explorer.Utility.Microservice @recv_timeout 60_000 @@ -31,8 +31,8 @@ defmodule Explorer.ThirdPartyIntegrations.NovesFi do conn.query_params |> Map.drop(["hashes"]) - case HTTPoison.post(url, Jason.encode!(hashes), headers, recv_timeout: @recv_timeout, params: prepared_params) do - {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + case HttpClient.post(url, Jason.encode!(hashes), headers, recv_timeout: @recv_timeout, params: prepared_params) do + {:ok, %{status_code: status, body: body}} -> {Helper.decode_json(body), status} {:error, reason} -> @@ -49,8 +49,8 @@ defmodule Explorer.ThirdPartyIntegrations.NovesFi do url_with_params = url <> "?" <> conn.query_string - case HTTPoison.get(url_with_params, headers, recv_timeout: @recv_timeout) do - {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + case HttpClient.get(url_with_params, headers, recv_timeout: @recv_timeout) do + {:ok, %{status_code: status, body: body}} -> {Helper.decode_json(body), status} {:error, reason} -> diff --git a/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex index fc34f0339fee..dd6f95fa6a14 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex @@ -4,7 +4,7 @@ defmodule Explorer.ThirdPartyIntegrations.SolidityScan do """ require Logger - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} @recv_timeout 60_000 @@ -18,8 +18,8 @@ defmodule Explorer.ThirdPartyIntegrations.SolidityScan do url = base_url(address_hash_string) if url do - case HTTPoison.get(url, headers, recv_timeout: @recv_timeout) do - {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + case HttpClient.get(url, headers, recv_timeout: @recv_timeout) do + {:ok, %{status_code: 200, body: body}} -> Helper.decode_json(body) _ -> diff --git a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex index d23491a94cd4..38c716b65b80 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex @@ -2,11 +2,10 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do @moduledoc """ Adapter for contracts verification with https://sourcify.dev/ """ - use Tesla alias Explorer.Helper, as: ExplorerHelper + alias Explorer.HttpClient alias Explorer.SmartContract.{Helper, RustVerifierInterface} - alias HTTPoison.{Error, Response} alias Tesla.Multipart @post_timeout :timer.seconds(30) @@ -153,22 +152,22 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end def http_get_request(url, params) do - request = HTTPoison.get(url, [], params: params) + request = HttpClient.get(url, [], params: params) case request do - {:ok, %Response{body: body, status_code: 200}} -> + {:ok, %{body: body, status_code: 200}} -> process_sourcify_response(url, body) - {:ok, %Response{body: body, status_code: status_code}} when status_code in 400..526 -> + {:ok, %{body: body, status_code: status_code}} when status_code in 400..526 -> parse_http_error_response(body) - {:ok, %Response{status_code: status_code}} when status_code in 300..308 -> + {:ok, %{status_code: status_code}} when status_code in 300..308 -> {:error, "Sourcify redirected"} - {:ok, %Response{status_code: _status_code}} -> + {:ok, %{status_code: _status_code}} -> {:error, "Sourcify unexpected status code"} - {:error, %Error{reason: reason}} -> + {:error, reason} -> {:error, reason} _ -> @@ -190,10 +189,10 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do def http_post_request_rust_microservice(url, body) do request = - HTTPoison.post(url, Jason.encode!(body), [{"Content-Type", "application/json"}], recv_timeout: @post_timeout) + HttpClient.post(url, Jason.encode!(body), [{"Content-Type", "application/json"}], recv_timeout: @post_timeout) case request do - {:ok, %Response{body: body, status_code: 200}} -> + {:ok, %{body: body, status_code: 200}} -> process_sourcify_response(url, body) _ -> diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex index 8d6ef169cb5b..439a60643d9c 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex @@ -2,9 +2,8 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @moduledoc """ Module for universal proxying 3rd party API endpoints """ - use Tesla - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} @recv_timeout 60_000 @@ -54,7 +53,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do 1. Retrieves the platform-specific configuration based on the `platform_id` key in `proxy_params`. 2. Constructs the request URL using the `base_url` and `base` endpoint path. 3. Parses and applies any API key and endpoint parameters. - 4. Sends the HTTP request using the Tesla library with the specified method, URL, headers, and body. + 4. Sends the HTTP request using `Explorer.HttpClient` with the specified method, URL, headers, and body. ## Returns @@ -74,7 +73,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do ## Notes - - The function uses the `Tesla` library with the `Tesla.Adapter.Mint` adapter. + - The function uses the `Explorer.HttpClient` with pre-configured adapter. - A timeout is applied to the request using the `@recv_timeout` module attribute. """ @spec api_request(map()) :: {any(), integer()} @@ -105,14 +104,8 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do with {:invalid_config, false} <- {:invalid_config, is_nil(url)}, {:invalid_config, false} <- {:invalid_config, is_nil(method)}, - {:ok, %Tesla.Env{status: status, body: body}} <- - Tesla.request( - method: method, - url: url, - headers: headers, - body: body, - opts: [timeout: @recv_timeout, adapter: [protocols: [:http1]]] - ) do + {:ok, %{status_code: status, body: body}} <- + HttpClient.request(method, url, headers, body, timeout: @recv_timeout) do {Helper.decode_json(body), status} else {:invalid_config, true} -> @@ -333,8 +326,8 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do safe_parse_config_string(config_string) nil -> - case Tesla.get(config_url()) do - {:ok, %Tesla.Env{status: 200, body: config_string}} -> + case HttpClient.get(config_url()) do + {:ok, %{status_code: 200, body: config_string}} -> safe_parse_config_string(config_string, true) _ -> diff --git a/apps/explorer/lib/explorer/third_party_integrations/xname.ex b/apps/explorer/lib/explorer/third_party_integrations/xname.ex index aa86995cbe6f..2305267d61e8 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/xname.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/xname.ex @@ -5,7 +5,7 @@ defmodule Explorer.ThirdPartyIntegrations.Xname do require Logger - alias Explorer.Helper + alias Explorer.{Helper, HttpClient} alias Explorer.Utility.Microservice @recv_timeout 60_000 @@ -19,8 +19,8 @@ defmodule Explorer.ThirdPartyIntegrations.Xname do def api_request(url, _conn, :get) do headers = [{"x-api-key", api_key()}] - case HTTPoison.get(url, headers, recv_timeout: @recv_timeout) do - {:ok, %HTTPoison.Response{status_code: status, body: body}} -> + case HttpClient.get(url, headers, recv_timeout: @recv_timeout) do + {:ok, %{status_code: status, body: body}} -> {Helper.decode_json(body), status} {:error, reason} -> diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 980b8df07a9b..c17586b64a4e 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -5,11 +5,10 @@ defmodule Explorer.Token.MetadataRetriever do require Logger - alias Explorer.{MetadataURIValidator, Repo} + alias Explorer.{HttpClient, MetadataURIValidator, Repo} alias Explorer.Chain.{Hash, Token} alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.Reader - alias HTTPoison.{Error, Response} @no_uri_error "no uri" @vm_execution_error "VM execution error" @@ -848,12 +847,12 @@ defmodule Explorer.Token.MetadataRetriever do defp fetch_metadata_from_uri_request(uri, hex_token_id, ipfs_params) do headers = if ipfs?(ipfs_params), do: ipfs_headers(), else: @default_headers - case Application.get_env(:explorer, :http_adapter).get(uri, headers, + case HttpClient.get(uri, headers, recv_timeout: 30_000, follow_redirect: true, - hackney: [pool: :token_instance_fetcher] + pool: :token_instance_fetcher ) do - {:ok, %Response{body: body, status_code: 200, headers: response_headers}} -> + {:ok, %{body: body, status_code: 200, headers: response_headers}} -> content_type = get_content_type_from_headers(response_headers) case check_content_type(content_type, uri, hex_token_id, body, ipfs_params) do @@ -864,7 +863,7 @@ defmodule Explorer.Token.MetadataRetriever do {:error, reason} end - {:ok, %Response{body: body, status_code: code}} -> + {:ok, %{body: body, status_code: code}} -> Logger.debug( ["Request to token uri: #{inspect(uri)} failed with code #{code}. Body:", inspect(body)], fetcher: :token_instances @@ -872,7 +871,7 @@ defmodule Explorer.Token.MetadataRetriever do {:error_code, code} - {:error, %Error{reason: reason}} -> + {:error, reason} -> Logger.warning( ["Request to token uri failed: #{inspect(uri)}.", inspect(reason)], fetcher: :token_instances diff --git a/apps/explorer/lib/explorer/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex index 459531a36299..c26f5d1b894e 100644 --- a/apps/explorer/lib/explorer/visualize/sol2uml.ex +++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex @@ -2,8 +2,8 @@ defmodule Explorer.Visualize.Sol2uml do @moduledoc """ Adapter for sol2uml visualizer with https://github.com/blockscout/blockscout-rs/blob/main/visualizer """ + alias Explorer.HttpClient alias Explorer.Utility.Microservice - alias HTTPoison.Response require Logger @post_timeout 60_000 @@ -16,11 +16,11 @@ defmodule Explorer.Visualize.Sol2uml do def http_post_request(url, body) do headers = [{"Content-Type", "application/json"}] - case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %{body: body, status_code: 200}} -> process_visualizer_response(body) - {:ok, %Response{body: body, status_code: status_code}} -> + {:ok, %{body: body, status_code: status_code}} -> Logger.error(fn -> ["Invalid status code from visualizer: #{status_code}. body: ", inspect(body)] end) {:error, "failed to visualize contract"} diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index f4cc46babf59..0de75c0871bb 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -115,7 +115,7 @@ defmodule Explorer.Mixfile do # `Timex.Duration` for `Explorer.Chain.Cache.Counters.AverageBlockTime.average_block_time/0` {:timex, "~> 3.7.1"}, {:con_cache, "~> 1.0"}, - {:tesla, "~> 1.14.1"}, + {:tesla, "~> 1.14.2"}, {:cbor, "~> 1.0"}, {:cloak_ecto, "~> 1.3.0"}, {:redix, "~> 1.1"}, @@ -134,7 +134,8 @@ defmodule Explorer.Mixfile do {:dns, "~> 2.4.0"}, {:inet_cidr, "~> 1.0.0"}, {:hammer, "~> 7.0"}, - {:ton, "~> 0.5.0"} + {:ton, "~> 0.5.0"}, + {:mint, "~> 1.0"} ] end diff --git a/apps/explorer/test/explorer/market/fetcher/history_test.exs b/apps/explorer/test/explorer/market/fetcher/history_test.exs index ed7eccd8da69..db62601f6c3a 100644 --- a/apps/explorer/test/explorer/market/fetcher/history_test.exs +++ b/apps/explorer/test/explorer/market/fetcher/history_test.exs @@ -57,10 +57,12 @@ defmodule Explorer.Market.Fetcher.HistoryTest do Application.put_env(:explorer, Source, native_coin_history_source: CryptoCompare) Application.put_env(:explorer, CryptoCompare, base_url: "http://localhost:#{bypass.port}", coin_symbol: "TEST") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Source, source_configuration) Application.put_env(:explorer, CryptoCompare, crypto_compare_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) resp = @@ -190,10 +192,12 @@ defmodule Explorer.Market.Fetcher.HistoryTest do Application.put_env(:explorer, Source, native_coin_history_source: CryptoCompare) Application.put_env(:explorer, CryptoCompare, base_url: "http://localhost:#{bypass.port}", coin_symbol: "TEST") + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, CryptoCompare, crypto_compare_configuration) Application.put_env(:explorer, Source, source_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) resp = diff --git a/apps/explorer/test/explorer/market/fetcher/token_test.exs b/apps/explorer/test/explorer/market/fetcher/token_test.exs index 29f100fb3983..eb3df9f5c217 100644 --- a/apps/explorer/test/explorer/market/fetcher/token_test.exs +++ b/apps/explorer/test/explorer/market/fetcher/token_test.exs @@ -34,10 +34,13 @@ defmodule Explorer.Market.Fetcher.TokenTest do base_url: "http://localhost:#{bypass.port}" ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, Explorer.Market.Source, source_configuration) Application.put_env(:explorer, Explorer.Market.Fetcher.Token, fetcher_configuration) Application.put_env(:explorer, Explorer.Market.Source.CoinGecko, coin_gecko_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) [_token_with_no_exchange_rate | tokens] = diff --git a/apps/explorer/test/explorer/market/source/coin_gecko_test.exs b/apps/explorer/test/explorer/market/source/coin_gecko_test.exs index 78d93167b8e9..de9619a42b12 100644 --- a/apps/explorer/test/explorer/market/source/coin_gecko_test.exs +++ b/apps/explorer/test/explorer/market/source/coin_gecko_test.exs @@ -30,9 +30,12 @@ defmodule Explorer.Market.Source.CoinGeckoTest do platform: "test" ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, Explorer.Market.Source, source_configuration) Application.put_env(:explorer, CoinGecko, coin_gecko_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, bypass: bypass} diff --git a/apps/explorer/test/explorer/market/source/coin_market_cap_test.exs b/apps/explorer/test/explorer/market/source/coin_market_cap_test.exs index 5f5eaed7dbdf..9c57b75deaac 100644 --- a/apps/explorer/test/explorer/market/source/coin_market_cap_test.exs +++ b/apps/explorer/test/explorer/market/source/coin_market_cap_test.exs @@ -28,9 +28,12 @@ defmodule Explorer.Market.Source.CoinMarketCapTest do currency_id: "2781" ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, Explorer.Market.Source, source_configuration) Application.put_env(:explorer, CoinMarketCap, coin_market_cap_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, bypass: bypass} diff --git a/apps/explorer/test/explorer/market/source/crypto_compare_test.exs b/apps/explorer/test/explorer/market/source/crypto_compare_test.exs index 2ad70fe09b7b..963ecaa93d71 100644 --- a/apps/explorer/test/explorer/market/source/crypto_compare_test.exs +++ b/apps/explorer/test/explorer/market/source/crypto_compare_test.exs @@ -30,10 +30,13 @@ defmodule Explorer.Market.Source.CryptoCompareTest do currency: "AED" ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, :coin, coin) Application.put_env(:explorer, Explorer.Market.Source, source_configuration) Application.put_env(:explorer, CryptoCompare, crypto_compare_configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, bypass: bypass} diff --git a/apps/explorer/test/explorer/market/source/crypto_rank_test.exs b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs index 5640eca28bf4..fefa411f0386 100644 --- a/apps/explorer/test/explorer/market/source/crypto_rank_test.exs +++ b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs @@ -21,8 +21,11 @@ defmodule Explorer.Market.Source.CryptoRankTest do ) ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> Application.put_env(:explorer, CryptoRank, old_env) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, old_env: old_env, bypass: bypass} diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 54da6c4a7aa5..e51a30a8b59f 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -16,6 +16,10 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + :ok end @@ -32,6 +36,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do test "processes chunks and returns {:ok, result} when the service is enabled" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:explorer, MultichainSearch, service_url: "http://localhost:#{bypass.port}", api_key: "12345", @@ -77,6 +83,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_main_export_queue' table is populated" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:explorer, MultichainSearch, service_url: "http://localhost:#{bypass.port}", api_key: "12345", @@ -150,11 +158,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do addresses_chunk_size: 7000 ) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - on_exit(fn -> Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) - Application.put_env(:explorer, :http_adapter, HTTPoison) end) TestHelper.get_chain_id_mock() @@ -173,18 +178,28 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do insert(:address) end - Explorer.Mox.HTTPoison - |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", _expected_body, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"status" => "ok"}) + }} + end + ) - Explorer.Mox.HTTPoison - |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", - _expected_body, - [{"Content-Type", "application/json"}], - _options -> - {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", headers: [{"Content-Type", "application/json"}]}, + _opts -> + {:ok, + %Tesla.Env{ + status: 500, + body: Jason.encode!(%{"code" => 0, "message" => "Error"}) + }} + end + ) params = %{ addresses: addresses, @@ -203,6 +218,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_main_export_queue' table is populated with all the input data" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:explorer, MultichainSearch, service_url: "http://localhost:#{bypass.port}", api_key: "12345", @@ -245,6 +262,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_main_export_queue' table is populated with all the input data" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:explorer, MultichainSearch, service_url: "http://localhost:#{bypass.port}", api_key: "12345", diff --git a/apps/explorer/test/explorer/smart_contract/compiler_version_test.exs b/apps/explorer/test/explorer/smart_contract/compiler_version_test.exs index 86ec4c3d8b73..c07ab7f85c95 100644 --- a/apps/explorer/test/explorer/smart_contract/compiler_version_test.exs +++ b/apps/explorer/test/explorer/smart_contract/compiler_version_test.exs @@ -19,6 +19,11 @@ defmodule Explorer.SmartContract.CompilerVersionTest do setup do bypass = Bypass.open() + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) Application.put_env(:explorer, :solc_bin_api_url, "http://localhost:#{bypass.port}") {:ok, bypass: bypass} @@ -59,7 +64,7 @@ defmodule Explorer.SmartContract.CompilerVersionTest do test "returns error when there is server error", %{bypass: bypass} do Bypass.down(bypass) - assert {:error, :econnrefused} = CompilerVersion.fetch_versions(:solc) + assert {:error, %{reason: :econnrefused}} = CompilerVersion.fetch_versions(:solc) end end diff --git a/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs index cef3b5465ea7..d66d3533b30c 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs @@ -16,9 +16,11 @@ defmodule Explorer.SmartContract.Solidity.CodeCompilerTest do setup do configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) {:ok, diff --git a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs index 32e792861888..0c8096ca294c 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs @@ -15,9 +15,11 @@ if Application.compile_env(:explorer, :chain_type) !== :zksync do setup do configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) end diff --git a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs index 89dbd97ba48c..7489a6c555c6 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs @@ -67,9 +67,11 @@ if Application.compile_env(:explorer, :chain_type) !== :zksync do setup do configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) end diff --git a/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs index 7e31394cccaa..42e96719d2fe 100644 --- a/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs +++ b/apps/explorer/test/explorer/smart_contract/vyper/publisher_test.exs @@ -15,9 +15,11 @@ if Application.compile_env(:explorer, :chain_type) !== :zksync do setup do configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) on_exit(fn -> Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) end) end diff --git a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs index cbe2d2b13f0a..97867e1c53d6 100644 --- a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs +++ b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs @@ -1,5 +1,5 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxyTest do - use ExUnit.Case, async: true + use ExUnit.Case import Mox diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index 87bd8544cb7d..ce8300e85529 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -587,6 +587,10 @@ defmodule Explorer.Token.MetadataRetrieverTest do setup do bypass = Bypass.open() + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + {:ok, bypass: bypass} end @@ -673,14 +677,17 @@ defmodule Explorer.Token.MetadataRetrieverTest do "collectionId" => "1871_1665123820823" } - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP?x-apikey=mykey", - _headers, - _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(result)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP?x-apikey=mykey"}, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(result) + }} + end + ) assert {:ok, %{ @@ -693,7 +700,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do } }} == MetadataRetriever.fetch_json({:ok, [data]}) - Application.put_env(:explorer, :http_adapter, HTTPoison) Application.put_env(:indexer, :ipfs, configuration) end @@ -717,12 +723,16 @@ defmodule Explorer.Token.MetadataRetrieverTest do "collectionId" => "1871_1665123820823" } - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP", _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(result)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(result) + }} + end + ) assert {:ok, %{ @@ -735,7 +745,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do } }} == MetadataRetriever.fetch_json({:ok, [data]}) - Application.put_env(:explorer, :http_adapter, HTTPoison) Application.put_env(:indexer, :ipfs, configuration) end @@ -759,14 +768,20 @@ defmodule Explorer.Token.MetadataRetrieverTest do "collectionId" => "1871_1665123820823" } - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP", - [{"x-apikey", "mykey"}, {"User-Agent", _}], - _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(result)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{ + url: "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP", + headers: [{"x-apikey", "mykey"}, {"User-Agent", _}] + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(result) + }} + end + ) assert {:ok, %{ @@ -779,7 +794,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do } }} == MetadataRetriever.fetch_json({:ok, [data]}) - Application.put_env(:explorer, :http_adapter, HTTPoison) Application.put_env(:indexer, :ipfs, configuration) end @@ -792,6 +806,8 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect(bypass, "GET", path, fn conn -> Conn.resp(conn, 200, json) end) @@ -822,6 +838,8 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect(bypass, "GET", path, fn conn -> Conn.resp(conn, 200, json) end) @@ -898,14 +916,16 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/bafybeig6nlmyzui7llhauc52j2xo5hoy4lzp6442lkve5wysdvjkizxonu", - _headers, - _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: json}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/bafybeig6nlmyzui7llhauc52j2xo5hoy4lzp6442lkve5wysdvjkizxonu"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: json + }} + end + ) data = {:ok, @@ -919,8 +939,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do "image" => "https://ipfs.io/ipfs/bafybeig6nlmyzui7llhauc52j2xo5hoy4lzp6442lkve5wysdvjkizxonu" } }} == MetadataRetriever.fetch_json(data) - - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "Fetches metadata from ipfs" do @@ -932,14 +950,17 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/bafybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json", - _headers, - _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: json}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/bafybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json"}, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: json + }} + end + ) data = {:ok, @@ -953,7 +974,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do }} = MetadataRetriever.fetch_json(data) assert "ipfs://bafybeihxuj3gxk7x5p36amzootyukbugmx3pw7dyntsrohg3se64efkuga/51.png" == Map.get(metadata, "image") - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "Fetches metadata from '${url}'", %{bypass: bypass} do @@ -980,6 +1000,8 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect(bypass, "GET", path, fn conn -> Conn.resp(conn, 200, json) end) @@ -1009,12 +1031,16 @@ defmodule Explorer.Token.MetadataRetrieverTest do "collectionId" => "1871_1665123820823" } - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP", _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(result)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/QmT1Yz43R1PLn2RVovAnEM5dHQEvpTcnwgX8zftvY1FcjP"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(result) + }} + end + ) assert {:ok, %{ @@ -1026,8 +1052,6 @@ defmodule Explorer.Token.MetadataRetrieverTest do "salePrice" => 34 } }} == MetadataRetriever.fetch_json({:ok, [data]}) - - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "Process URI directly from link", %{bypass: bypass} do @@ -1063,6 +1087,8 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect(bypass, "GET", path, fn conn -> Conn.resp(conn, 200, json) end) diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 53de43f744bb..b545e49423fa 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -29,6 +29,5 @@ Mox.defmock(Explorer.Market.Source.TestSource, for: Explorer.Market.Source) Mox.defmock(Explorer.History.TestHistorian, for: Explorer.History.Historian) Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport) -Mox.defmock(Explorer.Mox.HTTPoison, for: HTTPoison.Base) Mox.defmock(Explorer.Mock.TeslaAdapter, for: Tesla.Adapter) diff --git a/apps/indexer/config/dev/anvil.exs b/apps/indexer/config/dev/anvil.exs index 28e8a0d5f9d0..6022fe550aeb 100644 --- a/apps/indexer/config/dev/anvil.exs +++ b/apps/indexer/config/dev/anvil.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), fallback_urls: ConfigHelper.parse_urls_list(:fallback_http), @@ -24,7 +21,7 @@ config :indexer, method_to_url: [ eth_call: :eth_call ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Anvil ], diff --git a/apps/indexer/config/dev/besu.exs b/apps/indexer/config/dev/besu.exs index d85be5890ae1..3d843dc768d1 100644 --- a/apps/indexer/config/dev/besu.exs +++ b/apps/indexer/config/dev/besu.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -17,7 +14,7 @@ config :indexer, ), else: EthereumJSONRPC.IPC, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -31,7 +28,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Besu ], diff --git a/apps/indexer/config/dev/erigon.exs b/apps/indexer/config/dev/erigon.exs index ae702ba3497b..baddc1e4330d 100644 --- a/apps/indexer/config/dev/erigon.exs +++ b/apps/indexer/config/dev/erigon.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -17,7 +14,7 @@ config :indexer, ), else: EthereumJSONRPC.IPC, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -31,7 +28,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Erigon ], diff --git a/apps/indexer/config/dev/filecoin.exs b/apps/indexer/config/dev/filecoin.exs index 4b95daaadb2e..be5860f69f85 100644 --- a/apps/indexer/config/dev/filecoin.exs +++ b/apps/indexer/config/dev/filecoin.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http, "http://localhost:1234/rpc/v1"), trace_urls: ConfigHelper.parse_urls_list(:trace, "http://localhost:1234/rpc/v1"), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call, "http://localhost:1234/rpc/v1"), @@ -27,7 +24,7 @@ config :indexer, eth_call: :eth_call, trace_block: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Filecoin ], diff --git a/apps/indexer/config/dev/geth.exs b/apps/indexer/config/dev/geth.exs index fdeb51f4cb15..825d6abe8df8 100644 --- a/apps/indexer/config/dev/geth.exs +++ b/apps/indexer/config/dev/geth.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -28,7 +25,7 @@ config :indexer, debug_traceTransaction: :trace, debug_traceBlockByNumber: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Geth ], diff --git a/apps/indexer/config/dev/nethermind.exs b/apps/indexer/config/dev/nethermind.exs index abdf7fff639e..934ba050caaf 100644 --- a/apps/indexer/config/dev/nethermind.exs +++ b/apps/indexer/config/dev/nethermind.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -17,7 +14,7 @@ config :indexer, ), else: EthereumJSONRPC.IPC, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -31,7 +28,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Nethermind ], diff --git a/apps/indexer/config/dev/rsk.exs b/apps/indexer/config/dev/rsk.exs index f0c75824564d..6b1aae31fbee 100644 --- a/apps/indexer/config/dev/rsk.exs +++ b/apps/indexer/config/dev/rsk.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), blocks_concurrency: 1, @@ -18,7 +15,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -32,7 +29,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.RSK ], diff --git a/apps/indexer/config/prod/anvil.exs b/apps/indexer/config/prod/anvil.exs index 28e8a0d5f9d0..6022fe550aeb 100644 --- a/apps/indexer/config/prod/anvil.exs +++ b/apps/indexer/config/prod/anvil.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(1) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), fallback_urls: ConfigHelper.parse_urls_list(:fallback_http), @@ -24,7 +21,7 @@ config :indexer, method_to_url: [ eth_call: :eth_call ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(1) ], variant: EthereumJSONRPC.Anvil ], diff --git a/apps/indexer/config/prod/besu.exs b/apps/indexer/config/prod/besu.exs index 60341ae55d1d..006bf9296db8 100644 --- a/apps/indexer/config/prod/besu.exs +++ b/apps/indexer/config/prod/besu.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -30,7 +27,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Besu ], diff --git a/apps/indexer/config/prod/erigon.exs b/apps/indexer/config/prod/erigon.exs index f49677fc218e..83e1be5061ee 100644 --- a/apps/indexer/config/prod/erigon.exs +++ b/apps/indexer/config/prod/erigon.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -30,7 +27,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Erigon ], diff --git a/apps/indexer/config/prod/filecoin.exs b/apps/indexer/config/prod/filecoin.exs index 8bbf0307d930..3a720cbc0f79 100644 --- a/apps/indexer/config/prod/filecoin.exs +++ b/apps/indexer/config/prod/filecoin.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -27,7 +24,7 @@ config :indexer, eth_call: :eth_call, trace_block: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Filecoin ], diff --git a/apps/indexer/config/prod/geth.exs b/apps/indexer/config/prod/geth.exs index 7a213fbbfffe..44ba660e9849 100644 --- a/apps/indexer/config/prod/geth.exs +++ b/apps/indexer/config/prod/geth.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -28,7 +25,7 @@ config :indexer, debug_traceTransaction: :trace, debug_traceBlockByNumber: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.Geth ], diff --git a/apps/indexer/config/prod/nethermind.exs b/apps/indexer/config/prod/nethermind.exs index 1f42d5ee155d..83a1145d9b70 100644 --- a/apps/indexer/config/prod/nethermind.exs +++ b/apps/indexer/config/prod/nethermind.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout() - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), json_rpc_named_arguments: [ @@ -16,7 +13,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -30,7 +27,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options() ], variant: EthereumJSONRPC.Nethermind ], diff --git a/apps/indexer/config/prod/rsk.exs b/apps/indexer/config/prod/rsk.exs index f0c75824564d..6b1aae31fbee 100644 --- a/apps/indexer/config/prod/rsk.exs +++ b/apps/indexer/config/prod/rsk.exs @@ -4,9 +4,6 @@ import Config |> Path.join() |> Code.eval_file() -hackney_opts = ConfigHelper.hackney_options() -timeout = ConfigHelper.timeout(10) - config :indexer, block_interval: ConfigHelper.parse_time_env_var("INDEXER_CATCHUP_BLOCK_INTERVAL", "0s"), blocks_concurrency: 1, @@ -18,7 +15,7 @@ config :indexer, else: EthereumJSONRPC.IPC ), transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: ConfigHelper.parse_urls_list(:http), trace_urls: ConfigHelper.parse_urls_list(:trace), eth_call_urls: ConfigHelper.parse_urls_list(:eth_call), @@ -32,7 +29,7 @@ config :indexer, trace_replayBlockTransactions: :trace, trace_replayTransaction: :trace ], - http_options: [recv_timeout: timeout, timeout: timeout, hackney: hackney_opts] + http_options: ConfigHelper.http_options(10) ], variant: EthereumJSONRPC.RSK ], diff --git a/apps/indexer/lib/indexer/fetcher/beacon/client.ex b/apps/indexer/lib/indexer/fetcher/beacon/client.ex index 294730eda7fa..d568d4637cb3 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/client.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/client.ex @@ -2,17 +2,18 @@ defmodule Indexer.Fetcher.Beacon.Client do @moduledoc """ HTTP Client for Beacon Chain RPC """ - alias HTTPoison.Response require Logger + alias Explorer.HttpClient + @request_error_msg "Error while sending request to beacon rpc" def http_get_request(url) do - case Application.get_env(:explorer, :http_adapter).get(url) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url) do + {:ok, %{body: body, status_code: 200}} -> Jason.decode(body) - {:ok, %Response{body: body, status_code: _}} -> + {:ok, %{body: body, status_code: _}} -> {:error, body} {:error, error} -> diff --git a/apps/indexer/lib/indexer/fetcher/filecoin/beryx_api.ex b/apps/indexer/lib/indexer/fetcher/filecoin/beryx_api.ex index 90445acce52a..0d2515b266f8 100644 --- a/apps/indexer/lib/indexer/fetcher/filecoin/beryx_api.ex +++ b/apps/indexer/lib/indexer/fetcher/filecoin/beryx_api.ex @@ -4,8 +4,7 @@ defmodule Indexer.Fetcher.Filecoin.BeryxAPI do address hash """ - alias Explorer.Helper - alias HTTPoison.Response + alias Explorer.{Helper, HttpClient} @doc """ Fetches account information for a given Ethereum address hash from the Beryx API. @@ -16,12 +15,12 @@ defmodule Indexer.Fetcher.Filecoin.BeryxAPI do ## Returns - `{:ok, map()}`: On success, returns the account information as a map. - `{:error, integer(), map()}`: On failure, returns the HTTP status code and the error message as a map. - - `{:error, HTTPoison.Error.t()}`: On network or other HTTP errors, returns the error structure. + - `{:error, any()}`: On network or other HTTP errors, returns the error reason. """ @spec fetch_address_info(EthereumJSONRPC.address()) :: {:ok, map()} | {:error, integer(), map()} - | {:error, HTTPoison.Error.t()} + | {:error, any()} def fetch_address_info(eth_address_hash) do config = Application.get_env(:indexer, __MODULE__) base_url = config |> Keyword.get(:base_url) |> String.trim_trailing("/") @@ -34,16 +33,16 @@ defmodule Indexer.Fetcher.Filecoin.BeryxAPI do {"Content-Type", "application/json"} ] - case HTTPoison.get(url, headers) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, headers) do + {:ok, %{body: body, status_code: 200}} -> json = Helper.decode_json(body) {:ok, json} - {:ok, %Response{body: body, status_code: status_code}} -> + {:ok, %{body: body, status_code: status_code}} -> json = Helper.decode_json(body) {:error, status_code, json} - {:error, %HTTPoison.Error{}} = error -> + error -> error end end diff --git a/apps/indexer/lib/indexer/fetcher/filecoin/filfox_api.ex b/apps/indexer/lib/indexer/fetcher/filecoin/filfox_api.ex index 62772f0e90fa..bcfb59de4b0b 100644 --- a/apps/indexer/lib/indexer/fetcher/filecoin/filfox_api.ex +++ b/apps/indexer/lib/indexer/fetcher/filecoin/filfox_api.ex @@ -4,8 +4,7 @@ defmodule Indexer.Fetcher.Filecoin.FilfoxAPI do address hash """ - alias Explorer.Helper - alias HTTPoison.Response + alias Explorer.{Helper, HttpClient} @doc """ Fetches account information for a given Ethereum address hash from the Filfox API. @@ -16,12 +15,12 @@ defmodule Indexer.Fetcher.Filecoin.FilfoxAPI do ## Returns - `{:ok, map()}`: On success, returns the account information as a map. - `{:error, integer(), map()}`: On failure, returns the HTTP status code and the error message as a map. - - `{:error, HTTPoison.Error.t()}`: On network or other HTTP errors, returns the error structure. + - `{:error, any()}`: On network or other HTTP errors, returns the error reason. """ @spec fetch_address_info(EthereumJSONRPC.address()) :: {:ok, map()} | {:error, integer(), map()} - | {:error, HTTPoison.Error.t()} + | {:error, any()} def fetch_address_info(eth_address_hash) do config = Application.get_env(:indexer, __MODULE__) base_url = config |> Keyword.get(:base_url) |> String.trim_trailing("/") @@ -32,16 +31,16 @@ defmodule Indexer.Fetcher.Filecoin.FilfoxAPI do {"Content-Type", "application/json"} ] - case HTTPoison.get(url, headers) do - {:ok, %Response{body: body, status_code: 200}} -> + case HttpClient.get(url, headers) do + {:ok, %{body: body, status_code: 200}} -> json = Helper.decode_json(body) {:ok, json} - {:ok, %Response{body: body, status_code: status_code}} -> + {:ok, %{body: body, status_code: status_code}} -> json = Helper.decode_json(body) {:error, status_code, json} - {:error, %HTTPoison.Error{}} = error -> + error -> error end end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge.ex index 835cb56ced18..cea07427f76e 100644 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex +++ b/apps/indexer/lib/indexer/fetcher/polygon_edge.ex @@ -572,12 +572,12 @@ defmodule Indexer.Fetcher.PolygonEdge do [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: [polygon_edge_l1_rpc], http_options: [ recv_timeout: :timer.minutes(10), timeout: :timer.minutes(10), - hackney: [pool: :ethereum_jsonrpc] + pool: :ethereum_jsonrpc ] ] ] diff --git a/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex index f65498e88445..ff283612d6dc 100644 --- a/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex +++ b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex @@ -563,12 +563,12 @@ defmodule Indexer.Fetcher.Shibarium.L1 do [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: [rpc_url], http_options: [ recv_timeout: :timer.minutes(10), timeout: :timer.minutes(10), - hackney: [pool: :ethereum_jsonrpc] + pool: :ethereum_jsonrpc ] ] ] diff --git a/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex b/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex index 7c733bba89fc..2149bcd61cd2 100644 --- a/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex +++ b/apps/indexer/lib/indexer/fetcher/zksync/batches_status_tracker.ex @@ -87,12 +87,12 @@ defmodule Indexer.Fetcher.ZkSync.BatchesStatusTracker do json_l1_rpc_named_arguments: [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: [l1_rpc], http_options: [ recv_timeout: :timer.minutes(10), timeout: :timer.minutes(10), - hackney: [pool: :ethereum_jsonrpc] + pool: :ethereum_jsonrpc ] ] ], diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 0c27faaa9045..ec46bf216c2b 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -223,12 +223,12 @@ defmodule Indexer.Helper do [ transport: EthereumJSONRPC.HTTP, transport_options: [ - http: EthereumJSONRPC.HTTP.HTTPoison, + http: EthereumJSONRPC.HTTP.Tesla, urls: [rpc_url], http_options: [ recv_timeout: :timer.minutes(10), timeout: :timer.minutes(10), - hackney: [pool: :ethereum_jsonrpc] + pool: :ethereum_jsonrpc ] ] ] diff --git a/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs b/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs index c0816d017831..ac95ef1e34cc 100644 --- a/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs +++ b/apps/indexer/test/indexer/fetcher/beacon/blob_test.exs @@ -45,8 +45,6 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do assert {:error, :not_found} = Reader.blob(blob_c.hash, true) assert {:ok, _} = Reader.blob(blob_d.hash, true) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - result_ab = """ { "data": [ @@ -79,16 +77,18 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do } """ - Explorer.Mox.HTTPoison - |> expect(:get, 2, fn url -> - case url do - "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269188" -> - {:ok, %HTTPoison.Response{status_code: 200, body: result_c}} + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: url}, _opts -> + case url do + "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269188" -> + {:ok, %Tesla.Env{status: 200, body: result_c}} - "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198" -> - {:ok, %HTTPoison.Response{status_code: 200, body: result_ab}} + "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198" -> + {:ok, %Tesla.Env{status: 200, body: result_ab}} + end end - end) + ) BlobSupervisor.Case.start_supervised!() @@ -100,8 +100,6 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do assert {:ok, _} = Reader.blob(blob_b.hash, true) assert {:ok, _} = Reader.blob(blob_c.hash, true) assert {:ok, _} = Reader.blob(blob_d.hash, true) - - Application.put_env(:explorer, :http_adapter, HTTPoison) end end @@ -116,8 +114,6 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do end test "fetches blobs for block timestamp" do - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - {:ok, now, _} = DateTime.from_iso8601("2024-01-24 00:00:00Z") block_a = insert(:block, timestamp: now) @@ -141,10 +137,16 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do } """ - Explorer.Mox.HTTPoison - |> expect(:get, fn "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198" -> - {:ok, %HTTPoison.Response{status_code: 200, body: result_a}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:5052/eth/v1/beacon/blob_sidecars/8269198"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: result_a + }} + end + ) BlobSupervisor.Case.start_supervised!() @@ -162,8 +164,6 @@ defmodule Indexer.Fetcher.Beacon.BlobTest do kzg_commitment: ^kzg_commitment_a, kzg_proof: ^kzg_proof_a } = blob - - Application.put_env(:explorer, :http_adapter, HTTPoison) end end end diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index 337f87191375..e251a3e4bdf8 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -81,6 +81,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do on_exit(fn -> Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end) @@ -104,6 +105,8 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do TestHelper.get_chain_id_mock() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> Conn.resp( conn, @@ -124,11 +127,8 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do addresses_chunk_size: 1 ) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - on_exit(fn -> Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) - Application.put_env(:explorer, :http_adapter, HTTPoison) end) address_1 = insert(:address) @@ -156,24 +156,21 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do TestHelper.get_chain_id_mock() - for _ <- 0..2 do - Explorer.Mox.HTTPoison - |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", - body, - [{"Content-Type", "application/json"}], - _options -> + Tesla.Test.expect_tesla_call( + times: 3, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> case Jason.decode(body) do {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> - {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} {:ok, %{"addresses" => [%{"hash" => ^address_2_hash_string}]}} -> - {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} _ -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} end - end) - end + end + ) log = capture_log(fn -> @@ -229,21 +226,18 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do TestHelper.get_chain_id_mock() - for _ <- 0..1 do - Explorer.Mox.HTTPoison - |> expect(:post, fn "http://localhost:1234/api/v1/import:batch", - body, - [{"Content-Type", "application/json"}], - _options -> + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> case Jason.decode(body) do {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> - {:ok, %HTTPoison.Response{status_code: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} _ -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(%{"status" => "ok"})}} + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} end - end) - end + end + ) MultichainSearchDbMainExportQueue.run(export_data_2, nil) diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs index 617005781b7a..d1323aff1ac6 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/token_instance_metadata_refetch_test.exs @@ -34,12 +34,6 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do Subscriber.to(:fetched_token_instance_metadata, :on_demand) Subscriber.to(:not_fetched_token_instance_metadata, :on_demand) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - on_exit(fn -> - Application.put_env(:explorer, :http_adapter, HTTPoison) - end) - :ok end @@ -61,10 +55,16 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == :ok @@ -108,10 +108,16 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: Jason.encode!(metadata)}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == :ok @@ -154,10 +160,16 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetchTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Explorer.Mox.HTTPoison - |> expect(:get, fn ^url, _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: nil}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: nil + }} + end + ) assert TokenInstanceMetadataRefetchOnDemand.trigger_refetch(token_instance) == :ok diff --git a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs index a8a34d34362c..9acb91b52e21 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs @@ -15,22 +15,29 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do setup do bypass = Bypass.open() - on_exit(fn -> Bypass.down(bypass) end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) {:ok, bypass: bypass} end describe "fetch instance tests" do test "fetches json metadata for kitties" do - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - result = "{\"id\":100500,\"name\":\"KittyBlue_2_Lemonade\",\"generation\":20,\"genes\":\"623509754227292470437941473598751240781530569131665917719736997423495595\",\"created_at\":\"2017-12-06T01:56:27.000Z\",\"birthday\":\"2017-12-06T00:00:00.000Z\",\"image_url\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/100500.svg\",\"image_url_cdn\":\"https://img.cn.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/100500.svg\",\"color\":\"strawberry\",\"background_color\":\"#ffe0e5\",\"bio\":\"Shalom! I'm KittyBlue_2_Lemonade. I'm a professional Foreign Film Director and I love cantaloupe. I'm convinced that the world is flat. One day I'll prove it. It's pawesome to meet you!\",\"kitty_type\":null,\"is_fancy\":false,\"is_exclusive\":false,\"is_special_edition\":false,\"fancy_type\":null,\"language\":\"en\",\"is_prestige\":false,\"prestige_type\":null,\"prestige_ranking\":null,\"prestige_time_limit\":null,\"status\":{\"is_ready\":true,\"is_gestating\":false,\"cooldown\":1410310201506,\"dynamic_cooldown\":1475064986478,\"cooldown_index\":10,\"cooldown_end_block\":0,\"pending_transaction_type\":null,\"pending_tx_since\":null},\"purrs\":{\"count\":1,\"is_purred\":false},\"watchlist\":{\"count\":0,\"is_watchlisted\":false},\"hatcher\":{\"address\":\"0x7b9ea9ac69b8fde875554321472c732eeff06ca0\",\"image\":\"14\",\"nickname\":\"KittyBlu\",\"hasDapper\":false,\"twitter_id\":null,\"twitter_image_url\":null,\"twitter_handle\":null},\"auction\":{},\"offer\":{},\"owner\":{\"address\":\"0x7b9ea9ac69b8fde875554321472c732eeff06ca0\",\"hasDapper\":false,\"twitter_id\":null,\"twitter_image_url\":null,\"twitter_handle\":null,\"image\":\"14\",\"nickname\":\"KittyBlu\"},\"matron\":{\"id\":46234,\"name\":\"KittyBlue_1_Limegreen\",\"generation\":10,\"enhanced_cattributes\":[{\"type\":\"body\",\"kittyId\":19631,\"position\":105,\"description\":\"cymric\"},{\"type\":\"coloreyes\",\"kittyId\":40356,\"position\":263,\"description\":\"limegreen\"},{\"type\":\"eyes\",\"kittyId\":3185,\"position\":16,\"description\":\"raisedbrow\"},{\"type\":\"pattern\",\"kittyId\":46234,\"position\":-1,\"description\":\"totesbasic\"},{\"type\":\"mouth\",\"kittyId\":46234,\"position\":-1,\"description\":\"happygokitty\"},{\"type\":\"colorprimary\",\"kittyId\":46234,\"position\":-1,\"description\":\"greymatter\"},{\"type\":\"colorsecondary\",\"kittyId\":46234,\"position\":-1,\"description\":\"lemonade\"},{\"type\":\"colortertiary\",\"kittyId\":46234,\"position\":-1,\"description\":\"granitegrey\"}],\"owner_wallet_address\":\"0x7b9ea9ac69b8fde875554321472c732eeff06ca0\",\"owner\":{\"address\":\"0x7b9ea9ac69b8fde875554321472c732eeff06ca0\"},\"created_at\":\"2017-12-03T21:29:17.000Z\",\"image_url\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/46234.svg\",\"image_url_cdn\":\"https://img.cn.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/46234.svg\",\"color\":\"limegreen\",\"is_fancy\":false,\"kitty_type\":null,\"is_exclusive\":false,\"is_special_edition\":false,\"fancy_type\":null,\"status\":{\"is_ready\":true,\"is_gestating\":false,\"cooldown\":1486487069384},\"hatched\":true,\"wrapped\":false,\"image_url_png\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/46234.png\"},\"sire\":{\"id\":82090,\"name\":null,\"generation\":19,\"enhanced_cattributes\":[{\"type\":\"body\",\"kittyId\":82090,\"position\":-1,\"description\":\"himalayan\"},{\"type\":\"coloreyes\",\"kittyId\":82090,\"position\":-1,\"description\":\"strawberry\"},{\"type\":\"eyes\",\"kittyId\":82090,\"position\":-1,\"description\":\"thicccbrowz\"},{\"type\":\"pattern\",\"kittyId\":82090,\"position\":-1,\"description\":\"totesbasic\"},{\"type\":\"mouth\",\"kittyId\":82090,\"position\":-1,\"description\":\"pouty\"},{\"type\":\"colorprimary\",\"kittyId\":82090,\"position\":-1,\"description\":\"aquamarine\"},{\"type\":\"colorsecondary\",\"kittyId\":82090,\"position\":-1,\"description\":\"chocolate\"},{\"type\":\"colortertiary\",\"kittyId\":82090,\"position\":-1,\"description\":\"granitegrey\"}],\"owner_wallet_address\":\"0x798fdad0cedc4b298fc7d53a982fa0c5f447eaa5\",\"owner\":{\"address\":\"0x798fdad0cedc4b298fc7d53a982fa0c5f447eaa5\"},\"created_at\":\"2017-12-05T06:30:05.000Z\",\"image_url\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/82090.svg\",\"image_url_cdn\":\"https://img.cn.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/82090.svg\",\"color\":\"strawberry\",\"is_fancy\":false,\"is_exclusive\":false,\"is_special_edition\":false,\"fancy_type\":null,\"status\":{\"is_ready\":true,\"is_gestating\":false,\"cooldown\":1486619010030},\"kitty_type\":null,\"hatched\":true,\"wrapped\":false,\"image_url_png\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/82090.png\"},\"children\":[],\"hatched\":true,\"wrapped\":false,\"enhanced_cattributes\":[{\"type\":\"colorprimary\",\"description\":\"greymatter\",\"position\":null,\"kittyId\":100500},{\"type\":\"coloreyes\",\"description\":\"strawberry\",\"position\":null,\"kittyId\":100500},{\"type\":\"body\",\"description\":\"himalayan\",\"position\":null,\"kittyId\":100500},{\"type\":\"colorsecondary\",\"description\":\"lemonade\",\"position\":null,\"kittyId\":100500},{\"type\":\"mouth\",\"description\":\"pouty\",\"position\":null,\"kittyId\":100500},{\"type\":\"pattern\",\"description\":\"totesbasic\",\"position\":null,\"kittyId\":100500},{\"type\":\"eyes\",\"description\":\"thicccbrowz\",\"position\":null,\"kittyId\":100500},{\"type\":\"colortertiary\",\"description\":\"kittencream\",\"position\":null,\"kittyId\":100500},{\"type\":\"secret\",\"description\":\"se5\",\"position\":-1,\"kittyId\":100500},{\"type\":\"purrstige\",\"description\":\"pu20\",\"position\":-1,\"kittyId\":100500}],\"variation\":null,\"variation_ranking\":null,\"image_url_png\":\"https://img.cryptokitties.co/0x06012c8cf97bead5deae237070f9587f8e7a266d/100500.png\",\"items\":[]}" - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://api.cryptokitties.co/kitties/100500", _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: result}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://api.cryptokitties.co/kitties/100500"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: result + }} + end + ) token = insert(:token, @@ -42,8 +49,6 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do Helper.batch_fetch_instances([{token.contract_address_hash, 100_500}]) assert Map.get(metadata, "name") == "KittyBlue_2_Lemonade" - - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "replace {id} with actual token_id", %{bypass: bypass} do @@ -90,6 +95,8 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect( bypass, "GET", @@ -145,12 +152,17 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) - Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) - - Explorer.Mox.HTTPoison - |> expect(:get, fn "https://ipfs.io/ipfs/Qmd9pvThEwgjTBbEkNmmGFbcpJKw17fnRBAT4Td4rcog22", _headers, _options -> - {:ok, %HTTPoison.Response{status_code: 200, body: "123", headers: [{"Content-Type", "image/jpg"}]}} - end) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://ipfs.io/ipfs/Qmd9pvThEwgjTBbEkNmmGFbcpJKw17fnRBAT4Td4rcog22"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: "123", + headers: [{"Content-Type", "image/jpg"}] + }} + end + ) token = insert(:token, @@ -165,8 +177,6 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do } } ] = Helper.batch_fetch_instances([{token.contract_address_hash, 0}]) - - Application.put_env(:explorer, :http_adapter, HTTPoison) end test "re-fetch metadata from baseURI", %{bypass: bypass} do @@ -239,6 +249,8 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect( bypass, "GET", @@ -362,6 +374,8 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect( bypass, "GET", @@ -441,6 +455,8 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect( bypass, "GET", @@ -812,6 +828,8 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do ]} end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Bypass.expect( bypass, "GET", diff --git a/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs index 542b2954ad6d..698aae207791 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs @@ -29,6 +29,8 @@ defmodule Indexer.Fetcher.TokenInstance.RealtimeTest do test "retry once after timeout" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + [] |> TokenInstanceRealtime.Supervisor.child_spec() |> ExUnit.Callbacks.start_supervised!() @@ -117,6 +119,7 @@ defmodule Indexer.Fetcher.TokenInstance.RealtimeTest do assert is_nil(instance.error) assert instance.metadata == %{"name" => "name"} + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs index 0ca957570d22..77d6b02af84e 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs @@ -76,6 +76,8 @@ defmodule Indexer.Fetcher.TokenInstance.RefetchTest do test "filters and fetches token instances marked to refetch" do bypass = Bypass.open() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + json = """ { "name": "nice nft" @@ -205,6 +207,7 @@ defmodule Indexer.Fetcher.TokenInstance.RefetchTest do assert updated_token_instance.error == nil end + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) Bypass.down(bypass) end end diff --git a/apps/indexer/test/test_helper.exs b/apps/indexer/test/test_helper.exs index 381b6272bc29..9024fcf37bc8 100644 --- a/apps/indexer/test/test_helper.exs +++ b/apps/indexer/test/test_helper.exs @@ -17,7 +17,7 @@ end Mox.defmock(EthereumJSONRPC.Mox, for: EthereumJSONRPC.Transport) Mox.defmock(Indexer.BufferedTaskTest.RetryableTask, for: Indexer.BufferedTask) Mox.defmock(Indexer.BufferedTaskTest.ShrinkableTask, for: Indexer.BufferedTask) -Mox.defmock(Explorer.Mox.HTTPoison, for: HTTPoison.Base) +Mox.defmock(Explorer.Mock.TeslaAdapter, for: Tesla.Adapter) ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) ExUnit.start() diff --git a/apps/utils/lib/utils/http_client/httpoison_helper.ex b/apps/utils/lib/utils/http_client/httpoison_helper.ex new file mode 100644 index 000000000000..c4dcd9f4ccb3 --- /dev/null +++ b/apps/utils/lib/utils/http_client/httpoison_helper.ex @@ -0,0 +1,62 @@ +defmodule Utils.HttpClient.HTTPoisonHelper do + @moduledoc """ + Helper module for building HTTPoison request options. + + This module provides utilities to construct keyword lists of options + for HTTPoison HTTP client requests, including timeouts, authentication, + redirects, and connection pooling. + """ + + def request_opts(options) do + [] + |> add_recv_timeout_option(options[:recv_timeout]) + |> add_timeout_option(options[:timeout]) + |> add_basic_auth_option(options[:basic_auth]) + |> add_pool_option(options[:pool]) + |> add_follow_redirect_option(options[:follow_redirect]) + |> add_insecure_option(options[:insecure]) + |> add_params_option(options[:params]) + end + + defp add_recv_timeout_option(options, nil), do: options + + defp add_recv_timeout_option(options, timeout) do + Keyword.put(options, :recv_timeout, timeout) + end + + defp add_params_option(options, nil), do: options + + defp add_params_option(options, params) do + Keyword.put(options, :params, params) + end + + defp add_timeout_option(options, nil), do: options + + defp add_timeout_option(options, timeout) do + Keyword.put(options, :timeout, timeout) + end + + defp add_follow_redirect_option(options, nil), do: options + + defp add_follow_redirect_option(options, follow_redirect) do + Keyword.put(options, :follow_redirect, follow_redirect) + end + + defp add_insecure_option(options, true) do + Keyword.put(options, :hackney, [:insecure | options[:hackney] || []]) + end + + defp add_insecure_option(options, _insecure), do: options + + defp add_basic_auth_option(options, {username, password}) do + Keyword.put(options, :hackney, [{:basic_auth, {username, password}} | options[:hackney] || []]) + end + + defp add_basic_auth_option(options, _basic_auth), do: options + + defp add_pool_option(options, nil), do: options + + defp add_pool_option(options, pool) do + Keyword.put(options, :hackney, [{:pool, pool} | options[:hackney] || []]) + end +end diff --git a/apps/utils/lib/utils/http_client/tesla_helper.ex b/apps/utils/lib/utils/http_client/tesla_helper.ex new file mode 100644 index 000000000000..74b1c8d35939 --- /dev/null +++ b/apps/utils/lib/utils/http_client/tesla_helper.ex @@ -0,0 +1,85 @@ +defmodule Utils.HttpClient.TeslaHelper do + @moduledoc """ + Helper module for building Tesla HTTP clients and adapter options. + + This module provides utilities to construct Tesla clients with appropriate + middleware and adapter options, including timeouts, authentication, redirects, + and TLS settings. + """ + + @doc """ + Builds a Tesla client with configured middleware based on the provided options. + + ## Parameters + - `options`: A keyword list or map containing HTTP client options + + ## Returns + - A Tesla client struct with configured middleware + """ + @spec client(keyword() | map()) :: Tesla.Client.t() + def client(options) do + options[:recv_timeout] + |> add_timeout_middleware() + |> add_follow_redirect_middleware(options[:follow_redirect]) + |> add_basic_auth_middleware(options[:basic_auth]) + |> Tesla.client() + end + + @doc """ + Builds adapter options for Tesla HTTP client requests from the provided options map. + + ## Parameters + - `options`: A keyword list or map containing HTTP client options + + ## Returns + - A keyword list containing adapter options for Tesla + """ + @spec request_opts(keyword() | map()) :: keyword() + def request_opts(options) do + adapter_options = + [protocols: [:http1]] + |> add_recv_timeout_option(options[:recv_timeout]) + |> add_timeout_option(options[:timeout]) + |> add_insecure_option(options[:insecure]) + + [adapter: adapter_options] + end + + defp add_timeout_middleware(middleware \\ [], timeout) + + defp add_timeout_middleware(middleware, nil), do: middleware + + defp add_timeout_middleware(middleware, timeout) do + [{Tesla.Middleware.Timeout, timeout: timeout} | middleware] + end + + defp add_follow_redirect_middleware(middleware, true) do + [Tesla.Middleware.FollowRedirects | middleware] + end + + defp add_follow_redirect_middleware(middleware, _follow_redirect?), do: middleware + + defp add_basic_auth_middleware(middleware, {username, password}) do + [{Tesla.Middleware.BasicAuth, %{username: username, password: password}} | middleware] + end + + defp add_basic_auth_middleware(middleware, _basic_auth), do: middleware + + defp add_recv_timeout_option(options, nil), do: options + + defp add_recv_timeout_option(options, timeout) do + Keyword.put(options, :timeout, timeout) + end + + defp add_timeout_option(options, nil), do: options + + defp add_timeout_option(options, timeout) do + Keyword.put(options, :transport_opts, [{:timeout, timeout} | options[:transport_opts] || []]) + end + + defp add_insecure_option(options, true) do + Keyword.put(options, :transport_opts, [{:verify, :verify_none} | options[:transport_opts] || []]) + end + + defp add_insecure_option(options, _insecure), do: options +end diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index a8d2315e4575..534f2bc3afa7 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -23,7 +23,7 @@ defmodule Utils.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger] + extra_applications: [:logger, :tesla] ] end @@ -32,7 +32,8 @@ defmodule Utils.MixProject do [ {:credo, "~> 1.5", only: [:test, :dev], runtime: false}, {:httpoison, "~> 2.0"}, - {:mime, "~> 2.0"} + {:mime, "~> 2.0"}, + {:tesla, "~> 1.14.2"} ] end diff --git a/config/config_helper.exs b/config/config_helper.exs index a4fa60691c0f..98cff21997b5 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -44,13 +44,14 @@ defmodule ConfigHelper do base_repos ++ chain_type_repos ++ ext_repos end - @spec hackney_options() :: any() - def hackney_options() do + @spec http_options(non_neg_integer()) :: list() + def http_options(default_timeout \\ 1) do + http_timeout = timeout(default_timeout) basic_auth_user = System.get_env("ETHEREUM_JSONRPC_USER", "") basic_auth_pass = System.get_env("ETHEREUM_JSONRPC_PASSWORD", nil) - [pool: :ethereum_jsonrpc] - |> (&if(System.get_env("ETHEREUM_JSONRPC_HTTP_INSECURE", "") == "true", do: [:insecure] ++ &1, else: &1)).() + [pool: :ethereum_jsonrpc, recv_timeout: http_timeout, timeout: http_timeout] + |> (&if(System.get_env("ETHEREUM_JSONRPC_HTTP_INSECURE", "") == "true", do: [insecure: true] ++ &1, else: &1)).() |> (&if(basic_auth_user != "" && !is_nil(basic_auth_pass), do: [basic_auth: {basic_auth_user, basic_auth_pass}] ++ &1, else: &1 diff --git a/mix.exs b/mix.exs index 5db3c1bc80c3..761421eb0438 100644 --- a/mix.exs +++ b/mix.exs @@ -96,7 +96,7 @@ defmodule BlockScout.Mixfile do [ {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, - {:tesla, "~> 1.14.1"}, + {:tesla, "~> 1.14.2"}, {:mint, "~> 1.7.1"}, # Documentation {:ex_doc, "~> 0.38.1", only: :dev, runtime: false}, From efc8007b2c67b112b043190bbd43cc6ac343689e Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Wed, 16 Jul 2025 15:55:56 +0300 Subject: [PATCH 0172/1006] fix: filter out creation internal transaction with `index == 0` (#12777) * fix: filter for creation internal transaction - filter out transactions with `index == 0` - apply additional sorting by `block_index` * chore: process review comments - Update the documentation to clarify that only internal transactions with index > 0 are considered. - Check that contractCreator is not empty in regression test * chore: add changelog entry --- CHANGELOG.md | 1 + .../api/rpc/contract_controller_test.exs | 40 +++++++++++++++++++ apps/explorer/lib/explorer/chain/address.ex | 9 +++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e97d6ec1275..cb4090024a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Filter out creation internal transaction with `index == 0` ([#12777](https://github.com/blockscout/blockscout/issues/12777)) - Filter out scilla transactions in internal transactions fetcher ([#12793](https://github.com/blockscout/blockscout/issues/12793)) - Change default ordering in `/api/v2/smart-contracts` ([#12767](https://github.com/blockscout/blockscout/issues/12767)) - Filter scilla transactions by status ([#12756](https://github.com/blockscout/blockscout/issues/12756)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 08d194d4ded3..e6b6c4e30b1a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -1149,6 +1149,46 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert contract_factory == to_string(internal_transaction.from_address_hash) assert creation_bytecode == to_string(internal_transaction.init) end + + test "get contract creation info via internal transaction with index 0 and parent transaction - contractFactory should be empty", + %{ + conn: conn, + params: params + } do + {:ok, block_timestamp, _} = DateTime.from_iso8601("2021-05-05T21:42:11.000000Z") + block = insert(:block, timestamp: block_timestamp) + contract_address = insert(:contract_address) + + # Create a transaction that creates the contract + transaction = + :transaction + |> insert(created_contract_address: contract_address) + |> with_block(block) + + # Also create an internal transaction with index 0 for the same contract + insert(:internal_transaction_create, + transaction: transaction, + # index 0 should result in empty contractFactory + index: 0, + created_contract_address: contract_address, + block_hash: transaction.block_hash, + block_index: transaction.index + ) + + assert %{ + "result" => [ + %{ + "contractFactory" => "", + "contractCreator" => contract_creator + } + ] + } = + conn + |> get("/api", Map.put(params, "contractaddresses", to_string(contract_address))) + |> json_response(200) + + assert contract_creator == to_string(transaction.from_address_hash) + end end describe "verifyproxycontract & checkproxyverification" do diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index fc85d335bb75..1d64aea9bf27 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -787,7 +787,7 @@ defmodule Explorer.Chain.Address do @doc """ Creates a query for preloading contract creation internal transactions. - This query sorts internal transactions by: + This query filters for internal transactions with index > 0, sorts them by: 1. error (ascending with nulls first) 2. block number (descending) @@ -797,15 +797,18 @@ defmodule Explorer.Chain.Address do ## Returns - A `Ecto.Query` that can be used to preload the contract creation internal transaction. + A `Ecto.Query` that can be used to preload the contract creation internal + transaction. """ @spec contract_creation_internal_transaction_preload_query() :: Ecto.Query.t() def contract_creation_internal_transaction_preload_query do from( it in InternalTransaction, + where: it.index > 0, order_by: [ asc_nulls_first: it.error, - desc: it.block_number + desc: it.block_number, + desc: it.block_index ], limit: 1 ) From 08646e17f678feac831f8a66e9b729a2cb9ce304 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 17 Jul 2025 15:36:43 +0300 Subject: [PATCH 0173/1006] feat: Export balances to Multichain DB (#12726) * feat: Export balances to Multichain DB * Fix tests * Process first batch of review comments * Process review comment: remove unused function clause * Process review comment: uniformity of address_hash processing * Process review comment: implement Hash.Address type for balance address_hash * Process review comment: refactor composing balances queue * HTTPoison to Tesla in tests --- apps/explorer/lib/explorer/chain.ex | 3 +- .../balances_export_queue.ex | 137 ++++++++ .../multichain_search_db/main_export_queue.ex | 7 +- .../multichain_search.ex | 319 +++++++++++++---- ...ultichain_search_db_export_queue_table.exs | 2 +- ...ultichain_search_db_export_queue_table.exs | 2 +- ..._search_db_balances_export_queue_table.exs | 24 ++ .../multichain_search_test.exs | 122 +++++-- apps/explorer/test/support/factory.ex | 4 + .../balances_export_queue.ex | 206 +++++++++++ .../multichain_search_db/main_export_queue.ex | 2 +- apps/indexer/lib/indexer/supervisor.ex | 2 + .../balances_export_queue_test.exs | 332 ++++++++++++++++++ .../main_export_queue_test.exs | 7 +- config/runtime.exs | 31 +- cspell.json | 2 + docker-compose/envs/common-blockscout.env | 15 +- 17 files changed, 1094 insertions(+), 123 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250609130618_add_multichain_search_db_balances_export_queue_table.exs create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex create mode 100644 apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 1e10f172ef6b..0a2e72d9e6bf 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1158,7 +1158,8 @@ defmodule Explorer.Chain do assets_to_import = %{ addresses: imported[:addresses] || [], blocks: imported[:blocks] || [], - transactions: imported[:transactions] || [] + transactions: imported[:transactions] || [], + address_current_token_balances: imported[:address_current_token_balances] || [] } MultichainSearch.send_data_to_queue(assets_to_import) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex new file mode 100644 index 000000000000..d19d0a617e04 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -0,0 +1,137 @@ +defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do + @moduledoc """ + Tracks token and coin balances, pending for export to the Multichain Service database. + """ + + use Explorer.Schema + import Ecto.Query + alias Ecto.Multi + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Hash, Wei} + + @required_attrs ~w(address_hash token_contract_address_hash_or_native)a + @optional_attrs ~w(value token_id retries_number)a + @allowed_attrs @optional_attrs ++ @required_attrs + + @primary_key false + typed_schema "multichain_search_db_export_balances_queue" do + field(:id, :integer, primary_key: false, null: false) + field(:address_hash, Hash.Address, null: false, primary_key: true) + field(:token_contract_address_hash_or_native, :binary, null: false, primary_key: true) + field(:value, Wei) + field(:token_id, :decimal) + field(:retries_number, :integer) + + timestamps() + end + + def changeset(%__MODULE__{} = pending_ops, attrs) do + pending_ops + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + end + + @doc """ + Streams a batch of multichain database balances that need to be retried for export. + + This function selects specific fields from the export records and applies a reducer function to each entry in the stream, accumulating the result. Optionally, the stream can be limited based on the `limited?` flag. + + ## Parameters + + - `initial`: The initial accumulator value. + - `reducer`: A function that takes an entry (as a map) and the current accumulator, returning the updated accumulator. + - `limited?` (optional): A boolean indicating whether to apply a fetch limit to the stream. Defaults to `false`. + + ## Returns + + - `{:ok, accumulator}`: A tuple containing `:ok` and the final accumulator after processing the stream. + """ + @spec stream_multichain_db_balances_batch_to_retry_export( + initial :: accumulator, + reducer :: (entry :: map(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_multichain_db_balances_batch_to_retry_export(initial, reducer, limited? \\ false) + when is_function(reducer, 2) do + __MODULE__ + |> select([export], %{ + address_hash: export.address_hash, + token_contract_address_hash_or_native: export.token_contract_address_hash_or_native, + value: export.value, + token_id: export.token_id + }) + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Returns an Ecto query that defines the default conflict resolution strategy for the + `multichain_search_db_export_balances_queue` table. On conflict, it increments the `retries_number` + (by using the value from `EXCLUDED.retries_number` or 0 if not present) and updates the + `updated_at` field to the greatest value between the current and the new timestamp. + + This is typically used in upsert operations to ensure retry counts are tracked and + timestamps are properly updated. + """ + @spec default_on_conflict :: Ecto.Query.t() + def default_on_conflict do + from( + multichain_search_db_export_balances_queue in __MODULE__, + update: [ + set: [ + retries_number: fragment("COALESCE(EXCLUDED.retries_number, 0) + 1"), + updated_at: + fragment("GREATEST(?, EXCLUDED.updated_at)", multichain_search_db_export_balances_queue.updated_at) + ] + ] + ) + end + + # sobelow_skip ["DOS.StringToAtom"] + @spec delete_elements_from_queue_by_params([map()]) :: list() + def delete_elements_from_queue_by_params(balances) do + q = + Enum.reduce(balances, nil, fn balance, acc -> + balance_address_hash = balance.address_hash + balance_token_contract_address_hash_or_native = balance.token_contract_address_hash_or_native + + balance_token_id = balance.token_id + + query = + from( + b in __MODULE__, + where: b.address_hash == ^balance_address_hash, + # \x6e6174697665 == hex('native') + where: + fragment( + "?::text = (CASE WHEN LENGTH(?) = 42 THEN '\\' || SUBSTRING(?, 2, LENGTH(?) - 1) ELSE '\\x6e6174697665' END)", + b.token_contract_address_hash_or_native, + ^balance_token_contract_address_hash_or_native, + ^balance_token_contract_address_hash_or_native, + ^balance_token_contract_address_hash_or_native + ), + where: fragment("COALESCE(?, -1) = COALESCE(?, -1)", b.token_id, ^balance_token_id) + ) + + if is_nil(acc) do + query + else + acc + |> union(^query) + end + end) + + elements = Repo.all(q) + + delete_elements = + elements + |> Enum.with_index() + |> Enum.reduce(Multi.new(), fn {elem, ind}, acc -> + acc + |> Multi.delete(String.to_atom("delete_#{ind}"), elem) + end) + + Repo.transact(delete_elements) + end +end diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex index 1c5dea917e37..0f04b85ff944 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex @@ -1,6 +1,7 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do @moduledoc """ - Tracks data pending for export to the Multichain Service database. + Tracks main blockchain data: block, transaction hashes, addresses with the metadata and block ranges, + pending for export to the Multichain Service database. """ use Explorer.Schema @@ -9,6 +10,8 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do alias Explorer.Chain.Block.Range @required_attrs ~w(hash hash_type)a + @optional_attrs ~w(block_range retries_number)a + @allowed_attrs @optional_attrs ++ @required_attrs @primary_key false typed_schema "multichain_search_db_main_export_queue" do @@ -31,7 +34,7 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do def changeset(%__MODULE__{} = pending_ops, attrs) do pending_ops - |> cast(attrs, @required_attrs) + |> cast(attrs, @allowed_attrs) |> validate_required(@required_attrs) end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 263ec3702c02..3a3c2ba551b3 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -4,12 +4,13 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do """ alias Ecto.Association.NotLoaded alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.{Block, Hash, Transaction} + alias Explorer.Chain.{Block, Hash, Transaction, Wei} alias Explorer.Chain.Block.Range - alias Explorer.Chain.MultichainSearchDb.MainExportQueue + alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue} alias Explorer.{Helper, HttpClient, Repo} alias Explorer.Utility.Microservice + require Decimal require Logger @max_concurrency 5 @@ -63,14 +64,19 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do %{ addresses: error.data_to_retry.addresses, block_ranges: error.data_to_retry.block_ranges, - hashes: error.data_to_retry.hashes + hashes: error.data_to_retry.hashes, + address_coin_balances: error.data_to_retry.address_coin_balances, + address_token_balances: error.data_to_retry.address_token_balances }} {:error, data_to_retry} -> merged_data_to_retry = %{ addresses: error.data_to_retry.addresses ++ data_to_retry.addresses, block_ranges: error.data_to_retry.block_ranges ++ data_to_retry.block_ranges, - hashes: error.data_to_retry.hashes ++ data_to_retry.hashes + hashes: error.data_to_retry.hashes ++ data_to_retry.hashes, + address_coin_balances: error.data_to_retry.address_coin_balances ++ data_to_retry.address_coin_balances, + address_token_balances: + error.data_to_retry.address_token_balances ++ data_to_retry.address_token_balances } {:error, merged_data_to_retry} @@ -88,11 +94,13 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do {:ok, {:chunks_processed, _}} -> {:error, export_body} - {:error, data_to_retry} -> + {:error, error} -> merged_data_to_retry = %{ - addresses: data_to_retry.data_to_retry.addresses ++ export_body.addresses, - block_ranges: data_to_retry.data_to_retry.block_ranges ++ export_body.block_ranges, - hashes: data_to_retry.data_to_retry.hashes ++ export_body.hashes + addresses: error.data_to_retry.addresses ++ export_body.addresses, + block_ranges: error.data_to_retry.block_ranges ++ export_body.block_ranges, + hashes: error.data_to_retry.hashes ++ export_body.hashes, + address_coin_balances: error.data_to_retry.address_coin_balances ++ export_body.address_coin_balances, + address_token_balances: error.data_to_retry.address_token_balances ++ export_body.address_token_balances } {:error, merged_data_to_retry} @@ -114,8 +122,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Logger.error(fn -> [ "Error while sending request to microservice url: #{url}, ", - "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}, ", - "error_reason: #{inspect(reason, limit: :infinity, printable_limit: :infinity)}" + "error_reason: #{inspect(reason, limit: :infinity, printable_limit: :infinity)}, ", + "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}" ] end) @@ -134,9 +142,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Logger.error(fn -> [ "Error while sending request to microservice url: #{url}, ", - "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}, ", "status_code: #{inspect(status_code)}, ", - "response_body: #{inspect(response_body, limit: :infinity, printable_limit: :infinity)}" + "response_body: #{inspect(response_body, limit: :infinity, printable_limit: :infinity)}, ", + "request_body: #{inspect(data_to_retry |> Map.drop([:api_key]), limit: :infinity, printable_limit: :infinity)}" ] end) @@ -151,50 +159,66 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do ) do log_error(error) - prepared_data = prepare_export_data_for_queue(data_to_retry) + {prepared_main_data, prepared_balances_data} = prepare_export_data_for_queue(data_to_retry) Repo.insert_all( MainExportQueue, - Helper.add_timestamps(prepared_data), + Helper.add_timestamps(prepared_main_data), on_conflict: MainExportQueue.default_on_conflict(), conflict_target: [:hash, :hash_type] ) + + Repo.insert_all( + BalancesExportQueue, + Helper.add_timestamps(prepared_balances_data), + on_conflict: BalancesExportQueue.default_on_conflict(), + conflict_target: + {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash_or_native, COALESCE(token_id, -1))>} + ) end defp on_error(_), do: :ignore @doc """ - Sends the provided data to the export queue if the feature is enabled. - - Accepts a map with `:addresses`, `:blocks`, and `:transactions` keys, each containing a list. - If all lists are empty, returns `:ignore`. + Sends provided blockchain data to the appropriate export queues for further processing. - Otherwise, splits the data into chunks, prepares each chunk for export, and inserts them into the `MainExportQueue` table. - If the feature is not enabled, returns `:ignore`. + Accepts a map containing lists of addresses, blocks, transactions, and address current token balances. + If all lists are empty, returns `:ignore`. Otherwise, if the export functionality is enabled, + the data is split into chunks, prepared, and inserted into the `MainExportQueue` and `BalancesExportQueue` tables. + If the export functionality is disabled, returns `:ignore`. ## Parameters - - data: a map with the following keys: - - `:addresses` - list of addresses - - `:blocks` - list of blocks - - `:transactions` - list of transactions + - `data`: A map with the following keys: + - `:addresses` - List of address data. + - `:blocks` - List of block data. + - `:transactions` - List of transaction data. + - `:address_current_token_balances` - List of address token balance data. ## Returns - - `:ok` on successful insertion - - `:ignore` if the feature is disabled or the data is empty + - `:ok` if the data was successfully sent to the queues. + - `:ignore` if the data is empty or the export functionality is disabled. """ - @spec send_data_to_queue(%{addresses: list(), blocks: list(), transactions: list()}) :: :ignore | :ok - def send_data_to_queue(%{addresses: [], blocks: [], transactions: []}), do: :ignore + @spec send_data_to_queue(%{ + addresses: list(), + blocks: list(), + transactions: list(), + address_current_token_balances: list() + }) :: :ignore | :ok + def send_data_to_queue(%{addresses: [], blocks: [], transactions: [], address_current_token_balances: []}), + do: :ignore def send_data_to_queue(data) do if enabled?() do data |> extract_batch_import_params_into_chunks() |> Enum.each(fn data_chunk -> - prepared_data = prepare_export_data_for_queue(data_chunk) + {prepared_main_data, prepared_balances_data} = prepare_export_data_for_queue(data_chunk) - Repo.insert_all(MainExportQueue, Helper.add_timestamps(prepared_data), on_conflict: :nothing) + Repo.insert_all(MainExportQueue, Helper.add_timestamps(prepared_main_data), on_conflict: :nothing) + + Repo.insert_all(BalancesExportQueue, Helper.add_timestamps(prepared_balances_data), on_conflict: :nothing) end) :ok @@ -204,9 +228,18 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end # sobelow_skip ["DOS.StringToAtom"] - defp prepare_export_data_for_queue(%{addresses: addresses, hashes: hashes, block_ranges: block_ranges}) do + defp prepare_export_data_for_queue(%{ + addresses: addresses, + hashes: hashes, + block_ranges: block_ranges, + address_coin_balances: address_coin_balances, + address_token_balances: address_token_balances + }) do block_range = case block_ranges do + [%{min_block_number: nil, max_block_number: nil} | _] -> + nil + [%{min_block_number: min_str, max_block_number: max_str} | _] -> with {min_num, ""} <- Integer.parse(min_str), {max_num, ""} <- Integer.parse(max_str) do @@ -239,7 +272,52 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do } end) - hashes_to_queue ++ addresses_to_queue + main_queue = hashes_to_queue ++ addresses_to_queue + + balances_queue = compose_balances_queue(address_coin_balances, address_token_balances) + + {main_queue, balances_queue} + end + + defp prepare_export_data_for_queue(%{ + address_coin_balances: address_coin_balances, + address_token_balances: address_token_balances + }) do + balances_queue = compose_balances_queue(address_coin_balances, address_token_balances) + + {[], balances_queue} + end + + defp compose_balances_queue(address_coin_balances, address_token_balances) do + coin_balances_queue = + address_coin_balances + |> Enum.map(fn %{address_hash: address_hash, value: value} -> + %{ + address_hash: address_hash, + token_contract_address_hash_or_native: "native", + value: value + } + end) + + token_balances_queue = + address_token_balances + |> Enum.map(fn %{ + address_hash: address_hash, + token_address_hash: token_address_hash, + value: value, + token_id: token_id + } -> + %{ + address_hash: address_hash, + token_contract_address_hash_or_native: Helper.hash_to_binary(token_address_hash), + # value is of Wei type in Explorer.Chain.Address.CoinBalance + # value is of Decimal type in Explorer.Chain.Address.TokenBalance + value: if(Decimal.is_decimal(value), do: value |> Wei.cast() |> elem(1), else: value), + token_id: token_id + } + end) + + coin_balances_queue ++ token_balances_queue end @spec http_post_request(String.t(), map()) :: {:ok, any()} | {:error, String.t()} @@ -296,38 +374,83 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do chain_id: String.t(), addresses: [String.t()], block_ranges: [%{min_block_number: String.t(), max_block_number: String.t()}], - hashes: [%{hash: String.t(), hash_type: String.t()}] + hashes: [%{hash: String.t(), hash_type: String.t()}], + address_token_balances: [map()], + address_coin_balances: [map()] } ] - def extract_batch_import_params_into_chunks( - %{ - addresses: raw_addresses, - transactions: transactions - } = params - ) do + def extract_batch_import_params_into_chunks(params) do block_ranges = - if Map.has_key?(params, :block_ranges), do: params.block_ranges, else: get_block_ranges(params.blocks) + if Map.has_key?(params, :block_ranges), + do: params.block_ranges, + else: get_block_ranges(Map.get(params, :blocks, [])) - addresses = - raw_addresses - |> Enum.uniq() + {addresses, coin_balances_from_addresses_list} = + params + |> Map.get(:addresses, []) |> Repo.preload([:token, :smart_contract]) - |> Enum.map(&format_address(&1)) + |> Enum.reduce({[], []}, fn address, {acc_addresses, acc_coin_balances} -> + {[format_address(address) | acc_addresses], [format_address_coin_balance(address) | acc_coin_balances]} + end) + + address_coin_balances = + if Map.has_key?(params, :address_coin_balances) do + params.address_coin_balances + |> Enum.map(fn address_coin_balance -> + %{ + address_hash: address_coin_balance.address_hash, + token_contract_address_hash_or_native: "native", + token_id: nil, + value: address_coin_balance.value + } + end) + else + coin_balances_from_addresses_list + end block_hashes = if Map.has_key?(params, :block_hashes), do: format_block_hashes(params.block_hashes), - else: format_blocks(params.blocks) + else: format_blocks(Map.get(params, :blocks, [])) - transaction_hashes = format_transactions(transactions) + transaction_hashes = format_transactions(Map.get(params, :transactions, [])) block_transaction_hashes = block_hashes ++ transaction_hashes indexed_addresses_chunks = addresses + |> Enum.uniq() |> Enum.chunk_every(addresses_chunk_size()) |> Enum.with_index() + indexed_address_coin_balances_chunks = + address_coin_balances + |> Enum.uniq() + |> Enum.reject(&is_nil(&1.value)) + |> Enum.chunk_every(addresses_chunk_size()) + |> Enum.with_index() + + address_token_balances = + cond do + Map.has_key?(params, :address_current_token_balances) == true -> + params.address_current_token_balances + |> Enum.map(&format_address_token_balance/1) + + Map.has_key?(params, :address_token_balances) == true -> + params.address_token_balances + |> Enum.map(fn address_token_balance -> + %{ + address_hash: address_token_balance.address_hash, + token_address_hash: address_token_balance.token_contract_address_hash, + token_id: address_token_balance.token_id, + value: address_token_balance.value + } + end) + + true -> + [] + end + api_key = api_key() chain_id = ChainId.get_id() chain_id_string = to_string(chain_id) @@ -336,27 +459,77 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do api_key: api_key, chain_id: chain_id_string, addresses: [], - block_ranges: [] + block_ranges: [], + hashes: [], + address_token_balances: [], + address_coin_balances: [] } - if Enum.empty?(indexed_addresses_chunks) do - [ - base_data_chunk - |> Map.put(:hashes, block_transaction_hashes) - |> Map.put(:block_ranges, block_ranges) - ] - else - Enum.map(indexed_addresses_chunks, fn {addresses_chunk, index} -> - # credo:disable-for-lines:2 Credo.Check.Refactor.Nesting - hashes_in_chunk = if index == 0, do: block_transaction_hashes, else: [] - block_ranges_in_chunk = if index == 0, do: block_ranges, else: [] - - base_data_chunk - |> Map.put(:addresses, addresses_chunk) - |> Map.put(:hashes, hashes_in_chunk) - |> Map.put(:block_ranges, block_ranges_in_chunk) - end) - end + prepare_chunk(indexed_addresses_chunks, indexed_address_coin_balances_chunks, base_data_chunk, %{ + block_transaction_hashes: block_transaction_hashes, + block_ranges: block_ranges, + address_token_balances: address_token_balances + }) + end + + defp address_token_balances_chunk_by_index(address_token_balances, 0), do: address_token_balances + + defp address_token_balances_chunk_by_index(_address_token_balances, _index), do: [] + + defp prepare_chunk([], [], base_data_chunk, %{ + block_transaction_hashes: block_transaction_hashes, + block_ranges: block_ranges, + address_token_balances: address_token_balances + }) do + [ + base_data_chunk + |> Map.put(:hashes, block_transaction_hashes) + |> Map.put(:block_ranges, block_ranges) + |> Map.put(:address_token_balances, address_token_balances) + ] + end + + defp prepare_chunk(indexed_addresses_chunks, indexed_address_coin_balances_chunks, base_data_chunk, %{ + block_transaction_hashes: block_transaction_hashes, + block_ranges: block_ranges, + address_token_balances: address_token_balances + }) + when indexed_addresses_chunks != [] do + Enum.map(indexed_addresses_chunks, fn {addresses_chunk, index} -> + # credo:disable-for-lines:3 Credo.Check.Refactor.Nesting + hashes_in_chunk = if index == 0, do: block_transaction_hashes, else: [] + block_ranges_in_chunk = if index == 0, do: block_ranges, else: [] + address_token_balances_in_chunk = address_token_balances_chunk_by_index(address_token_balances, index) + + base_data_chunk + |> Map.put(:addresses, addresses_chunk) + |> Map.put( + :address_coin_balances, + if(Enum.empty?(indexed_address_coin_balances_chunks), + do: [], + else: indexed_address_coin_balances_chunks |> Enum.at(index) |> elem(0) || [] + ) + ) + |> Map.put(:hashes, hashes_in_chunk) + |> Map.put(:block_ranges, block_ranges_in_chunk) + |> Map.put(:address_token_balances, address_token_balances_in_chunk) + end) + end + + defp prepare_chunk(_indexed_addresses_chunks, indexed_address_coin_balances_chunks, base_data_chunk, %{ + address_token_balances: address_token_balances + }) + when indexed_address_coin_balances_chunks != [] do + Enum.map(indexed_address_coin_balances_chunks, fn {indexed_address_coin_balance_chunk, index} -> + address_token_balances_in_chunk = address_token_balances_chunk_by_index(address_token_balances, index) + + base_data_chunk + |> Map.put( + :address_coin_balances, + indexed_address_coin_balance_chunk + ) + |> Map.put(:address_token_balances, address_token_balances_in_chunk) + end) end defp format_address(address) do @@ -372,6 +545,22 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do } end + defp format_address_coin_balance(address) do + %{ + address_hash: address.hash, + value: address.fetched_coin_balance + } + end + + defp format_address_token_balance(address_current_token_balance) do + %{ + address_hash: address_current_token_balance.address_hash, + token_address_hash: Hash.to_string(address_current_token_balance.token_contract_address_hash), + token_id: address_current_token_balance.token_id, + value: address_current_token_balance.value + } + end + @spec format_blocks([Block.t() | %{hash: String.t(), hash_type: String.t()}]) :: [ %{hash: String.t(), hash_type: String.t()} ] diff --git a/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs index 178795f1bced..581dda6fe6dc 100644 --- a/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs +++ b/apps/explorer/priv/repo/migrations/20250526114340_add_block_range_and_retries_number_to_multichain_search_db_export_queue_table.exs @@ -1,4 +1,4 @@ -defmodule Explorer.Repo.Migrations.AddBlockRangeAndRetriesNumberToMultichainSearchDbExportQueueTable do +defmodule Explorer.Repo.Migrations.AddBlockRangeAndRetriesNumberToMultichainSearchDbMainExportQueueTable do use Ecto.Migration def change do diff --git a/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs index 6dc27a8b9bba..f631c3395a97 100644 --- a/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs +++ b/apps/explorer/priv/repo/migrations/20250529150808_add_block_range_index_to_multichain_search_db_export_queue_table.exs @@ -1,4 +1,4 @@ -defmodule Explorer.Repo.Migrations.AddBlockRangeIndexToMultichainSearchDbExportQueueTable do +defmodule Explorer.Repo.Migrations.AddBlockRangeIndexToMultichainSearchDbMainExportQueueTable do use Ecto.Migration def change do diff --git a/apps/explorer/priv/repo/migrations/20250609130618_add_multichain_search_db_balances_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250609130618_add_multichain_search_db_balances_export_queue_table.exs new file mode 100644 index 000000000000..44ef6f6ce029 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250609130618_add_multichain_search_db_balances_export_queue_table.exs @@ -0,0 +1,24 @@ +defmodule Explorer.Repo.Migrations.AddMultichainSearchDbBalancesExportQueueTable do + use Ecto.Migration + + def change do + create table(:multichain_search_db_export_balances_queue, primary_key: false) do + add(:id, :serial, null: false, primary_key: true) + add(:address_hash, :bytea, null: false) + add(:token_contract_address_hash_or_native, :bytea, null: false) + add(:value, :numeric, precision: 100, scale: 0, null: true) + add(:token_id, :numeric, precision: 78, scale: 0, null: true) + add(:retries_number, :smallint, null: true) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create_if_not_exists( + unique_index( + :multichain_search_db_export_balances_queue, + [:address_hash, :token_contract_address_hash_or_native, "COALESCE(token_id, -1)"], + name: :unique_multichain_search_db_current_token_balances + ) + ) + end +end diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index e51a30a8b59f..2741d6d75773 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -5,6 +5,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.MultichainSearchDb.MainExportQueue + alias Explorer.Chain.Wei alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.{Repo, TestHelper} alias Plug.Conn @@ -27,7 +28,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [], blocks: [], - transactions: [] + transactions: [], + address_current_token_balances: [] } assert MultichainSearch.batch_import(params) == {:ok, :service_disabled} @@ -73,7 +75,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [address_1, address_2], blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [] } assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_import(params) @@ -118,14 +121,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [address_1, address_2], blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [] } assert {:error, %{ addresses: [ - address_export_data(address_1), - address_export_data(address_2) + address_export_data(address_2), + address_export_data(address_1) ], block_ranges: [ %{max_block_number: to_string(block_2.number), min_block_number: to_string(block_1.number)} @@ -135,7 +139,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do block_export_data(block_2), transaction_export_data(transaction_1), transaction_export_data(transaction_2) - ] + ], + address_coin_balances: [], + address_token_balances: [] }} == MultichainSearch.batch_import(params) assert Repo.aggregate(MainExportQueue, :count, :hash) == 6 @@ -204,7 +210,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: addresses, blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [] } assert {:error, results} = MultichainSearch.batch_import(params) @@ -248,7 +255,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: addresses, blocks: [], - transactions: [] + transactions: [], + address_current_token_balances: [] } assert {:error, results} = MultichainSearch.batch_import(params) @@ -292,7 +300,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: addresses, blocks: [], - transactions: [] + transactions: [], + address_current_token_balances: [] } assert {:error, results} = MultichainSearch.batch_import(params) @@ -325,8 +334,19 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert MultichainSearch.extract_batch_import_params_into_chunks(%{ addresses: [], blocks: [], - transactions: [] - }) == [%{api_key: "12345", addresses: [], block_ranges: [], chain_id: "1", hashes: []}] + transactions: [], + address_current_token_balances: [] + }) == [ + %{ + api_key: "12345", + addresses: [], + block_ranges: [], + chain_id: "1", + hashes: [], + address_coin_balances: [], + address_token_balances: [] + } + ] end test "returns chunks with transactions and blocks when no addresses provided" do @@ -338,7 +358,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [], blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [] } chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) @@ -379,17 +400,29 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do end test "returns chunks with the correct structure when all types of data is provided" do - address_1 = insert(:address) - address_2 = insert(:address) + address_1 = insert(:address, fetched_coin_balance: Decimal.new(100)) + address_2 = insert(:address, fetched_coin_balance: Decimal.new(200)) block_1 = insert(:block) block_2 = insert(:block) transaction_1 = insert(:transaction) transaction_2 = insert(:transaction) + token = insert(:token, contract_address: address_1, type: "ERC-20", name: "Test Token") + + current_token_balance = + insert(:address_current_token_balance, + address: address_1, + token_type: "ERC-20", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: 30_000 + ) + params = %{ addresses: [address_1, address_2], blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [current_token_balance] } chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) @@ -431,6 +464,20 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do hash_type: "TRANSACTION" } ] + + assert chunk[:address_coin_balances] == [ + %{value: %Wei{value: Decimal.new("200")}, address_hash: address_2.hash}, + %{value: %Wei{value: Decimal.new("100")}, address_hash: address_1.hash} + ] + + assert chunk[:address_token_balances] == [ + %{ + value: Decimal.new("30000"), + address_hash: address_1.hash, + token_id: nil, + token_address_hash: to_string(token.contract_address_hash) + } + ] end test "returns multiple chunks with the correct structure when all types of data is provided" do @@ -447,7 +494,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: addresses, blocks: [block_1, block_2], - transactions: [transaction_1, transaction_2] + transactions: [transaction_1, transaction_2], + address_current_token_balances: [] } chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) @@ -503,8 +551,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert second_chunk[:hashes] == [] assert Enum.all?(second_chunk[:addresses], fn item -> - item.hash == "0x" <> Base.encode16(Enum.at(addresses, -2).hash.bytes, case: :lower) || - item.hash == "0x" <> Base.encode16(List.last(addresses).hash.bytes, case: :lower) + item.hash == "0x" <> Base.encode16(Enum.at(addresses, 0).hash.bytes, case: :lower) || + item.hash == "0x" <> Base.encode16(Enum.at(addresses, 1).hash.bytes, case: :lower) end) end @@ -515,7 +563,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [address_1, address_2], blocks: [], - transactions: [] + transactions: [], + address_current_token_balances: [] } chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) @@ -548,7 +597,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do params = %{ addresses: [address_1, address_2, address_3_with_preloads], blocks: [], - transactions: [] + transactions: [], + address_current_token_balances: [] } chunks = MultichainSearch.extract_batch_import_params_into_chunks(params) @@ -562,14 +612,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert chunk[:addresses] == [ %{ - hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), - is_contract: false, - is_verified_contract: false, - contract_name: nil, - token_name: nil, - token_type: "UNSPECIFIED", - is_token: false, - ens_name: "te.eth" + hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), + is_contract: true, + is_verified_contract: true, + contract_name: "SimpleStorage", + token_name: "Main Token", + token_type: "ERC-721", + is_token: true, + ens_name: nil }, %{ hash: "0x" <> Base.encode16(address_2.hash.bytes, case: :lower), @@ -582,14 +632,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ens_name: nil }, %{ - hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), - is_contract: true, - is_verified_contract: true, - contract_name: "SimpleStorage", - token_name: "Main Token", - token_type: "ERC-721", - is_token: true, - ens_name: nil + hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), + is_contract: false, + is_verified_contract: false, + contract_name: nil, + token_name: nil, + token_type: "UNSPECIFIED", + is_token: false, + ens_name: "te.eth" } ] diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index ec6a00f3f210..3bdb82e25912 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -749,6 +749,10 @@ defmodule Explorer.Factory do %MultichainSearchDb.MainExportQueue{} end + def multichain_search_db_export_balances_queue_factory do + %MultichainSearchDb.BalancesExportQueue{} + end + def internal_transaction_factory() do gas = Enum.random(21_000..100_000) gas_used = Enum.random(0..gas) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex new file mode 100644 index 000000000000..e99f5bfa4c48 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -0,0 +1,206 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do + @moduledoc """ + Exports token and coin balances to Multichain Search DB service from the queue. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.Chain.Wei + alias Explorer.Chain.{Hash, MultichainSearchDb.BalancesExportQueue} + alias Explorer.MicroserviceInterfaces.MultichainSearch + + alias Indexer.BufferedTask + alias Indexer.Helper, as: IndexerHelper + + @behaviour BufferedTask + + @default_max_batch_size 1000 + @default_max_concurrency 10 + @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + BalancesExportQueue.stream_multichain_db_balances_batch_to_retry_export( + initial_acc, + fn data, acc -> + IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end, + true + ) + + acc + end + + @impl BufferedTask + def run(data, _json_rpc_named_arguments) when is_list(data) do + prepared_export_data = prepare_export_data(data) + + export_data_to_multichain(prepared_export_data) + end + + defp export_data_to_multichain(prepared_export_data) do + case MultichainSearch.batch_import(prepared_export_data) do + {:ok, {:chunks_processed, result}} -> + all_balances = + result + |> Enum.flat_map(fn params -> + coin_balances = prepare_coin_balances_for_db_query(params[:address_coin_balances]) + token_balances = prepare_token_balances_for_db_query(params[:address_token_balances]) + + coin_balances ++ token_balances + end) + + unless Enum.empty?(all_balances) do + all_balances + |> BalancesExportQueue.delete_elements_from_queue_by_params() + end + + :ok + + {:error, retry} -> + Logger.error(fn -> + ["#{@failed_to_re_export_data_error}", "#{inspect(prepared_export_data)}"] + end) + + {:retry, retry} + end + end + + defp prepare_token_balances_for_db_query(token_balances) do + token_balances + |> Enum.map(fn token_balance -> + %{ + address_hash: token_balance.address_hash, + token_contract_address_hash_or_native: token_balance.token_address_hash, + token_id: + if(is_nil(token_balance.token_id), + do: nil, + else: token_balance.token_id |> Decimal.to_integer() + ), + value: + if(is_nil(token_balance.value), + do: nil, + else: token_balance.value |> Wei.dump() |> elem(1) |> Decimal.to_integer() + ) + } + end) + end + + defp prepare_coin_balances_for_db_query(coin_balances) do + coin_balances + |> Enum.map(fn coin_balance -> + %{ + address_hash: coin_balance.address_hash, + token_contract_address_hash_or_native: "native", + token_id: nil, + value: + if(is_nil(coin_balance.value), + do: nil, + else: coin_balance.value |> Wei.dump() |> elem(1) |> Decimal.to_integer() + ) + } + end) + end + + @doc """ + Prepares export data by separating balances into coin and token balances. + + ## Parameters + + - `export_data`: A list of maps, each containing: + - `:address_hash` - The address hash of Hash.Address.t(). + - `:token_contract_address_hash_or_native` - The token contract address hash as a binary, or the string `"native"` for native coins. + - `:value` - The balance value as a `Decimal.t()`. + - `:token_id` (optional) - The token ID, present for token balances. + + ## Returns + + A map with the following keys: + - `:address_coin_balances` - A list of maps with `:address_hash` and `:value` for native coin balances. + - `:address_token_balances` - A list of maps with `:address_hash`, `:token_contract_address_hash`, `:token_id`, and `:value` for token balances. + + Native coin balances are grouped under `:address_coin_balances`, while token balances are grouped under `:address_token_balances`. The function also converts binary hashes to string representations using the `Hash` struct. + """ + @spec prepare_export_data([ + %{ + address_hash: Hash.Address.t(), + token_contract_address_hash_or_native: binary(), + value: Decimal.t() | nil, + token_id: Decimal.t() | nil + } + ]) :: %{ + address_coin_balances: list(), + address_token_balances: list() + } + def prepare_export_data(export_data) do + pre_prepared_export_data = + export_data + |> Enum.reduce( + %{ + address_coin_balances: [], + address_token_balances: [] + }, + fn res, acc -> + case res.token_contract_address_hash_or_native do + "native" -> + acc + |> Map.update( + :address_coin_balances, + [%{address_hash: res.address_hash, value: res.value}], + &[%{address_hash: res.address_hash, value: res.value} | &1] + ) + + _ -> + acc + |> Map.update( + :address_token_balances, + [ + %{ + address_hash: res.address_hash, + token_contract_address_hash: + to_string(%Hash{byte_count: 20, bytes: res.token_contract_address_hash_or_native}), + token_id: res.token_id, + value: res.value + } + ], + &[ + %{ + address_hash: res.address_hash, + token_contract_address_hash: + to_string(%Hash{byte_count: 20, bytes: res.token_contract_address_hash_or_native}), + token_id: res.token_id, + value: res.value + } + | &1 + ] + ) + end + end + ) + + pre_prepared_export_data + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex index 8c7d79210a42..a5f188d6d790 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -166,7 +166,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do A map with prepared export data, including addresses, transactions, block ranges, and block hashes. """ - @spec prepare_export_data([%{hash: binary, hash_type: atom, block_range: any()}]) :: %{ + @spec prepare_export_data([%{hash: binary(), hash_type: atom(), block_range: any()}]) :: %{ addresses: [Address.t()], transactions: [Transaction.t() | %{hash: String.t(), hash_type: String.t()}], block_ranges: [%{min_block_number: String.t(), max_block_number: String.t()}], diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 0dd8ab3f0403..e4306b12a830 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -23,6 +23,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.Blackfort.Validator, as: ValidatorBlackfort alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime @@ -150,6 +151,7 @@ defmodule Indexer.Supervisor do [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbMainExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbBalancesExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, configure( Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs new file mode 100644 index 000000000000..fb7426b52485 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs @@ -0,0 +1,332 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do + use ExUnit.Case + use Explorer.DataCase + + import ExUnit.CaptureLog, only: [capture_log: 1] + + alias Explorer.Chain + alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.MultichainSearchDb.BalancesExportQueue + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.TestHelper + + alias Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, + as: MultichainSearchDbExportBalancesExportQueue + + alias Plug.Conn + + @moduletag :capture_log + + setup do + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + Application.put_env(:indexer, MultichainSearchDbExportBalancesExportQueue.Supervisor, disabled?: false) + + on_exit(fn -> + Application.put_env(:indexer, MultichainSearchDbExportBalancesExportQueue.Supervisor, disabled?: true) + end) + + :ok + end + + describe "init/3" do + setup do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + end) + end + + test "initializes with data from the retry queue" do + {:ok, address_hash} = + Chain.string_to_address_hash("0x66A9B160F6a06f53f23785F069882Ee7337180E8") + + erc_20_contract_address_hash_bytes = "A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" |> Base.decode16!(case: :mixed) + nft_contract_address_hash_bytes = "B01b6a7EF8560017AA59A990e39f26b8df29F80f" |> Base.decode16!(case: :mixed) + + insert(:multichain_search_db_export_balances_queue, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: "native", + value: 100, + token_id: nil + }) + + insert(:multichain_search_db_export_balances_queue, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: erc_20_contract_address_hash_bytes, + value: 200, + token_id: nil + }) + + insert(:multichain_search_db_export_balances_queue, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: nft_contract_address_hash_bytes, + value: nil, + token_id: 12345 + }) + + reducer = fn data, acc -> [data | acc] end + + pid = + [] + |> MultichainSearchDbExportBalancesExportQueue.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + + results = MultichainSearchDbExportBalancesExportQueue.init([], reducer, nil) + + assert Enum.count(results) == 3 + + assert Enum.member?(results, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: "native", + value: %Wei{value: Decimal.new(100)}, + token_id: nil + }) + + assert Enum.member?(results, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: erc_20_contract_address_hash_bytes, + value: %Wei{value: Decimal.new(200)}, + token_id: nil + }) + + assert Enum.member?(results, %{ + address_hash: address_hash, + token_contract_address_hash_or_native: nft_contract_address_hash_bytes, + value: nil, + token_id: Decimal.new(12345) + }) + + :timer.sleep(10) + GenServer.stop(pid) + end + end + + describe "run/2" do + setup do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + addresses_chunk_size: 7000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + Bypass.down(bypass) + end) + + {:ok, bypass: bypass} + end + + test "successfully processes multichain search db export retry queue data", %{bypass: bypass} do + {:ok, address_hash} = + Chain.string_to_address_hash("0x66A9B160F6a06f53f23785F069882Ee7337180E8") + + {:ok, erc_20_contract_address_hash} = + Chain.string_to_address_hash("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48") + + {:ok, nft_contract_address_hash} = + Chain.string_to_address_hash("0xB01b6a7EF8560017AA59A990e39f26b8df29F80f") + + export_data = [ + %{ + address_hash: address_hash, + token_contract_address_hash_or_native: "native", + value: %Wei{value: Decimal.new(100)} + }, + %{ + address_hash: erc_20_contract_address_hash, + token_contract_address_hash_or_native: erc_20_contract_address_hash.bytes, + value: %Wei{value: Decimal.new(200)}, + token_id: nil + }, + %{ + address_hash: nft_contract_address_hash, + token_contract_address_hash_or_native: nft_contract_address_hash.bytes, + value: nil, + token_id: Decimal.new(12345) + } + ] + + TestHelper.get_chain_id_mock() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + assert :ok = MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) + end + + test "returns {:retry, failed_data} on error where failed_data is only chunks that failed to export", %{ + bypass: _bypass + } do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + addresses_chunk_size: 1 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + end) + + address_1 = insert(:address) + address_2 = insert(:address) + address_2_hash = address_2.hash + address_3 = insert(:address) + address_3_hash = address_3.hash + address_4 = insert(:address) + address_4_hash = address_4.hash + address_4_hash_string_checksummed = Address.checksum(address_4) + address_5 = insert(:address) + address_5_hash = address_5.hash + token_address_1 = insert(:address) + token_address_1_hash_string = to_string(token_address_1) |> String.downcase() + token_address_2 = insert(:address) + token_address_2_hash_string = to_string(token_address_2) |> String.downcase() + + export_data = [ + %{ + address_hash: address_1.hash, + token_contract_address_hash_or_native: "native", + value: %Wei{value: Decimal.new(100)} + }, + %{ + address_hash: address_2.hash, + token_contract_address_hash_or_native: token_address_1.hash.bytes, + value: %Wei{value: Decimal.new(200)}, + token_id: nil + }, + %{ + address_hash: address_3.hash, + token_contract_address_hash_or_native: token_address_2.hash.bytes, + value: %Wei{value: Decimal.new(300)}, + token_id: nil + }, + %{ + address_hash: address_4.hash, + token_contract_address_hash_or_native: "native", + value: %Wei{value: Decimal.new(400)} + }, + %{ + address_hash: address_5.hash, + token_contract_address_hash_or_native: token_address_1.hash.bytes, + value: %Wei{value: Decimal.new(500)}, + token_id: nil + } + ] + + TestHelper.get_chain_id_mock() + + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string_checksummed}]}} -> + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + + val1 = Decimal.new(200) |> Wei.cast() |> elem(1) + val2 = Decimal.new(300) |> Wei.cast() |> elem(1) + val3 = Decimal.new(400) |> Wei.cast() |> elem(1) + val4 = Decimal.new(500) |> Wei.cast() |> elem(1) + + log = + capture_log(fn -> + assert {:retry, + %{ + address_coin_balances: [ + %{ + address_hash: ^address_4_hash, + token_contract_address_hash_or_native: "native", + value: ^val3 + } + ], + address_token_balances: [ + %{ + address_hash: ^address_5_hash, + token_address_hash: ^token_address_1_hash_string, + value: ^val4, + token_id: nil + }, + %{ + address_hash: ^address_3_hash, + token_address_hash: ^token_address_2_hash_string, + value: ^val2, + token_id: nil + }, + %{ + address_hash: ^address_2_hash, + token_address_hash: ^token_address_1_hash_string, + value: ^val1, + token_id: nil + } + ] + }} = MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) + end) + + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 4 + assert log =~ "Batch export retry to the Multichain Search DB failed" + + export_data_2 = [ + %{ + address_hash: address_2.hash, + token_contract_address_hash_or_native: token_address_1.hash.bytes, + value: %Wei{value: Decimal.new(200)}, + token_id: nil + }, + %{ + address_hash: address_3.hash, + token_contract_address_hash_or_native: token_address_2.hash.bytes, + value: %Wei{value: Decimal.new(300)}, + token_id: nil + }, + %{ + address_hash: address_4.hash, + token_contract_address_hash_or_native: "native", + value: %Wei{value: Decimal.new(400)} + }, + %{ + address_hash: address_5.hash, + token_contract_address_hash_or_native: token_address_1.hash.bytes, + value: %Wei{value: Decimal.new(500)}, + token_id: nil + } + ] + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + + MultichainSearchDbExportBalancesExportQueue.run(export_data_2, nil) + + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 0 + + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index e251a3e4bdf8..996d6abc7726 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -3,7 +3,6 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do use Explorer.DataCase import ExUnit.CaptureLog, only: [capture_log: 1] - import Mox alias Explorer.Chain.MultichainSearchDb.MainExportQueue alias Explorer.Chain.Block.Range @@ -132,10 +131,10 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do end) address_1 = insert(:address) + address_1_hash_string = to_string(address_1) |> String.downcase() address_2 = insert(:address) address_2_hash_string = to_string(address_2) |> String.downcase() address_3 = insert(:address) - address_3_hash_string = to_string(address_3) |> String.downcase() block = insert(:block, number: 1) block_number_string = to_string(block.number) block_hash_string = to_string(block.hash) @@ -188,7 +187,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do is_verified_contract: false }, %{ - hash: ^address_3_hash_string, + hash: ^address_1_hash_string, token_type: "UNSPECIFIED", is_contract: false, token_name: nil, @@ -215,7 +214,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do export_data_2 = [ %{hash: address_2.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, - %{hash: address_3.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, + %{hash: address_1.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, %{hash: block.hash.bytes, hash_type: :block, block_range: %Range{from: block.number, to: block.number}}, %{ hash: transaction.hash.bytes, diff --git a/config/runtime.exs b/config/runtime.exs index 5cd30c6480fe..a38f965eb60a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1034,7 +1034,12 @@ config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue.Supervisor, disabled?: - ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_FETCHER") || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER") || + is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + +config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue.Supervisor, + disabled?: + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER") || is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, @@ -1119,21 +1124,33 @@ config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, high_verbosity: ConfigHelper.parse_bool_env_var("MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY", "true") config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, - concurrency: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_CONCURRENCY", 10), - batch_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_BATCH_SIZE", 1000), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY", 10), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE", 1000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var( - "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_ENQUEUE_BUSY_WAITING_TIMEOUT", + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s" ), max_queue_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE", 1000) + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1000) config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) +config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY", 10), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE", 1000), + enqueue_busy_waiting_timeout: + ConfigHelper.parse_time_env_var( + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", + "1s" + ), + max_queue_size: + ConfigHelper.parse_integer_env_var( + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE", + 1000 + ) + config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.DisputeGame.Supervisor, enabled: ConfigHelper.chain_type() == :optimism diff --git a/cspell.json b/cspell.json index 0ab5bc6af3d8..12e32dea2dad 100644 --- a/cspell.json +++ b/cspell.json @@ -447,6 +447,7 @@ "omni", "onclick", "onconnect", + "ondemand", "ondisconnect", "opos", "Optim", @@ -653,6 +654,7 @@ "tsvector", "tsym", "txid", + "txlsit", "txlistinternal", "Txns", "txpool", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 5e75a0b55315..1b30e7dbb32e 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -184,7 +184,8 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER= # INDEXER_DISABLE_WITHDRAWALS_FETCHER= # INDEXER_DISABLE_REPLACED_TRANSACTION_FETCHER= -# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -203,11 +204,15 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY= # INDEXER_TOKEN_INSTANCE_REFETCH_BATCH_SIZE=10 # INDEXER_TOKEN_INSTANCE_REFETCH_CONCURRENCY= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_BATCH_SIZE= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_CONCURRENCY= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_ENQUEUE_BUSY_WAITING_TIMEOUT= -# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_PROCESSOR_MAX_QUEUE_SIZE= # INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= From e7a23921f9a6aa4f0629251195868d28773bb104 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 17 Jul 2025 15:38:08 +0300 Subject: [PATCH 0174/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4090024a0e..c3ecf933b1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Export balances to Multichain DB([#12726](https://github.com/blockscout/blockscout/pull/12726)) - Add eip7702 authorization status fetcher ([#12451](https://github.com/blockscout/blockscout/issues/12451)) - Add token1155tx token404tx api v1 endpoints ([#12720](https://github.com/blockscout/blockscout/issues/12720)) - Async multichain data export ([#12490](https://github.com/blockscout/blockscout/issues/12490)) From 0e01ecda664d32bfdb5c30ef65ea2bbd7d909977 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 17 Jul 2025 16:35:45 +0300 Subject: [PATCH 0175/1006] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3ecf933b1d8..b7569d02bdab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ ### ⚙️ Miscellaneous Tasks +- Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) - Merge adjacent missing block ranges ([#12778](https://github.com/blockscout/blockscout/issues/12778)) - Optimize missing block ranges operations ([#12705](https://github.com/blockscout/blockscout/issues/12705)) - Hold parity with Etherscan APIv1 for `getcontractcreation` and `getblocknobytime` endpoints ([#12721](https://github.com/blockscout/blockscout/issues/12721)) @@ -72,7 +73,6 @@ - Support multiple interop messages view on transaction page ([#12455](https://github.com/blockscout/blockscout/issues/12455)) - Remove `is_self_destructed` field in `/api/v2/smart-contracts/{address_hash}` response ([#12239](https://github.com/blockscout/blockscout/issues/12239)) - Set home directory for blockscout user ([#12337](https://github.com/blockscout/blockscout/issues/12337)) -- Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) | Variable | Description | Parameters | From c5c3d27f24b484bd6fd1673940b1cea16aeb81c8 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:29:54 +0300 Subject: [PATCH 0176/1006] chore: migrate Auth0 to mint as well (#12807) --- apps/block_scout_web/config/config.exs | 2 ++ .../lib/explorer/third_party_integrations/auth0/internal.ex | 2 +- .../lib/explorer/third_party_integrations/auth0/legacy.ex | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 80cbef2152b8..24f380eadd14 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -118,6 +118,8 @@ config :ueberauth, Ueberauth, } ] +config :oauth2, adapter: Tesla.Adapter.Mint + config :tesla, adapter: Tesla.Adapter.Mint # Import environment specific config. This must remain at the bottom diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex index bdf153be55b4..685f2a43df47 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex @@ -92,7 +92,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Internal do with token when is_binary(token) <- Auth0.get_m2m_jwt(), client = OAuth.client(token: token), {:ok, %OAuth2.Response{status_code: 200, body: users}} when is_list(users) <- - Client.get(client, @users_path, [], params: %{"q" => q}) do + Client.get(client, @users_path, [], params: [q: q]) do {:ok, users} else error -> handle_common_errors(error, error_message) diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex index b01e9abdde82..ee8face6d66f 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex @@ -144,7 +144,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do defp maybe_link_email(%{"email" => email, "user_id" => "email|" <> identity_id = user_id} = user) do case Internal.find_users( - ~s(email:"#{URI.encode(email)}" AND NOT user_id:"#{URI.encode(user_id)}"), + ~s(email:"#{email}" AND NOT user_id:"#{user_id}"), "Failed to find legacy users by email" ) do {:ok, []} -> From ae610253069c42f75f8b5556fcbf41b4b2c24b0e Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:26:52 +0400 Subject: [PATCH 0177/1006] Update runtime.exs (#12814) --- config/runtime.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/runtime.exs b/config/runtime.exs index a38f965eb60a..474d3baf8a4d 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1169,6 +1169,9 @@ config :indexer, Indexer.Fetcher.Optimism.Interop.MessageFailed.Supervisor, config :indexer, Indexer.Fetcher.Optimism.Interop.MessageQueue.Supervisor, disabled?: ConfigHelper.chain_type() != :optimism +config :indexer, Indexer.Fetcher.Optimism.Interop.MultichainExport.Supervisor, + disabled?: ConfigHelper.chain_type() != :optimism + config :indexer, Indexer.Fetcher.Optimism, optimism_l1_rpc: System.get_env("INDEXER_OPTIMISM_L1_RPC"), optimism_l1_system_config: System.get_env("INDEXER_OPTIMISM_L1_SYSTEM_CONFIG_CONTRACT"), From 1ea4735455501930db383723cef9f97cc79d2736 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 18 Jul 2025 13:55:06 +0300 Subject: [PATCH 0178/1006] fix: Fix flaky test for exporting balances to Multichain DB (#12813) * fix: Fix flaky test for exporting balances to Multichain DB * Fix payload sent to multichain service API --- .../multichain_search.ex | 13 +++++++------ .../multichain_search_test.exs | 14 +++++++------- .../balances_export_queue_test.exs | 17 ++++++++++------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 3a3c2ba551b3..8635654611f4 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -3,6 +3,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Module to interact with Multichain search microservice """ alias Ecto.Association.NotLoaded + alias Explorer.Chain alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Block, Hash, Transaction, Wei} alias Explorer.Chain.Block.Range @@ -293,7 +294,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do address_coin_balances |> Enum.map(fn %{address_hash: address_hash, value: value} -> %{ - address_hash: address_hash, + address_hash: address_hash |> Chain.string_to_address_hash() |> elem(1), token_contract_address_hash_or_native: "native", value: value } @@ -308,7 +309,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do token_id: token_id } -> %{ - address_hash: address_hash, + address_hash: address_hash |> Chain.string_to_address_hash() |> elem(1), token_contract_address_hash_or_native: Helper.hash_to_binary(token_address_hash), # value is of Wei type in Explorer.Chain.Address.CoinBalance # value is of Decimal type in Explorer.Chain.Address.TokenBalance @@ -398,7 +399,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do params.address_coin_balances |> Enum.map(fn address_coin_balance -> %{ - address_hash: address_coin_balance.address_hash, + address_hash: Hash.to_string(address_coin_balance.address_hash), token_contract_address_hash_or_native: "native", token_id: nil, value: address_coin_balance.value @@ -440,7 +441,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do params.address_token_balances |> Enum.map(fn address_token_balance -> %{ - address_hash: address_token_balance.address_hash, + address_hash: Hash.to_string(address_token_balance.address_hash), token_address_hash: address_token_balance.token_contract_address_hash, token_id: address_token_balance.token_id, value: address_token_balance.value @@ -547,14 +548,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp format_address_coin_balance(address) do %{ - address_hash: address.hash, + address_hash: Hash.to_string(address.hash), value: address.fetched_coin_balance } end defp format_address_token_balance(address_current_token_balance) do %{ - address_hash: address_current_token_balance.address_hash, + address_hash: Hash.to_string(address_current_token_balance.address_hash), token_address_hash: Hash.to_string(address_current_token_balance.token_contract_address_hash), token_id: address_current_token_balance.token_id, value: address_current_token_balance.value diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 2741d6d75773..1c93c0a2f8e0 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -448,32 +448,32 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert chunk[:hashes] == [ %{ - hash: "0x" <> Base.encode16(block_1.hash.bytes, case: :lower), + hash: to_string(block_1.hash), hash_type: "BLOCK" }, %{ - hash: "0x" <> Base.encode16(block_2.hash.bytes, case: :lower), + hash: to_string(block_2.hash), hash_type: "BLOCK" }, %{ - hash: "0x" <> Base.encode16(transaction_1.hash.bytes, case: :lower), + hash: to_string(transaction_1.hash), hash_type: "TRANSACTION" }, %{ - hash: "0x" <> Base.encode16(transaction_2.hash.bytes, case: :lower), + hash: to_string(transaction_2.hash), hash_type: "TRANSACTION" } ] assert chunk[:address_coin_balances] == [ - %{value: %Wei{value: Decimal.new("200")}, address_hash: address_2.hash}, - %{value: %Wei{value: Decimal.new("100")}, address_hash: address_1.hash} + %{value: %Wei{value: Decimal.new("200")}, address_hash: to_string(address_2.hash)}, + %{value: %Wei{value: Decimal.new("100")}, address_hash: to_string(address_1.hash)} ] assert chunk[:address_token_balances] == [ %{ value: Decimal.new("30000"), - address_hash: address_1.hash, + address_hash: to_string(address_1.hash), token_id: nil, token_address_hash: to_string(token.contract_address_hash) } diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs index fb7426b52485..82896305c7ff 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs @@ -5,7 +5,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do import ExUnit.CaptureLog, only: [capture_log: 1] alias Explorer.Chain - alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.Wei alias Explorer.Chain.MultichainSearchDb.BalancesExportQueue alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.TestHelper @@ -186,13 +186,16 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do address_1 = insert(:address) address_2 = insert(:address) address_2_hash = address_2.hash + address_2_hash_string = to_string(address_2_hash) address_3 = insert(:address) address_3_hash = address_3.hash + address_3_hash_string = to_string(address_3_hash) address_4 = insert(:address) address_4_hash = address_4.hash - address_4_hash_string_checksummed = Address.checksum(address_4) + address_4_hash_string = to_string(address_4_hash) address_5 = insert(:address) address_5_hash = address_5.hash + address_5_hash_string = to_string(address_5_hash) token_address_1 = insert(:address) token_address_1_hash_string = to_string(token_address_1) |> String.downcase() token_address_2 = insert(:address) @@ -235,7 +238,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do times: 2, returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> case Jason.decode(body) do - {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string_checksummed}]}} -> + {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string}]}} -> {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} _ -> @@ -255,26 +258,26 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do %{ address_coin_balances: [ %{ - address_hash: ^address_4_hash, + address_hash: ^address_4_hash_string, token_contract_address_hash_or_native: "native", value: ^val3 } ], address_token_balances: [ %{ - address_hash: ^address_5_hash, + address_hash: ^address_5_hash_string, token_address_hash: ^token_address_1_hash_string, value: ^val4, token_id: nil }, %{ - address_hash: ^address_3_hash, + address_hash: ^address_3_hash_string, token_address_hash: ^token_address_2_hash_string, value: ^val2, token_id: nil }, %{ - address_hash: ^address_2_hash, + address_hash: ^address_2_hash_string, token_address_hash: ^token_address_1_hash_string, value: ^val1, token_id: nil From a8e3ef61c1870f285ed07826ae1297f5ae5ffc24 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 18 Jul 2025 16:33:09 +0300 Subject: [PATCH 0179/1006] fix: Sanitize empty block_ranges payload before sending HTTP request to Multichain service (#12816) --- .../fetcher/multichain_search_db/main_export_queue.ex | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex index a5f188d6d790..6d6d93f612e2 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -226,6 +226,16 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do pre_prepared_export_data |> Map.put(:addresses, addresses) |> Map.drop([:address_hashes]) + |> (&if( + Map.get(&1, :block_ranges) == [ + %{ + max_block_number: nil, + min_block_number: nil + } + ], + do: Map.drop(&1, [:block_ranges]), + else: &1 + )).() end defp defaults do From 1e4b5c4d750f326738657c9da4266d4146b26a5d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 18 Jul 2025 16:37:04 +0300 Subject: [PATCH 0180/1006] Add CHANGELOG entries --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7569d02bdab..62af7490c752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ ### 🐛 Bug Fixes +- Sanitize empty block_ranges payload before sending HTTP request to Multichain service([#12816](https://github.com/blockscout/blockscout/pull/12816)) +- Disable Indexer.Fetcher.Optimism.Interop.MultichainExport for non-OP chains ([#12814](https://github.com/blockscout/blockscout/pull/12814)) +- Fix flaky test for exporting balances to Multichain DB ([#12813](https://github.com/blockscout/blockscout/pull/12813)) - Filter out creation internal transaction with `index == 0` ([#12777](https://github.com/blockscout/blockscout/issues/12777)) - Filter out scilla transactions in internal transactions fetcher ([#12793](https://github.com/blockscout/blockscout/issues/12793)) - Change default ordering in `/api/v2/smart-contracts` ([#12767](https://github.com/blockscout/blockscout/issues/12767)) @@ -56,6 +59,7 @@ ### ⚙️ Miscellaneous Tasks +- migrate Auth0 to mint as well ([#12807](https://github.com/blockscout/blockscout/pull/12807)) - Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) - Merge adjacent missing block ranges ([#12778](https://github.com/blockscout/blockscout/issues/12778)) - Optimize missing block ranges operations ([#12705](https://github.com/blockscout/blockscout/issues/12705)) From 6ec96b566532d54f4b64d6dff957c73fd77bc7e9 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 18 Jul 2025 17:54:48 +0300 Subject: [PATCH 0181/1006] fix: remove token_id from coin balance payload to Multichain service API endpoint (#12817) --- CHANGELOG.md | 1 + .../lib/explorer/microservice_interfaces/multichain_search.ex | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62af7490c752..0a8260934162 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Remove token_id parameter from coin balance payload to Multichain service API endpoint ([#12817](https://github.com/blockscout/blockscout/pull/12817)) - Sanitize empty block_ranges payload before sending HTTP request to Multichain service([#12816](https://github.com/blockscout/blockscout/pull/12816)) - Disable Indexer.Fetcher.Optimism.Interop.MultichainExport for non-OP chains ([#12814](https://github.com/blockscout/blockscout/pull/12814)) - Fix flaky test for exporting balances to Multichain DB ([#12813](https://github.com/blockscout/blockscout/pull/12813)) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 8635654611f4..8ef059602306 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -401,7 +401,6 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do %{ address_hash: Hash.to_string(address_coin_balance.address_hash), token_contract_address_hash_or_native: "native", - token_id: nil, value: address_coin_balance.value } end) From 71b9ffd5c3a0305f41e8078178b5e28c75dd67e5 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sat, 19 Jul 2025 11:35:20 +0300 Subject: [PATCH 0182/1006] fix: Fix multichain export queues processing (#12822) --- CHANGELOG.md | 1 + .../balances_export_queue.ex | 17 +++++++++++++---- .../multichain_search_db/main_export_queue.ex | 13 +++++++++++-- .../multichain_search.ex | 1 + .../balances_export_queue.ex | 4 ++-- .../multichain_search_db/main_export_queue.ex | 2 +- .../balances_export_queue_test.exs | 2 +- .../main_export_queue_test.exs | 2 +- config/runtime.exs | 14 +++++++++----- docker-compose/envs/common-blockscout.env | 2 ++ 10 files changed, 42 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a8260934162..0354f1f37dac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Fix multichain export queues processing ([#12822](https://github.com/blockscout/blockscout/pull/12822)) - Remove token_id parameter from coin balance payload to Multichain service API endpoint ([#12817](https://github.com/blockscout/blockscout/pull/12817)) - Sanitize empty block_ranges payload before sending HTTP request to Multichain service([#12816](https://github.com/blockscout/blockscout/pull/12816)) - Disable Indexer.Fetcher.Optimism.Interop.MultichainExport for non-OP chains ([#12814](https://github.com/blockscout/blockscout/pull/12814)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index d19d0a617e04..d71d79d8a196 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do use Explorer.Schema import Ecto.Query alias Ecto.Multi - alias Explorer.{Chain, Repo} + alias Explorer.Repo alias Explorer.Chain.{Hash, Wei} @required_attrs ~w(address_hash token_contract_address_hash_or_native)a @@ -46,13 +46,13 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do - `{:ok, accumulator}`: A tuple containing `:ok` and the final accumulator after processing the stream. """ - @spec stream_multichain_db_balances_batch_to_retry_export( + @spec stream_multichain_db_balances_batch( initial :: accumulator, reducer :: (entry :: map(), accumulator -> accumulator), limited? :: boolean() ) :: {:ok, accumulator} when accumulator: term() - def stream_multichain_db_balances_batch_to_retry_export(initial, reducer, limited? \\ false) + def stream_multichain_db_balances_batch(initial, reducer, limited? \\ false) when is_function(reducer, 2) do __MODULE__ |> select([export], %{ @@ -61,10 +61,19 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do value: export.value, token_id: export.token_id }) - |> Chain.add_fetcher_limit(limited?) + |> add_balances_queue_fetcher_limit(limited?) |> Repo.stream_reduce(initial, reducer) end + defp add_balances_queue_fetcher_limit(query, false), do: query + + defp add_balances_queue_fetcher_limit(query, true) do + balances_queue_fetcher_limit = + Application.get_env(:indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue)[:init_limit] + + limit(query, ^balances_queue_fetcher_limit) + end + @doc """ Returns an Ecto query that defines the default conflict resolution strategy for the `multichain_search_db_export_balances_queue` table. On conflict, it increments the `retries_number` diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex index 0f04b85ff944..4de20844a978 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex @@ -6,8 +6,8 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do use Explorer.Schema import Ecto.Query - alias Explorer.{Chain, Repo} alias Explorer.Chain.Block.Range + alias Explorer.Repo @required_attrs ~w(hash hash_type)a @optional_attrs ~w(block_range retries_number)a @@ -53,10 +53,19 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do block_range: export.block_range }) |> order_by([export], fragment("upper(?) DESC", export.block_range)) - |> Chain.add_fetcher_limit(limited?) + |> add_main_queue_fetcher_limit(limited?) |> Repo.stream_reduce(initial, reducer) end + defp add_main_queue_fetcher_limit(query, false), do: query + + defp add_main_queue_fetcher_limit(query, true) do + main_queue_fetcher_limit = + Application.get_env(:indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue)[:init_limit] + + limit(query, ^main_queue_fetcher_limit) + end + @doc """ Builds a query to retrieve records from the `Explorer.Chain.MultichainSearchDb.MainExportQueue` module where the `hash` field matches any of the given `hashes`. diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 8ef059602306..3ec16b8f07bf 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -446,6 +446,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do value: address_token_balance.value } end) + |> Enum.reject(&(is_nil(&1.value) && is_nil(&1.token_id))) true -> [] diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex index e99f5bfa4c48..36027ece9b27 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -19,7 +19,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do @default_max_batch_size 1000 @default_max_concurrency 10 - @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" + @failed_to_re_export_data_error "Batch balances export retry to the Multichain Search DB failed" @doc false def child_spec([init_options, gen_server_options]) do @@ -34,7 +34,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do @impl BufferedTask def init(initial_acc, reducer, _) do {:ok, acc} = - BalancesExportQueue.stream_multichain_db_balances_batch_to_retry_export( + BalancesExportQueue.stream_multichain_db_balances_batch( initial_acc, fn data, acc -> IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex index 6d6d93f612e2..9bc81022404e 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -19,7 +19,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do @default_max_batch_size 1000 @default_max_concurrency 10 - @failed_to_re_export_data_error "Batch export retry to the Multichain Search DB failed" + @failed_to_re_export_data_error "Batch main export retry to the Multichain Search DB failed" @doc false def child_spec([init_options, gen_server_options]) do diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs index 82896305c7ff..4764c1816247 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs @@ -287,7 +287,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do end) assert Repo.aggregate(BalancesExportQueue, :count, :id) == 4 - assert log =~ "Batch export retry to the Multichain Search DB failed" + assert log =~ "Batch balances export retry to the Multichain Search DB failed" export_data_2 = [ %{ diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index 996d6abc7726..1c04cda5687f 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -210,7 +210,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do assert Repo.aggregate(MainExportQueue, :count, :hash) == 4 - assert log =~ "Batch export retry to the Multichain Search DB failed" + assert log =~ "Batch main export retry to the Multichain Search DB failed" export_data_2 = [ %{hash: address_2.hash.bytes, hash_type: :address, block_range: %Range{from: block.number, to: block.number}}, diff --git a/config/runtime.exs b/config/runtime.exs index 474d3baf8a4d..22bb93087932 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1132,10 +1132,9 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, "1s" ), max_queue_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1000) - -config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1000), + init_limit: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_INIT_QUERY_LIMIT", 1_000) config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY", 10), @@ -1149,7 +1148,12 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, ConfigHelper.parse_integer_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE", 1000 - ) + ), + init_limit: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT", 1_000) + +config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 1b30e7dbb32e..8ef4d8aaf5c5 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -209,10 +209,12 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_INIT_QUERY_LIMIT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= From 64509a4e07b3c160c97a42f9d207e2662df7bedd Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 21 Jul 2025 13:20:23 +0300 Subject: [PATCH 0183/1006] Fix: Reject empty token_id and value in export of token balances to the Multichain DB (#12829) --- CHANGELOG.md | 1 + .../explorer/microservice_interfaces/multichain_search.ex | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0354f1f37dac..034b77affaf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Fix: Reject empty token_id and value in export of token balances to the Multichain DB ([#12829](https://github.com/blockscout/blockscout/pull/12829)) - Fix multichain export queues processing ([#12822](https://github.com/blockscout/blockscout/pull/12822)) - Remove token_id parameter from coin balance payload to Multichain service API endpoint ([#12817](https://github.com/blockscout/blockscout/pull/12817)) - Sanitize empty block_ranges payload before sending HTTP request to Multichain service([#12816](https://github.com/blockscout/blockscout/pull/12816)) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 3ec16b8f07bf..a7df428aac47 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -446,12 +446,15 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do value: address_token_balance.value } end) - |> Enum.reject(&(is_nil(&1.value) && is_nil(&1.token_id))) true -> [] end + sanitized_address_token_balances = + address_token_balances + |> Enum.reject(&(is_nil(&1.value) && is_nil(&1.token_id))) + api_key = api_key() chain_id = ChainId.get_id() chain_id_string = to_string(chain_id) @@ -469,7 +472,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do prepare_chunk(indexed_addresses_chunks, indexed_address_coin_balances_chunks, base_data_chunk, %{ block_transaction_hashes: block_transaction_hashes, block_ranges: block_ranges, - address_token_balances: address_token_balances + address_token_balances: sanitized_address_token_balances }) end From ab8e3407273d3b4d8925467a2c003f47e25838f2 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Mon, 21 Jul 2025 18:54:01 +0400 Subject: [PATCH 0184/1006] chore: Parsing L2 block number of OP Dispute Game on BOB chain (#12831) * chore: Parsing L2 block number of dispute game on BOB chain * Update apps/explorer/lib/explorer/chain/optimism/dispute_game.ex Co-authored-by: Victor Baranov * Handle nil case * Add CHANGELOG entry --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> Co-authored-by: Victor Baranov --- CHANGELOG.md | 5 +++ .../views/api/v2/optimism_view.ex | 5 ++- .../explorer/chain/optimism/dispute_game.ex | 35 +++++++++++++++++++ .../lib/explorer/chain/optimism/withdrawal.ex | 2 +- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 034b77affaf0..dd7ac477d41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,11 @@ | `CELO_VALIDATORS_CONTRACT` | The address[^1] of the `Validators` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| | `CELO_EPOCH_MANAGER_CONTRACT` | The address[^1] of the `EpochManager` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). |

Version: v9.0.0+
Default: (empty)
Applications: Indexer

| +## 8.1.2 + +### ⚙️ Miscellaneous Tasks + +- Parsing L2 block number of OP Dispute Game on BOB chain ([#12831](https://github.com/blockscout/blockscout/pull/12831)) ## 8.1.1 diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 3419261cab59..e25944d09fd1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -5,9 +5,8 @@ defmodule BlockScoutWeb.API.V2.OptimismView do alias BlockScoutWeb.API.V2.Helper alias Explorer.{Chain, Repo} - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Block, Transaction} - alias Explorer.Chain.Optimism.{FrameSequence, FrameSequenceBlob, InteropMessage, Withdrawal} + alias Explorer.Chain.Optimism.{DisputeGame, FrameSequence, FrameSequenceBlob, InteropMessage, Withdrawal} @doc """ Function to render GET requests to `/api/v2/optimism/txn-batches` endpoint. @@ -121,7 +120,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do 2 -> "Defender wins" end - [l2_block_number] = ExplorerHelper.decode_data(g.extra_data, [{:uint, 256}]) + l2_block_number = DisputeGame.l2_block_number_from_extra_data(g.extra_data) %{ "index" => g.index, diff --git a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex index 80461e605d87..b0d313add873 100644 --- a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex +++ b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex @@ -6,12 +6,16 @@ defmodule Explorer.Chain.Optimism.DisputeGame do import Ecto.Query import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] + alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Data, Hash} alias Explorer.{PagingOptions, Repo} @required_attrs ~w(index game_type address_hash created_at)a @optional_attrs ~w(extra_data resolved_at status)a + @chain_id_bob_mainnet 60_808 + @chain_id_bob_sepolia 808_813 + @typedoc """ * `index` - A unique index of the dispute game. * `game_type` - A number encoding a type of the dispute game. @@ -81,6 +85,37 @@ defmodule Explorer.Chain.Optimism.DisputeGame do |> select_repo(options).all(timeout: :infinity) end + @doc """ + Retrieves L2 block number from the `extraData` field of the dispute game. The L2 block number can be encoded in + different ways depending on the chain. + + ## Parameters + - `extra_data`: The byte sequence of the extra data to retrieve L2 block number from. + + ## Returns + - L2 block number of the dispute game. + """ + @spec l2_block_number_from_extra_data(Data.t() | nil) :: non_neg_integer() + def l2_block_number_from_extra_data(nil), do: 0 + + def l2_block_number_from_extra_data(%Data{bytes: extra_data}) do + current_chain_id = + case ChainId.get_id() do + nil -> Application.get_env(:block_scout_web, :chain_id) + chain_id -> chain_id + end + + first_bits = + if current_chain_id in [@chain_id_bob_mainnet, @chain_id_bob_sepolia] do + 64 + else + 256 + end + + <> = extra_data + l2_block_number + end + defp page_dispute_games(query, %PagingOptions{key: nil}), do: query defp page_dispute_games(query, %PagingOptions{key: {index}}) do diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex index 649482062c80..5850d1d8d88d 100644 --- a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex @@ -257,7 +257,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do defp appropriate_games_found(withdrawal_l2_block_number, respected_games) do respected_games |> Enum.any?(fn game -> - [l2_block_number] = Helper.decode_data(game.extra_data, [{:uint, 256}]) + l2_block_number = DisputeGame.l2_block_number_from_extra_data(game.extra_data) withdrawal_l2_block_number <= l2_block_number end) end From 473bedc5ccbcebd50fd29cf732f0503701adfb45 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 22 Jul 2025 14:26:09 +0300 Subject: [PATCH 0185/1006] Fix: Fix various errors on export of balances to Multichain DB (#12837) * Fix: Fix various errors on export of balances to Multichain DB * Update CHANGELOG entry --- CHANGELOG.md | 3 ++- .../balances_export_queue.ex | 26 +++++++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd7ac477d41b..57853f745f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,8 @@ ### 🐛 Bug Fixes -- Fix: Reject empty token_id and value in export of token balances to the Multichain DB ([#12829](https://github.com/blockscout/blockscout/pull/12829)) +- Fix various errors on export of balances to Multichain DB ([#12837](https://github.com/blockscout/blockscout/pull/12837)) +- Reject empty token_id and value in export of token balances to the Multichain DB ([#12829](https://github.com/blockscout/blockscout/pull/12829)) - Fix multichain export queues processing ([#12822](https://github.com/blockscout/blockscout/pull/12822)) - Remove token_id parameter from coin balance payload to Multichain service API endpoint ([#12817](https://github.com/blockscout/blockscout/pull/12817)) - Sanitize empty block_ranges payload before sending HTTP request to Multichain service([#12816](https://github.com/blockscout/blockscout/pull/12816)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index d71d79d8a196..dcbf762f02dc 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -15,9 +15,9 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do @primary_key false typed_schema "multichain_search_db_export_balances_queue" do - field(:id, :integer, primary_key: false, null: false) - field(:address_hash, Hash.Address, null: false, primary_key: true) - field(:token_contract_address_hash_or_native, :binary, null: false, primary_key: true) + field(:id, :integer, primary_key: true, null: false) + field(:address_hash, Hash.Address, null: false) + field(:token_contract_address_hash_or_native, :binary, null: false) field(:value, Wei) field(:token_id, :decimal) field(:retries_number, :integer) @@ -105,9 +105,7 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do balance_address_hash = balance.address_hash balance_token_contract_address_hash_or_native = balance.token_contract_address_hash_or_native - balance_token_id = balance.token_id - - query = + intermediate_query = from( b in __MODULE__, where: b.address_hash == ^balance_address_hash, @@ -119,10 +117,13 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do ^balance_token_contract_address_hash_or_native, ^balance_token_contract_address_hash_or_native, ^balance_token_contract_address_hash_or_native - ), - where: fragment("COALESCE(?, -1) = COALESCE(?, -1)", b.token_id, ^balance_token_id) + ) ) + query = + intermediate_query + |> where_token_id(balance.token_id) + if is_nil(acc) do query else @@ -143,4 +144,13 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do Repo.transact(delete_elements) end + + defp where_token_id(query, nil) do + where(query, [q], is_nil(q.token_id)) + end + + defp where_token_id(query, token_id) do + balance_token_id_string = to_string(token_id) + where(query, [q], fragment("?::text = ?", q.token_id, ^balance_token_id_string)) + end end From 0ad246eaf3832a0d530e85d0a6d155556c672c37 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 22 Jul 2025 17:56:37 +0300 Subject: [PATCH 0186/1006] chore: Change name of Swagger generation workflow (#12840) --- .github/workflows/generate-swagger.yml | 2 +- CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 5644363187b2..8959f611ccb4 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -1,4 +1,4 @@ -name: Blockscout +name: Generate Swagger on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 57853f745f79..cf08f469422a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ ### ⚙️ Miscellaneous Tasks +- Change name of Swagger generation workflow ([#12840](https://github.com/blockscout/blockscout/pull/12840)) - migrate Auth0 to mint as well ([#12807](https://github.com/blockscout/blockscout/pull/12807)) - Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) - Merge adjacent missing block ranges ([#12778](https://github.com/blockscout/blockscout/issues/12778)) From 1968ba172f1a2377a400c547a7b6fb964d410dcc Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 23 Jul 2025 16:29:16 +0300 Subject: [PATCH 0187/1006] Chore: Balances Multichain export: Refactor rows acquisition for deletion query (#12839) * Chore: Balances Multichain export: Refactor queue acquisition for deletion query and restructure DB index * Simplify query for balances export queue deletion accquisition * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../balances_export_queue.ex | 36 ++++++++----------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf08f469422a..52815f83664b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,6 +63,7 @@ ### ⚙️ Miscellaneous Tasks +- Balances Multichain export: Refactor rows acquisition for deletion query ([#12839](https://github.com/blockscout/blockscout/pull/12839)) - Change name of Swagger generation workflow ([#12840](https://github.com/blockscout/blockscout/pull/12840)) - migrate Auth0 to mint as well ([#12807](https://github.com/blockscout/blockscout/pull/12807)) - Migrate from HTTPoison to Tesla.Mint ([#12699](https://github.com/blockscout/blockscout/pull/12699)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index dcbf762f02dc..8d8df62897d9 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -103,27 +103,30 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do q = Enum.reduce(balances, nil, fn balance, acc -> balance_address_hash = balance.address_hash - balance_token_contract_address_hash_or_native = balance.token_contract_address_hash_or_native - intermediate_query = + balance_token_contract_address_hash_or_native_binary = + if byte_size(balance.token_contract_address_hash_or_native) == 6 do + balance.token_contract_address_hash_or_native + else + "0x" <> hex = balance.token_contract_address_hash_or_native + hex |> Base.decode16(case: :lower) |> elem(1) + end + + balance_token_id = balance.token_id + + query = from( b in __MODULE__, where: b.address_hash == ^balance_address_hash, - # \x6e6174697665 == hex('native') + where: b.token_contract_address_hash_or_native == ^balance_token_contract_address_hash_or_native_binary, where: fragment( - "?::text = (CASE WHEN LENGTH(?) = 42 THEN '\\' || SUBSTRING(?, 2, LENGTH(?) - 1) ELSE '\\x6e6174697665' END)", - b.token_contract_address_hash_or_native, - ^balance_token_contract_address_hash_or_native, - ^balance_token_contract_address_hash_or_native, - ^balance_token_contract_address_hash_or_native + "COALESCE(?, -1::numeric) = COALESCE(?::numeric, -1::numeric)", + b.token_id, + ^balance_token_id ) ) - query = - intermediate_query - |> where_token_id(balance.token_id) - if is_nil(acc) do query else @@ -144,13 +147,4 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do Repo.transact(delete_elements) end - - defp where_token_id(query, nil) do - where(query, [q], is_nil(q.token_id)) - end - - defp where_token_id(query, token_id) do - balance_token_id_string = to_string(token_id) - where(query, [q], fragment("?::text = ?", q.token_id, ^balance_token_id_string)) - end end From 7a170c93ae917c9e76135bcaa423968f665d8265 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 23 Jul 2025 18:49:27 +0300 Subject: [PATCH 0188/1006] fix: Fix increment of `retries_number` in exporting data to Multichain DB (#12847) * fix: Fix increment of `retries_number` in Multichain export * Add CHANGELOG entry * Process AI Agent comments --- CHANGELOG.md | 1 + .../balances_export_queue.ex | 15 +++--- .../multichain_search_db/main_export_queue.ex | 15 +++--- .../balances_export_queue_test.exs | 52 ++++++++++++++----- .../main_export_queue_test.exs | 42 +++++++++++---- 5 files changed, 88 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52815f83664b..9951150bc960 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Fix increment of retries_number in exporting data to Multichain DB ([#12847](https://github.com/blockscout/blockscout/pull/12847)) - Fix various errors on export of balances to Multichain DB ([#12837](https://github.com/blockscout/blockscout/pull/12837)) - Reject empty token_id and value in export of token balances to the Multichain DB ([#12829](https://github.com/blockscout/blockscout/pull/12829)) - Fix multichain export queues processing ([#12822](https://github.com/blockscout/blockscout/pull/12822)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index 8d8df62897d9..93e12f70d1e0 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -75,13 +75,14 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do end @doc """ - Returns an Ecto query that defines the default conflict resolution strategy for the - `multichain_search_db_export_balances_queue` table. On conflict, it increments the `retries_number` - (by using the value from `EXCLUDED.retries_number` or 0 if not present) and updates the - `updated_at` field to the greatest value between the current and the new timestamp. + Returns an Ecto query that defines the default behavior for handling conflicts + when inserting into the `multichain_search_db_export_balances_queue` table. - This is typically used in upsert operations to ensure retry counts are tracked and - timestamps are properly updated. + On conflict, this query: + - Increments the `retries_number` field by 1 (or sets it to 1 if it was `nil`). + - Sets the `updated_at` field to the greatest value between the current and the excluded `updated_at`. + + This is typically used with `on_conflict: default_on_conflict()` in Ecto insert operations. """ @spec default_on_conflict :: Ecto.Query.t() def default_on_conflict do @@ -89,7 +90,7 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do multichain_search_db_export_balances_queue in __MODULE__, update: [ set: [ - retries_number: fragment("COALESCE(EXCLUDED.retries_number, 0) + 1"), + retries_number: fragment("COALESCE(?, 0) + 1", multichain_search_db_export_balances_queue.retries_number), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", multichain_search_db_export_balances_queue.updated_at) ] diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex index 4de20844a978..60eea7cbbcab 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex @@ -85,13 +85,14 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do end @doc """ - Returns an Ecto query that defines the default conflict resolution strategy for the - `multichain_search_db_main_export_queue` table. On conflict, it increments the `retries_number` - (by using the value from `EXCLUDED.retries_number` or 0 if not present) and updates the - `updated_at` field to the greatest value between the current and the new timestamp. + Returns an Ecto query that defines the default behavior for handling conflicts + when inserting into the `multichain_search_db_main_export_queue` table. - This is typically used in upsert operations to ensure retry counts are tracked and - timestamps are properly updated. + On conflict, this query: + - Increments the `retries_number` field by 1 (or sets it to 1 if it was `nil`). + - Sets the `updated_at` field to the greatest value between the current and the excluded `updated_at`. + + This is typically used with `on_conflict` options in Ecto insert operations. """ @spec default_on_conflict :: Ecto.Query.t() def default_on_conflict do @@ -99,7 +100,7 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do multichain_search_db_main_export_queue in __MODULE__, update: [ set: [ - retries_number: fragment("COALESCE(EXCLUDED.retries_number, 0) + 1"), + retries_number: fragment("COALESCE(?, 0) + 1", multichain_search_db_main_export_queue.retries_number), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", multichain_search_db_main_export_queue.updated_at) ] ] diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs index 4764c1816247..6fc0d36f9df6 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs @@ -234,18 +234,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do TestHelper.get_chain_id_mock() - Tesla.Test.expect_tesla_call( - times: 2, - returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> - case Jason.decode(body) do - {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string}]}} -> - {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} - - _ -> - {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} - end - end - ) + tesla_expectations(address_4_hash_string) val1 = Decimal.new(200) |> Wei.cast() |> elem(1) val2 = Decimal.new(300) |> Wei.cast() |> elem(1) @@ -287,8 +276,32 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do end) assert Repo.aggregate(BalancesExportQueue, :count, :id) == 4 + results = Repo.all(BalancesExportQueue) + assert Enum.all?(results, &(&1.retries_number == nil)) assert log =~ "Batch balances export retry to the Multichain Search DB failed" + TestHelper.get_chain_id_mock() + + tesla_expectations(address_4_hash_string) + + MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) + + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 4 + results = Repo.all(BalancesExportQueue) + assert Enum.all?(results, &(&1.retries_number == 1)) + + # Check, that `retries_number` is incrementing + + TestHelper.get_chain_id_mock() + + tesla_expectations(address_4_hash_string) + + MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) + + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 4 + results = Repo.all(BalancesExportQueue) + assert Enum.all?(results, &(&1.retries_number == 2)) + export_data_2 = [ %{ address_hash: address_2.hash, @@ -332,4 +345,19 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, addresses_chunk_size: 7000) end end + + defp tesla_expectations(address_4_hash_string) do + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string}]}} -> + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + end end diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index 1c04cda5687f..41d4600c89de 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -209,6 +209,8 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do end) assert Repo.aggregate(MainExportQueue, :count, :hash) == 4 + results = Repo.all(MainExportQueue) + assert Enum.all?(results, &(&1.retries_number == nil)) assert log =~ "Batch main export retry to the Multichain Search DB failed" @@ -225,22 +227,40 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do TestHelper.get_chain_id_mock() - Tesla.Test.expect_tesla_call( - times: 2, - returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> - case Jason.decode(body) do - {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> - {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + tesla_expectations() - _ -> - {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} - end - end - ) + MultichainSearchDbMainExportQueue.run(export_data_2, nil) + + assert Repo.aggregate(MainExportQueue, :count, :hash) == 3 + results = Repo.all(MainExportQueue) + assert Enum.all?(results, &(&1.retries_number == 1)) + + # Check, that `retries_number` is incrementing + + TestHelper.get_chain_id_mock() + + tesla_expectations() MultichainSearchDbMainExportQueue.run(export_data_2, nil) assert Repo.aggregate(MainExportQueue, :count, :hash) == 3 + results = Repo.all(MainExportQueue) + assert Enum.all?(results, &(&1.retries_number == 2)) end end + + defp tesla_expectations() do + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + {:ok, %{"block_ranges" => [%{"max_block_number" => _, "min_block_number" => _}]}} -> + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + end end From 4d712db9846b18c7e3774c8d4eff877d6e5e3817 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 29 Jul 2025 12:27:24 +0300 Subject: [PATCH 0189/1006] fix: Allow using temporary token for api/account/v2 by default (#12869) * fix: Allow using temporary token for api/account/v2 by default * Add changelog entry * Add isolate_rate_limit?=true for api/account/v2/user/info --- CHANGELOG.md | 1 + apps/block_scout_web/priv/rate_limit_config.json | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9951150bc960..e39d9af31ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Allow using temporary token for api/account/v2 by default ([#12869](https://github.com/blockscout/blockscout/pull/12869)) - Fix increment of retries_number in exporting data to Multichain DB ([#12847](https://github.com/blockscout/blockscout/pull/12847)) - Fix various errors on export of balances to Multichain DB ([#12837](https://github.com/blockscout/blockscout/pull/12837)) - Reject empty token_id and value in export of token balances to the Multichain DB ([#12829](https://github.com/blockscout/blockscout/pull/12829)) diff --git a/apps/block_scout_web/priv/rate_limit_config.json b/apps/block_scout_web/priv/rate_limit_config.json index 8208d59f52e5..6f8187784bef 100644 --- a/apps/block_scout_web/priv/rate_limit_config.json +++ b/apps/block_scout_web/priv/rate_limit_config.json @@ -18,6 +18,13 @@ }, "isolate_rate_limit?": true }, + "api/account/v2/user/info": { + "static_api_key": true, + "whitelisted_ip": true, + "ip": true, + "temporary_token": true, + "isolate_rate_limit?": true + }, "api/v2/tokens/:param/instances/:param/refetch-metadata": { "recaptcha_to_bypass_429": true, "ip": { @@ -101,6 +108,12 @@ "ip": true, "temporary_token": true }, + "api/account/v2/*": { + "static_api_key": true, + "whitelisted_ip": true, + "ip": true, + "temporary_token": true + }, "default": { "static_api_key": true, "account_api_key": true, From 529dfd22c41fd9d12cb6200d3f7e9d8687c77764 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 30 Jul 2025 17:08:43 +0300 Subject: [PATCH 0190/1006] fix: balances export queue to multichain replace do_nothing with replace_all on insertion to the queue (#12888) * fix: balances export queue to multichain replace do_nothing with replace_all on insertion to the queue * Add CHANGELOG entry * Add regression test --- CHANGELOG.md | 1 + .../multichain_search.ex | 18 ++-- apps/explorer/test/explorer/chain_test.exs | 97 ++++++++++++++++++- config/runtime.exs | 39 ++++---- 4 files changed, 126 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e39d9af31ec4..ba1195b9cfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Balances export queue to multichain replace do_nothing with replace_all on insertion to the queue ([#12888](https://github.com/blockscout/blockscout/pull/12888)) - Allow using temporary token for api/account/v2 by default ([#12869](https://github.com/blockscout/blockscout/pull/12869)) - Fix increment of retries_number in exporting data to Multichain DB ([#12847](https://github.com/blockscout/blockscout/pull/12847)) - Fix various errors on export of balances to Multichain DB ([#12837](https://github.com/blockscout/blockscout/pull/12837)) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index a7df428aac47..66eeedc49f10 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -201,12 +201,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do - `:ok` if the data was successfully sent to the queues. - `:ignore` if the data is empty or the export functionality is disabled. """ - @spec send_data_to_queue(%{ - addresses: list(), - blocks: list(), - transactions: list(), - address_current_token_balances: list() - }) :: :ignore | :ok + @spec send_data_to_queue(map()) :: :ignore | :ok def send_data_to_queue(%{addresses: [], blocks: [], transactions: [], address_current_token_balances: []}), do: :ignore @@ -219,7 +214,12 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Repo.insert_all(MainExportQueue, Helper.add_timestamps(prepared_main_data), on_conflict: :nothing) - Repo.insert_all(BalancesExportQueue, Helper.add_timestamps(prepared_balances_data), on_conflict: :nothing) + Repo.insert_all(BalancesExportQueue, Helper.add_timestamps(prepared_balances_data), + on_conflict: :replace_all, + conflict_target: + {:unsafe_fragment, + ~s<(address_hash, token_contract_address_hash_or_native, COALESCE(token_id, -1::integer::numeric))>} + ) end) :ok @@ -432,11 +432,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do address_token_balances = cond do - Map.has_key?(params, :address_current_token_balances) == true -> + Map.has_key?(params, :address_current_token_balances) -> params.address_current_token_balances |> Enum.map(&format_address_token_balance/1) - Map.has_key?(params, :address_token_balances) == true -> + Map.has_key?(params, :address_token_balances) -> params.address_token_balances |> Enum.map(fn address_token_balance -> %{ diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 1e2463740285..24086a04bcd5 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -26,13 +26,16 @@ defmodule Explorer.ChainTest do } alias Explorer.{Chain, Etherscan} - alias Explorer.Chain.Address.Counters + alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.Cache.Counters.{BlocksCount, TransactionsCount, PendingBlockOperationCount} alias Explorer.Chain.InternalTransaction.Type + alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue} alias Explorer.Chain.Supply.ProofOfAuthority alias Explorer.Chain.Cache.Counters.AddressesCount + alias Explorer.TestHelper + @first_topic_hex_string "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" @second_topic_hex_string "0x000000000000000000000000e8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" @third_topic_hex_string "0x000000000000000000000000515c09c5bba1ed566b02a5b0599ec5d5d0aee73d" @@ -1425,6 +1428,98 @@ defmodule Explorer.ChainTest do } ] }} = Chain.import(@import_data) + + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 + end + + test "populates main multichain export queue, if the multichain service is enabled" do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + multichain_configuration = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, multichain_configuration) + end) + + bypass = Bypass.open() + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + addresses_chunk_size: 7_000 + ) + + TestHelper.get_chain_id_mock() + Chain.import(@import_data) + + # 3 addresses + 1 block + 1 transaction + assert Repo.aggregate(MainExportQueue, :count, :hash) == 5 + end + + test "populates balances multichain export queue and updates it, if the multichain service is enabled" do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + multichain_configuration = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, multichain_configuration) + end) + + bypass = Bypass.open() + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + addresses_chunk_size: 7_000 + ) + + import_data_1 = %{ + address_current_token_balances: %{ + params: [ + %{ + address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: "37", + value: 200, + value_fetched_at: DateTime.utc_now(), + token_type: "ERC-20", + token_id: nil + } + ] + } + } + + TestHelper.get_chain_id_mock() + Chain.import(import_data_1) + + # 1 token balance + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 1 + + [token_balance_export_item] = Repo.all(BalancesExportQueue) + assert token_balance_export_item.value == %Explorer.Chain.Wei{value: Decimal.new(200)} + + import_data_2 = %{ + address_current_token_balances: %{ + params: [ + %{ + address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b", + block_number: "40", + value: 500, + value_fetched_at: DateTime.utc_now(), + token_type: "ERC-20", + token_id: nil + } + ] + } + } + + Chain.import(import_data_2) + + # 1 token balance + assert Repo.aggregate(BalancesExportQueue, :count, :id) == 1 + + [token_balance_export_item] = Repo.all(BalancesExportQueue) + # token balance value has been updated + assert token_balance_export_item.value == %Explorer.Chain.Wei{value: Decimal.new(500)} end end diff --git a/config/runtime.exs b/config/runtime.exs index 22bb93087932..7f1218120c39 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -291,7 +291,7 @@ config :explorer, csv_export_limit: ConfigHelper.parse_integer_env_var("CSV_EXPORT_LIMIT", 10_000), shrink_internal_transactions_enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), replica_max_lag: ConfigHelper.parse_time_env_var("REPLICA_MAX_LAG", "5m"), - hackney_default_pool_size: ConfigHelper.parse_integer_env_var("HACKNEY_DEFAULT_POOL_SIZE", 1000) + hackney_default_pool_size: ConfigHelper.parse_integer_env_var("HACKNEY_DEFAULT_POOL_SIZE", 1_000) config :explorer, Explorer.Chain.Health.Monitor, check_interval: ConfigHelper.parse_time_env_var("HEALTH_MONITOR_CHECK_INTERVAL", "1m"), @@ -350,7 +350,7 @@ config :explorer, Explorer.Chain.Cache.Counters.PendingBlockOperationCount, config :explorer, Explorer.Chain.Cache.GasPriceOracle, global_ttl: ConfigHelper.parse_time_env_var("GAS_PRICE_ORACLE_CACHE_PERIOD", "30s"), - simple_transaction_gas: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS", 21000), + simple_transaction_gas: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS", 21_000), num_of_blocks: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_NUM_OF_BLOCKS", 200), safelow_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SAFELOW_PERCENTILE", 35), average_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_AVERAGE_PERCENTILE", 60), @@ -653,7 +653,7 @@ config :explorer, Explorer.SmartContract.StylusVerifierInterface, config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL"), - addresses_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7000) + addresses_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7_000) config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), @@ -686,10 +686,10 @@ config :explorer, Explorer.Account, verification_email_resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL", "5m"), otp_resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_OTP_RESEND_INTERVAL", "1m"), - private_tags_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_PRIVATE_TAGS_LIMIT", 2000), + private_tags_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_PRIVATE_TAGS_LIMIT", 2_000), watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15), notifications_limit_for_30_days: - ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS", 1000), + ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS", 1_000), siwe_message: System.get_env("ACCOUNT_SIWE_MESSAGE", "Sign in to Blockscout Account V2") config :explorer, Explorer.Chain.Cache.MinMissingBlockNumber, @@ -926,7 +926,7 @@ config :indexer, token_balances_fetcher_init_limit: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_FETCHER_INIT_QUERY_LIMIT", 100_000), coin_balances_fetcher_init_limit: - ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_FETCHER_INIT_QUERY_LIMIT", 2000), + ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_FETCHER_INIT_QUERY_LIMIT", 2_000), graceful_shutdown_period: ConfigHelper.parse_time_env_var("INDEXER_GRACEFUL_SHUTDOWN_PERIOD", "5m"), internal_transactions_fetch_order: ConfigHelper.parse_catalog_value("INDEXER_INTERNAL_TRANSACTIONS_FETCH_ORDER", ["asc", "desc"], true, "asc") @@ -1047,7 +1047,7 @@ config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s") config :indexer, Indexer.Block.Realtime.Fetcher, - max_gap: ConfigHelper.parse_integer_env_var("INDEXER_REALTIME_FETCHER_MAX_GAP", 1000), + max_gap: ConfigHelper.parse_integer_env_var("INDEXER_REALTIME_FETCHER_MAX_GAP", 1_000), polling_period: ConfigHelper.parse_time_env_var("INDEXER_REALTIME_FETCHER_POLLING_PERIOD") config :indexer, Indexer.Block.Catchup.MissingRangesCollector, @@ -1102,7 +1102,7 @@ config :indexer, Indexer.Fetcher.InternalTransaction, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE", 10), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY", 4), indexing_finished_threshold: - ConfigHelper.parse_integer_env_var("API_INTERNAL_TRANSACTIONS_INDEXING_FINISHED_THRESHOLD", 1000) + ConfigHelper.parse_integer_env_var("API_INTERNAL_TRANSACTIONS_INDEXING_FINISHED_THRESHOLD", 1_000) coin_balances_batch_size = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 100) coin_balances_concurrency = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) @@ -1125,20 +1125,21 @@ config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY", 10), - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE", 1000), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE", 1_000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s" ), max_queue_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1000), + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1_000), init_limit: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_INIT_QUERY_LIMIT", 1_000) config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY", 10), - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE", 1000), + batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE", 1_000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", @@ -1147,7 +1148,7 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, max_queue_size: ConfigHelper.parse_integer_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE", - 1000 + 1_000 ), init_limit: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT", 1_000) @@ -1241,7 +1242,7 @@ config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, config :indexer, Indexer.Fetcher.PolygonEdge, polygon_edge_l1_rpc: System.get_env("INDEXER_POLYGON_EDGE_L1_RPC"), polygon_edge_eth_get_logs_range_size: - ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE", 1000) + ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE", 1_000) config :indexer, Indexer.Fetcher.PolygonEdge.Deposit, start_block_l1: System.get_env("INDEXER_POLYGON_EDGE_L1_DEPOSITS_START_BLOCK"), @@ -1281,11 +1282,11 @@ config :indexer, Indexer.Fetcher.Arbitrum.Messaging, config :indexer, Indexer.Fetcher.Arbitrum, l1_rpc: System.get_env("INDEXER_ARBITRUM_L1_RPC"), l1_rpc_chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_RPC_CHUNK_SIZE", 20), - l1_rpc_block_range: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_RPC_HISTORICAL_BLOCKS_RANGE", 1000), + l1_rpc_block_range: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_RPC_HISTORICAL_BLOCKS_RANGE", 1_000), l1_rollup_address: System.get_env("INDEXER_ARBITRUM_L1_ROLLUP_CONTRACT"), l1_rollup_init_block: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_ROLLUP_INIT_BLOCK", 1), l1_start_block: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_COMMON_START_BLOCK", 0), - l1_finalization_threshold: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_FINALIZATION_THRESHOLD", 1000), + l1_finalization_threshold: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_L1_FINALIZATION_THRESHOLD", 1_000), rollup_chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_ROLLUP_CHUNK_SIZE", 20) config :indexer, Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1, @@ -1304,7 +1305,7 @@ config :indexer, Indexer.Fetcher.Arbitrum.TrackingBatchesStatuses, new_batches_limit: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_NEW_BATCHES_LIMIT", 10), node_interface_contract: ConfigHelper.safe_get_env("INDEXER_ARBITRUM_NODE_INTERFACE_CONTRACT", "0x00000000000000000000000000000000000000C8"), - missing_batches_range: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_MISSING_BATCHES_RANGE", 10000), + missing_batches_range: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_MISSING_BATCHES_RANGE", 10_000), failure_interval_threshold: ConfigHelper.parse_time_env_var("INDEXER_ARBITRUM_BATCHES_TRACKING_FAILURE_THRESHOLD", "10m") @@ -1314,7 +1315,7 @@ config :indexer, Indexer.Fetcher.Arbitrum.TrackingBatchesStatuses.Supervisor, config :indexer, Indexer.Fetcher.Arbitrum.RollupMessagesCatchup, recheck_interval: ConfigHelper.parse_time_env_var("INDEXER_ARBITRUM_MISSED_MESSAGES_RECHECK_INTERVAL", "1h"), missed_messages_blocks_depth: - ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_MISSED_MESSAGES_BLOCKS_DEPTH", 10000) + ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_MISSED_MESSAGES_BLOCKS_DEPTH", 10_000) config :indexer, Indexer.Fetcher.Arbitrum.RollupMessagesCatchup.Supervisor, enabled: ConfigHelper.parse_bool_env_var("INDEXER_ARBITRUM_BRIDGE_MESSAGES_TRACKING_ENABLED") @@ -1442,7 +1443,7 @@ config :indexer, Indexer.Fetcher.Filecoin.AddressInfo, config :indexer, Indexer.Fetcher.Scroll, l1_eth_get_logs_range_size: ConfigHelper.parse_integer_env_var("INDEXER_SCROLL_L1_ETH_GET_LOGS_RANGE_SIZE", 250), - l2_eth_get_logs_range_size: ConfigHelper.parse_integer_env_var("INDEXER_SCROLL_L2_ETH_GET_LOGS_RANGE_SIZE", 1000), + l2_eth_get_logs_range_size: ConfigHelper.parse_integer_env_var("INDEXER_SCROLL_L2_ETH_GET_LOGS_RANGE_SIZE", 1_000), rpc: System.get_env("INDEXER_SCROLL_L1_RPC") config :indexer, Indexer.Fetcher.Scroll.L1FeeParam, gas_oracle: System.get_env("INDEXER_SCROLL_L2_GAS_ORACLE_CONTRACT") @@ -1505,7 +1506,7 @@ config :nft_media_handler, config :nft_media_handler, Indexer.NFTMediaHandler.Backfiller, enabled?: ConfigHelper.parse_bool_env_var("NFT_MEDIA_HANDLER_BACKFILL_ENABLED"), - queue_size: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_BACKFILL_QUEUE_SIZE", 1000), + queue_size: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_BACKFILL_QUEUE_SIZE", 1_000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var("NFT_MEDIA_HANDLER_BACKFILL_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s") From e063ef354d244292c00ccaa15651345e7cf3d27f Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 31 Jul 2025 13:13:38 +0300 Subject: [PATCH 0191/1006] fix: Fix naming for apikey param in OpenAPI spec (#12891) * fix: Fix naming for apikey param in OpenAPI spec * Changelog --- CHANGELOG.md | 1 + .../lib/block_scout_web/schemas/api/v2/general.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba1195b9cfab..f7bbeefa625b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) - Balances export queue to multichain replace do_nothing with replace_all on insertion to the queue ([#12888](https://github.com/blockscout/blockscout/pull/12888)) - Allow using temporary token for api/account/v2 by default ([#12869](https://github.com/blockscout/blockscout/pull/12869)) - Fix increment of retries_number in exporting data to Multichain DB ([#12847](https://github.com/blockscout/blockscout/pull/12847)) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 46564d91ca2f..343b762113f6 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -432,7 +432,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do schema: %Schema{type: :string}, required: false, description: "API key for rate limiting", - name: :api_key + name: :apikey } end From be7f0462d25916b0f60c25ea8259e4b2fa9b9930 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 31 Jul 2025 14:23:30 +0300 Subject: [PATCH 0192/1006] fix: Balances export queue: replace `replace_all` with replace only `value` and `updated_at` (#12892) * fix: Balances export queue: replace `replace_all` with replace only `value` and `updated_at` * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../lib/explorer/microservice_interfaces/multichain_search.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7bbeefa625b..19efd90ca4c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ ### 🐛 Bug Fixes +- Balances export queue: replace replace_all with replace only value and updated_at ([#12892](https://github.com/blockscout/blockscout/pull/12892)) - Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) - Balances export queue to multichain replace do_nothing with replace_all on insertion to the queue ([#12888](https://github.com/blockscout/blockscout/pull/12888)) - Allow using temporary token for api/account/v2 by default ([#12869](https://github.com/blockscout/blockscout/pull/12869)) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 66eeedc49f10..54eb7bb08eb6 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -215,7 +215,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do Repo.insert_all(MainExportQueue, Helper.add_timestamps(prepared_main_data), on_conflict: :nothing) Repo.insert_all(BalancesExportQueue, Helper.add_timestamps(prepared_balances_data), - on_conflict: :replace_all, + on_conflict: {:replace, [:value, :updated_at]}, conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash_or_native, COALESCE(token_id, -1::integer::numeric))>} From c3b003c738628c401c6ae212e413af5e3d379729 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 31 Jul 2025 15:59:48 +0400 Subject: [PATCH 0193/1006] feat: Export token info to Multichain service (#12867) * feat: Export balances to Multichain DB * Fix tests * Process first batch of review comments * feat: Export balances to Multichain DB * initial commit * Fix tests * Adding token metadata to export queue * Adding token total supply to multichain export queue * Fix rebasing * Adding token market data to export queue * Keep token transfers count in db * Adding token counters to export queue * Add TokenCountersUpdater to Supervisor * Add Explorer.MicroserviceInterfaces.MultichainSearch.batch_import_token_info function * Add Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue * Add specs and docs * Fix specs * fixes * Add queue size logging and change decimals field format * Fixes and improvements * Add part of unit tests * Fixes and refactorings * Update Changelog * Extend unit tests --------- Co-authored-by: Victor Baranov Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- CHANGELOG.md | 1 + .../cache/counters/token_holders_count.ex | 14 +- .../cache/counters/token_transfers_count.ex | 36 +- .../explorer/chain/import/runner/tokens.ex | 8 +- .../token_info_export_queue.ex | 136 +++++++ apps/explorer/lib/explorer/chain/token.ex | 27 ++ .../lib/explorer/market/fetcher/token.ex | 34 +- .../multichain_search.ex | 368 +++++++++++++++++- ...earch_db_token_info_export_queue_table.exs | 23 ++ .../multichain_search_test.exs | 244 +++++++++++- apps/explorer/test/support/factory.ex | 31 ++ .../token_info_export_queue.ex | 136 +++++++ .../fetcher/optimism/transaction_batch.ex | 5 +- apps/indexer/lib/indexer/fetcher/token.ex | 10 + .../indexer/fetcher/token_counters_updater.ex | 75 ++++ .../fetcher/token_total_supply_updater.ex | 30 +- .../lib/indexer/fetcher/token_updater.ex | 17 +- apps/indexer/lib/indexer/supervisor.ex | 5 + config/runtime.exs | 34 +- docker-compose/envs/common-blockscout.env | 12 +- 20 files changed, 1205 insertions(+), 41 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250714093329_add_multichain_search_db_token_info_export_queue_table.exs create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex create mode 100644 apps/indexer/lib/indexer/fetcher/token_counters_updater.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 19efd90ca4c9..b6444bdeb1c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Export token info to Multichain service ([#12867](https://github.com/blockscout/blockscout/pull/12867)) - Export balances to Multichain DB([#12726](https://github.com/blockscout/blockscout/pull/12726)) - Add eip7702 authorization status fetcher ([#12451](https://github.com/blockscout/blockscout/issues/12451)) - Add token1155tx token404tx api v1 endpoints ([#12720](https://github.com/blockscout/blockscout/issues/12720)) diff --git a/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex index 6d861d3dbc28..188d0fe152f5 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex @@ -42,9 +42,7 @@ defmodule Explorer.Chain.Cache.Counters.TokenHoldersCount do def fetch(address_hash) do if cache_expired?(address_hash) do - Task.start_link(fn -> - update_cache(address_hash) - end) + update_cache(address_hash) end fetch_count_from_cache(address_hash) @@ -71,26 +69,26 @@ defmodule Explorer.Chain.Cache.Counters.TokenHoldersCount do put_into_db_cache(address_hash, new_data) end - def fetch_count_from_cache(address_hash) do + defp fetch_count_from_cache(address_hash) do address_hash_string = to_string(address_hash) key = "hash_#{address_hash_string}" Helper.fetch_from_ets_cache(@cache_name, key) || fetch_from_db_cache(address_hash) end - def fetch_updated_at_from_cache(address_hash, cache_name) do + defp fetch_updated_at_from_cache(address_hash, cache_name) do address_hash_string = to_string(address_hash) key = "hash_#{address_hash_string}_#{@ets_last_update_key}" Helper.fetch_from_ets_cache(cache_name, key) end - def fetch_from_db_cache(address_hash) do + defp fetch_from_db_cache(address_hash) do token = Token.get_by_contract_address_hash(address_hash, @api_true) - token.holder_count || 0 + (token && token.holder_count) || 0 end - def put_into_db_cache(address_hash, count) do + defp put_into_db_cache(address_hash, count) do Token.update_token_holder_count(address_hash, count) end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex index 97db13c3c729..01de27ceba46 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex @@ -7,9 +7,11 @@ defmodule Explorer.Chain.Cache.Counters.TokenTransfersCount do alias Explorer.Chain alias Explorer.Chain.Cache.Counters.Helper + alias Explorer.Chain.Token + @api_true [api?: true] @cache_name :token_transfers_counter - @last_update_key "last_update" + @ets_last_update_key "last_update" @spec start_link(term()) :: GenServer.on_start() def start_link(_) do @@ -43,16 +45,14 @@ defmodule Explorer.Chain.Cache.Counters.TokenTransfersCount do update_cache(address_hash) end - address_hash_string = to_string(address_hash) - fetch_from_cache("hash_#{address_hash_string}") + fetch_count_from_cache(address_hash) end def cache_name, do: @cache_name defp cache_expired?(address_hash) do cache_period = Application.get_env(:explorer, __MODULE__)[:cache_period] - address_hash_string = to_string(address_hash) - updated_at = fetch_from_cache("hash_#{address_hash_string}_#{@last_update_key}") + updated_at = fetch_updated_at_from_cache(address_hash, @cache_name) cond do is_nil(updated_at) -> true @@ -63,13 +63,33 @@ defmodule Explorer.Chain.Cache.Counters.TokenTransfersCount do defp update_cache(address_hash) do address_hash_string = to_string(address_hash) - Helper.put_into_ets_cache(@cache_name, "hash_#{address_hash_string}_#{@last_update_key}", Helper.current_time()) new_data = Chain.count_token_transfers_from_token_hash(address_hash) Helper.put_into_ets_cache(@cache_name, "hash_#{address_hash_string}", new_data) + Helper.put_into_ets_cache(@cache_name, "hash_#{address_hash_string}_#{@ets_last_update_key}", Helper.current_time()) + put_into_db_cache(address_hash, new_data) + end + + defp fetch_count_from_cache(address_hash) do + address_hash_string = to_string(address_hash) + key = "hash_#{address_hash_string}" + + Helper.fetch_from_ets_cache(@cache_name, key) || fetch_from_db_cache(address_hash) + end + + defp fetch_updated_at_from_cache(address_hash, cache_name) do + address_hash_string = to_string(address_hash) + key = "hash_#{address_hash_string}_#{@ets_last_update_key}" + + Helper.fetch_from_ets_cache(cache_name, key) + end + + defp fetch_from_db_cache(address_hash) do + token = Token.get_by_contract_address_hash(address_hash, @api_true) + (token && token.transfer_count) || 0 end - defp fetch_from_cache(key) do - Helper.fetch_from_ets_cache(@cache_name, key) + defp put_into_db_cache(address_hash, count) do + Token.update_token_transfer_count(address_hash, count) end defp enable_consolidation?, do: @enable_consolidation diff --git a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex index d69b2f4c66db..79676ff72d00 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex @@ -143,11 +143,13 @@ defmodule Explorer.Chain.Import.Runner.Tokens do ordered_changes_list = changes_list - # brand new tokens start with no holders + # brand new tokens start with no holders and transfers # set cataloged: nil, if not set before, to get proper COALESCE result # if don't set it, cataloged will default to false (as in DB schema) # and COALESCE in on_conflict will return false - |> Stream.map(fn token -> token |> Map.put_new(:holder_count, 0) |> Map.put_new(:cataloged, nil) end) + |> Stream.map(fn token -> + token |> Map.put_new(:holder_count, 0) |> Map.put_new(:transfer_count, 0) |> Map.put_new(:cataloged, nil) + end) # Enforce Token ShareLocks order (see docs: sharelocks.md) |> Enum.sort_by(& &1.contract_address_hash) @@ -196,7 +198,7 @@ defmodule Explorer.Chain.Import.Runner.Tokens do cataloged: fragment("COALESCE(EXCLUDED.cataloged, ?)", token.cataloged), bridged: fragment("COALESCE(EXCLUDED.bridged, ?)", token.bridged), skip_metadata: fragment("COALESCE(EXCLUDED.skip_metadata, ?)", token.skip_metadata), - # `holder_count` is not updated as a pre-existing token means the `holder_count` is already initialized OR + # `holder_count` and `transfer_count` are not updated as a pre-existing token means these counts are already initialized OR # need to be migrated with `priv/repo/migrations/scripts/update_new_tokens_holder_count_in_batches.sql.exs` # Don't update `contract_address_hash` as it is the primary key and used for the conflict target inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex new file mode 100644 index 000000000000..0020607a4c8b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex @@ -0,0 +1,136 @@ +defmodule Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue do + @moduledoc """ + Tracks token data, pending for export to the Multichain Service database. + """ + + use Explorer.Schema + + import Ecto.Query + + alias Ecto.Multi + alias Explorer.Repo + + @required_attrs ~w(address_hash data_type data)a + @optional_attrs ~w(retries_number)a + @allowed_attrs @optional_attrs ++ @required_attrs + + @primary_key false + typed_schema "multichain_search_db_export_token_info_queue" do + field(:address_hash, :binary, null: false, primary_key: true) + + field(:data_type, Ecto.Enum, + values: [ + :metadata, + :total_supply, + :counters, + :market_data + ], + null: false, + primary_key: true + ) + + field(:data, :map) + field(:retries_number, :integer) + + timestamps() + end + + def changeset(%__MODULE__{} = queue, attrs) do + queue + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + end + + @doc """ + Streams a batch of multichain database token info entries that need to be retried for export. + + This function selects specific fields from the export records and applies a reducer function to each entry in the stream, accumulating the result. Optionally, the stream can be limited based on the `limited?` flag. + + ## Parameters + - `initial`: The initial accumulator value. + - `reducer`: A function that takes an entry (as a map) and the current accumulator, returning the updated accumulator. + - `limited?` (optional): A boolean indicating whether to apply a fetch limit to the stream. Defaults to `false`. + + ## Returns + - `{:ok, accumulator}`: A tuple containing `:ok` and the final accumulator after processing the stream. + """ + @spec stream_multichain_db_token_info_batch( + initial :: accumulator, + reducer :: (entry :: map(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_multichain_db_token_info_batch(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + __MODULE__ + |> select([export], %{ + address_hash: export.address_hash, + data_type: export.data_type, + data: export.data + }) + |> add_queue_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + defp add_queue_fetcher_limit(query, false), do: query + + defp add_queue_fetcher_limit(query, true) do + limit = Application.get_env(:indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue)[:init_limit] + limit(query, ^limit) + end + + @doc """ + Constructs DELETE FROM queries for the token info items to be deleted from the queue. + + ## Parameters + - `queue_items`: A list of items to be deleted from the queue. Each item is identified by its primary key. + + ## Returns + - An `Ecto.Multi` struct containing the delete operations. + """ + @spec delete_query([%{:address_hash => binary(), :data_type => atom(), optional(:data) => map()}]) :: Multi.t() + def delete_query(queue_items) do + queue_items + |> Enum.reduce(Multi.new(), fn queue_item, multi_acc -> + Multi.delete_all( + multi_acc, + {queue_item.address_hash, queue_item.data_type}, + from(q in __MODULE__, + where: q.address_hash == ^queue_item.address_hash and q.data_type == ^queue_item.data_type + ) + ) + end) + end + + @doc """ + Returns the current number of items in the queue. + + ## Returns + - The current number of items in the queue. + """ + @spec queue_size() :: non_neg_integer() + def queue_size do + Repo.aggregate(__MODULE__, :count) + end + + @doc """ + Returns an Ecto query that defines the conflict resolution strategy for the + `multichain_search_db_export_token_info_queue` table. On conflict, it increments the `retries_number` + (by using the db stored value or 0 if not present) and updates the + `updated_at` field to the greatest value between the current and the new timestamp. + + This is typically used in upsert operations to ensure retry counts are tracked and + timestamps are properly updated. + """ + @spec increase_retries_on_conflict :: Ecto.Query.t() + def increase_retries_on_conflict do + from( + q in __MODULE__, + update: [ + set: [ + retries_number: fragment("COALESCE(?, 0) + 1", q.retries_number), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", q.updated_at) + ] + ] + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 58f8a01dbe3b..df479eba880c 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -33,6 +33,7 @@ defmodule Explorer.Chain.Token.Schema do field(:icon_url, :string) field(:is_verified_via_admin_panel, :boolean) field(:volume_24h, :decimal) + field(:transfer_count, :integer) belongs_to( :contract_address, @@ -129,6 +130,7 @@ defmodule Explorer.Chain.Token do * `contract_address_hash` - Address hash foreign key * `holder_count` - the number of `t:Explorer.Chain.Address.t/0` (except the burn address) that have a `t:Explorer.Chain.CurrentTokenBalance.t/0` `value > 0`. Can be `nil` when data not migrated. + * `transfer_count` - the number of token transfers for `t:Explorer.Chain.Address.t/0` token * `fiat_value` - The price of a token in a configured currency (USD by default). * `circulating_market_cap` - The circulating market cap of a token in a configured currency (USD by default). * `icon_url` - URL of the token's icon. @@ -420,6 +422,31 @@ defmodule Explorer.Chain.Token do ) end + @doc """ + Updates `transfer_count` field for a given `contract_address_hash`. + Used by the `Explorer.Chain.Cache.Counters.TokenTransfersCount` module. + + ## Parameters + - `contract_address_hash`: The address of the token contract. + - `transfer_count`: The updated counter value. + + ## Returns + - `{updated_count, nil}` tuple where `updated_count` is the number of updated rows in the db table. + """ + @spec update_token_transfer_count(Hash.Address.t(), non_neg_integer()) :: {non_neg_integer(), nil} + def update_token_transfer_count(contract_address_hash, transfer_count) when not is_nil(transfer_count) do + now = DateTime.utc_now() + + Repo.update_all( + from(t in __MODULE__, + where: t.contract_address_hash == ^contract_address_hash, + update: [set: [transfer_count: ^transfer_count, updated_at: ^now]] + ), + [], + timeout: @timeout + ) + end + @doc """ Drops token info for the given token: Sets is_verified_via_admin_panel to false, icon_url to nil, symbol to nil, name to nil. diff --git a/apps/explorer/lib/explorer/market/fetcher/token.ex b/apps/explorer/lib/explorer/market/fetcher/token.ex index d67bda0cfafe..4a48875b0a84 100644 --- a/apps/explorer/lib/explorer/market/fetcher/token.ex +++ b/apps/explorer/lib/explorer/market/fetcher/token.ex @@ -7,8 +7,10 @@ defmodule Explorer.Market.Fetcher.Token do require Logger alias Explorer.Chain + alias Explorer.Chain.Hash.Address alias Explorer.Chain.Import.Runner.Tokens alias Explorer.Market.Source + alias Explorer.MicroserviceInterfaces.MultichainSearch defstruct [ :source, @@ -60,7 +62,7 @@ defmodule Explorer.Market.Fetcher.Token do {:ok, source_state, fetch_finished?, tokens_data} -> case update_tokens(tokens_data) do {:ok, _imported} -> - :ok + enqueue_to_multichain(tokens_data) {:error, err} -> Logger.error("Error while importing tokens market data: #{inspect(err)}") @@ -104,6 +106,36 @@ defmodule Explorer.Market.Fetcher.Token do {:noreply, state} end + # Adds market data of the token (such as price and market cap) to the queue to send that to Multichain service. + # + # ## Parameters + # - `tokens_data`: A list of token data. + # + # ## Returns + # - `:ok` if the data is accepted for insertion. + # - `:ignore` if the Multichain service is not used. + @spec enqueue_to_multichain([ + %{ + :contract_address_hash => Address.t(), + optional(:fiat_value) => Decimal.t(), + optional(:circulating_market_cap) => Decimal.t(), + optional(any()) => any() + } + ]) :: :ok | :ignore + defp enqueue_to_multichain(tokens_data) do + tokens_data + |> Enum.reduce(%{}, fn token, acc -> + data_for_multichain = MultichainSearch.prepare_token_market_data_for_queue(token) + + if data_for_multichain == %{} do + acc + else + Map.put(acc, token.contract_address_hash.bytes, data_for_multichain) + end + end) + |> MultichainSearch.send_token_info_to_queue(:market_data) + end + defp update_tokens(token_params) do Chain.import(%{ tokens: %{ diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 54eb7bb08eb6..beff8374db6b 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -5,9 +5,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do alias Ecto.Association.NotLoaded alias Explorer.Chain alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.{Block, Hash, Transaction, Wei} + alias Explorer.Chain.{Block, Hash, Token, Transaction, Wei} alias Explorer.Chain.Block.Range - alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue} + alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue, TokenInfoExportQueue} alias Explorer.{Helper, HttpClient, Repo} alias Explorer.Utility.Microservice @@ -112,6 +112,92 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end end + @doc """ + Processes a batch export of token info by splitting the items from db queue into chunks and sending each chunk as an HTTP POST request to a configured microservice endpoint. + + If the microservice is enabled, the function: + - Splits the input `items_from_db_queue` into manageable chunks. + - Sends each chunk concurrently using `Task.async_stream/5` with a maximum concurrency and timeout. + - Collects the results, merging any errors and accumulating data that needs to be retried. + - Returns `{:ok, {:chunks_processed, chunks}}` if all chunks are processed successfully. + - Returns `{:error, data_to_retry}` if any chunk fails, where `data_to_retry` contains tokens that need to be retried. + + If the microservice is disabled, returns `{:ok, :service_disabled}`. + + ## Parameters + - `items_from_db_queue`: The queue items to be exported, which will be split into chunks for processing. + + ## Returns + - `{:ok, any()}`: If all chunks are processed successfully or the service is disabled. + - `{:error, map()}`: If one or more chunks fail, with details about the data that needs to be retried. + """ + @spec batch_export_token_info([ + %{ + :address_hash => binary(), + :data_type => :metadata | :total_supply | :counters | :market_data, + :data => map() + } + ]) :: {:ok, any()} | {:error, map()} + def batch_export_token_info(items_from_db_queue) do + if enabled?() do + url = batch_import_url() + api_key = api_key() + chain_id = to_string(ChainId.get_id()) + + chunks = + items_from_db_queue + |> Enum.chunk_every(token_info_chunk_size()) + |> Enum.map(fn chunk_items -> + %{ + api_key: api_key, + chain_id: chain_id, + tokens: Enum.map(chunk_items, &token_info_queue_item_to_http_item(&1)) + } + end) + + chunks + |> Task.async_stream( + fn export_body -> http_post_request(url, export_body) end, + max_concurrency: @max_concurrency, + timeout: @post_timeout, + zip_input_on_exit: true + ) + |> Enum.reduce({:ok, {:chunks_processed, chunks}}, fn + {:ok, {:ok, _result}}, acc -> + acc + + {:ok, {:error, error}}, acc -> + token_info_on_error(error) + + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, %{tokens: error.data_to_retry.tokens}} + + {:error, data_to_retry} -> + {:error, %{tokens: data_to_retry.tokens ++ error.data_to_retry.tokens}} + end + + {:exit, {export_body, reason}}, acc -> + token_info_on_error(%{ + url: url, + data_to_retry: export_body, + reason: reason + }) + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, %{tokens: export_body.tokens}} + + {:error, data_to_retry} -> + {:error, %{tokens: data_to_retry.tokens ++ export_body.tokens}} + end + end) + else + {:ok, :service_disabled} + end + end + defp log_error(%{ url: url, data_to_retry: data_to_retry, @@ -180,6 +266,110 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp on_error(_), do: :ignore + # Logs error when trying to send token info from the queue to Multichain service + # and increments `retries_number` counter of the corresponding queue items. + # + # ## Parameters + # - `error`: A map with the queue items. + # + # ## Returns + # - Nothing. + @spec token_info_on_error(%{ + :data_to_retry => %{:tokens => [map()], optional(any()) => any()}, + optional(any()) => any() + }) :: any() + defp token_info_on_error(%{data_to_retry: data_to_retry} = error) do + log_error(error) + + prepared_token_info_data = + data_to_retry.tokens + |> Enum.map(&token_info_http_item_to_queue_item(&1)) + |> Helper.add_timestamps() + + Repo.insert_all( + TokenInfoExportQueue, + prepared_token_info_data, + on_conflict: TokenInfoExportQueue.increase_retries_on_conflict(), + conflict_target: [:address_hash, :data_type] + ) + end + + defp token_info_on_error(_), do: :ignore + + @doc """ + Converts database queue item with token info to the item ready to send to Multichain service via HTTP. + + ## Parameters + - `item_from_db_queue`: The queue item map from database. + + ## Returns + - A map ready to send to Multichain service via HTTP. + """ + @spec token_info_queue_item_to_http_item(%{ + :address_hash => binary(), + :data_type => :metadata | :total_supply | :counters | :market_data, + :data => map() + }) :: + %{:address_hash => String.t(), :metadata => map()} + | %{:address_hash => String.t(), :counters => map()} + | %{:address_hash => String.t(), :price_data => map()} + def token_info_queue_item_to_http_item(item_from_db_queue) do + token = %{address_hash: "0x" <> Base.encode16(item_from_db_queue.address_hash, case: :lower)} + + case item_from_db_queue.data_type do + :metadata -> Map.put(token, :metadata, item_from_db_queue.data) + :total_supply -> Map.put(token, :metadata, item_from_db_queue.data) + :counters -> Map.put(token, :counters, item_from_db_queue.data) + :market_data -> Map.put(token, :price_data, item_from_db_queue.data) + end + end + + @doc """ + Converts queue item (containing token info) ready to send to Multichain service via HTTP + to the queue item ready to be written to the database. + + ## Parameters + - `http_item`: The queue item for HTTP. + + ## Returns + - A map ready to write to the database. + """ + @spec token_info_http_item_to_queue_item( + %{:address_hash => String.t(), :metadata => map()} + | %{:address_hash => String.t(), :counters => map()} + | %{:address_hash => String.t(), :price_data => map()} + ) :: %{ + :address_hash => binary(), + :data_type => :metadata | :total_supply | :counters | :market_data, + :data => map() + } + def token_info_http_item_to_queue_item(%{address_hash: "0x" <> address_string} = http_item) do + {:ok, address_hash} = Base.decode16(address_string, case: :mixed) + + metadata = Map.get(http_item, :metadata) + + {data_type, data} = + cond do + !is_nil(metadata) and (!is_nil(Map.get(metadata, :token_type)) or !is_nil(Map.get(metadata, "token_type"))) -> + {:metadata, http_item[:metadata]} + + !is_nil(metadata) -> + {:total_supply, http_item[:metadata]} + + !is_nil(Map.get(http_item, :counters)) -> + {:counters, http_item[:counters]} + + !is_nil(Map.get(http_item, :price_data)) -> + {:market_data, http_item[:price_data]} + end + + %{ + address_hash: address_hash, + data_type: data_type, + data: data + } + end + @doc """ Sends provided blockchain data to the appropriate export queues for further processing. @@ -228,6 +418,176 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end end + @doc """ + Prepares token metadata for writing to database queue and subsequent sending to Multichain service. + + ## Parameters + - `token`: An instance of `Token.t()` containing token type and probably `icon_url`. + - `metadata`: A map with token metadata. + + ## Returns + - A map containing token type and its metadata in the format approved on Multichain service. + """ + @spec prepare_token_metadata_for_queue(Token.t(), %{ + :token_type => String.t(), + optional(:name) => String.t(), + optional(:symbol) => String.t(), + optional(:decimals) => non_neg_integer(), + optional(:total_supply) => non_neg_integer(), + optional(any()) => any() + }) :: %{ + optional(:token_type) => String.t(), + optional(:name) => String.t(), + optional(:symbol) => String.t(), + optional(:decimals) => String.t(), + optional(:total_supply) => String.t(), + optional(:icon_url) => String.t() + } + def prepare_token_metadata_for_queue(%Token{} = token, metadata) do + if enabled?() do + %{token_type: token.type} + |> token_optional_field(metadata, :name) + |> token_optional_field(metadata, :symbol) + |> token_optional_field(token, :icon_url) + |> token_optional_field(metadata, :decimals) + |> token_optional_field(metadata, :total_supply, true) + else + %{} + end + end + + @doc """ + Prepares token total supply for writing to database queue and subsequent sending to Multichain service. + + ## Parameters + - `total_supply`: The total supply value. Can be `nil`. + + ## Returns + - A map containing total supply in the format approved on Multichain service. + - `nil` if the `total_supply` parameter is `nil`. + """ + @spec prepare_token_total_supply_for_queue(non_neg_integer() | nil) :: %{:total_supply => String.t()} | nil + def prepare_token_total_supply_for_queue(nil), do: nil + + def prepare_token_total_supply_for_queue(total_supply) do + if enabled?() do + %{total_supply: to_string(total_supply)} + end + end + + @doc """ + Prepares token market data (such as price and market cap) for writing to database queue + and subsequent sending to Multichain service. + + ## Parameters + - `token`: A token map containing the market data. + + ## Returns + - A map containing the market data in the format approved on Multichain service. + """ + @spec prepare_token_market_data_for_queue(%{ + optional(:fiat_value) => Decimal.t(), + optional(:circulating_market_cap) => Decimal.t(), + optional(any()) => any() + }) :: map() + def prepare_token_market_data_for_queue(token) do + if enabled?() do + %{} + |> token_optional_field(token, :fiat_value) + |> token_optional_field(token, :circulating_market_cap) + |> Enum.map(fn {key, value} -> + {key, Decimal.to_string(value, :normal)} + end) + |> Enum.into(%{}) + else + %{} + end + end + + @doc """ + Prepares token counters for writing to database queue and subsequent sending to Multichain service. + + ## Parameters + - `transfer_count`: The number of the token transfers count. + - `holder_count`: The number of the token holders count. + + ## Returns + - A map containing the counters in the format approved on Multichain service. + """ + @spec prepare_token_counters_for_queue(non_neg_integer(), non_neg_integer()) :: %{ + :transfers_count => String.t(), + :holders_count => String.t() + } + def prepare_token_counters_for_queue(transfer_count, holder_count) do + if enabled?() do + %{transfers_count: to_string(transfer_count), holders_count: to_string(holder_count)} + else + %{} + end + end + + defp token_optional_field(data, metadata, key, convert_to_string \\ false) do + case Map.get(metadata, key) do + nil -> + data + + value -> + if convert_to_string do + Map.put(data, key, to_string(value)) + else + Map.put(data, key, value) + end + end + end + + @doc """ + Writes token info to database queue to send that to Multichain service later. + + ## Parameters + - `entries`: A map of token entries with data prepared with one of the `prepare_token_*` functions. + - `entries_type`: A type of the token entries. + + ## Returns + # - `:ok` if the data is accepted for insertion. + # - `:ignore` if the Multichain service is not used. + """ + @spec send_token_info_to_queue(%{binary() => map()}, :metadata | :total_supply | :counters | :market_data) :: + :ok | :ignore + def send_token_info_to_queue(entries, entries_type) do + if enabled?() do + entries + |> extract_token_info_entries_into_chunks(entries_type) + |> Enum.each(fn chunk -> + Repo.insert_all( + TokenInfoExportQueue, + Helper.add_timestamps(chunk), + on_conflict: {:replace, [:data, :updated_at]}, + conflict_target: [:address_hash, :data_type] + ) + end) + + :ok + else + :ignore + end + end + + @spec extract_token_info_entries_into_chunks( + %{binary() => map()}, + :metadata | :total_supply | :counters | :market_data + ) :: list() + defp extract_token_info_entries_into_chunks(entries, entries_type) do + entries + |> Enum.map(fn {address_hash, data} -> + %{ + address_hash: address_hash, + data_type: entries_type, + data: data + } + end) + |> Enum.chunk_every(token_info_chunk_size()) + end + # sobelow_skip ["DOS.StringToAtom"] defp prepare_export_data_for_queue(%{ addresses: addresses, @@ -705,4 +1065,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp addresses_chunk_size do Application.get_env(:explorer, __MODULE__)[:addresses_chunk_size] end + + defp token_info_chunk_size do + Application.get_env(:explorer, __MODULE__)[:token_info_chunk_size] + end end diff --git a/apps/explorer/priv/repo/migrations/20250714093329_add_multichain_search_db_token_info_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250714093329_add_multichain_search_db_token_info_export_queue_table.exs new file mode 100644 index 000000000000..3fff03c98db2 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250714093329_add_multichain_search_db_token_info_export_queue_table.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Migrations.AddMultichainSearchDbTokenInfoExportQueueTable do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE multichain_search_token_data_type AS ENUM ('metadata', 'total_supply', 'counters', 'market_data')", + "DROP TYPE multichain_search_token_data_type" + ) + + create table(:multichain_search_db_export_token_info_queue, primary_key: false) do + add(:address_hash, :bytea, null: false, primary_key: true) + add(:data_type, :multichain_search_token_data_type, null: false, primary_key: true) + add(:data, :jsonb, null: false) + add(:retries_number, :smallint, null: true) + + timestamps(null: false, type: :utc_datetime_usec) + end + + alter table(:tokens) do + add(:transfer_count, :integer, null: true) + end + end +end diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 1c93c0a2f8e0..7e8b3e9c40af 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -4,7 +4,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do import Mox alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.MultichainSearchDb.MainExportQueue + alias Explorer.Chain.MultichainSearchDb.{MainExportQueue, TokenInfoExportQueue} alias Explorer.Chain.Wei alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.{Repo, TestHelper} @@ -12,7 +12,247 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do setup :verify_on_exit! - describe "batch_import/2" do + describe "batch_export_token_info/1" do + setup do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + :ok + end + + test "returns {:ok, :service_disabled} when the service is disabled" do + items_from_db_queue = [insert(:multichain_search_db_export_token_info_queue)] + assert MultichainSearch.batch_export_token_info(items_from_db_queue) == {:ok, :service_disabled} + end + + test "processes chunks and returns {:ok, {:chunks_processed, _}} when the service is enabled" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + items_from_db_queue = [token_info_item_1, token_info_item_2] + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + end + + test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_export_token_info_queue' table is populated" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + items_from_db_queue = [token_info_item_1, token_info_item_2] + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + assert {:error, + %{ + tokens: [ + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_1), + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_2) + ] + }} == MultichainSearch.batch_export_token_info(items_from_db_queue) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 2 + records = Repo.all(TokenInfoExportQueue) + + assert Enum.all?(records, fn record -> + (record.address_hash == token_info_item_1.address_hash && record.data_type == token_info_item_1.data_type) || + (record.address_hash == token_info_item_2.address_hash && + record.data_type == token_info_item_2.data_type) + end) + end + + test "returns {:error, data_to_retry} when at least one chunk is failed" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + TestHelper.get_chain_id_mock() + + # 1002 addresses (1000 in the first chunk and 2 in the second) + items_from_db_queue = + for _ <- 0..1001 do + insert(:multichain_search_db_export_token_info_queue) + end + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"status" => "ok"}) + }} + end + ) + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", headers: [{"Content-Type", "application/json"}]}, + _opts -> + {:ok, + %Tesla.Env{ + status: 500, + body: Jason.encode!(%{"code" => 0, "message" => "Error"}) + }} + end + ) + + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 1000 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 1000 + end + + test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_export_token_info_queue' table is populated with all the input data" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + items_from_db_queue = 10 |> insert_list(:multichain_search_db_export_token_info_queue) + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 10 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 10 + end + + test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_token_info_queue' table is populated" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + items_from_db_queue = 15 |> insert_list(:multichain_search_db_export_token_info_queue) + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 15 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 15 + end + end + + describe "batch_import/1" do setup do Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 3bdb82e25912..204699f8f70f 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -267,6 +267,37 @@ defmodule Explorer.Factory do } end + def multichain_search_db_export_token_info_queue_factory do + [data_type] = Enum.take_random([:metadata, :total_supply, :counters, :market_data], 1) + + data = + case data_type do + :metadata -> + %{ + token_type: "ERC-20", + name: sequence("TokenName"), + symbol: sequence("TS"), + decimals: 18, + total_supply: "1000" + } + + :total_supply -> + %{total_supply: "1000"} + + :counters -> + %{transfers_count: "456", holders_count: "123"} + + :market_data -> + %{fiat_value: "123.456", circulating_market_cap: "1000.0001"} + end + + %MultichainSearchDb.TokenInfoExportQueue{ + address_hash: address_hash().bytes, + data_type: data_type, + data: data + } + end + def address_factory do %Address{ hash: address_hash() diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex new file mode 100644 index 000000000000..1903100383b9 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex @@ -0,0 +1,136 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue do + @moduledoc """ + Exports blockchain data to Multichain Search DB service from the queue. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Repo + + alias Indexer.{BufferedTask, Helper} + + @behaviour BufferedTask + + @default_max_batch_size 1000 + @default_max_concurrency 10 + @delete_queries_chunk_size 10 + @failed_to_export_data_error "Batch token info export attempt to the Multichain Search DB failed" + @fetcher_name :multichain_search_db_token_info_export_queue + @queue_size_info "Queue size" + @successfully_sent_info "Successfully sent" + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + TokenInfoExportQueue.stream_multichain_db_token_info_batch( + initial_acc, + fn data, acc -> + Helper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end, + true + ) + + acc + end + + @impl BufferedTask + def run(items_from_db_queue, _) when is_list(items_from_db_queue) do + case MultichainSearch.batch_export_token_info(items_from_db_queue) do + {:ok, {:chunks_processed, chunks}} -> + chunks + |> Enum.map(fn chunk -> chunk.tokens end) + |> List.flatten() + |> Enum.map(&MultichainSearch.token_info_http_item_to_queue_item(&1)) + |> delete_queue_items() + |> log_queue_size() + + {:error, data_to_retry} -> + Logger.error(fn -> + ["#{@failed_to_export_data_error}", "#{inspect(data_to_retry.tokens)}"] + end) + + queue_items_to_retry = + data_to_retry.tokens + |> Enum.map(&MultichainSearch.token_info_http_item_to_queue_item(&1)) + + items_from_db_queue + |> Enum.reject(fn item_to_export -> + Enum.any?( + queue_items_to_retry, + &(&1.address_hash == item_to_export.address_hash and &1.data_type == item_to_export.data_type) + ) + end) + |> delete_queue_items() + |> log_queue_size() + + {:retry, queue_items_to_retry} + end + end + + # Removes items successfully sent to Multichain service from db queue. + # The list is split into small chunks to prevent db deadlocks. + # + # ## Parameters + # - `items`: The list of queue items to delete from the queue. + # + # ## Returns + # - The `items` list. + @spec delete_queue_items([map()]) :: [map()] + defp delete_queue_items(items) do + items + |> Enum.chunk_every(@delete_queries_chunk_size) + |> Enum.each(fn chunk_items -> + chunk_items + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + end) + + items + end + + # Logs the number of the current queue size and the number of successfully sent items. + # + # ## Parameters + # - `items_successful`: The list of items successfully sent to Multichain service. + # + # ## Returns + # - `:ok` + @spec log_queue_size(list()) :: any() + defp log_queue_size(items_successful) do + Logger.info( + fn -> + [ + "#{@queue_size_info}: ", + "#{TokenInfoExportQueue.queue_size()}, ", + "#{@successfully_sent_info}: ", + "#{Enum.count(items_successful)}" + ] + end, + fetcher: @fetcher_name + ) + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index 756e42a81ac2..b98bbafd0ffe 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -295,7 +295,8 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do timeout: :infinity }) - remove_prev_frame_sequences(inserted) + removed_sequence_ids = remove_prev_frame_sequences(inserted) + sequences = Enum.reject(sequences, fn s -> s.id in removed_sequence_ids end) set_frame_sequences_view_ready(sequences) last_batch = @@ -1343,6 +1344,8 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do # can still reference to the `op_frame_sequences` table _ -> nil end + + ids end defp set_frame_sequences_view_ready(sequences) do diff --git a/apps/indexer/lib/indexer/fetcher/token.ex b/apps/indexer/lib/indexer/fetcher/token.ex index ededd909e9f5..bcae7ca34218 100644 --- a/apps/indexer/lib/indexer/fetcher/token.ex +++ b/apps/indexer/lib/indexer/fetcher/token.ex @@ -9,6 +9,7 @@ defmodule Indexer.Fetcher.Token do alias Explorer.Chain alias Explorer.Chain.Hash.Address alias Explorer.Chain.Token + alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.Token.MetadataRetriever alias Indexer.{BufferedTask, Tracer} @@ -72,6 +73,15 @@ defmodule Indexer.Fetcher.Token do |> (&if(&1 == %{}, do: &1, else: Map.put(&1, :cataloged, true))).() {:ok, _} = Token.update(token, token_params) + + if Map.get(token_params, :cataloged) do + data_for_multichain = MultichainSearch.prepare_token_metadata_for_queue(token, token_params) + + %{} + |> Map.put(token.contract_address_hash.bytes, data_for_multichain) + |> MultichainSearch.send_token_info_to_queue(:metadata) + end + :ok end diff --git a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex new file mode 100644 index 000000000000..0ec74be5e8ff --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex @@ -0,0 +1,75 @@ +defmodule Indexer.Fetcher.TokenCountersUpdater do + @moduledoc """ + Updates counters for cataloged tokens. + """ + use Indexer.Fetcher, restart: :permanent + + require Logger + + alias Explorer.Chain + alias Explorer.Chain.Token + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Indexer.BufferedTask + alias Timex.Duration + + @behaviour BufferedTask + + @max_batch_size 10 + @max_concurrency 4 + @defaults [ + flush_interval: :timer.seconds(3), + max_concurrency: @max_concurrency, + max_batch_size: @max_batch_size, + task_supervisor: Indexer.Fetcher.TokenCountersUpdater.TaskSupervisor, + metadata: [fetcher: :token_counters_updater] + ] + + @doc false + def child_spec([init_options, gen_server_options]) do + {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) + + unless state do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> + "to allow for json_rpc calls when running." + end + + merged_init_opts = + @defaults + |> Keyword.merge(mergeable_init_options) + |> Keyword.put(:state, state) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + counters_updater_milliseconds_interval = Application.get_env(:indexer, __MODULE__)[:milliseconds_interval] + + interval_in_minutes = + counters_updater_milliseconds_interval + |> Duration.from_milliseconds() + |> Duration.to_minutes() + |> trunc() + + {:ok, tokens} = Token.stream_cataloged_tokens(initial, reducer, interval_in_minutes, true) + + tokens + end + + @impl BufferedTask + def run(entries, _json_rpc_named_arguments) do + Logger.debug("updating token counters") + + entries + |> Enum.reduce(%{}, fn token, acc -> + {transfer_count, holder_count} = Chain.fetch_token_counters(token.contract_address_hash, :infinity) + + data_for_multichain = MultichainSearch.prepare_token_counters_for_queue(transfer_count, holder_count) + Map.put(acc, token.contract_address_hash.bytes, data_for_multichain) + end) + |> MultichainSearch.send_token_info_to_queue(:counters) + + :ok + end +end diff --git a/apps/indexer/lib/indexer/fetcher/token_total_supply_updater.ex b/apps/indexer/lib/indexer/fetcher/token_total_supply_updater.ex index f67eec104457..a3a66ba1fd88 100644 --- a/apps/indexer/lib/indexer/fetcher/token_total_supply_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_total_supply_updater.ex @@ -7,7 +7,8 @@ defmodule Indexer.Fetcher.TokenTotalSupplyUpdater do alias Explorer.{Chain, Repo} alias Explorer.Chain.Cache.Counters.AverageBlockTime - alias Explorer.Chain.Token + alias Explorer.Chain.{Hash, Token} + alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.Token.MetadataRetriever alias Timex.Duration @@ -32,7 +33,17 @@ defmodule Indexer.Fetcher.TokenTotalSupplyUpdater do end def handle_info(:update, contract_address_hashes) do - Enum.each(contract_address_hashes, &update_token/1) + contract_address_hashes + |> Enum.reduce(%{}, fn contract_address_hash, acc -> + with {:ok, address_hash} <- Chain.string_to_address_hash(contract_address_hash), + data_for_multichain = update_token(address_hash), + false <- is_nil(data_for_multichain) do + Map.put(acc, address_hash.bytes, data_for_multichain) + else + _ -> acc + end + end) + |> MultichainSearch.send_token_info_to_queue(:total_supply) schedule_next_update() @@ -49,21 +60,20 @@ defmodule Indexer.Fetcher.TokenTotalSupplyUpdater do Process.send_after(self(), :update, update_interval) end - defp update_token(nil), do: :ok - - defp update_token(address_hash_string) do - {:ok, address_hash} = Chain.string_to_address_hash(address_hash_string) - + defp update_token(address_hash) do token = Repo.get_by(Token, contract_address_hash: address_hash) if token && !token.skip_metadata do - token_params = MetadataRetriever.get_total_supply_of(address_hash_string) + token_params = + address_hash + |> Hash.to_string() + |> MetadataRetriever.get_total_supply_of() if token_params !== %{} do {:ok, _} = Token.update(token, token_params) + + MultichainSearch.prepare_token_total_supply_for_queue(token_params.total_supply) end end - - :ok end end diff --git a/apps/indexer/lib/indexer/fetcher/token_updater.ex b/apps/indexer/lib/indexer/fetcher/token_updater.ex index 3c5cc64bbe40..4d59fcf59341 100644 --- a/apps/indexer/lib/indexer/fetcher/token_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_updater.ex @@ -8,6 +8,7 @@ defmodule Indexer.Fetcher.TokenUpdater do alias Explorer.Chain alias Explorer.Chain.{Hash, Token} + alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.Token.MetadataRetriever alias Indexer.BufferedTask alias Timex.Duration @@ -78,13 +79,23 @@ defmodule Indexer.Fetcher.TokenUpdater do @doc false def update_metadata(metadata_list) when is_list(metadata_list) do - Enum.each(metadata_list, fn %{contract_address_hash: contract_address_hash} = metadata -> + metadata_list + |> Enum.reduce(%{}, fn %{contract_address_hash: contract_address_hash} = metadata, acc -> {:ok, hash} = Hash.Address.cast(contract_address_hash) - with {:ok, %Token{cataloged: true} = token} <- Chain.token_from_address_hash(hash) do - update_metadata(token, metadata) + case Chain.token_from_address_hash(hash) do + {:ok, %Token{cataloged: true} = token} -> + update_metadata(token, metadata) + data_for_multichain = MultichainSearch.prepare_token_metadata_for_queue(token, metadata) + Map.put(acc, hash.bytes, data_for_multichain) + + _ -> + acc end end) + |> MultichainSearch.send_token_info_to_queue(:metadata) + + :ok end def update_metadata(%Token{} = token, metadata) do diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index e4306b12a830..4c3b9e6281b1 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -25,6 +25,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime alias Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue + alias Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, as: MultichainSearchDbTokenInfoExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry @@ -43,6 +44,7 @@ defmodule Indexer.Supervisor do RootstockData, Token, TokenBalance, + TokenCountersUpdater, TokenTotalSupplyUpdater, TokenUpdater, TransactionAction, @@ -149,9 +151,12 @@ defmodule Indexer.Supervisor do [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, + {TokenCountersUpdater.Supervisor, + [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbMainExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbBalancesExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbTokenInfoExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, configure( Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, diff --git a/config/runtime.exs b/config/runtime.exs index 7f1218120c39..d1bd64a3b3fc 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -653,7 +653,10 @@ config :explorer, Explorer.SmartContract.StylusVerifierInterface, config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL"), - addresses_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7_000) + addresses_chunk_size: + ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7_000), + token_info_chunk_size: + ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_TOKEN_INFO_CHUNK_SIZE", 1_000) config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), @@ -974,6 +977,9 @@ config :indexer, Indexer.Fetcher.TokenBalance, max_refetch_interval: ConfigHelper.parse_time_env_var("INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL", "168h"), exp_timeout_coeff: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF", 100) +config :indexer, Indexer.Fetcher.TokenCountersUpdater, + milliseconds_interval: ConfigHelper.parse_time_env_var("TOKEN_COUNTERS_UPDATE_INTERVAL", "3h") + config :indexer, Indexer.Fetcher.OnDemand.TokenBalance, threshold: ConfigHelper.parse_time_env_var("TOKEN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD", "1h"), fallback_threshold_in_blocks: 500 @@ -1003,6 +1009,12 @@ config :indexer, Indexer.Fetcher.CoinBalance.Realtime.Supervisor, disabled?: dis config :indexer, Indexer.Fetcher.TokenUpdater.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER") +config :indexer, Indexer.Fetcher.TokenTotalSupplyUpdater.Supervisor, + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER") + +config :indexer, Indexer.Fetcher.TokenCountersUpdater.Supervisor, + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER") + config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_EMPTY_BLOCKS_SANITIZER") @@ -1042,6 +1054,11 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue.Supervis ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER") || is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) +config :indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue.Supervisor, + disabled?: + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_FETCHER") || + is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 10), interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s") @@ -1153,6 +1170,21 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, init_limit: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT", 1_000) +config :indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, + concurrency: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_CONCURRENCY", 10), + batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_BATCH_SIZE", 1_000), + enqueue_busy_waiting_timeout: + ConfigHelper.parse_time_env_var( + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", + "1s" + ), + max_queue_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_MAX_QUEUE_SIZE", 1_000), + init_limit: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_INIT_QUERY_LIMIT", 1_000) + config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 8ef4d8aaf5c5..0c604e6ca006 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -127,7 +127,8 @@ RELEASE_LINK= # CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD= # TOKEN_INSTANCE_METADATA_REFETCH_ON_DEMAND_FETCHER_THRESHOLD= # ADDRESSES_TABS_COUNTERS_TTL=10m -# TOKEN_METADATA_UPDATE_INTERVAL=172800 +# TOKEN_METADATA_UPDATE_INTERVAL=48h +# TOKEN_COUNTERS_UPDATE_INTERVAL=3h # CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,default # CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default # CONTRACT_VERIFICATION_MAX_LIBRARIES=10 @@ -185,7 +186,8 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_DISABLE_WITHDRAWALS_FETCHER= # INDEXER_DISABLE_REPLACED_TRANSACTION_FETCHER= # INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER= -# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_FETCHER= # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -215,6 +217,11 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_INIT_QUERY_LIMIT= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= @@ -435,6 +442,7 @@ MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ # MICROSERVICE_MULTICHAIN_SEARCH_URL= # MICROSERVICE_MULTICHAIN_SEARCH_API_KEY= # MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE= +# MICROSERVICE_MULTICHAIN_SEARCH_TOKEN_INFO_CHUNK_SIZE= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL= DECODE_NOT_A_CONTRACT_CALLS=true From dff7814bb06327a9f80d0850470e8798e48301fe Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 1 Aug 2025 19:53:23 +0300 Subject: [PATCH 0194/1006] fix: Fix deadlocks in Multichain balances export (#12898) * fix: Fix deadlocks in Multichain balances export * Remove deletion in chunks for balances export queue * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../balances_export_queue.ex | 72 ++++++++----------- .../multichain_search.ex | 2 + .../multichain_search_test.exs | 44 ++++++------ .../balances_export_queue_test.exs | 24 +++---- 5 files changed, 66 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6444bdeb1c5..b16b87e442ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### 🐛 Bug Fixes +- Fix deadlocks in Multichain balances export ([#12898](https://github.com/blockscout/blockscout/pull/12898)) - Balances export queue: replace replace_all with replace only value and updated_at ([#12892](https://github.com/blockscout/blockscout/pull/12892)) - Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) - Balances export queue to multichain replace do_nothing with replace_all on insertion to the queue ([#12888](https://github.com/blockscout/blockscout/pull/12888)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index 93e12f70d1e0..35e255b82b5b 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -101,51 +101,37 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do # sobelow_skip ["DOS.StringToAtom"] @spec delete_elements_from_queue_by_params([map()]) :: list() def delete_elements_from_queue_by_params(balances) do - q = - Enum.reduce(balances, nil, fn balance, acc -> - balance_address_hash = balance.address_hash - - balance_token_contract_address_hash_or_native_binary = - if byte_size(balance.token_contract_address_hash_or_native) == 6 do - balance.token_contract_address_hash_or_native - else - "0x" <> hex = balance.token_contract_address_hash_or_native - hex |> Base.decode16(case: :lower) |> elem(1) - end - - balance_token_id = balance.token_id - - query = - from( - b in __MODULE__, - where: b.address_hash == ^balance_address_hash, - where: b.token_contract_address_hash_or_native == ^balance_token_contract_address_hash_or_native_binary, - where: - fragment( - "COALESCE(?, -1::numeric) = COALESCE(?::numeric, -1::numeric)", - b.token_id, - ^balance_token_id - ) - ) - - if is_nil(acc) do - query + balances + |> Enum.with_index() + |> Enum.reduce(Multi.new(), fn {balance, ind}, acc -> + balance_address_hash = balance.address_hash + + balance_token_contract_address_hash_or_native_binary = + if byte_size(balance.token_contract_address_hash_or_native) == 6 do + balance.token_contract_address_hash_or_native else - acc - |> union(^query) + "0x" <> hex = balance.token_contract_address_hash_or_native + hex |> Base.decode16(case: :lower) |> elem(1) end - end) - elements = Repo.all(q) - - delete_elements = - elements - |> Enum.with_index() - |> Enum.reduce(Multi.new(), fn {elem, ind}, acc -> - acc - |> Multi.delete(String.to_atom("delete_#{ind}"), elem) - end) - - Repo.transact(delete_elements) + balance_token_id = balance.token_id + + acc + |> Multi.delete_all( + String.to_atom("delete_#{ind}"), + from( + b in __MODULE__, + where: b.address_hash == ^balance_address_hash, + where: b.token_contract_address_hash_or_native == ^balance_token_contract_address_hash_or_native_binary, + where: + fragment( + "COALESCE(?, -1::numeric) = COALESCE(?::numeric, -1::numeric)", + b.token_id, + ^balance_token_id + ) + ) + ) + end) + |> Repo.transact() end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index beff8374db6b..8de6587b1488 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -779,12 +779,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do indexed_addresses_chunks = addresses + |> Enum.sort_by(& &1.hash) |> Enum.uniq() |> Enum.chunk_every(addresses_chunk_size()) |> Enum.with_index() indexed_address_coin_balances_chunks = address_coin_balances + |> Enum.sort_by(& &1.address_hash) |> Enum.uniq() |> Enum.reject(&is_nil(&1.value)) |> Enum.chunk_every(addresses_chunk_size()) diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 7e8b3e9c40af..da5ed73469fc 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -368,8 +368,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert {:error, %{ addresses: [ - address_export_data(address_2), - address_export_data(address_1) + address_export_data(address_1), + address_export_data(address_2) ], block_ranges: [ %{max_block_number: to_string(block_2.number), min_block_number: to_string(block_1.number)} @@ -706,8 +706,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do ] assert chunk[:address_coin_balances] == [ - %{value: %Wei{value: Decimal.new("200")}, address_hash: to_string(address_2.hash)}, - %{value: %Wei{value: Decimal.new("100")}, address_hash: to_string(address_1.hash)} + %{value: %Wei{value: Decimal.new("100")}, address_hash: to_string(address_1.hash)}, + %{value: %Wei{value: Decimal.new("200")}, address_hash: to_string(address_2.hash)} ] assert chunk[:address_token_balances] == [ @@ -791,8 +791,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert second_chunk[:hashes] == [] assert Enum.all?(second_chunk[:addresses], fn item -> - item.hash == "0x" <> Base.encode16(Enum.at(addresses, 0).hash.bytes, case: :lower) || - item.hash == "0x" <> Base.encode16(Enum.at(addresses, 1).hash.bytes, case: :lower) + item.hash == "0x" <> Base.encode16(Enum.at(addresses, 7000).hash.bytes, case: :lower) || + item.hash == "0x" <> Base.encode16(Enum.at(addresses, 7001).hash.bytes, case: :lower) end) end @@ -852,34 +852,34 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do assert chunk[:addresses] == [ %{ - hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), - is_contract: true, - is_verified_contract: true, - contract_name: "SimpleStorage", - token_name: "Main Token", - token_type: "ERC-721", - is_token: true, - ens_name: nil - }, - %{ - hash: "0x" <> Base.encode16(address_2.hash.bytes, case: :lower), - is_contract: true, + hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), + is_contract: false, is_verified_contract: false, contract_name: nil, token_name: nil, token_type: "UNSPECIFIED", is_token: false, - ens_name: nil + ens_name: "te.eth" }, %{ - hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), - is_contract: false, + hash: "0x" <> Base.encode16(address_2.hash.bytes, case: :lower), + is_contract: true, is_verified_contract: false, contract_name: nil, token_name: nil, token_type: "UNSPECIFIED", is_token: false, - ens_name: "te.eth" + ens_name: nil + }, + %{ + hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), + is_contract: true, + is_verified_contract: true, + contract_name: "SimpleStorage", + token_name: "Main Token", + token_type: "ERC-721", + is_token: true, + ens_name: nil } ] diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs index 6fc0d36f9df6..a04cbd0aa05e 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/balances_export_queue_test.exs @@ -184,6 +184,8 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do end) address_1 = insert(:address) + address_1_hash = address_1.hash + address_1_hash_string = to_string(address_1_hash) address_2 = insert(:address) address_2_hash = address_2.hash address_2_hash_string = to_string(address_2_hash) @@ -191,8 +193,6 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do address_3_hash = address_3.hash address_3_hash_string = to_string(address_3_hash) address_4 = insert(:address) - address_4_hash = address_4.hash - address_4_hash_string = to_string(address_4_hash) address_5 = insert(:address) address_5_hash = address_5.hash address_5_hash_string = to_string(address_5_hash) @@ -234,11 +234,11 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do TestHelper.get_chain_id_mock() - tesla_expectations(address_4_hash_string) + tesla_expectations(address_1_hash_string) + val0 = Decimal.new(100) |> Wei.cast() |> elem(1) val1 = Decimal.new(200) |> Wei.cast() |> elem(1) val2 = Decimal.new(300) |> Wei.cast() |> elem(1) - val3 = Decimal.new(400) |> Wei.cast() |> elem(1) val4 = Decimal.new(500) |> Wei.cast() |> elem(1) log = @@ -247,9 +247,9 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do %{ address_coin_balances: [ %{ - address_hash: ^address_4_hash_string, + address_hash: ^address_1_hash_string, token_contract_address_hash_or_native: "native", - value: ^val3 + value: ^val0 } ], address_token_balances: [ @@ -282,7 +282,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do TestHelper.get_chain_id_mock() - tesla_expectations(address_4_hash_string) + tesla_expectations(address_1_hash_string) MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) @@ -294,7 +294,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do TestHelper.get_chain_id_mock() - tesla_expectations(address_4_hash_string) + tesla_expectations(address_1_hash_string) MultichainSearchDbExportBalancesExportQueue.run(export_data, nil) @@ -316,9 +316,9 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do token_id: nil }, %{ - address_hash: address_4.hash, + address_hash: address_1.hash, token_contract_address_hash_or_native: "native", - value: %Wei{value: Decimal.new(400)} + value: %Wei{value: Decimal.new(100)} }, %{ address_hash: address_5.hash, @@ -346,12 +346,12 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueueTest do end end - defp tesla_expectations(address_4_hash_string) do + defp tesla_expectations(address_hash_string) do Tesla.Test.expect_tesla_call( times: 2, returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> case Jason.decode(body) do - {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_4_hash_string}]}} -> + {:ok, %{"address_coin_balances" => [%{"address_hash" => ^address_hash_string}]}} -> {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} _ -> From e188a49426944a0b32844bde2eb7831eb0f69b4a Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:22:38 +0300 Subject: [PATCH 0195/1006] =?UTF-8?q?fix:=20Ignore=20rate=20limit=20for=20?= =?UTF-8?q?api/v2/import/token-info=20and=20api/v2/import=E2=80=A6=20(#129?= =?UTF-8?q?17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: Ignore rate limit for api/v2/import/token-info and api/v2/import/smart-contracts/:param * Changelog --- CHANGELOG.md | 1 + apps/block_scout_web/priv/rate_limit_config.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b16b87e442ec..b5bd0924b0e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ ### 🐛 Bug Fixes +- Ignore rate limit for api/v2/import/token-info and api/v2/import/smart-contracts/:param ([#12917](https://github.com/blockscout/blockscout/pull/12917)) - Fix deadlocks in Multichain balances export ([#12898](https://github.com/blockscout/blockscout/pull/12898)) - Balances export queue: replace replace_all with replace only value and updated_at ([#12892](https://github.com/blockscout/blockscout/pull/12892)) - Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) diff --git a/apps/block_scout_web/priv/rate_limit_config.json b/apps/block_scout_web/priv/rate_limit_config.json index 6f8187784bef..81d72acd0bc1 100644 --- a/apps/block_scout_web/priv/rate_limit_config.json +++ b/apps/block_scout_web/priv/rate_limit_config.json @@ -2,6 +2,12 @@ "api/v2/key": { "ignore": true }, + "api/v2/import/token-info": { + "ignore": true + }, + "api/v2/import/smart-contracts/:param": { + "ignore": true + }, "api/account/v2/authenticate_via_wallet": { "recaptcha_to_bypass_429": true, "ip": { From 06fd0f45fbd6f6bce0e2274d17fa1f9953a78b76 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:22:59 +0300 Subject: [PATCH 0196/1006] fix: Don't send coin balances with zero delta via ws (#12890) * fix: Don't send coin balances with zero delta via ws * Changelog --- CHANGELOG.md | 1 + .../lib/block_scout_web/notifier.ex | 25 +++++++++++-------- .../import/runner/address/coin_balances.ex | 2 +- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5bd0924b0e5..45dac133b2ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Fix deadlocks in Multichain balances export ([#12898](https://github.com/blockscout/blockscout/pull/12898)) - Balances export queue: replace replace_all with replace only value and updated_at ([#12892](https://github.com/blockscout/blockscout/pull/12892)) - Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) +- Don't send coin balances with zero delta via ws ([#12890](https://github.com/blockscout/blockscout/pull/12890)) - Balances export queue to multichain replace do_nothing with replace_all on insertion to the queue ([#12888](https://github.com/blockscout/blockscout/pull/12888)) - Allow using temporary token for api/account/v2 by default ([#12869](https://github.com/blockscout/blockscout/pull/12869)) - Fix increment of retries_number in exporting data to Multichain DB ([#12847](https://github.com/blockscout/blockscout/pull/12847)) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 021bd5713691..d372f63f3e8a 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -36,8 +36,7 @@ defmodule BlockScoutWeb.Notifier do DenormalizationHelper, InternalTransaction, Token.Instance, - Transaction, - Wei + Transaction } alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime, Helper} @@ -102,7 +101,9 @@ defmodule BlockScoutWeb.Notifier do def handle_event({:chain_event, :address_coin_balances, type, address_coin_balances}) when type in [:realtime, :on_demand] do - Enum.each(address_coin_balances, &broadcast_address_coin_balance/1) + address_coin_balances + |> Enum.reject(fn balance -> is_nil(balance[:value]) end) + |> Enum.each(&broadcast_address_coin_balance/1) end def handle_event({:chain_event, :address_token_balances, type, address_token_balances}) @@ -551,15 +552,17 @@ defmodule BlockScoutWeb.Notifier do end defp broadcast_address_coin_balance(%{address_hash: address_hash, block_number: block_number}) do - coin_balance = CoinBalance.get_coin_balance(address_hash, block_number) + coin_balance = CoinBalance.get_coin_balance(address_hash, block_number, @api_true) - # TODO: delete duplicated event when old UI becomes deprecated - Endpoint.broadcast("addresses_old:#{address_hash}", "coin_balance", %{ - block_number: block_number, - coin_balance: coin_balance - }) + if coin_balance.delta && !Decimal.eq?(coin_balance.delta, Decimal.new(0)) do + # TODO: delete duplicated event when old UI becomes deprecated + Endpoint.broadcast("addresses_old:#{address_hash}", "coin_balance", %{ + block_number: block_number, + coin_balance: coin_balance + }) + end - if coin_balance.value && coin_balance.delta do + if coin_balance.value && coin_balance.delta && !Decimal.eq?(coin_balance.delta, Decimal.new(0)) do rendered_coin_balance = AddressView.render("coin_balance.json", %{coin_balance: coin_balance}) Endpoint.broadcast("addresses:#{address_hash}", "coin_balance", %{ @@ -567,7 +570,7 @@ defmodule BlockScoutWeb.Notifier do }) Endpoint.broadcast("addresses:#{address_hash}", "current_coin_balance", %{ - coin_balance: coin_balance.value || %Wei{value: Decimal.new(0)}, + coin_balance: coin_balance.value, exchange_rate: Market.get_coin_exchange_rate().fiat_value, block_number: block_number }) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/coin_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/coin_balances.ex index 62b0cfa9b2e5..b75ae6c2488e 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/coin_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/coin_balances.ex @@ -94,7 +94,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CoinBalances do timestamps: timestamps ) - {:ok, Enum.map(ordered_changes_list, &Map.take(&1, ~w(address_hash block_number)a))} + {:ok, Enum.map(ordered_changes_list, &Map.take(&1, ~w(address_hash block_number value)a))} end def default_on_conflict do From f7a7be256980d75b57f3456c21696c77e34736c6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 6 Aug 2025 09:21:13 +0300 Subject: [PATCH 0197/1006] fix: deletion in chunks for the balances and main data export queues to the multichain DB (#12928) * Revert "Remove deletion in chunks for balances export queue" * Update CHANGELOG entry * Add chunking in deletion from the `multichain_search_db_main_export_queue` table * Update CHANGELOG entry --- CHANGELOG.md | 2 +- .../multichain_search_db/balances_export_queue.ex | 1 - .../chain/multichain_search_db/main_export_queue.ex | 3 +++ .../multichain_search_db/balances_export_queue.ex | 11 ++++++++++- .../fetcher/multichain_search_db/main_export_queue.ex | 9 +++++++-- 5 files changed, 21 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45dac133b2ae..a74573b80598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ ### 🐛 Bug Fixes - Ignore rate limit for api/v2/import/token-info and api/v2/import/smart-contracts/:param ([#12917](https://github.com/blockscout/blockscout/pull/12917)) -- Fix deadlocks in Multichain balances export ([#12898](https://github.com/blockscout/blockscout/pull/12898)) +- Mitigate deadlocks while exporting balances and the main queue to the Multichain DB ([#12898](https://github.com/blockscout/blockscout/pull/12898), [#12928](https://github.com/blockscout/blockscout/pull/12928)) - Balances export queue: replace replace_all with replace only value and updated_at ([#12892](https://github.com/blockscout/blockscout/pull/12892)) - Fix naming for apikey param in OpenAPI spec ([#12891](https://github.com/blockscout/blockscout/pull/12891)) - Don't send coin balances with zero delta via ws ([#12890](https://github.com/blockscout/blockscout/pull/12890)) diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index 35e255b82b5b..f1ed8bf05374 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -132,6 +132,5 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do ) ) end) - |> Repo.transact() end end diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex index 60eea7cbbcab..d2a426d4ee7b 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/main_export_queue.ex @@ -80,6 +80,9 @@ defmodule Explorer.Chain.MultichainSearchDb.MainExportQueue do """ @spec by_hashes_query([binary()]) :: Ecto.Query.t() def by_hashes_query(hashes) do + # todo: In order to prevent deadlocks consider ordering by `hash` + # and rewrite logic to acquire items first with locking + # then deleting them. __MODULE__ |> where([export], export.hash in ^hashes) end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex index 36027ece9b27..1962e92575b4 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -11,12 +11,14 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do alias Explorer.Chain.Wei alias Explorer.Chain.{Hash, MultichainSearchDb.BalancesExportQueue} alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Repo alias Indexer.BufferedTask alias Indexer.Helper, as: IndexerHelper @behaviour BufferedTask + @delete_queries_chunk_size 10 @default_max_batch_size 1000 @default_max_concurrency 10 @failed_to_re_export_data_error "Batch balances export retry to the Multichain Search DB failed" @@ -66,7 +68,14 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do unless Enum.empty?(all_balances) do all_balances - |> BalancesExportQueue.delete_elements_from_queue_by_params() + |> Enum.sort_by(&{&1.address_hash, &1.token_contract_address_hash_or_native, &1.token_id}) + |> Enum.chunk_every(@delete_queries_chunk_size) + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + |> Enum.each(fn chunk_items -> + chunk_items + |> BalancesExportQueue.delete_elements_from_queue_by_params() + |> Repo.transact() + end) end :ok diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex index 9bc81022404e..32c31a7e98b7 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -17,6 +17,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do @behaviour BufferedTask + @delete_queries_chunk_size 100 @default_max_batch_size 1000 @default_max_concurrency 10 @failed_to_re_export_data_error "Batch main export retry to the Multichain Search DB failed" @@ -78,8 +79,12 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do end) all_hashes - |> MainExportQueue.by_hashes_query() - |> Repo.delete_all() + |> Enum.chunk_every(@delete_queries_chunk_size) + |> Enum.each(fn chunk_items -> + chunk_items + |> MainExportQueue.by_hashes_query() + |> Repo.delete_all() + end) :ok From 5a95e1916cc38f8128a477e76bb4474284093324 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 6 Aug 2025 16:12:00 +0300 Subject: [PATCH 0198/1006] chore: Remove obsolete API response props (#12931) --- CHANGELOG.md | 1 + .../lib/block_scout_web/chain.ex | 18 ++--- .../controllers/api/health_controller.ex | 53 ++------------- .../controllers/api/v2/optimism_controller.ex | 6 +- .../api/v2/smart_contract_controller.ex | 10 ++- .../controllers/api/v2/stats_controller.ex | 3 +- .../controllers/api/v2/token_controller.ex | 4 +- .../api/v2/validator_controller.ex | 12 +--- .../api/v2/withdrawal_controller.ex | 5 +- .../controllers/chain_controller.ex | 4 +- .../controllers/tokens/token_controller.ex | 4 +- .../lib/block_scout_web/paging_helper.ex | 3 - .../block_scout_web/schemas/api/v2/token.ex | 8 +-- .../views/api/v2/address_view.ex | 4 +- .../views/api/v2/arbitrum_view.ex | 24 +------ .../views/api/v2/block_view.ex | 2 - .../block_scout_web/views/api/v2/celo_view.ex | 8 +-- .../views/api/v2/ethereum_view.ex | 2 - .../block_scout_web/views/api/v2/mud_view.ex | 8 +-- .../views/api/v2/optimism_view.ex | 20 ++---- .../views/api/v2/polygon_zkevm_view.ex | 2 - .../views/api/v2/scroll_view.ex | 8 +-- .../views/api/v2/search_view.ex | 8 --- .../views/api/v2/smart_contract_view.ex | 2 - .../views/api/v2/token_view.ex | 8 --- .../views/api/v2/transaction_view.ex | 2 - .../views/api/v2/validator_view.ex | 6 +- .../views/api/v2/zksync_view.ex | 14 +--- .../api/v2/address_controller_test.exs | 23 +++---- .../api/v2/search_controller_test.exs | 66 +++++++++---------- .../api/v2/smart_contract_controller_test.exs | 11 +--- .../api/v2/token_controller_test.exs | 16 ++--- .../api/v2/transaction_controller_test.exs | 28 ++++---- .../api/v2/validator_controller_test.exs | 6 +- .../api/v2/withdrawal_controller_test.exs | 4 +- .../explorer/arbitrum/claim_rollup_message.ex | 4 -- .../lib/explorer/chain/address/counters.ex | 4 +- .../lib/explorer/chain/blackfort/validator.ex | 54 ++++++++------- .../explorer/chain/optimism/frame_sequence.ex | 22 ++----- apps/explorer/lib/explorer/chain/search.ex | 12 ++-- .../explorer/chain/smart_contract/proxy.ex | 7 +- apps/explorer/lib/explorer/chain/token.ex | 4 +- .../multichain_search.ex | 4 +- .../counters/transactions_24h_count_test.exs | 4 +- .../indexer/fetcher/token_counters_updater.ex | 4 +- 45 files changed, 171 insertions(+), 351 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a74573b80598..c6abd1da518d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,7 @@ ### ⚙️ Miscellaneous Tasks +- Remove obsolete API response props ([#12931](https://github.com/blockscout/blockscout/pull/12931)) - Balances Multichain export: Refactor rows acquisition for deletion query ([#12839](https://github.com/blockscout/blockscout/pull/12839)) - Change name of Swagger generation workflow ([#12840](https://github.com/blockscout/blockscout/pull/12840)) - migrate Auth0 to mint as well ([#12807](https://github.com/blockscout/blockscout/pull/12807)) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index f9b14893e375..50caebc2f68c 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -223,21 +223,19 @@ defmodule BlockScoutWeb.Chain do def paging_options( %{ "market_cap" => market_cap_string, - "holders_count" => holder_count_string, - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holder_count" => holder_count_string, + "holders_count" => holders_count_string, "name" => name_string, "contract_address_hash" => contract_address_hash_string, "is_name_null" => is_name_null_string } = params ) - when is_binary(market_cap_string) and is_binary(holder_count_string) and is_binary(name_string) and + when is_binary(market_cap_string) and is_binary(holders_count_string) and is_binary(name_string) and is_binary(contract_address_hash_string) and is_binary(is_name_null_string) do market_cap_decimal = decimal_parse(market_cap_string) fiat_value_decimal = decimal_parse(params["fiat_value"]) - holder_count = parse_integer(holder_count_string) + holders_count = parse_integer(holders_count_string) token_name = if is_name_null_string == "true", do: nil, else: name_string case Hash.Address.cast(contract_address_hash_string) do @@ -248,7 +246,7 @@ defmodule BlockScoutWeb.Chain do | key: %{ fiat_value: fiat_value_decimal, circulating_market_cap: market_cap_decimal, - holder_count: holder_count, + holder_count: holders_count, name: token_name, contract_address_hash: contract_address_hash } @@ -759,15 +757,13 @@ defmodule BlockScoutWeb.Chain do defp paging_params(%Token{ contract_address_hash: contract_address_hash, circulating_market_cap: circulating_market_cap, - holder_count: holder_count, + holder_count: holders_count, name: token_name, fiat_value: fiat_value }) do %{ "market_cap" => circulating_market_cap, - "holders_count" => holder_count, - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holder_count" => holder_count, + "holders_count" => holders_count, "contract_address_hash" => contract_address_hash, "name" => token_name, "is_name_null" => is_nil(token_name), @@ -862,8 +858,6 @@ defmodule BlockScoutWeb.Chain do %{ "smart_contract_id" => smart_contract.id, "transactions_count" => smart_contract.address.transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => smart_contract.address.transactions_count, "coin_balance" => smart_contract.address.fetched_coin_balance && Wei.to(smart_contract.address.fetched_coin_balance, :wei) } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex index 885446775e58..50b665b7528a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex @@ -37,10 +37,7 @@ defmodule BlockScoutWeb.API.HealthController do base_health_status = %{ metadata: %{ - # todo: this key is left for backward compatibility - # and should be removed after 8.0.0 release in favour of the new health check logic based on multiple modules - latest_block: indexing_status.blocks.old, - blocks: indexing_status.blocks.new + blocks: indexing_status.blocks } } @@ -53,14 +50,13 @@ defmodule BlockScoutWeb.API.HealthController do base_health_status |> put_in([:metadata, :batches], batches_indexing_status) # todo: return this when "latest block" metric starts remain non-empty all time - # |> Map.put(:healthy, indexing_status.blocks.new.healthy and batches_indexing_status.healthy) - |> Map.put(:healthy, indexing_status.blocks.new.healthy) + # |> Map.put(:healthy, indexing_status.blocks.healthy and batches_indexing_status.healthy) + |> Map.put(:healthy, indexing_status.blocks.healthy) else base_health_status - |> Map.put(:healthy, indexing_status.blocks.new.healthy) + |> Map.put(:healthy, indexing_status.blocks.healthy) end - # todo: this should be removed after 8.0.0. It is left for backward compatibility - it is artefact of the old response format. blocks_property = Map.get(Map.get(health_status, :metadata), :blocks) health_status_with_error = @@ -186,12 +182,11 @@ defmodule BlockScoutWeb.API.HealthController do def get_indexing_status do health_status = HealthHelper.get_indexing_health_data() - blocks_old = old_blocks_indexing_status(health_status) - blocks_new = new_blocks_indexing_status(health_status) + blocks = blocks_indexing_status(health_status) common_status = %{ - blocks: %{old: blocks_old, new: blocks_new} + blocks: blocks } status = @@ -207,41 +202,7 @@ defmodule BlockScoutWeb.API.HealthController do status end - # todo: it should be removed after 8.0.0 release in favour of the new health check logic based on multiple modules - defp old_blocks_indexing_status(health_status) do - latest_block_timestamp_from_db = - if is_nil(health_status[:health_latest_block_timestamp_from_db]) do - nil - else - {:ok, latest_block_timestamp_from_db} = - DateTime.from_unix(Decimal.to_integer(health_status[:health_latest_block_timestamp_from_db])) - - latest_block_timestamp_from_db - end - - latest_block_timestamp_from_cache = - if is_nil(health_status[:health_latest_block_timestamp_from_cache]) do - nil - else - {:ok, latest_block_timestamp_from_cache} = - DateTime.from_unix(Decimal.to_integer(health_status[:health_latest_block_timestamp_from_cache])) - - latest_block_timestamp_from_cache - end - - %{ - db: %{ - number: to_string(health_status[:health_latest_block_number_from_db]), - timestamp: to_string(latest_block_timestamp_from_db) - }, - cache: %{ - number: to_string(health_status[:health_latest_block_number_from_cache]), - timestamp: to_string(latest_block_timestamp_from_cache) - } - } - end - - defp new_blocks_indexing_status(health_status) do + defp blocks_indexing_status(health_status) do latest_block_timestamp_from_db = if is_nil(health_status[:health_latest_block_timestamp_from_db]) do nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index b11d5dbea2b9..69fe84d2e9cb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -100,7 +100,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end # credo:disable-for-lines:2 Credo.Check.Refactor.Nesting - transaction_count = + transactions_count = case l2_block_range do nil -> 0 range -> Transaction.transaction_count_for_block_range(range) @@ -110,9 +110,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do fs |> Map.put(:l2_block_range, l2_block_range) - |> Map.put(:transactions_count, transaction_count) - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - |> Map.put(:transaction_count, transaction_count) + |> Map.put(:transactions_count, transactions_count) |> Map.put(:batch_data_container, batch_data_container) end) end) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 3fa2e9d968c8..9c770fa66d51 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -161,13 +161,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do parameters. ## Returns - If 'hash', 'transaction_count', and 'coin_balance' parameters are provided, + If 'hash', 'transactions_count', and 'coin_balance' parameters are provided, uses them as pagination keys for address-based sorting. If 'smart_contract_id' parameter is provided, uses it as pagination key for smart contract ID-based sorting. Otherwise, returns default paging options. ## Examples - iex> smart_contract_addresses_paging_options(%{"hash" => "0x123...", "transaction_count" => "100", "coin_balance" => "1000"}) + iex> smart_contract_addresses_paging_options(%{"hash" => "0x123...", "transactions_count" => "100", "coin_balance" => "1000"}) [paging_options: %{key: %{hash: ..., transactions_count: 100, fetched_coin_balance: 1000}}] iex> smart_contract_addresses_paging_options(%{"smart_contract_id" => "42"}) @@ -189,7 +189,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do |> Chain.string_to_address_hash() |> case do {:ok, address_hash} -> - transactions_count = parse_integer(params["transaction_count"]) + transactions_count = parse_integer(params["transactions_count"]) coin_balance = parse_integer(params["coin_balance"]) %{ @@ -226,7 +226,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do ## Examples iex> address = %Explorer.Chain.Address{hash: "0x123...", transactions_count: 100, fetched_coin_balance: 1000} iex> smart_contract_addresses_paging_params(address) - %{"hash" => "0x123...", "transaction_count" => 100, "coin_balance" => 1000} + %{"hash" => "0x123...", "transactions_count" => 100, "coin_balance" => 1000} """ @spec smart_contract_addresses_paging_params(Explorer.Chain.Address.t()) :: %{ required(String.t()) => any() @@ -238,8 +238,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do }) do %{ "hash" => address_hash, - # todo: It should be removed in favour: transactions_count - "transaction_count" => transactions_count, "transactions_count" => transactions_count, "coin_balance" => coin_balance } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index d6b6f81b9ec8..d868faecbf97 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -107,8 +107,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do transaction_history_data = date_range |> Enum.map(fn row -> - # todo: `transaction_count` property should be removed in favour `transactions_count` property with the next release after 8.0.0 - %{date: row.date, transaction_count: row.number_of_transactions, transactions_count: row.number_of_transactions} + %{date: row.date, transactions_count: row.number_of_transactions} end) json(conn, %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index dc28ba7e7aaa..dc16a127dee1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -78,9 +78,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do - {transfer_count, token_holder_count} = Chain.fetch_token_counters(address_hash, 30_000) + {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 30_000) - json(conn, %{transfers_count: to_string(transfer_count), token_holders_count: to_string(token_holder_count)}) + json(conn, %{transfers_count: to_string(transfers_count), token_holders_count: to_string(holders_count)}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex index 05fa50d272ee..66a6ed81398b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex @@ -74,14 +74,8 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do conn |> json(%{ validators_count: validators_counter, - # todo: It should be removed in favour `validators_count` property with the next release after 8.0.0 - validators_counter: validators_counter, new_validators_count_24h: new_validators_counter, - # todo: It should be removed in favour `new_validators_count_24h` property with the next release after 8.0.0 - new_validators_counter_24h: new_validators_counter, active_validators_count: active_validators_counter, - # todo: It should be removed in favour `active_validators_count` property with the next release after 8.0.0 - active_validators_counter: active_validators_counter, active_validators_percentage: calculate_active_validators_percentage(active_validators_counter, validators_counter) }) @@ -129,11 +123,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do conn |> json(%{ validators_count: validators_counter, - # todo: It should be removed in favour `validators_count` property with the next release after 8.0.0 - validators_counter: validators_counter, - new_validators_count_24h: new_validators_counter, - # todo: It should be removed in favour `new_validators_count_24h` property with the next release after 8.0.0 - new_validators_counter_24h: new_validators_counter + new_validators_count_24h: new_validators_counter }) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index 1cf03e589928..b374a84a4ae7 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -38,10 +38,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do conn |> json(%{ withdrawals_count: Chain.count_withdrawals_from_cache(api?: true), - withdrawals_sum: Chain.sum_withdrawals_from_cache(api?: true), - # todo: Both properties below should be removed in favour `withdrawals_count` and `withdrawals_sum` properties with the next release after 8.0.0 - withdrawal_count: Chain.count_withdrawals_from_cache(api?: true), - withdrawal_sum: Chain.sum_withdrawals_from_cache(api?: true) + withdrawals_sum: Chain.sum_withdrawals_from_cache(api?: true) }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex index 8a47ff18c010..bb274bbb3f3e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex @@ -14,7 +14,7 @@ defmodule BlockScoutWeb.ChainController do alias Phoenix.View def show(conn, _params) do - transaction_count = TransactionsCount.get() + transactions_count = TransactionsCount.get() total_gas_usage = GasUsageSum.total() block_count = BlocksCount.get() address_count = AddressesCount.fetch() @@ -49,7 +49,7 @@ defmodule BlockScoutWeb.ChainController do chart_config_json: Jason.encode!(chart_config), chart_data_paths: chart_data_paths, market_cap_calculation: market_cap_calculation, - transaction_estimated_count: transaction_count, + transaction_estimated_count: transactions_count, total_gas_usage: total_gas_usage, transactions_path: recent_transactions_path(conn, :index), transaction_stats: transaction_stats, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex index a9f5c60d8a6d..2c728e9b47e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex @@ -13,9 +13,9 @@ defmodule BlockScoutWeb.Tokens.TokenController do def token_counters(conn, %{"id" => address_hash_string}) do case Chain.string_to_address_hash(address_hash_string) do {:ok, address_hash} -> - {transfer_count, token_holder_count} = Chain.fetch_token_counters(address_hash, 30_000) + {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 30_000) - json(conn, %{transfer_count: transfer_count, token_holder_count: token_holder_count}) + json(conn, %{transfer_count: transfers_count, token_holder_count: holders_count}) _ -> not_found(conn) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 4cfa005a53e5..29617cdfc969 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -282,9 +282,6 @@ defmodule BlockScoutWeb.PagingHelper do defp do_tokens_sorting("fiat_value", "desc"), do: [desc_nulls_last: :fiat_value] defp do_tokens_sorting("holders_count", "asc"), do: [asc_nulls_first: :holder_count] defp do_tokens_sorting("holders_count", "desc"), do: [desc_nulls_last: :holder_count] - # todo: Next 2 clauses should be removed in favour `holders_count` property with the next release after 8.0.0 - defp do_tokens_sorting("holder_count", "asc"), do: [asc_nulls_first: :holder_count] - defp do_tokens_sorting("holder_count", "desc"), do: [desc_nulls_last: :holder_count] defp do_tokens_sorting("circulating_market_cap", "asc"), do: [asc_nulls_first: :circulating_market_cap] defp do_tokens_sorting("circulating_market_cap", "desc"), do: [desc_nulls_last: :circulating_market_cap] defp do_tokens_sorting(_, _), do: [] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index c5d2f578b0b3..6827c6f6307d 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -49,7 +49,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do description: "Token struct", type: :object, properties: %{ - address: General.AddressHash, + address_hash: General.AddressHash, symbol: %Schema{type: :string, nullable: false}, name: %Schema{type: :string, nullable: false}, decimals: General.IntegerStringNullable, @@ -57,7 +57,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do anyOf: [Type], nullable: true }, - holders: General.IntegerStringNullable, + holders_count: General.IntegerStringNullable, exchange_rate: General.FloatStringNullable, volume_24h: General.FloatStringNullable, total_supply: General.IntegerStringNullable, @@ -65,12 +65,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do circulating_market_cap: General.FloatStringNullable }, required: [ - :address, + :address_hash, :symbol, :name, :decimals, :type, - :holders, + :holders_count, :exchange_rate, :volume_24h, :total_supply, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 6fdffefb63e7..a2da2fe63718 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -84,7 +84,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do - Map containing: - `:hash` - address hash - `:coin_balance` - current coin balance value - - `:transaction_count` - number of transactions as string + - `:transactions_count` - number of transactions as string - Additional address info fields from Helper.address_with_info/4 """ @spec prepare_address_for_list(Address.t()) :: map() @@ -92,8 +92,6 @@ defmodule BlockScoutWeb.API.V2.AddressView do nil |> Helper.address_with_info(address, address.hash, true) |> Map.put(:transactions_count, to_string(address.transactions_count)) - # todo: It should be removed in favour `transaction_count` property with the next release after 8.0.0 - |> Map.put(:transaction_count, to_string(address.transactions_count)) |> Map.put(:coin_balance, if(address.fetched_coin_balance, do: address.fetched_coin_balance.value)) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex index f0645649e9ac..7feb96632540 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex @@ -29,8 +29,6 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do %{ "id" => msg.message_id, "origination_address_hash" => msg.originator_address, - # todo: It should be removed in favour `origination_address_hash` property with the next release after 8.0.0 - "origination_address" => msg.originator_address, "origination_transaction_hash" => msg.originating_transaction_hash, "origination_timestamp" => msg.origination_timestamp, "origination_transaction_block_number" => msg.originating_transaction_block_number, @@ -76,9 +74,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do def render("arbitrum_claim_message.json", %{calldata: calldata, address: address}) do %{ "calldata" => calldata, - "outbox_address_hash" => address, - # todo: It should be removed in favour `contract_address_hash` property with the next release after 8.0.0 - "outbox_address" => address + "outbox_address_hash" => address } end @@ -93,11 +89,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do "id" => withdraw.message_id, "status" => withdraw.status, "caller_address_hash" => withdraw.caller, - # todo: "caller"" should be removed in favour `caller_address_hash` property with the next release after 8.0.0 - "caller" => withdraw.caller, "destination_address_hash" => withdraw.destination, - # todo: "destination" should be removed in favour `destination_address_hash` property with the next release after 8.0.0 - "destination" => withdraw.destination, "arb_block_number" => withdraw.arb_block_number, "eth_block_number" => withdraw.eth_block_number, "l2_timestamp" => withdraw.l2_timestamp, @@ -124,16 +116,8 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do "transactions_count" => batch.transactions_count, "start_block_number" => batch.start_block, "end_block_number" => batch.end_block, - # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0 - "start_block" => batch.start_block, - # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0 - "end_block" => batch.end_block, "before_acc_hash" => batch.before_acc, - # todo: It should be removed in favour `before_acc_hash` property with the next release after 8.0.0 - "before_acc" => batch.before_acc, - "after_acc_hash" => batch.after_acc, - # todo: It should be removed in favour `after_acc_hash` property with the next release after 8.0.0 - "after_acc" => batch.after_acc + "after_acc_hash" => batch.after_acc } |> add_l1_transaction_info(batch) |> add_da_info(batch) @@ -670,8 +654,6 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do %{ "message_id" => APIV2Helper.get_2map_data(arbitrum_transaction, :arbitrum_message_from_l2, :message_id), "associated_l1_transaction_hash" => l1_transaction, - # todo: It should be removed in favour `associated_l1_transaction_hash` property with the next release after 8.0.0 - "associated_l1_transaction" => l1_transaction, "message_status" => status } end @@ -724,8 +706,6 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do out_json |> Map.put("delayed_messages", Hash.to_integer(arbitrum_block.nonce)) |> Map.put("l1_block_number", Map.get(arbitrum_block, :l1_block_number)) - # todo: It should be removed in favour `l1_block_number` property with the next release after 8.0.0 - |> Map.put("l1_block_height", Map.get(arbitrum_block, :l1_block_number)) |> Map.put("send_count", Map.get(arbitrum_block, :send_count)) |> Map.put("send_root", Map.get(arbitrum_block, :send_root)) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 87db1c7074d1..ae3e30a01ed5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -48,8 +48,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do "height" => block.number, "timestamp" => block.timestamp, "transactions_count" => block.transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => block.transactions_count, "internal_transactions_count" => count_internal_transactions(block), "miner" => Helper.address_with_info(nil, block.miner, block.miner_hash, false), "size" => block.size, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index ee4841e18747..9ce1835d57a9 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -358,12 +358,12 @@ defmodule BlockScoutWeb.API.V2.CeloView do ## Example iex> transfers = %{ - ...> reserve_bolster_transfer: %{"token" => %{"address" => "0xABC..."}, "total" => %{"value" => Decimal.new("100")}}, - ...> community_transfer: %{"token" => %{"address" => "0xABC..."}, "total" => %{"value" => Decimal.new("200")}} + ...> reserve_bolster_transfer: %{"token" => %{"address_hash" => "0xABC..."}, "total" => %{"value" => Decimal.new("100")}}, + ...> community_transfer: %{"token" => %{"address_hash" => "0xABC..."}, "total" => %{"value" => Decimal.new("200")}} ...> } iex> calculate_total_epoch_rewards(transfers) %{ - token: %{"address" => "0xABC...", ...}, + token: %{"address_hash" => "0xABC...", ...}, total: %{decimals: Decimal.new("18"), value: Decimal.new("300")} } """ @@ -443,7 +443,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do defp fee_handler_base_fee_breakdown(base_fee, block_number) do with {:ok, fee_handler_contract_address_hash} <- CeloCoreContracts.get_address(:fee_handler, block_number), - {:ok, %{"address" => fee_beneficiary_address_hash}} <- + {:ok, %{"address_hash" => fee_beneficiary_address_hash}} <- CeloCoreContracts.get_event(:fee_handler, :fee_beneficiary_set, block_number), {:ok, %{"value" => burn_fraction_fixidity_lib}} <- CeloCoreContracts.get_event(:fee_handler, :burn_fraction_set, block_number), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex index caab5e705117..dbf5a0237b6b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex @@ -28,8 +28,6 @@ defmodule BlockScoutWeb.API.V2.EthereumView do extended_out_json = out_json |> Map.put("blob_transactions_count", blob_transaction_count) - # todo: It should be removed in favour `blob_transactions_count` property with the next release after 8.0.0 - |> Map.put("blob_transaction_count", blob_transaction_count) |> Map.put("blob_gas_used", blob_gas_used) |> Map.put("excess_blob_gas", excess_blob_gas) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex index 6a49913d632f..83e5c7622d3e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex @@ -83,11 +83,7 @@ defmodule BlockScoutWeb.API.V2.MudView do defp prepare_world_for_list(%Address{} = address) do %{ "address_hash" => Helper.address_with_info(address, address.hash), - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => Helper.address_with_info(address, address.hash), "transactions_count" => address.transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => address.transactions_count, "coin_balance" => if(address.fetched_coin_balance, do: address.fetched_coin_balance.value) } end @@ -95,9 +91,7 @@ defmodule BlockScoutWeb.API.V2.MudView do defp prepare_system_for_list({system_id, system}) do %{ name: system_id |> Table.from() |> Map.get(:table_full_name), - address_hash: system, - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 - address: system + address_hash: system } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index e25944d09fd1..830765ae1965 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -20,7 +20,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do batches |> Enum.map(fn batch -> Task.async(fn -> - transaction_count = + transactions_count = Repo.replica().aggregate( from( t in Transaction, @@ -34,9 +34,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do %{ "l2_block_number" => batch.l2_block_number, - "transactions_count" => transaction_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => transaction_count, + "transactions_count" => transactions_count, "l1_transaction_hashes" => batch.frame_sequence.l1_transaction_hashes, "l1_timestamp" => batch.frame_sequence.l1_timestamp } @@ -125,8 +123,6 @@ defmodule BlockScoutWeb.API.V2.OptimismView do %{ "index" => g.index, "game_type" => g.game_type, - # todo: It should be removed in favour `contract_address_hash` property with the next release after 8.0.0 - "contract_address" => g.address_hash, "contract_address_hash" => g.address_hash, "l2_block_number" => l2_block_number, "created_at" => g.created_at, @@ -343,7 +339,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do # - `internal_id`: The internal ID of the batch. # - `l2_block_number_from`: Start L2 block number of the batch block range. # - `l2_block_number_to`: End L2 block number of the batch block range. - # - `transaction_count`: The L2 transaction count included into the blocks of the range. + # - `transactions_count`: The L2 transaction count included into the blocks of the range. # - `batch`: Either an `Explorer.Chain.Optimism.FrameSequence` entry or a map with # the corresponding fields. # @@ -358,23 +354,19 @@ defmodule BlockScoutWeb.API.V2.OptimismView do | %{:l1_timestamp => DateTime.t(), :l1_transaction_hashes => list(), optional(any()) => any()} ) :: %{ :number => non_neg_integer(), - :internal_id => non_neg_integer(), :l1_timestamp => DateTime.t(), :l2_start_block_number => non_neg_integer(), - :l2_block_start => non_neg_integer(), :l2_end_block_number => non_neg_integer(), - :l2_block_end => non_neg_integer(), :transactions_count => non_neg_integer(), - :transaction_count => non_neg_integer(), :l1_transaction_hashes => list(), :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil } - defp render_base_info_for_batch(internal_id, l2_block_number_from, l2_block_number_to, transaction_count, batch) do + defp render_base_info_for_batch(internal_id, l2_block_number_from, l2_block_number_to, transactions_count, batch) do FrameSequence.prepare_base_info_for_batch( internal_id, l2_block_number_from, l2_block_number_to, - transaction_count, + transactions_count, batch.batch_data_container, batch ) @@ -408,8 +400,6 @@ defmodule BlockScoutWeb.API.V2.OptimismView do batch_info = %{ "number" => frame_sequence.id, - # todo: It should be removed in favour `number` property with the next release after 8.0.0 - "internal_id" => frame_sequence.id, "l1_timestamp" => frame_sequence.l1_timestamp, "l1_transaction_hashes" => frame_sequence.l1_transaction_hashes, "batch_data_container" => batch_data_container diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex index 112c36c5ed92..63b726f35663 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_zkevm_view.ex @@ -156,8 +156,6 @@ defmodule BlockScoutWeb.API.V2.PolygonZkevmView do "status" => batch_status(batch), "timestamp" => batch.timestamp, "transactions_count" => batch.l2_transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => batch.l2_transactions_count, "sequence_transaction_hash" => sequence_transaction_hash, "verify_transaction_hash" => verify_transaction_hash } diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex index bb11003bb15e..4599c2a02a57 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex @@ -126,13 +126,7 @@ defmodule BlockScoutWeb.API.V2.ScrollView do }, "start_block_number" => start_block_number, "end_block_number" => end_block_number, - # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0 - "start_block" => start_block_number, - # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0 - "end_block" => end_block_number, - "transactions_count" => transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => transactions_count + "transactions_count" => transactions_count } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index d0efb14ddc06..27399c7ad9d0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -32,8 +32,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do "name" => search_result.name, "symbol" => search_result.symbol, "address_hash" => search_result.address_hash, - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => search_result.address_hash, "token_url" => token_path(Endpoint, :show, search_result.address_hash), "address_url" => address_path(Endpoint, :show, search_result.address_hash), "icon_url" => search_result.icon_url, @@ -54,8 +52,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do "type" => search_result.type, "name" => search_result.name, "address_hash" => search_result.address_hash, - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => search_result.address_hash, "url" => address_path(Endpoint, :show, search_result.address_hash), "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], @@ -70,8 +66,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do "type" => search_result.type, "name" => search_result.name, "address_hash" => search_result.address_hash, - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => search_result.address_hash, "url" => address_path(Endpoint, :show, search_result.address_hash), "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], @@ -85,8 +79,6 @@ defmodule BlockScoutWeb.API.V2.SearchView do "type" => search_result.type, "name" => search_result.name, "address_hash" => search_result.address_hash, - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => search_result.address_hash, "url" => address_path(Endpoint, :show, search_result.address_hash), "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index cc105e897599..c5469b1118ad 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -313,8 +313,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "compiler_version" => smart_contract.compiler_version, "optimization_enabled" => smart_contract.optimization, "transactions_count" => address.transactions_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => address.transactions_count, "language" => SmartContract.language(smart_contract), "verified_at" => smart_contract.inserted_at, "market_cap" => token && token.circulating_market_cap, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index 43d178f240be..d8b10f82716d 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -11,15 +11,11 @@ defmodule BlockScoutWeb.API.V2.TokenView do def render("token.json", %{token: nil = token, contract_address_hash: contract_address_hash}) do %{ "address_hash" => Address.checksum(contract_address_hash), - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => Address.checksum(contract_address_hash), "symbol" => nil, "name" => nil, "decimals" => nil, "type" => nil, "holders_count" => nil, - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holders" => nil, "exchange_rate" => nil, "volume_24h" => nil, "total_supply" => nil, @@ -36,15 +32,11 @@ defmodule BlockScoutWeb.API.V2.TokenView do def render("token.json", %{token: token}) do %{ "address_hash" => Address.checksum(token.contract_address_hash), - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => Address.checksum(token.contract_address_hash), "symbol" => token.symbol, "name" => token.name, "decimals" => token.decimals, "type" => token.type, "holders_count" => prepare_holders_count(token.holder_count), - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holders" => prepare_holders_count(token.holder_count), "exchange_rate" => exchange_rate(token), "volume_24h" => token.volume_24h, "total_supply" => token.total_supply, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 4243eec757a7..abdc79805038 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -380,8 +380,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def prepare_signed_authorization(signed_authorization) do %{ "address_hash" => Address.checksum(signed_authorization.address), - # todo: It should be removed in favour `address_hash` property with the next release after 8.0.0 - "address" => Address.checksum(signed_authorization.address), "chain_id" => signed_authorization.chain_id, "nonce" => signed_authorization.nonce, "r" => signed_authorization.r, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex index 354340be5b5f..10813623399c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/validator_view.ex @@ -49,11 +49,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorView do "slashed" => validator.slashing_status_is_slashed, "block_number" => validator.slashing_status_by_block, "multiplier" => validator.slashing_status_multiplier - }, - # todo: Next 3 props should be removed in favour `slashing_status` property with the next release after 8.0.0 - "slashing_status_is_slashed" => validator.slashing_status_is_slashed, - "slashing_status_by_block" => validator.slashing_status_by_block, - "slashing_status_multiplier" => validator.slashing_status_multiplier + } } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex index 25f6bd447294..ff76194930d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zksync_view.ex @@ -16,19 +16,11 @@ defmodule BlockScoutWeb.API.V2.ZkSyncView do "timestamp" => batch.timestamp, "root_hash" => batch.root_hash, "l1_transactions_count" => batch.l1_transaction_count, - # todo: It should be removed in favour `l1_transactions_count` property with the next release after 8.0.0 - "l1_transaction_count" => batch.l1_transaction_count, "l2_transactions_count" => batch.l2_transaction_count, - # todo: It should be removed in favour `l2_transactions_count` property with the next release after 8.0.0 - "l2_transaction_count" => batch.l2_transaction_count, "l1_gas_price" => batch.l1_gas_price, "l2_fair_gas_price" => batch.l2_fair_gas_price, "start_block_number" => batch.start_block, - "end_block_number" => batch.end_block, - # todo: It should be removed in favour `start_block_number` property with the next release after 8.0.0 - "start_block" => batch.start_block, - # todo: It should be removed in favour `end_block_number` property with the next release after 8.0.0 - "end_block" => batch.end_block + "end_block_number" => batch.end_block } |> add_l1_transactions_info_and_status(batch) end @@ -72,9 +64,7 @@ defmodule BlockScoutWeb.API.V2.ZkSyncView do %{ "number" => batch.number, "timestamp" => batch.timestamp, - "transactions_count" => batch.l1_transaction_count + batch.l2_transaction_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - "transaction_count" => batch.l1_transaction_count + batch.l2_transaction_count + "transactions_count" => batch.l1_transaction_count + batch.l2_transaction_count } |> add_l1_transactions_info_and_status(batch) end) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 6b93c0f4d575..4adf9032e60d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -328,7 +328,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "implementations" => [ %{ "address_hash" => ^checksummed_implementation_contract_address_hash, - "address" => ^checksummed_implementation_contract_address_hash, "name" => ^name } ] @@ -383,7 +382,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "implementations" => [ %{ "address_hash" => ^implementation_address_hash_string, - "address" => ^implementation_address_hash_string, "name" => nil } ] @@ -460,7 +458,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "implementations" => [ %{ "address_hash" => ^implementation_address_hash_string, - "address" => ^implementation_address_hash_string, "name" => nil } ] @@ -621,7 +618,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do insert(:block, miner: address) - Counters.transaction_count(address) + Counters.transactions_count(address) Counters.token_transfers_count(address) Counters.gas_usage_count(address) @@ -3376,14 +3373,14 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do :timer.seconds(1) assert Decimal.to_integer(ctb_erc_20["value"]) == - other_balances[ctb_erc_20["token"]["address"] |> String.downcase()] + other_balances[ctb_erc_20["token"]["address_hash"] |> String.downcase()] assert Decimal.to_integer(ctb_erc_721["value"]) == - other_balances[ctb_erc_721["token"]["address"] |> String.downcase()] + other_balances[ctb_erc_721["token"]["address_hash"] |> String.downcase()] assert Decimal.to_integer(ctb_erc_1155["value"]) == balances_erc_1155[ - {ctb_erc_1155["token"]["address"] |> String.downcase(), to_string(ctb_erc_1155["token_id"])} + {ctb_erc_1155["token"]["address_hash"] |> String.downcase(), to_string(ctb_erc_1155["token_id"])} ] end end @@ -4459,7 +4456,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do defp compare_item(%Address{} = address, json) do assert Address.checksum(address.hash) == json["hash"] - assert to_string(address.transactions_count) == json["transaction_count"] + assert to_string(address.transactions_count) == json["transactions_count"] end defp compare_item(%Transaction{} = transaction, json) do @@ -4510,12 +4507,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end defp compare_item(%Token{} = token, json) do - assert Address.checksum(token.contract_address_hash) == json["address"] + assert Address.checksum(token.contract_address_hash) == json["address_hash"] assert to_string(token.symbol) == json["symbol"] assert to_string(token.name) == json["name"] assert to_string(token.type) == json["type"] assert to_string(token.decimals) == json["decimals"] - assert (token.holder_count && to_string(token.holder_count)) == json["holders"] + assert (token.holder_count && to_string(token.holder_count)) == json["holders_count"] assert Map.has_key?(json, "exchange_rate") end @@ -4549,7 +4546,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "id" => ^id, "metadata" => ^metadata, "owner" => nil, - "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "token" => %{"address_hash" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, "external_app_url" => ^app_url, "animation_url" => ^animation_url, "image_url" => ^image_url, @@ -4573,7 +4570,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end) assert %{ - "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "token" => %{"address_hash" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, "amount" => ^amount } = json end @@ -4594,7 +4591,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end) assert %{ - "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "token" => %{"address_hash" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, "amount" => ^amount } = json end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index a153b45531fd..0b00186f24ba 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -102,7 +102,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["type"] == "address" assert item["name"] == name.name - assert item["address"] == Address.checksum(address.hash) + assert item["address_hash"] == Address.checksum(address.hash) assert item["url"] =~ Address.checksum(address.hash) assert item["is_smart_contract_verified"] == address.verified end @@ -120,7 +120,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["type"] == "contract" assert item["name"] == contract.name - assert item["address"] == Address.checksum(contract.address_hash) + assert item["address_hash"] == Address.checksum(contract.address_hash) assert item["url"] =~ Address.checksum(contract.address_hash) assert item["is_smart_contract_verified"] == true end @@ -205,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(labels_from_api) |> Enum.all?(fn {label, item} -> label.tag.display_name == item["name"] && item["type"] == "label" && - item["address"] == Address.checksum(label.address_hash) + item["address_hash"] == Address.checksum(label.address_hash) end) tokens_from_api = Enum.slice(response_2["items"], 1, 49) ++ Enum.slice(response_3["items"], 0, 2) @@ -214,7 +214,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(tokens_from_api) |> Enum.all?(fn {token, item} -> token.name == item["name"] && item["type"] == "token" && - item["address"] == Address.checksum(token.contract_address_hash) + item["address_hash"] == Address.checksum(token.contract_address_hash) end) contracts_from_api = Enum.slice(response_3["items"], 2, 48) ++ response_4["items"] @@ -223,7 +223,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(contracts_from_api) |> Enum.all?(fn {contract, item} -> contract.name == item["name"] && item["type"] == "contract" && - item["address"] == Address.checksum(contract.address_hash) + item["address_hash"] == Address.checksum(contract.address_hash) end) end @@ -278,7 +278,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(labels_from_api) |> Enum.all?(fn {label, item} -> label.tag.display_name == item["name"] && item["type"] == "label" && - item["address"] == Address.checksum(label.address_hash) + item["address_hash"] == Address.checksum(label.address_hash) end) tokens_from_api = Enum.slice(response_2["items"], 1, 49) ++ Enum.slice(response_3["items"], 0, 2) @@ -287,7 +287,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(tokens_from_api) |> Enum.all?(fn {token, item} -> token.name == item["name"] && item["type"] == "token" && - item["address"] == Address.checksum(token.contract_address_hash) + item["address_hash"] == Address.checksum(token.contract_address_hash) end) contracts_from_api = Enum.slice(response_3["items"], 2, 48) ++ response_4["items"] @@ -296,7 +296,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(contracts_from_api) |> Enum.all?(fn {contract, item} -> contract.name == item["name"] && item["type"] == "contract" && - item["address"] == Address.checksum(contract.address_hash) + item["address_hash"] == Address.checksum(contract.address_hash) end) end @@ -314,7 +314,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["type"] == "token" assert item["name"] == token.name assert item["symbol"] == token.symbol - assert item["address"] == Address.checksum(token.contract_address_hash) + assert item["address_hash"] == Address.checksum(token.contract_address_hash) assert item["token_url"] =~ Address.checksum(token.contract_address_hash) assert item["address_url"] =~ Address.checksum(token.contract_address_hash) assert item["token_type"] == token.type @@ -339,7 +339,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert item["type"] == "token" assert item["name"] == token.name assert item["symbol"] == token.symbol - assert item["address"] == Address.checksum(token.contract_address_hash) + assert item["address_hash"] == Address.checksum(token.contract_address_hash) assert item["token_url"] =~ Address.checksum(token.contract_address_hash) assert item["address_url"] =~ Address.checksum(token.contract_address_hash) assert item["token_type"] == token.type @@ -410,7 +410,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do item = Enum.at(response["items"], 0) assert item["type"] == "label" - assert item["address"] == Address.checksum(tag.address.hash) + assert item["address_hash"] == Address.checksum(tag.address.hash) assert item["name"] == tag.tag.display_name assert item["url"] =~ Address.checksum(tag.address.hash) assert item["is_smart_contract_verified"] == tag.address.verified @@ -723,7 +723,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(labels_from_api) |> Enum.all?(fn {label, item} -> label.tag.display_name == item["name"] && item["type"] == "label" && - item["address"] == Address.checksum(label.address_hash) + item["address_hash"] == Address.checksum(label.address_hash) end) tokens_from_api = Enum.slice(response_2["items"], 2, 48) ++ Enum.slice(response_3["items"], 0, 3) @@ -732,7 +732,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(tokens_from_api) |> Enum.all?(fn {token, item} -> token.name == item["name"] && item["type"] == "token" && - item["address"] == Address.checksum(token.contract_address_hash) + item["address_hash"] == Address.checksum(token.contract_address_hash) end) contracts_from_api = Enum.slice(response_4["items"], 5, 45) ++ response_5["items"] @@ -741,7 +741,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(contracts_from_api) |> Enum.all?(fn {contract, item} -> contract.name == item["name"] && item["type"] == "contract" && - item["address"] == Address.checksum(contract.address_hash) + item["address_hash"] == Address.checksum(contract.address_hash) end) metadata_tags_from_api = Enum.slice(response_3["items"], 3, 47) ++ Enum.slice(response_4["items"], 0, 5) @@ -760,11 +760,11 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.all?(fn {{address_hash, tag}, api_item} -> tag["name"] == api_item["metadata"]["name"] && tag["slug"] == api_item["metadata"]["slug"] && api_item["type"] == "metadata_tag" && - api_item["address"] == address_hash + api_item["address_hash"] == address_hash end) ens = Enum.at(response["items"], 0) - assert ens["address"] == to_string(ens_address) + assert ens["address_hash"] == to_string(ens_address) assert ens["ens_info"]["name"] == name end @@ -1077,7 +1077,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(labels_from_api) |> Enum.all?(fn {label, item} -> label.tag.display_name == item["name"] && item["type"] == "label" && - item["address"] == Address.checksum(label.address_hash) + item["address_hash"] == Address.checksum(label.address_hash) end) tokens_from_api = Enum.slice(response_2["items"], 2, 48) ++ Enum.slice(response_3["items"], 0, 3) @@ -1086,7 +1086,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(tokens_from_api) |> Enum.all?(fn {token, item} -> token.name == item["name"] && item["type"] == "token" && - item["address"] == Address.checksum(token.contract_address_hash) + item["address_hash"] == Address.checksum(token.contract_address_hash) end) contracts_from_api = Enum.slice(response_4["items"], 5, 45) ++ response_5["items"] @@ -1095,7 +1095,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.zip(contracts_from_api) |> Enum.all?(fn {contract, item} -> contract.name == item["name"] && item["type"] == "contract" && - item["address"] == Address.checksum(contract.address_hash) + item["address_hash"] == Address.checksum(contract.address_hash) end) metadata_tags_from_api = Enum.slice(response_3["items"], 3, 47) ++ Enum.slice(response_4["items"], 0, 5) @@ -1114,11 +1114,11 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do |> Enum.all?(fn {{address_hash, tag}, api_item} -> tag["name"] == api_item["metadata"]["name"] && tag["slug"] == api_item["metadata"]["slug"] && api_item["type"] == "metadata_tag" && - api_item["address"] == address_hash + api_item["address_hash"] == address_hash end) ens = Enum.at(response["items"], 0) - assert ens["address"] == to_string(ens_address) + assert ens["address_hash"] == to_string(ens_address) assert ens["ens_info"]["name"] == name end @@ -1678,7 +1678,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" - transaction = insert(:transaction, hash: operation_id) + insert(:transaction, hash: operation_id) tac_first_response = """ { @@ -1863,16 +1863,16 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert response = json_response(request, 200) assert Enum.count(response) == 50 - assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address_hash"] end) == tags |> Enum.reverse() |> Enum.take(16) |> Enum.map(fn tag -> Address.checksum(tag.address.hash) end) - assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address_hash"] end) == contracts |> Enum.reverse() |> Enum.take(16) |> Enum.map(fn contract -> Address.checksum(contract.address_hash) end) - assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address_hash"] end) == tokens |> Enum.reverse() |> Enum.sort_by(fn x -> x.is_verified_via_admin_panel end, :desc) @@ -2010,25 +2010,26 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert response = json_response(request, 200) assert Enum.count(response) == 50 - assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "label" end) |> Enum.map(fn x -> x["address_hash"] end) == tags |> Enum.reverse() |> Enum.take(12) |> Enum.map(fn tag -> Address.checksum(tag.address.hash) end) - assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "contract" end) |> Enum.map(fn x -> x["address_hash"] end) == contracts |> Enum.reverse() |> Enum.take(12) |> Enum.map(fn contract -> Address.checksum(contract.address_hash) end) - assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address"] end) == + assert response |> Enum.filter(fn x -> x["type"] == "token" end) |> Enum.map(fn x -> x["address_hash"] end) == tokens |> Enum.reverse() |> Enum.sort_by(fn x -> x.is_verified_via_admin_panel end, :desc) |> Enum.take(13) |> Enum.map(fn token -> Address.checksum(token.contract_address_hash) end) - assert response |> Enum.filter(fn x -> x["type"] == "ens_domain" end) |> Enum.map(fn x -> x["address"] end) == [ - to_string(ens_address) - ] + assert response |> Enum.filter(fn x -> x["type"] == "ens_domain" end) |> Enum.map(fn x -> x["address_hash"] end) == + [ + to_string(ens_address) + ] metadata_tags = response |> Enum.filter(fn x -> x["type"] == "metadata_tag" end) @@ -2037,7 +2038,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert metadata_tags |> Enum.with_index() |> Enum.all?(fn {x, index} -> - x["address"] == to_string(address_1) && x["metadata"]["name"] == "#{name} #{index}" + x["address_hash"] == to_string(address_1) && x["metadata"]["name"] == "#{name} #{index}" end) end @@ -2296,7 +2297,6 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do correct_response = [ %{ - "address" => address_hash, "address_hash" => address_hash, "certified" => false, "ens_info" => nil, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index f22bbbe637f3..cb989999e3e7 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -204,7 +204,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => formatted_implementation_address_hash_string, - "address" => formatted_implementation_address_hash_string, "name" => nil } ], @@ -514,7 +513,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => Address.checksum(implementation_contract.address_hash), - "address" => Address.checksum(implementation_contract.address_hash), "name" => implementation_contract.name } ] @@ -670,7 +668,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => formatted_implementation_address_hash_string, - "address" => formatted_implementation_address_hash_string, "name" => implementation_contract.name } ], @@ -1119,7 +1116,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ prepare_implementation(%{ "address_hash" => formatted_implementation_address_hash_string, - "address" => formatted_implementation_address_hash_string, "name" => nil }) ], @@ -1137,7 +1133,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => ^formatted_implementation_address_hash_string, - "address" => ^formatted_implementation_address_hash_string, "name" => nil } ] @@ -1238,7 +1233,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ prepare_implementation(%{ "address_hash" => formatted_implementation_address_hash_string, - "address" => formatted_implementation_address_hash_string, "name" => nil }) ], @@ -1256,7 +1250,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => ^formatted_implementation_address_hash_string, - "address" => ^formatted_implementation_address_hash_string, "name" => nil } ] @@ -1357,7 +1350,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ prepare_implementation(%{ "address_hash" => formatted_implementation_address_hash_string, - "address" => formatted_implementation_address_hash_string, "name" => nil }) ], @@ -1375,7 +1367,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [ %{ "address_hash" => ^formatted_implementation_address_hash_string, - "address" => ^formatted_implementation_address_hash_string, "name" => nil } ] @@ -1787,7 +1778,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Enum.map(items, &prepare_implementation/1) end - defp prepare_implementation(%{"address" => _, "name" => _} = implementation) do + defp prepare_implementation(%{"address_hash" => _, "name" => _} = implementation) do case Application.get_env(:explorer, :chain_type) do :filecoin -> Map.put(implementation, "filecoin_robust_address", nil) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index 08d0dd83a37c..da95abc6122b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -497,7 +497,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do tokens_ordered_by_holders = Enum.sort(tokens, &(&1.holder_count <= &2.holder_count)) request_ordered_by_holders = - get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "desc"})) + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holders_count", "order" => "desc"})) assert response_ordered_by_holders = json_response(request_ordered_by_holders, 200) @@ -506,7 +506,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do conn, "/api/v2/tokens", additional_params - |> Map.merge(%{"sort" => "holder_count", "order" => "desc"}) + |> Map.merge(%{"sort" => "holders_count", "order" => "desc"}) |> Map.merge(response_ordered_by_holders["next_page_params"]) ) @@ -521,7 +521,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do tokens_ordered_by_holders_asc = Enum.sort(tokens, &(&1.holder_count >= &2.holder_count)) request_ordered_by_holders_asc = - get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "asc"})) + get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holders_count", "order" => "asc"})) assert response_ordered_by_holders_asc = json_response(request_ordered_by_holders_asc, 200) @@ -530,7 +530,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do conn, "/api/v2/tokens", additional_params - |> Map.merge(%{"sort" => "holder_count", "order" => "asc"}) + |> Map.merge(%{"sort" => "holders_count", "order" => "asc"}) |> Map.merge(response_ordered_by_holders_asc["next_page_params"]) ) @@ -2312,14 +2312,14 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do end def compare_item(%Token{} = token, json) do - assert Address.checksum(token.contract_address.hash) == json["address"] + assert Address.checksum(token.contract_address.hash) == json["address_hash"] assert token.symbol == json["symbol"] assert token.name == json["name"] assert to_string(token.decimals) == json["decimals"] assert token.type == json["type"] - assert (is_nil(token.holder_count) and is_nil(json["holders"])) or - (to_string(token.holder_count) == json["holders"] and !is_nil(token.holder_count)) + assert (is_nil(token.holder_count) and is_nil(json["holders_count"])) or + (to_string(token.holder_count) == json["holders_count"] and !is_nil(token.holder_count)) assert to_string(token.total_supply) == json["total_supply"] assert Map.has_key?(json, "exchange_rate") @@ -2360,7 +2360,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do "value" => ^value, "id" => ^id, "metadata" => ^metadata, - "token" => %{"address" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, + "token" => %{"address_hash" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, "external_app_url" => ^app_url, "animation_url" => ^animation_url, "image_url" => ^image_url, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 2a0d2274e0f1..e60e6724600b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -1265,7 +1265,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^token_address_hash, + "address_hash" => ^token_address_hash, "name" => ^token_name, "symbol" => ^token_symbol, "type" => ^token_type @@ -1280,7 +1280,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert %{ "celo" => %{ "gas_token" => %{ - "address" => ^token_address_hash, + "address_hash" => ^token_address_hash, "name" => ^token_name, "symbol" => ^token_symbol, "type" => ^token_type @@ -1295,7 +1295,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^token_address_hash, + "address_hash" => ^token_address_hash, "name" => ^token_name, "symbol" => ^token_symbol, "type" => ^token_type @@ -1311,7 +1311,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^token_address_hash, + "address_hash" => ^token_address_hash, "name" => ^token_name, "symbol" => ^token_symbol, "type" => ^token_type @@ -1338,7 +1338,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^unknown_token_address_hash + "address_hash" => ^unknown_token_address_hash } } } @@ -1350,7 +1350,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert %{ "celo" => %{ "gas_token" => %{ - "address" => ^unknown_token_address_hash + "address_hash" => ^unknown_token_address_hash } } } = json_response(request, 200) @@ -1362,7 +1362,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^unknown_token_address_hash + "address_hash" => ^unknown_token_address_hash } } } @@ -1375,7 +1375,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do %{ "celo" => %{ "gas_token" => %{ - "address" => ^unknown_token_address_hash + "address_hash" => ^unknown_token_address_hash } } } @@ -1491,7 +1491,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do "items" => [ %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", @@ -1506,7 +1506,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", @@ -1521,7 +1521,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do "items" => [ %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", @@ -1553,7 +1553,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do "items" => [ %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", @@ -1568,7 +1568,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", @@ -1583,7 +1583,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do "items" => [ %{ "stability_fee" => %{ - "token" => %{"address" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, + "token" => %{"address_hash" => "0xDc2B93f3291030F3F7a6D9363ac37757f7AD5C43"}, "validator_address" => %{"hash" => "0x46B555CB3962bF9533c437cBD04A2f702dfdB999"}, "dapp_address" => %{"hash" => "0xFAf7a981360c2FAb3a5Ab7b3D6d8D0Cf97a91Eb9"}, "total_fee" => "44136000000000", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs index a9afd8f17fe1..b19113e64c6f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/validator_controller_test.exs @@ -179,10 +179,10 @@ defmodule BlockScoutWeb.API.V2.ValidatorControllerTest do request = get(conn, "/api/v2/validators/stability/counters") assert %{ - "active_validators_counter" => "3", + "active_validators_count" => "3", "active_validators_percentage" => ^percentage, - "new_validators_counter_24h" => "6", - "validators_counter" => "9" + "new_validators_count_24h" => "6", + "validators_count" => "9" } = json_response(request, 200) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs index 88320ec50b5b..539da68d0bc5 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs @@ -43,8 +43,8 @@ defmodule BlockScoutWeb.API.V2.WithdrawalControllerTest do request = get(conn, "/api/v2/withdrawals/counters") assert %{ - "withdrawal_count" => _, - "withdrawal_sum" => _ + "withdrawals_count" => _, + "withdrawals_sum" => _ } = json_response(request, 200) end end diff --git a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex index f89c0ca99987..8826cd3a7d5f 100644 --- a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex +++ b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex @@ -419,11 +419,7 @@ defmodule Explorer.Arbitrum.ClaimRollupMessage do %{ address_hash: token_bin, - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 - address: token_bin, destination_address_hash: to_bin, - # todo: "destination" should be removed in favour `destination_address_hash` property with the next release after 8.0.0 - destination: to_bin, amount: amount, decimals: token_info.decimals, name: token_info.name, diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index fabd7f91e438..d11729f5271d 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -249,7 +249,7 @@ defmodule Explorer.Chain.Address.Counters do end) Task.start_link(fn -> - transaction_count(address) + transactions_count(address) end) Task.start_link(fn -> @@ -279,7 +279,7 @@ defmodule Explorer.Chain.Address.Counters do |> List.to_tuple() end - def transaction_count(address) do + def transactions_count(address) do AddressTransactionsCount.fetch(address) end diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index c82fa0c57f92..b3169f2862f0 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -163,31 +163,35 @@ defmodule Explorer.Chain.Blackfort.Validator do defp parse_validators_info({:ok, validators}) do {:ok, validators - |> Enum.map(fn %{ - "address" => address_hash_string, - "name" => name, - "commission" => commission, - "self_bonded_amount" => self_bonded_amount, - "delegated_amount" => delegated_amount, - "slashing_status" => %{ - "is_slashed" => slashing_status_is_slashed, - "by_block" => slashing_status_by_block, - "multiplier" => slashing_status_multiplier - } - } -> - {:ok, address_hash} = HashAddress.cast(address_hash_string) - - %{ - address_hash: address_hash, - name: name, - commission: parse_number(commission), - self_bonded_amount: parse_number(self_bonded_amount), - delegated_amount: parse_number(delegated_amount), - slashing_status_is_slashed: slashing_status_is_slashed, - slashing_status_by_block: slashing_status_by_block, - slashing_status_multiplier: slashing_status_multiplier - } - end)} + |> Enum.map( + # todo: remove it in favour `address_hash` when the frontend is bound to the new filed (create the task to frontend repo). + fn %{ + "address_hash" => address_hash_string, + "address" => address_hash_string, + "name" => name, + "commission" => commission, + "self_bonded_amount" => self_bonded_amount, + "delegated_amount" => delegated_amount, + "slashing_status" => %{ + "is_slashed" => slashing_status_is_slashed, + "by_block" => slashing_status_by_block, + "multiplier" => slashing_status_multiplier + } + } -> + {:ok, address_hash} = HashAddress.cast(address_hash_string) + + %{ + address_hash: address_hash, + name: name, + commission: parse_number(commission), + self_bonded_amount: parse_number(self_bonded_amount), + delegated_amount: parse_number(delegated_amount), + slashing_status_is_slashed: slashing_status_is_slashed, + slashing_status_by_block: slashing_status_by_block, + slashing_status_multiplier: slashing_status_multiplier + } + end + )} end defp parse_validators_info({:error, error}) do diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex index c44bfc34720a..7b5cb1a54f7c 100644 --- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex +++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex @@ -107,7 +107,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do if not is_nil(batch) do l2_block_number_from = TransactionBatch.edge_l2_block_number(internal_id, :min) l2_block_number_to = TransactionBatch.edge_l2_block_number(internal_id, :max) - transaction_count = Transaction.transaction_count_for_block_range(l2_block_number_from..l2_block_number_to) + transactions_count = Transaction.transaction_count_for_block_range(l2_block_number_from..l2_block_number_to) {batch_data_container, blobs} = if Keyword.get(options, :include_blobs?, true) do @@ -121,7 +121,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do internal_id, l2_block_number_from, l2_block_number_to, - transaction_count, + transactions_count, batch_data_container, batch ) @@ -144,7 +144,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do - `internal_id`: The internal ID of the batch. - `l2_block_number_from`: Start L2 block number of the batch block range. - `l2_block_number_to`: End L2 block number of the batch block range. - - `transaction_count`: The L2 transaction count included into the blocks of the range. + - `transactions_count`: The L2 transaction count included into the blocks of the range. - `batch_data_container`: Designates where the batch info is stored: :in_blob4844, :in_celestia, or :in_calldata. Can be `nil` if the container is unknown. - `batch`: Either an `Explorer.Chain.Optimism.FrameSequence` entry or a map with @@ -163,14 +163,10 @@ defmodule Explorer.Chain.Optimism.FrameSequence do | %{:l1_timestamp => DateTime.t(), :l1_transaction_hashes => list(), optional(any()) => any()} ) :: %{ :number => non_neg_integer(), - :internal_id => non_neg_integer(), :l1_timestamp => DateTime.t(), :l2_start_block_number => non_neg_integer(), - :l2_block_start => non_neg_integer(), :l2_end_block_number => non_neg_integer(), - :l2_block_end => non_neg_integer(), :transactions_count => non_neg_integer(), - :transaction_count => non_neg_integer(), :l1_transaction_hashes => list(), :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil } @@ -178,24 +174,16 @@ defmodule Explorer.Chain.Optimism.FrameSequence do internal_id, l2_block_number_from, l2_block_number_to, - transaction_count, + transactions_count, batch_data_container, batch ) do %{ :number => internal_id, - # todo: "internal_id" should be removed in favour `number` property with the next release after 8.0.0 - :internal_id => internal_id, :l1_timestamp => batch.l1_timestamp, :l2_start_block_number => l2_block_number_from, - # todo: It should be removed in favour `l2_start_block_number` property with the next release after 8.0.0 - :l2_block_start => l2_block_number_from, :l2_end_block_number => l2_block_number_to, - # todo: It should be removed in favour `l2_end_block_number` property with the next release after 8.0.0 - :l2_block_end => l2_block_number_to, - :transactions_count => transaction_count, - # todo: It should be removed in favour `transactions_count` property with the next release after 8.0.0 - :transaction_count => transaction_count, + :transactions_count => transactions_count, :l1_transaction_hashes => batch.l1_transaction_hashes, :batch_data_container => batch_data_container } diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index b5f3cd206507..f3fa09592896 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -825,9 +825,7 @@ defmodule Explorer.Chain.Search do "circulating_market_cap" => circulating_market_cap, "fiat_value" => fiat_value, "is_verified_via_admin_panel" => is_verified_via_admin_panel, - "holders_count" => holder_count, - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holder_count" => holder_count, + "holder_count" => holders_count, "name" => name, "inserted_at" => inserted_at } @@ -843,7 +841,7 @@ defmodule Explorer.Chain.Search do circulating_market_cap: parse_possible_nil(circulating_market_cap), fiat_value: parse_possible_nil(fiat_value), is_verified_via_admin_panel: parse_possible_nil(is_verified_via_admin_panel), - holder_count: parse_possible_nil(holder_count), + holder_count: parse_possible_nil(holders_count), name: name, inserted_at: inserted_at }, @@ -1153,7 +1151,7 @@ defmodule Explorer.Chain.Search do circulating_market_cap: circulating_market_cap, exchange_rate: exchange_rate, is_verified_via_admin_panel: is_verified_via_admin_panel, - holder_count: holder_count, + holder_count: holders_count, name: name, inserted_at: inserted_at, type: "token" @@ -1166,9 +1164,7 @@ defmodule Explorer.Chain.Search do "circulating_market_cap" => circulating_market_cap, "fiat_value" => exchange_rate, "is_verified_via_admin_panel" => is_verified_via_admin_panel, - "holders_count" => holder_count, - # todo: It should be removed in favour `holders_count` property with the next release after 8.0.0 - "holder_count" => holder_count, + "holder_count" => holders_count, "name" => name, "inserted_at" => inserted_at_datetime } diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex index 7a75b59bfb1c..6de7e9bbd1d3 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -499,10 +499,8 @@ defmodule Explorer.Chain.SmartContract.Proxy do case address do %Hash{} = address_hash -> [ - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + UPDATE PATTERN MATCHING in `chain_type_fields()` function %{ "address_hash" => Address.checksum(address_hash), - "address" => Address.checksum(address_hash), "name" => name } |> chain_type_fields(implementations_info) @@ -513,8 +511,7 @@ defmodule Explorer.Chain.SmartContract.Proxy do with {:ok, address_hash} <- string_to_address_hash(address), checksummed_address <- Address.checksum(address_hash) do [ - # todo: "address" should be removed in favour `address_hash` property with the next release after 8.0.0 + UPDATE PATTERN MATCHING in `chain_type_fields()` function - %{"address_hash" => checksummed_address, "address" => checksummed_address, "name" => name} + %{"address_hash" => checksummed_address, "name" => name} |> chain_type_fields(implementations_info) | acc ] @@ -526,7 +523,7 @@ defmodule Explorer.Chain.SmartContract.Proxy do end if @chain_type == :filecoin do - def chain_type_fields(%{"address" => address_hash} = address, implementations_info) do + def chain_type_fields(%{"address_hash" => address_hash} = address, implementations_info) do Map.put(address, "filecoin_robust_address", implementations_info[address_hash]) end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index df479eba880c..9959c1af760d 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -409,13 +409,13 @@ defmodule Explorer.Chain.Token do It used by Explorer.Chain.Cache.Counters.TokenHoldersCount module. """ @spec update_token_holder_count(Hash.Address.t(), integer()) :: {non_neg_integer(), nil} - def update_token_holder_count(contract_address_hash, holder_count) when not is_nil(holder_count) do + def update_token_holder_count(contract_address_hash, holders_count) when not is_nil(holders_count) do now = DateTime.utc_now() Repo.update_all( from(t in __MODULE__, where: t.contract_address_hash == ^contract_address_hash, - update: [set: [holder_count: ^holder_count, updated_at: ^now]] + update: [set: [holder_count: ^holders_count, updated_at: ^now]] ), [], timeout: @timeout diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 8de6587b1488..6ad7160985b9 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -518,9 +518,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do :transfers_count => String.t(), :holders_count => String.t() } - def prepare_token_counters_for_queue(transfer_count, holder_count) do + def prepare_token_counters_for_queue(transfers_count, holders_count) do if enabled?() do - %{transfers_count: to_string(transfer_count), holders_count: to_string(holder_count)} + %{transfers_count: to_string(transfers_count), holders_count: to_string(holders_count)} else %{} end diff --git a/apps/explorer/test/explorer/chain/cache/counters/transactions_24h_count_test.exs b/apps/explorer/test/explorer/chain/cache/counters/transactions_24h_count_test.exs index 042ea2c818bc..9adbaf9d5195 100644 --- a/apps/explorer/test/explorer/chain/cache/counters/transactions_24h_count_test.exs +++ b/apps/explorer/test/explorer/chain/cache/counters/transactions_24h_count_test.exs @@ -50,11 +50,11 @@ defmodule Explorer.Chain.Cache.Counters.Transactions24hCountTest do start_supervised!(Transactions24hCount) Transactions24hCount.consolidate() - transaction_count = Transactions24hCount.fetch_count([]) + transactions_count = Transactions24hCount.fetch_count([]) transaction_fee_sum = Transactions24hCount.fetch_fee_sum([]) transaction_fee_average = Transactions24hCount.fetch_fee_average([]) - assert transaction_count == Decimal.new("3") + assert transactions_count == Decimal.new("3") assert transaction_fee_sum == Decimal.new("35000") assert transaction_fee_average == Decimal.new("11667") end diff --git a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex index 0ec74be5e8ff..e9cfc9996b95 100644 --- a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex @@ -63,9 +63,9 @@ defmodule Indexer.Fetcher.TokenCountersUpdater do entries |> Enum.reduce(%{}, fn token, acc -> - {transfer_count, holder_count} = Chain.fetch_token_counters(token.contract_address_hash, :infinity) + {transfers_count, holders_count} = Chain.fetch_token_counters(token.contract_address_hash, :infinity) - data_for_multichain = MultichainSearch.prepare_token_counters_for_queue(transfer_count, holder_count) + data_for_multichain = MultichainSearch.prepare_token_counters_for_queue(transfers_count, holders_count) Map.put(acc, token.contract_address_hash.bytes, data_for_multichain) end) |> MultichainSearch.send_token_info_to_queue(:counters) From cc5b5ce4adf7d4c2e6df14292260b78ea72ab371 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 6 Aug 2025 21:23:38 +0300 Subject: [PATCH 0199/1006] Update CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6abd1da518d..3a987ba82f04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,7 +105,6 @@ | `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_CONCURRENCY` | Number of parallel reindexing block batches processing. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: 1
Applications: Indexer

| | `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_TIMEOUT` | Timeout between reindexing block batches processing. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: 0
Applications: Indexer

| | `MIGRATION_REINDEX_BLOCKS_WITH_MISSING_TRANSACTIONS_ENABLED` | Enable reindex blocks with missing transactions migration. Implemented in [#12559](https://github.com/blockscout/blockscout/pull/12559). |

Version: v9.0.0+
Default: false
Applications: Indexer

| -| `MIGRATION_RESTORE_OMITTED_WETH_TOKEN_TRANSFERS_BATCH_SIZE` | Number of logs to process in the batch. Implemented in [#10466](https://github.com/blockscout/blockscout/pull/10466) |

Version: v6.8.0+
Default: 50
Applications: API, Indexer

| | `MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE` | Specifies the missing block range batch size selected for the merge migration. Implemented in [#12778](https://github.com/blockscout/blockscout/pull/12778). |

Version: v9.0.0+
Default: 100
Applications: Indexer

| | `API_RATE_LIMIT_CONFIG_URL` | URL to fetch API rate limit configuration from external source. Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: (empty)
Applications: API

| | `API_RATE_LIMIT_BY_KEY_TIME_INTERVAL` | Time interval for API rate limit by key. [Time format](backend-env-variables.md#time-format). Implemented in [#12386](https://github.com/blockscout/blockscout/pull/12386) |

Version: v9.0.0+
Default: 1s
Applications: API

| From e637662cf5ac88a84838d063e0d0df0844cc9a27 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:35:55 +0300 Subject: [PATCH 0200/1006] chore(deps-dev): bump @babel/preset-env in /apps/block_scout_web/assets (#12911) Bumps [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env) from 7.27.2 to 7.28.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.0/packages/babel-preset-env) --- updated-dependencies: - dependency-name: "@babel/preset-env" dependency-version: 7.28.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 671 +++++++++--------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 347 insertions(+), 326 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index b41a3564f69e..7dbbf352faf1 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -72,7 +72,7 @@ }, "devDependencies": { "@babel/core": "^7.27.7", - "@babel/preset-env": "^7.27.2", + "@babel/preset-env": "^7.28.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", @@ -290,9 +290,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", - "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -334,15 +334,15 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -350,12 +350,12 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -446,6 +446,15 @@ "@babel/core": "^7.4.0-0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -618,12 +627,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", - "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.7" + "@babel/types": "^7.28.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -970,15 +979,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1022,9 +1031,9 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1072,18 +1081,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1092,16 +1101,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-classes/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/plugin-transform-computed-properties": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", @@ -1120,13 +1119,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", - "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1201,6 +1201,23 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", @@ -1468,16 +1485,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", - "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3", - "@babel/plugin-transform-parameters": "^7.27.1" + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" }, "engines": { "node": ">=6.9.0" @@ -1537,9 +1555,9 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { @@ -1604,9 +1622,9 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", + "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -1832,13 +1850,13 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", - "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -1852,19 +1870,20 @@ "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -1881,15 +1900,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -1902,10 +1921,10 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "engines": { @@ -1916,29 +1935,31 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", - "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.1", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" }, "peerDependencies": { @@ -1946,12 +1967,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", - "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1" + "@babel/helper-define-polyfill-provider": "^0.6.5" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -1998,36 +2020,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", - "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", - "@babel/parser": "^7.27.7", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.7", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/types": "^7.28.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/types": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", - "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -3019,16 +3032,13 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3059,14 +3069,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -4892,31 +4904,31 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5261,9 +5273,9 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "funding": [ { "type": "opencollective", @@ -5280,10 +5292,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -5484,9 +5496,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", "funding": [ { "type": "opencollective", @@ -5788,12 +5800,12 @@ } }, "node_modules/core-js-compat": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", - "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.1" }, "funding": { "type": "opencollective", @@ -6515,11 +6527,12 @@ } }, "node_modules/debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -6815,9 +6828,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.109", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz", - "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==", + "version": "1.5.194", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", + "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==", "license": "ISC" }, "node_modules/elliptic": { @@ -12329,9 +12342,10 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/nanoassert": { "version": "1.1.0", @@ -14257,17 +14271,21 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -15766,9 +15784,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -15783,9 +15801,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -17136,9 +17155,9 @@ } }, "@babel/compat-data": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.3.tgz", - "integrity": "sha512-V42wFfx1ymFte+ecf6iXghnnP8kWTO+ZLXIyZq+1LAXHHvTZdVxicn4yiVYdYMGaCO3tmqub11AorKkv+iodqw==" + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==" }, "@babel/core": { "version": "7.27.7", @@ -17170,23 +17189,23 @@ } }, "@babel/generator": { - "version": "7.27.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", - "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", - "requires": { - "@babel/parser": "^7.27.5", - "@babel/types": "^7.27.3", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "requires": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "@babel/helper-annotate-as-pure": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", - "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "requires": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.27.3" } }, "@babel/helper-compilation-targets": { @@ -17255,6 +17274,11 @@ "semver": "^6.1.2" } }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==" + }, "@babel/helper-member-expression-to-functions": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", @@ -17366,11 +17390,11 @@ } }, "@babel/parser": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.7.tgz", - "integrity": "sha512-qnzXzDXdr/po3bOTbTIQZ7+TxNKxpkN5IifVLXS+r7qwynkZfPyjZfE7hCXbo7IoO9TNcSyibgONsf2HauUd3Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", "requires": { - "@babel/types": "^7.27.7" + "@babel/types": "^7.28.0" } }, "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { @@ -17593,14 +17617,14 @@ } }, "@babel/plugin-transform-async-generator-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", - "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.0" } }, "@babel/plugin-transform-async-to-generator": { @@ -17624,9 +17648,9 @@ } }, "@babel/plugin-transform-block-scoping": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz", - "integrity": "sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.0.tgz", + "integrity": "sha512-gKKnwjpdx5sER/wl0WN0efUBFzF/56YZO0RJrSYP4CljXnP31ByY7fol89AzomdlLNzI36AvOTmYHsnZTCkq8Q==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.27.1" @@ -17653,25 +17677,17 @@ } }, "@babel/plugin-transform-classes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", - "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.0.tgz", + "integrity": "sha512-IjM1IoJNw72AZFlj33Cu8X0q2XK/6AaVC3jQu+cgQ5lThWD5ajnuUAml80dqRmOhmPkTH8uAwnpMu9Rvj0LTRA==", "dev": true, "requires": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.27.1", - "globals": "^11.1.0" - }, - "dependencies": { - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - } + "@babel/traverse": "^7.28.0" } }, "@babel/plugin-transform-computed-properties": { @@ -17685,12 +17701,13 @@ } }, "@babel/plugin-transform-destructuring": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", - "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" } }, "@babel/plugin-transform-dotall-regex": { @@ -17731,6 +17748,16 @@ "@babel/helper-plugin-utils": "^7.27.1" } }, + "@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + } + }, "@babel/plugin-transform-exponentiation-operator": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", @@ -17886,15 +17913,16 @@ } }, "@babel/plugin-transform-object-rest-spread": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", - "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.0.tgz", + "integrity": "sha512-9VNGikXxzu5eCiQjdE4IZn8sb9q7Xsk5EXLDBKUYg1e/Tve8/05+KJEtcxGxAgCY5t/BpKQM+JEL/yT4tvgiUA==", "dev": true, "requires": { "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.3", - "@babel/plugin-transform-parameters": "^7.27.1" + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.0" } }, "@babel/plugin-transform-object-super": { @@ -17927,9 +17955,9 @@ } }, "@babel/plugin-transform-parameters": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", - "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.27.1" @@ -17966,9 +17994,9 @@ } }, "@babel/plugin-transform-regenerator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz", - "integrity": "sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw==", + "version": "7.28.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.1.tgz", + "integrity": "sha512-P0QiV/taaa3kXpLY+sXla5zec4E+4t4Aqc9ggHlfZ7a2cp8/x/Gv08jfwEtn9gnnYIMvHx6aoOZ8XJL8eU71Dg==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.27.1" @@ -18103,12 +18131,12 @@ } }, "@babel/preset-env": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", - "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.0.tgz", + "integrity": "sha512-VmaxeGOwuDqzLl5JUkIRM1X2Qu2uKGxHEQWh+cvvbl7JuJRgKGJSfsEF/bUaxFhJl/XAyxBe7q7qSuTbKFuCyg==", "dev": true, "requires": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", @@ -18122,19 +18150,20 @@ "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", "@babel/plugin-transform-async-to-generator": "^7.27.1", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", "@babel/plugin-transform-class-properties": "^7.27.1", "@babel/plugin-transform-class-static-block": "^7.27.1", - "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-classes": "^7.28.0", "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", "@babel/plugin-transform-dotall-regex": "^7.27.1", "@babel/plugin-transform-duplicate-keys": "^7.27.1", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", "@babel/plugin-transform-exponentiation-operator": "^7.27.1", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", @@ -18151,15 +18180,15 @@ "@babel/plugin-transform-new-target": "^7.27.1", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", "@babel/plugin-transform-object-super": "^7.27.1", "@babel/plugin-transform-optional-catch-binding": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.27.1", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.0", "@babel/plugin-transform-regexp-modifiers": "^7.27.1", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", @@ -18172,44 +18201,44 @@ "@babel/plugin-transform-unicode-regex": "^7.27.1", "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", "semver": "^6.3.1" }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.1.tgz", - "integrity": "sha512-o7SDgTJuvx5vLKD6SFvkydkSMBvahDKGiNJzG22IZYXhiqoe9efY7zocICBgzHV4IRg5wdgl2nEL/tulKIEIbA==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" } }, "babel-plugin-polyfill-corejs2": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.10.tgz", - "integrity": "sha512-rpIuu//y5OX6jVU+a5BCn1R5RSZYWAl2Nar76iwaOdycqb6JPxediskWFMMl7stfwNJR4b7eiQvh5fB5TEQJTQ==", + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", "dev": true, "requires": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.1", + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", "semver": "^6.3.1" } }, "babel-plugin-polyfill-regenerator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.1.tgz", - "integrity": "sha512-JfTApdE++cgcTWjsiCQlLyFBMbTUft9ja17saCc93lgV33h4tuCVj7tlvu//qpLwaG+3yEz7/KhahGrUMkVq9g==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.1" + "@babel/helper-define-polyfill-provider": "^0.6.5" } } } @@ -18244,30 +18273,23 @@ } }, "@babel/traverse": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.7.tgz", - "integrity": "sha512-X6ZlfR/O/s5EQ/SnUSLzr+6kGnkg8HXGMzpgsMsrJVcfDtH1vIp6ctCN4eZ1LS5c0+te5Cb6Y514fASjMRJ1nw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", "requires": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", - "@babel/parser": "^7.27.7", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/types": "^7.27.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "dependencies": { - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - } + "@babel/types": "^7.28.0", + "debug": "^4.3.1" } }, "@babel/types": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.7.tgz", - "integrity": "sha512-8OLQgDScAOHXnAz2cV+RfzzNMipuLVBz2biuAJFMV9bfkNf393je3VM8CLkjQodW5+iWsSJdSgSWT6rsZoXHPw==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "requires": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" @@ -18989,12 +19011,11 @@ } }, "@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", "requires": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, @@ -19019,14 +19040,14 @@ } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==" }, "@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", "requires": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -20389,26 +20410,26 @@ } }, "babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", "dev": true, "requires": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" }, "dependencies": { "@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", "dev": true, "requires": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.10" } } } @@ -20683,14 +20704,14 @@ } }, "browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", "requires": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" } }, "bs58": { @@ -20836,9 +20857,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==" + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==" }, "caseless": { "version": "0.12.0", @@ -21062,11 +21083,11 @@ "integrity": "sha512-N6wEbTTZSYOY2rYAn85CuvWWkCK6QweMn7/4Nr3w+gDBeBhk/x4EJeY6FPo4QzDoJZxVTv8U7CMvgWk6pOHHqA==" }, "core-js-compat": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", - "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", "requires": { - "browserslist": "^4.24.4" + "browserslist": "^4.25.1" } }, "core-util-is": { @@ -21560,11 +21581,11 @@ } }, "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "decamelize": { @@ -21779,9 +21800,9 @@ } }, "electron-to-chromium": { - "version": "1.5.109", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.109.tgz", - "integrity": "sha512-AidaH9JETVRr9DIPGfp1kAarm/W6hRJTPuCnkF+2MqhF4KaAgRIcBc8nvjk+YMXZhwfISof/7WG29eS4iGxQLQ==" + "version": "1.5.194", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.194.tgz", + "integrity": "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA==" }, "elliptic": { "version": "6.6.1", @@ -25961,9 +25982,9 @@ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==" }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "nanoassert": { "version": "1.1.0", @@ -27288,11 +27309,11 @@ "dev": true }, "resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "requires": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -28346,12 +28367,12 @@ "dev": true }, "update-browserslist-db": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", - "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "requires": { "escalade": "^3.2.0", - "picocolors": "^1.1.0" + "picocolors": "^1.1.1" } }, "uri-js": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 3ee5156d2328..d74b33e1de63 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -84,7 +84,7 @@ }, "devDependencies": { "@babel/core": "^7.27.7", - "@babel/preset-env": "^7.27.2", + "@babel/preset-env": "^7.28.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", "copy-webpack-plugin": "^13.0.0", From c390911b6dfa9aff1d9204fe1e7ffd4cc7ed975a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:36:32 +0300 Subject: [PATCH 0201/1006] chore(deps): bump bignumber.js in /apps/block_scout_web/assets (#12909) Bumps [bignumber.js](https://github.com/MikeMcl/bignumber.js) from 9.3.0 to 9.3.1. - [Release notes](https://github.com/MikeMcl/bignumber.js/releases) - [Changelog](https://github.com/MikeMcl/bignumber.js/blob/main/CHANGELOG.md) - [Commits](https://github.com/MikeMcl/bignumber.js/compare/v9.3.0...v9.3.1) --- updated-dependencies: - dependency-name: bignumber.js dependency-version: 9.3.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 7dbbf352faf1..f832443409c0 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -12,7 +12,7 @@ "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", - "bignumber.js": "^9.3.0", + "bignumber.js": "^9.3.1", "bootstrap": "^4.6.0", "chart.js": "^4.5.0", "chartjs-adapter-luxon": "^1.3.1", @@ -5075,9 +5075,9 @@ } }, "node_modules/bignumber.js": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", - "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", "license": "MIT", "engines": { "node": "*" @@ -20535,9 +20535,9 @@ "dev": true }, "bignumber.js": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz", - "integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==" + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==" }, "blakejs": { "version": "1.1.1", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index d74b33e1de63..19c1cd6a4431 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -24,7 +24,7 @@ "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", "assert": "^2.1.0", - "bignumber.js": "^9.3.0", + "bignumber.js": "^9.3.1", "bootstrap": "^4.6.0", "chart.js": "^4.5.0", "chartjs-adapter-luxon": "^1.3.1", From a379a552f39df0805a548ae84ffe3615a3a8212d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:37:21 +0300 Subject: [PATCH 0202/1006] chore(deps-dev): bump jest-environment-jsdom (#12907) Bumps [jest-environment-jsdom](https://github.com/jestjs/jest/tree/HEAD/packages/jest-environment-jsdom) from 29.7.0 to 30.0.5. - [Release notes](https://github.com/jestjs/jest/releases) - [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md) - [Commits](https://github.com/jestjs/jest/commits/v30.0.5/packages/jest-environment-jsdom) --- updated-dependencies: - dependency-name: jest-environment-jsdom dependency-version: 30.0.5 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 4897 +++++------------ apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 1524 insertions(+), 3375 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index f832443409c0..0c7cb5a2e0da 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -86,7 +86,7 @@ "file-loader": "^6.2.0", "globals": "^16.2.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.0.5", "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", @@ -275,6 +275,27 @@ "node": ">=6.0.0" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2056,6 +2077,121 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -2384,76 +2520,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/console/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/console/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/console/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/core": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", @@ -2501,125 +2567,294 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.0.5.tgz", + "integrity": "sha512-gpWwiVxZunkoglP8DCnT3As9x5O8H6gveAOpvaJd2ATAoSh7ZSSCWbr9LQtUMvr8WD3VjG9YnDhsmkCK5WN1rQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": ">=10" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/@jest/core/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" }, "engines": { - "node": ">=7.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/core/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/core/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/core/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "jest-mock": "^29.7.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "type-detect": "4.0.8" } }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "jest-get-type": "^29.6.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@jest/fake-timers": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", - "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" @@ -2643,6 +2878,30 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -2686,55 +2945,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/reporters/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/reporters/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/@jest/reporters/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -2774,18 +2984,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/@jest/reporters/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/schemas": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", @@ -2868,82 +3066,12 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/@jest/transform/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@jest/types": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", @@ -2961,84 +3089,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { @@ -3614,10 +3672,11 @@ } }, "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.0", @@ -3629,19 +3688,21 @@ } }, "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==", + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", @@ -3683,16 +3744,18 @@ } }, "node_modules/@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" }, "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", - "dev": true + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.5.3", @@ -3703,10 +3766,11 @@ } }, "node_modules/@types/yargs": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", - "integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -4170,12 +4234,6 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, - "node_modules/abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, "node_modules/abitype": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.7.1.tgz", @@ -4211,28 +4269,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "dependencies": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - } - }, - "node_modules/acorn-globals/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -4243,30 +4279,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/aes-js": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", "dev": true, - "dependencies": { - "debug": "4" - }, + "license": "MIT", "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -4697,76 +4722,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/babel-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/babel-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/babel-loader": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", @@ -5140,12 +5095,6 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, - "node_modules/browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "node_modules/browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -5536,6 +5485,82 @@ "node": ">=4" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/chalk/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/chalk/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/chalk/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -5932,80 +5957,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/create-jest/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/create-jest/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/create-jest/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/create-jest/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cross-fetch": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.6.tgz", - "integrity": "sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA==", + "node_modules/cross-fetch": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-2.2.6.tgz", + "integrity": "sha512-9JZz+vXCmfKUZ68zAptS7k4Nu8e2qcibe7WVZYps7sAgk5R8GYTc+T1WR0v1rlP9HxgARmOX1UTIJZFytajpNA==", "dependencies": { "node-fetch": "^2.6.7", "whatwg-fetch": "^2.0.4" @@ -6423,30 +6378,20 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, + "license": "MIT", "dependencies": { - "cssom": "~0.3.6" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -6459,17 +6404,17 @@ } }, "node_modules/data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, + "license": "MIT", "dependencies": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -6552,10 +6497,11 @@ } }, "node_modules/decimal.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.0.tgz", - "integrity": "sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg==", - "dev": true + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" }, "node_modules/decode-uri-component": { "version": "0.2.2", @@ -6756,18 +6702,6 @@ ], "license": "BSD-2-Clause" }, - "node_modules/domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "dependencies": { - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -6890,19 +6824,6 @@ "iconv-lite": "^0.6.2" } }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -7128,88 +7049,6 @@ "node": ">=6" } }, - "node_modules/escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/escodegen/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/escodegen/node_modules/levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "dependencies": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/escodegen/node_modules/type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "dependencies": { - "prelude-ls": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/eslint": { "version": "9.30.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", @@ -7491,55 +7330,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/eslint/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7608,15 +7398,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/eslint/node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -7656,18 +7437,6 @@ "node": ">=8" } }, - "node_modules/eslint/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -8915,15 +8684,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, + "license": "MIT", "dependencies": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/html-escaper": { @@ -8932,6 +8702,20 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -8952,16 +8736,17 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -8978,6 +8763,19 @@ "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -9447,7 +9245,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -9816,80 +9615,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-circus/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-circus/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-circus/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", - "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", "dev": true, "dependencies": { "@jest/core": "^29.7.0", @@ -9919,76 +9648,6 @@ } } }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-cli/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-cli/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-config": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", @@ -10034,285 +9693,284 @@ } } }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "detect-newline": "^3.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-config/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-config/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-config/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-config/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-environment-jsdom": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.0.5.tgz", + "integrity": "sha512-BmnDEoAH+jEjkPrvE9DTKS2r3jYSJWlN/r46h0/DBUxKrkgt2jAZ5Nj4wXLAcV1KWkRpcFqA5zri9SWzJZ1cCg==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/environment": "30.0.5", + "@jest/environment-jsdom-abstract": "30.0.5", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, - "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": ">=7.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" }, - "node_modules/jest-diff/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "engines": { - "node": ">=8" + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" } }, - "node_modules/jest-diff/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "@sinonjs/commons": "^3.0.1" } }, - "node_modules/jest-docblock": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", - "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "dependencies": { - "detect-newline": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/jest-each": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", - "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=8" } }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" }, "engines": { - "node": ">=7.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-each/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/jest-each/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^4.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", + "node_modules/jest-environment-jsdom/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "canvas": "^2.5.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } + "license": "MIT" }, "node_modules/jest-environment-node": { "version": "29.7.0", @@ -10432,76 +10090,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-matcher-utils/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-matcher-utils/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -10522,76 +10110,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-message-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-message-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-message-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", @@ -10665,80 +10183,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-resolve/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-resolve/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-resolve/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runner": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", - "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", "dev": true, "dependencies": { "@jest/console": "^29.7.0", @@ -10767,55 +10215,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runner/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "node_modules/jest-runner/node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -10865,18 +10264,6 @@ "source-map": "^0.6.0" } }, - "node_modules/jest-runner/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-runtime": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", @@ -10910,76 +10297,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-runtime/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-runtime/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-runtime/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-runtime/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", @@ -11011,64 +10328,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-snapshot/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-snapshot/node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -11084,18 +10343,6 @@ "node": ">=10" } }, - "node_modules/jest-snapshot/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -11113,76 +10360,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/jest-validate": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", @@ -11194,181 +10371,41 @@ "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-validate/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-validate/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-validate/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-validate/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-watcher": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", - "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", - "dev": true, - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" + "pretty-format": "^29.7.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-watcher/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-watcher/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-watcher/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-worker": { @@ -11468,44 +10505,38 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "node_modules/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==", - "dev": true, - "dependencies": { - "abab": "^2.0.6", - "acorn": "^8.7.1", - "acorn-globals": "^6.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.3.1", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "^7.0.0", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^3.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.8.0", - "xml-name-validator": "^4.0.0" + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -11513,43 +10544,6 @@ } } }, - "node_modules/jsdom/node_modules/@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/jsdom/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/jsdom/node_modules/http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "dependencies": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -12507,10 +11501,11 @@ } }, "node_modules/nwsapi": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", - "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==", - "dev": true + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" }, "node_modules/oauth-sign": { "version": "0.9.0", @@ -12831,17 +11826,31 @@ } }, "node_modules/parse5": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", - "integrity": "sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, + "license": "MIT", "dependencies": { - "entities": "^4.3.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -13783,9 +12792,10 @@ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" }, "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -13981,12 +12991,6 @@ "node": ">=0.6" } }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14264,12 +13268,6 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -14374,6 +13372,13 @@ "rrweb-snapshot": "^2.0.0-alpha.18" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/rrweb-snapshot": { "version": "2.0.0-alpha.18", "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz", @@ -14557,6 +13562,7 @@ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", "dev": true, + "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, @@ -14961,10 +13967,11 @@ } }, "node_modules/stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -15305,7 +14312,8 @@ "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tapable": { "version": "2.2.1", @@ -15441,6 +14449,26 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -15505,39 +14533,29 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "node_modules/tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, + "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/tsconfig-paths": { @@ -15838,16 +14856,6 @@ "node": ">= 0.4" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", @@ -15939,25 +14947,17 @@ "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.11.7.tgz", "integrity": "sha512-0JuVqOmL5v1jmEAlG5EBDR3XquxY8DWFQbFMprOXgaBB0F7Q/X9xWdEaQc59D8xzwkdUgXEMSSknTpriq95igg==" }, - "node_modules/w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "dependencies": { - "browser-process-hrtime": "^1.0.0" - } - }, "node_modules/w3c-xmlserializer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", - "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, + "license": "MIT", "dependencies": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/walker": { @@ -16453,6 +15453,7 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" } @@ -16580,27 +15581,16 @@ } }, "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, + "license": "MIT", "dependencies": { "iconv-lite": "0.6.3" }, "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, "node_modules/whatwg-fetch": { @@ -16609,25 +15599,27 @@ "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" }, "node_modules/whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, + "license": "MIT", "dependencies": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/which": { @@ -16751,15 +15743,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -16869,19 +15852,21 @@ } }, "node_modules/xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/xss": { "version": "1.0.15", @@ -17144,6 +16129,27 @@ } } }, + "@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "requires": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, "@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -18301,6 +17307,42 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true + }, + "@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "requires": {} + }, + "@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "requires": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + } + }, + "@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "requires": {} + }, + "@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true + }, "@discoveryjs/json-ext": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", @@ -18516,59 +17558,8 @@ "@types/node": "*", "chalk": "^4.0.0", "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "jest-util": "^29.7.0", + "slash": "^3.0.0" } }, "@jest/core": { @@ -18605,71 +17596,188 @@ "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + } + }, + "@jest/environment-jsdom-abstract": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.0.5.tgz", + "integrity": "sha512-gpWwiVxZunkoglP8DCnT3As9x5O8H6gveAOpvaJd2ATAoSh7ZSSCWbr9LQtUMvr8WD3VjG9YnDhsmkCK5WN1rQ==", + "dev": true, + "requires": { + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" } }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" } }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, "requires": { - "color-name": "~1.1.4" + "@sinclair/typebox": "^0.34.0" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true + }, + "jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + } + }, + "jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "requires": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + } + }, + "jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "requires": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + } + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + }, + "pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "requires": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true } } }, - "@jest/environment": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", - "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", - "dev": true, - "requires": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - } - }, "@jest/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", @@ -18715,6 +17823,24 @@ "jest-mock": "^29.7.0" } }, + "@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "requires": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "dependencies": { + "jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true + } + } + }, "@jest/reporters": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", @@ -18747,40 +17873,6 @@ "v8-to-istanbul": "^9.0.1" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -18809,15 +17901,6 @@ } } } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -18888,60 +17971,11 @@ "write-file-atomic": "^4.0.2" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -18957,57 +17991,6 @@ "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "@jridgewell/gen-mapping": { @@ -19367,9 +18350,9 @@ } }, "@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", "dev": true }, "@types/istanbul-lib-report": { @@ -19382,18 +18365,18 @@ } }, "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, "requires": { "@types/istanbul-lib-report": "*" } }, "@types/jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-YfAchFs0yM1QPDrLm2VHe+WHGtqms3NXnXAMolrgrVP6fgBHHXy1ozAbo/dFtPNtZC/m66bPiCTWYmqp1F14gA==", + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, "requires": { "@types/node": "*", @@ -19435,15 +18418,15 @@ } }, "@types/stack-utils": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "dev": true }, "@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true }, "@types/ws": { @@ -19455,9 +18438,9 @@ } }, "@types/yargs": { - "version": "17.0.11", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz", - "integrity": "sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA==", + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -19882,12 +18865,6 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, - "abab": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", - "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", - "dev": true - }, "abitype": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/abitype/-/abitype-0.7.1.tgz", @@ -19902,29 +18879,11 @@ "xtend": "~4.0.0" } }, - "acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", - "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, - "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } - } + "acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true }, "acorn-jsx": { "version": "5.3.2", @@ -19933,25 +18892,16 @@ "dev": true, "requires": {} }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, "aes-js": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" }, "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true }, "ajv": { "version": "6.12.6", @@ -20261,57 +19211,6 @@ "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "babel-loader": { @@ -20585,12 +19484,6 @@ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", - "dev": true - }, "browserify-aes": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", @@ -20879,6 +19772,57 @@ "type-detect": "^4.0.5" } }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "char-regex": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", @@ -21183,57 +20127,6 @@ "jest-config": "^29.7.0", "jest-util": "^29.7.0", "prompts": "^2.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "cross-fetch": { @@ -21505,27 +20398,14 @@ } } }, - "cssom": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", - "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", - "dev": true - }, "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, "requires": { - "cssom": "~0.3.6" - }, - "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" } }, "dashdash": { @@ -21537,14 +20417,13 @@ } }, "data-urls": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", - "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, "requires": { - "abab": "^2.0.6", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" } }, "data-view-buffer": { @@ -21594,9 +20473,9 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decimal.js": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.0.tgz", - "integrity": "sha512-Nv6ENEzyPQ6AItkGwLE2PGKinZZ9g59vSh2BeH6NqPu0OTKZ5ruJsVqh/orbAnqXc9pBbgXAIrc2EyaCj8NpGg==", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "dev": true }, "decode-uri-component": { @@ -21746,15 +20625,6 @@ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true }, - "domexception": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", - "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", - "dev": true, - "requires": { - "webidl-conversions": "^7.0.0" - } - }, "domhandler": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", @@ -21851,18 +20721,6 @@ "peer": true, "requires": { "iconv-lite": "^0.6.2" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "optional": true, - "peer": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } } }, "enhanced-resolve": { @@ -22033,66 +20891,6 @@ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", - "dev": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } - } - }, "eslint": { "version": "9.30.1", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.30.1.tgz", @@ -22136,40 +20934,6 @@ "optionator": "^0.9.3" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -22208,12 +20972,6 @@ "path-exists": "^4.0.0" } }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -22229,23 +20987,14 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "requires": { - "p-limit": "^3.0.2" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" + "p-limit": "^3.0.2" } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true } } }, @@ -23406,12 +22155,12 @@ } }, "html-encoding-sniffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", - "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "requires": { - "whatwg-encoding": "^2.0.0" + "whatwg-encoding": "^3.1.1" } }, "html-escaper": { @@ -23420,6 +22169,16 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -23436,12 +22195,12 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" }, "https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, "requires": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" } }, @@ -23456,6 +22215,15 @@ "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", "integrity": "sha1-3QLqYIG9BWjcXQcxhEY5V7qe+ao=" }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "devOptional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + }, "icss-utils": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", @@ -24005,57 +22773,6 @@ "pure-rand": "^6.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-cli": { @@ -24075,57 +22792,6 @@ "jest-util": "^29.7.0", "jest-validate": "^29.7.0", "yargs": "^17.3.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-config": { @@ -24156,57 +22822,6 @@ "pretty-format": "^29.7.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-diff": { @@ -24219,57 +22834,6 @@ "diff-sequences": "^29.6.3", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-docblock": { @@ -24292,75 +22856,174 @@ "jest-get-type": "^29.6.3", "jest-util": "^29.7.0", "pretty-format": "^29.7.0" + } + }, + "jest-environment-jsdom": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.0.5.tgz", + "integrity": "sha512-BmnDEoAH+jEjkPrvE9DTKS2r3jYSJWlN/r46h0/DBUxKrkgt2jAZ5Nj4wXLAcV1KWkRpcFqA5zri9SWzJZ1cCg==", + "dev": true, + "requires": { + "@jest/environment": "30.0.5", + "@jest/environment-jsdom-abstract": "30.0.5", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jsdom": "^26.1.0" }, "dependencies": { + "@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "dev": true, + "requires": { + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + } + }, + "@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "dev": true, + "requires": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + } + }, + "@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.34.0" + } + }, + "@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "requires": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + } + }, + "@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true + }, + "@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^3.0.1" + } + }, "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + }, + "ci-info": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true + }, + "jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" } }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "dev": true, "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + }, + "jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "dev": true, "requires": { - "color-name": "~1.1.4" + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" } }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" } + }, + "react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true } } }, - "jest-environment-jsdom": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", - "integrity": "sha512-k9iQbsf9OyOfdzWH8HDmrRT0gSIcX+FLNW7IQq94tFX0gynPwqDTW0Ho6iMVNjGz/nb+l/vW3dWM2bbLLpkbXA==", - "dev": true, - "requires": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/jsdom": "^20.0.0", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0", - "jsdom": "^20.0.0" - } - }, "jest-environment-node": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", @@ -24450,57 +23113,6 @@ "jest-diff": "^29.7.0", "jest-get-type": "^29.6.3", "pretty-format": "^29.7.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-message-util": { @@ -24518,57 +23130,6 @@ "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-mock": { @@ -24610,57 +23171,6 @@ "resolve": "^1.20.0", "resolve.exports": "^2.0.0", "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-resolve-dependencies": { @@ -24702,40 +23212,6 @@ "source-map-support": "0.5.13" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -24774,15 +23250,6 @@ "buffer-from": "^1.0.0", "source-map": "^0.6.0" } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -24810,61 +23277,10 @@ "jest-mock": "^29.7.0", "jest-regex-util": "^29.6.3", "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" } }, "jest-snapshot": { @@ -24895,46 +23311,6 @@ "semver": "^7.5.3" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -24943,15 +23319,6 @@ "requires": { "lru-cache": "^6.0.0" } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -24967,57 +23334,6 @@ "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-validate": { @@ -25034,60 +23350,11 @@ "pretty-format": "^29.7.0" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, "camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } } } }, @@ -25105,57 +23372,6 @@ "emittery": "^0.13.1", "jest-util": "^29.7.0", "string-length": "^4.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } } }, "jest-worker": { @@ -25233,68 +23449,31 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" }, "jsdom": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.0.tgz", - "integrity": "sha512-x4a6CKCgx00uCmP+QakBDFXwjAJ69IkkIWHmtmjd3wvXPcdOS44hfX2vqkOQrVrq8l9DhNNADZRXaCEWvgXtVA==", - "dev": true, - "requires": { - "abab": "^2.0.6", - "acorn": "^8.7.1", - "acorn-globals": "^6.0.0", - "cssom": "^0.5.0", - "cssstyle": "^2.3.0", - "data-urls": "^3.0.2", - "decimal.js": "^10.3.1", - "domexception": "^4.0.0", - "escodegen": "^2.0.0", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^3.0.0", - "http-proxy-agent": "^5.0.0", - "https-proxy-agent": "^5.0.1", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "requires": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "^7.0.0", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^3.0.0", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^2.0.0", - "whatwg-mimetype": "^3.0.0", - "whatwg-url": "^11.0.0", - "ws": "^8.8.0", - "xml-name-validator": "^4.0.0" - }, - "dependencies": { - "@tootallnate/once": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", - "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", - "dev": true - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "http-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", - "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", - "dev": true, - "requires": { - "@tootallnate/once": "2", - "agent-base": "6", - "debug": "4" - } - } + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" } }, "jsesc": { @@ -26102,9 +24281,9 @@ "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=" }, "nwsapi": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.1.tgz", - "integrity": "sha512-JYOWTeFoS0Z93587vRJgASD5Ut11fYl5NyihP3KrYBvMe1FRRs6RN7m20SA/16GM4P6hTnZjT+UmDOt38UeXNg==", + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", "dev": true }, "oauth-sign": { @@ -26322,12 +24501,20 @@ } }, "parse5": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.0.0.tgz", - "integrity": "sha512-y/t8IXSPWTuRZqXc0ajH/UwDj4mnqLEbSttNbThcFhGrZuOyoyvNBO85PBp2jQa55wY9d07PBNjsK8ZP3K5U6g==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "requires": { - "entities": "^4.3.0" + "entities": "^6.0.0" + }, + "dependencies": { + "entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true + } } }, "path-exists": { @@ -26931,9 +25118,9 @@ } }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==" }, "pure-rand": { "version": "6.0.3", @@ -27079,12 +25266,6 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -27302,12 +25483,6 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, "resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -27387,6 +25562,12 @@ "rrweb-snapshot": "^2.0.0-alpha.18" } }, + "rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true + }, "rrweb-snapshot": { "version": "2.0.0-alpha.18", "resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.0.0-alpha.18.tgz", @@ -27814,9 +25995,9 @@ } }, "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "requires": { "escape-string-regexp": "^2.0.0" @@ -28122,6 +26303,21 @@ } } }, + "tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "requires": { + "tldts-core": "^6.1.86" + } + }, + "tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true + }, "tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -28165,32 +26361,21 @@ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "tough-cookie": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz", - "integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "dependencies": { - "universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "dev": true - } + "tldts": "^6.1.32" } }, "tr46": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", - "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", "dev": true, "requires": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" } }, "tsconfig-paths": { @@ -28412,16 +26597,6 @@ } } }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "utf-8-validate": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.7.tgz", @@ -28480,22 +26655,13 @@ "resolved": "https://registry.npmjs.org/viewerjs/-/viewerjs-1.11.7.tgz", "integrity": "sha512-0JuVqOmL5v1jmEAlG5EBDR3XquxY8DWFQbFMprOXgaBB0F7Q/X9xWdEaQc59D8xzwkdUgXEMSSknTpriq95igg==" }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", - "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" - } - }, "w3c-xmlserializer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-3.0.0.tgz", - "integrity": "sha512-3WFqGEgSXIyGhOmAFtlicJNMjEps8b1MG31NCA0/vOF9+nKMUW1ckhi9cnNHmf88Rzw5V+dwIwsm2C7X8k9aQg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, "requires": { - "xml-name-validator": "^4.0.0" + "xml-name-validator": "^5.0.0" } }, "walker": { @@ -28998,23 +27164,12 @@ } }, "whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", "dev": true, "requires": { "iconv-lite": "0.6.3" - }, - "dependencies": { - "iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - } - } } }, "whatwg-fetch": { @@ -29023,18 +27178,18 @@ "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" }, "whatwg-mimetype": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", - "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true }, "whatwg-url": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", - "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "requires": { - "tr46": "^3.0.0", + "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, @@ -29126,12 +27281,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "word-wrap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", - "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", - "dev": true - }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -29211,9 +27360,9 @@ } }, "xml-name-validator": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", - "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", "dev": true }, "xmlchars": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 19c1cd6a4431..789296c13c8e 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -98,7 +98,7 @@ "file-loader": "^6.2.0", "globals": "^16.2.0", "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", + "jest-environment-jsdom": "^30.0.5", "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.5.6", "postcss-loader": "^8.1.1", From db6d7a9cc608379bbdd2a1734a6d54f35ec67084 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 15:53:14 +0300 Subject: [PATCH 0203/1006] chore(deps-dev): bump @babel/core in /apps/block_scout_web/assets (#12905) Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.27.7 to 7.28.0. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.28.0/packages/babel-core) --- updated-dependencies: - dependency-name: "@babel/core" dependency-version: 7.28.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 30 +++++++++---------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 0c7cb5a2e0da..d4b0ae6d8e89 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -71,7 +71,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.7", + "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", @@ -320,21 +320,21 @@ } }, "node_modules/@babel/core": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", - "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.27.7", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -16166,20 +16166,20 @@ "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==" }, "@babel/core": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.7.tgz", - "integrity": "sha512-BU2f9tlKQ5CAthiMIgpzAh4eDTLWo1mqi9jqE2OxMG0E/OM199VJt2q8BztTxpnSW0i1ymdwLXRJnYzvDM5r2w==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "requires": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.5", + "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.27.7", + "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.27.7", - "@babel/types": "^7.27.7", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 789296c13c8e..7c339390be41 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -83,7 +83,7 @@ "xss": "^1.0.15" }, "devDependencies": { - "@babel/core": "^7.27.7", + "@babel/core": "^7.28.0", "@babel/preset-env": "^7.28.0", "autoprefixer": "^10.4.21", "babel-loader": "^10.0.0", From 207fb54467e4e8b6f002dec1cdaae547cf9e0147 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:10:36 +0300 Subject: [PATCH 0204/1006] chore(deps): bump @amplitude/analytics-browser (#12912) Bumps [@amplitude/analytics-browser](https://github.com/amplitude/Amplitude-TypeScript) from 2.17.6 to 2.21.1. - [Release notes](https://github.com/amplitude/Amplitude-TypeScript/releases) - [Commits](https://github.com/amplitude/Amplitude-TypeScript/compare/@amplitude/analytics-browser@2.17.6...@amplitude/analytics-browser@2.21.1) --- updated-dependencies: - dependency-name: "@amplitude/analytics-browser" dependency-version: 2.21.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 210 +++++++++++++----- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 158 insertions(+), 54 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index d4b0ae6d8e89..9353801b43e7 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -7,7 +7,7 @@ "name": "blockscout", "license": "GPL-3.0", "dependencies": { - "@amplitude/analytics-browser": "^2.17.6", + "@amplitude/analytics-browser": "^2.21.1", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", @@ -122,16 +122,17 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "node_modules/@amplitude/analytics-browser": { - "version": "2.17.6", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.6.tgz", - "integrity": "sha512-1Lbm5KNXG/6Zpg1Zr0qAmft562kXWGIXZ1ah12oEf6Wcp2ePxIPlbkKgagOTzeS8hwOlfuKVeBy9oCuOdf8yFw==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.21.1.tgz", + "integrity": "sha512-3Snmz4OgtSFWnFcqIC5QCFuUSIaVIK2iX0Wl1KIV+DzDhRwc+JIxuJCiwgWkvTPiO1N0Vfi9CO/bdockHqEMzg==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.2.6", - "@amplitude/plugin-network-capture-browser": "^1.1.6", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.27", + "@amplitude/plugin-autocapture-browser": "^1.8.1", + "@amplitude/plugin-network-capture-browser": "^1.4.2", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.37", + "@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.12", "tslib": "^2.4.1" } }, @@ -141,13 +142,13 @@ "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/@amplitude/analytics-client-common": { - "version": "2.3.21", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.21.tgz", - "integrity": "sha512-7KfSIr9d+Dnn+jW3UxQS/VlTaWpQz0nxX4LCi006x7YtjFgReFU2dxwB/48ZkfORLBEQokhm7cKilvD+JZveeg==", + "version": "2.3.31", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.31.tgz", + "integrity": "sha512-YkhKM78htSVNpRd+ul6MgDekaKseRCnYVLDyBQ0u3vtIt6m5kVtbBMffcrZz7DbOHIsyrUxA8u/RfLo+7xkouA==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" } @@ -164,9 +165,9 @@ "license": "MIT" }, "node_modules/@amplitude/analytics-core": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.12.0.tgz", - "integrity": "sha512-mfA6J2krFionypPHka65fK13eFi058EjSpLk8XgGmGS6Yvw+eidkgHvbouNDKwxBkhQFDH7cY8vOZeIAdkrktw==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.19.0.tgz", + "integrity": "sha512-G1RwMOjkEV1Ccx7vU3CsPFt0w5yNFlVFNpCKaWO7zVbsqA9PRljPXj3fKXPrto6s2vKTXmuGG5NuWM/iSVfBwA==", "license": "MIT", "dependencies": { "@amplitude/analytics-connector": "^1.6.4", @@ -201,16 +202,44 @@ "license": "MIT" }, "node_modules/@amplitude/plugin-autocapture-browser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.2.6.tgz", - "integrity": "sha512-+WB6DSGrKOxWC0muT/z0bcc5ftzjPY9L1D3CB5nHIHJTHAzxkTuc1+TFRtjc8L5Tdly9h2YADPNo1H8dkWNOkg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.8.1.tgz", + "integrity": "sha512-Wg5Ogi2rW9DM40ZI7iTZuGxm7TXUfkfAZ5yTUrRDy5MIXr+zdv89IpXhj4fsKW9ZO3xc7OFkFklXCWq+QJ580A==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", + "@amplitude/analytics-remote-config": "^0.6.3", "rxjs": "^7.8.1", "tslib": "^2.4.1" } }, + "node_modules/@amplitude/plugin-autocapture-browser/node_modules/@amplitude/analytics-remote-config": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-remote-config/-/analytics-remote-config-0.6.3.tgz", + "integrity": "sha512-icE0ogCzdHAtQi9jiOFQUmKrvWQc5YEO6bLZUfQXCT/yTTNXppWnT1zHMKzXa3SMDosfrLwU/X8sro1PTI+jZQ==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": ">=1 <2", + "@amplitude/analytics-types": ">=1 <2", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/plugin-autocapture-browser/node_modules/@amplitude/analytics-remote-config/node_modules/@amplitude/analytics-core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.8.tgz", + "integrity": "sha512-Krxpr5uvS3HmmjvpYqPfbMbs2kcZZu09L+6KwQnPiofWRzoXWIM217fRfy6aSD/QrAoPGbZjvtVitw9cB7Cx+A==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-types": "^1.4.0", + "tslib": "^2.4.1" + } + }, + "node_modules/@amplitude/plugin-autocapture-browser/node_modules/@amplitude/analytics-types": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.4.0.tgz", + "integrity": "sha512-RiMPHBqdrJ8ktTqG+Wzj2htnN/PCG9jGZG0SXtTFnWwVvcAJYbYm55/nrP1TTyrx1OlLhvF2VG3lVUP/xGAU8w==", + "license": "MIT" + }, "node_modules/@amplitude/plugin-autocapture-browser/node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -218,12 +247,12 @@ "license": "0BSD" }, "node_modules/@amplitude/plugin-network-capture-browser": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.1.6.tgz", - "integrity": "sha512-Ck39vvvizJ7qlBUxkOqq8k4/hrD9yCdvceBpSDfCr9jGzd3R3VisYUHsChOVChHd1fOoLtpoKF+HsgfrXmn17g==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.4.2.tgz", + "integrity": "sha512-Is1uBYfqEeS3bm0mVyqksW1AOTr5B9JWQOinT9WcF4R9c/56CoIXBGcLnRG+4p5p6xAQcmP/tggjc8xsDeI52g==", "license": "MIT", "dependencies": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" } @@ -235,12 +264,12 @@ "license": "0BSD" }, "node_modules/@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.27", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.27.tgz", - "integrity": "sha512-iJeuCn+jvBymQwV2EEkZhmJg+bitpZulubCxRMI0ihIdh/Nvk1XWhwmSfcgKaxMdk2qpa9CYXD9/pDewI88IDg==", + "version": "2.3.37", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.37.tgz", + "integrity": "sha512-Jikvke4Af8qQMwWcF6LHBF/HYUaAaWxZPcJoFYS1HK0/Gzxsa/T5SIJC35EDEERsHMKRryrNUdchlBY2xFKULw==", "license": "MIT", "dependencies": { - "@amplitude/analytics-client-common": "^2.3.21", + "@amplitude/analytics-client-common": "^2.3.31", "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" } @@ -251,6 +280,24 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@amplitude/plugin-web-vitals-browser": { + "version": "0.1.0-frustrationanalytics.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-0.1.0-frustrationanalytics.0.tgz", + "integrity": "sha512-xv4sje6/D8r+SgNFTA22FJ5PhtdhN+VSydvs63Frll+qWlyQwaZ1IgDbPyqjzryEkldHRPD7GUaQual+geoIYg==", + "license": "MIT", + "dependencies": { + "@amplitude/analytics-core": "^2.14.0-frustrationanalytics.0", + "rxjs": "^7.8.1", + "tslib": "^2.4.1", + "web-vitals": "^5.0.1" + } + }, + "node_modules/@amplitude/plugin-web-vitals-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -14982,6 +15029,12 @@ "node": ">=10.13.0" } }, + "node_modules/web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==", + "license": "Apache-2.0" + }, "node_modules/web3": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/web3/-/web3-4.12.1.tgz", @@ -15977,15 +16030,16 @@ "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" }, "@amplitude/analytics-browser": { - "version": "2.17.6", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.17.6.tgz", - "integrity": "sha512-1Lbm5KNXG/6Zpg1Zr0qAmft562kXWGIXZ1ah12oEf6Wcp2ePxIPlbkKgagOTzeS8hwOlfuKVeBy9oCuOdf8yFw==", + "version": "2.21.1", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-browser/-/analytics-browser-2.21.1.tgz", + "integrity": "sha512-3Snmz4OgtSFWnFcqIC5QCFuUSIaVIK2iX0Wl1KIV+DzDhRwc+JIxuJCiwgWkvTPiO1N0Vfi9CO/bdockHqEMzg==", "requires": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "@amplitude/analytics-remote-config": "^0.4.0", - "@amplitude/plugin-autocapture-browser": "^1.2.6", - "@amplitude/plugin-network-capture-browser": "^1.1.6", - "@amplitude/plugin-page-view-tracking-browser": "^2.3.27", + "@amplitude/plugin-autocapture-browser": "^1.8.1", + "@amplitude/plugin-network-capture-browser": "^1.4.2", + "@amplitude/plugin-page-view-tracking-browser": "^2.3.37", + "@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.12", "tslib": "^2.4.1" }, "dependencies": { @@ -15997,12 +16051,12 @@ } }, "@amplitude/analytics-client-common": { - "version": "2.3.21", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.21.tgz", - "integrity": "sha512-7KfSIr9d+Dnn+jW3UxQS/VlTaWpQz0nxX4LCi006x7YtjFgReFU2dxwB/48ZkfORLBEQokhm7cKilvD+JZveeg==", + "version": "2.3.31", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-client-common/-/analytics-client-common-2.3.31.tgz", + "integrity": "sha512-YkhKM78htSVNpRd+ul6MgDekaKseRCnYVLDyBQ0u3vtIt6m5kVtbBMffcrZz7DbOHIsyrUxA8u/RfLo+7xkouA==", "requires": { "@amplitude/analytics-connector": "^1.4.8", - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" }, @@ -16020,9 +16074,9 @@ "integrity": "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q==" }, "@amplitude/analytics-core": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.12.0.tgz", - "integrity": "sha512-mfA6J2krFionypPHka65fK13eFi058EjSpLk8XgGmGS6Yvw+eidkgHvbouNDKwxBkhQFDH7cY8vOZeIAdkrktw==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-2.19.0.tgz", + "integrity": "sha512-G1RwMOjkEV1Ccx7vU3CsPFt0w5yNFlVFNpCKaWO7zVbsqA9PRljPXj3fKXPrto6s2vKTXmuGG5NuWM/iSVfBwA==", "requires": { "@amplitude/analytics-connector": "^1.6.4", "tslib": "^2.4.1" @@ -16059,15 +16113,42 @@ "integrity": "sha512-juhTz396dDP/jLJYP9zDOEAZBtJM0JVvP8G10p1OxUDBVwVIprpQL598F9GRQwVFyqV4CEhDmNyAY0HqqU5bhA==" }, "@amplitude/plugin-autocapture-browser": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.2.6.tgz", - "integrity": "sha512-+WB6DSGrKOxWC0muT/z0bcc5ftzjPY9L1D3CB5nHIHJTHAzxkTuc1+TFRtjc8L5Tdly9h2YADPNo1H8dkWNOkg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-autocapture-browser/-/plugin-autocapture-browser-1.8.1.tgz", + "integrity": "sha512-Wg5Ogi2rW9DM40ZI7iTZuGxm7TXUfkfAZ5yTUrRDy5MIXr+zdv89IpXhj4fsKW9ZO3xc7OFkFklXCWq+QJ580A==", "requires": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", + "@amplitude/analytics-remote-config": "^0.6.3", "rxjs": "^7.8.1", "tslib": "^2.4.1" }, "dependencies": { + "@amplitude/analytics-remote-config": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-remote-config/-/analytics-remote-config-0.6.3.tgz", + "integrity": "sha512-icE0ogCzdHAtQi9jiOFQUmKrvWQc5YEO6bLZUfQXCT/yTTNXppWnT1zHMKzXa3SMDosfrLwU/X8sro1PTI+jZQ==", + "requires": { + "@amplitude/analytics-core": ">=1 <2", + "@amplitude/analytics-types": ">=1 <2", + "tslib": "^2.4.1" + }, + "dependencies": { + "@amplitude/analytics-core": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-core/-/analytics-core-1.2.8.tgz", + "integrity": "sha512-Krxpr5uvS3HmmjvpYqPfbMbs2kcZZu09L+6KwQnPiofWRzoXWIM217fRfy6aSD/QrAoPGbZjvtVitw9cB7Cx+A==", + "requires": { + "@amplitude/analytics-types": "^1.4.0", + "tslib": "^2.4.1" + } + } + } + }, + "@amplitude/analytics-types": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@amplitude/analytics-types/-/analytics-types-1.4.0.tgz", + "integrity": "sha512-RiMPHBqdrJ8ktTqG+Wzj2htnN/PCG9jGZG0SXtTFnWwVvcAJYbYm55/nrP1TTyrx1OlLhvF2VG3lVUP/xGAU8w==" + }, "tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -16076,11 +16157,11 @@ } }, "@amplitude/plugin-network-capture-browser": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.1.6.tgz", - "integrity": "sha512-Ck39vvvizJ7qlBUxkOqq8k4/hrD9yCdvceBpSDfCr9jGzd3R3VisYUHsChOVChHd1fOoLtpoKF+HsgfrXmn17g==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-network-capture-browser/-/plugin-network-capture-browser-1.4.2.tgz", + "integrity": "sha512-Is1uBYfqEeS3bm0mVyqksW1AOTr5B9JWQOinT9WcF4R9c/56CoIXBGcLnRG+4p5p6xAQcmP/tggjc8xsDeI52g==", "requires": { - "@amplitude/analytics-core": "^2.12.0", + "@amplitude/analytics-core": "^2.19.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" }, @@ -16093,11 +16174,11 @@ } }, "@amplitude/plugin-page-view-tracking-browser": { - "version": "2.3.27", - "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.27.tgz", - "integrity": "sha512-iJeuCn+jvBymQwV2EEkZhmJg+bitpZulubCxRMI0ihIdh/Nvk1XWhwmSfcgKaxMdk2qpa9CYXD9/pDewI88IDg==", + "version": "2.3.37", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-page-view-tracking-browser/-/plugin-page-view-tracking-browser-2.3.37.tgz", + "integrity": "sha512-Jikvke4Af8qQMwWcF6LHBF/HYUaAaWxZPcJoFYS1HK0/Gzxsa/T5SIJC35EDEERsHMKRryrNUdchlBY2xFKULw==", "requires": { - "@amplitude/analytics-client-common": "^2.3.21", + "@amplitude/analytics-client-common": "^2.3.31", "@amplitude/analytics-types": "^2.9.2", "tslib": "^2.4.1" }, @@ -16109,6 +16190,24 @@ } } }, + "@amplitude/plugin-web-vitals-browser": { + "version": "0.1.0-frustrationanalytics.0", + "resolved": "https://registry.npmjs.org/@amplitude/plugin-web-vitals-browser/-/plugin-web-vitals-browser-0.1.0-frustrationanalytics.0.tgz", + "integrity": "sha512-xv4sje6/D8r+SgNFTA22FJ5PhtdhN+VSydvs63Frll+qWlyQwaZ1IgDbPyqjzryEkldHRPD7GUaQual+geoIYg==", + "requires": { + "@amplitude/analytics-core": "^2.14.0-frustrationanalytics.0", + "rxjs": "^7.8.1", + "tslib": "^2.4.1", + "web-vitals": "^5.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + } + } + }, "@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -26683,6 +26782,11 @@ "graceful-fs": "^4.1.2" } }, + "web-vitals": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz", + "integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==" + }, "web3": { "version": "4.12.1", "resolved": "https://registry.npmjs.org/web3/-/web3-4.12.1.tgz", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 7c339390be41..aad1db6d1065 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -19,7 +19,7 @@ "eslint": "eslint js/**" }, "dependencies": { - "@amplitude/analytics-browser": "^2.17.6", + "@amplitude/analytics-browser": "^2.21.1", "@fortawesome/fontawesome-free": "^6.7.2", "@tarekraafat/autocomplete.js": "^10.2.9", "@walletconnect/web3-provider": "^1.8.0", From 9982b9ee79507a959c67afef11512b32a6c4d743 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:11:23 +0300 Subject: [PATCH 0205/1006] chore(deps-dev): bump globals in /apps/block_scout_web/assets (#12906) Bumps [globals](https://github.com/sindresorhus/globals) from 16.2.0 to 16.3.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v16.2.0...v16.3.0) --- updated-dependencies: - dependency-name: globals dependency-version: 16.3.0 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 9353801b43e7..05582804106b 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -84,7 +84,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", - "globals": "^16.2.0", + "globals": "^16.3.0", "jest": "^29.7.0", "jest-environment-jsdom": "^30.0.5", "mini-css-extract-plugin": "^2.9.2", @@ -8502,9 +8502,9 @@ } }, "node_modules/globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true, "license": "MIT", "engines": { @@ -22107,9 +22107,9 @@ } }, "globals": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", - "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", "dev": true }, "globalthis": { diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index aad1db6d1065..b5bb2957e8c2 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -96,7 +96,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^7.2.1", "file-loader": "^6.2.0", - "globals": "^16.2.0", + "globals": "^16.3.0", "jest": "^29.7.0", "jest-environment-jsdom": "^30.0.5", "mini-css-extract-plugin": "^2.9.2", From 23ef0684bff61a359ae9f5208cdeb9da79b3b71f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 16:12:06 +0300 Subject: [PATCH 0206/1006] chore(deps): bump luxon in /apps/block_scout_web/assets (#12904) Bumps [luxon](https://github.com/moment/luxon) from 3.6.1 to 3.7.1. - [Changelog](https://github.com/moment/luxon/blob/master/CHANGELOG.md) - [Commits](https://github.com/moment/luxon/compare/3.6.1...3.7.1) --- updated-dependencies: - dependency-name: luxon dependency-version: 3.7.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 05582804106b..000b49af22df 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -44,7 +44,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.6.1", + "luxon": "^3.7.1", "malihu-custom-scrollbar-plugin": "3.1.5", "mixpanel-browser": "^2.65.0", "moment": "^2.30.1", @@ -11076,9 +11076,9 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", "license": "MIT", "engines": { "node": ">=12" @@ -23999,9 +23999,9 @@ "integrity": "sha1-81ypHEk/e3PaDgdJUwTxezH4fuU=" }, "luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==" + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", + "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==" }, "make-dir": { "version": "4.0.0", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index b5bb2957e8c2..6cb8762d4033 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -56,7 +56,7 @@ "lodash.omit": "^4.5.0", "lodash.rangeright": "^4.2.0", "lodash.reduce": "^4.6.0", - "luxon": "^3.6.1", + "luxon": "^3.7.1", "malihu-custom-scrollbar-plugin": "3.1.5", "mixpanel-browser": "^2.65.0", "moment": "^2.30.1", From 35ea067d93b256540f447782706943b632a716f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 10:23:19 +0300 Subject: [PATCH 0207/1006] chore(deps): bump mixpanel-browser in /apps/block_scout_web/assets (#12908) Bumps [mixpanel-browser](https://github.com/mixpanel/mixpanel-js) from 2.65.0 to 2.67.0. - [Release notes](https://github.com/mixpanel/mixpanel-js/releases) - [Changelog](https://github.com/mixpanel/mixpanel-js/blob/master/CHANGELOG.md) - [Commits](https://github.com/mixpanel/mixpanel-js/compare/v2.65.0...v2.67.0) --- updated-dependencies: - dependency-name: mixpanel-browser dependency-version: 2.67.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 000b49af22df..a35554302df0 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -46,7 +46,7 @@ "lodash.reduce": "^4.6.0", "luxon": "^3.7.1", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.65.0", + "mixpanel-browser": "^2.67.0", "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", @@ -11366,9 +11366,9 @@ "license": "MIT" }, "node_modules/mixpanel-browser": { - "version": "2.65.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.65.0.tgz", - "integrity": "sha512-BtrVYqilloAqx3TIhoIpNikHznTocEy/z3QIf6WEiz4PFxrgI6LgSMFIVKqLqGZJ8svrPlHbpp/CJp5wQYUZWw==", + "version": "2.67.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.67.0.tgz", + "integrity": "sha512-LudY4eRIkvjEpAlIAg10i2T2mbtiKZ4XlMGbTyF1kcAhEqMa9JhEEdEcjxYPwiKhuMVSBM3RVkNCZaNqcnE4ww==", "license": "Apache-2.0", "dependencies": { "rrweb": "2.0.0-alpha.18" @@ -24247,9 +24247,9 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, "mixpanel-browser": { - "version": "2.65.0", - "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.65.0.tgz", - "integrity": "sha512-BtrVYqilloAqx3TIhoIpNikHznTocEy/z3QIf6WEiz4PFxrgI6LgSMFIVKqLqGZJ8svrPlHbpp/CJp5wQYUZWw==", + "version": "2.67.0", + "resolved": "https://registry.npmjs.org/mixpanel-browser/-/mixpanel-browser-2.67.0.tgz", + "integrity": "sha512-LudY4eRIkvjEpAlIAg10i2T2mbtiKZ4XlMGbTyF1kcAhEqMa9JhEEdEcjxYPwiKhuMVSBM3RVkNCZaNqcnE4ww==", "requires": { "rrweb": "2.0.0-alpha.18" } diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 6cb8762d4033..20cf223099a0 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -58,7 +58,7 @@ "lodash.reduce": "^4.6.0", "luxon": "^3.7.1", "malihu-custom-scrollbar-plugin": "3.1.5", - "mixpanel-browser": "^2.65.0", + "mixpanel-browser": "^2.67.0", "moment": "^2.30.1", "nanomorph": "^5.4.0", "numeral": "^2.0.6", From cab09b3172f6d3018e24f27faeb417463754d551 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 11 Aug 2025 11:49:17 +0300 Subject: [PATCH 0208/1006] Change RELEASE_VERSION in generate-swagger.yml --- .github/workflows/generate-swagger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 8959f611ccb4..012c8aba7808 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 8.1.0 + RELEASE_VERSION: 9.0.0 jobs: matrix-builder: From f1e1abe906660ace859f75650268367f81b2a16a Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 11 Aug 2025 18:54:13 +0300 Subject: [PATCH 0209/1006] chore: restore `getblocknobytime` response format to use `blockNumber` key (#12955) * chore: restore `getblocknobytime` response format to use `blockNumber` key * fix: schema validation --- .../views/api/rpc/block_view.ex | 9 ++++++- .../api/rpc/block_controller_test.exs | 24 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex index ac840679bbe5..9144bcf43a3b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex @@ -78,7 +78,14 @@ defmodule BlockScoutWeb.API.RPC.BlockView do end def render("getblocknobytime.json", %{block_number: block_number}) do - RPCView.render("show.json", data: to_string(block_number)) + # TODO: migrate to the following format in the next release + # RPCView.render("show.json", data: to_string(block_number)) + + RPCView.render("show.json", + data: %{ + "blockNumber" => to_string(block_number) + } + ) end def render("eth_block_number.json", %{number: number, id: id}) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs index ce5c4e1bd713..e9af24b278ef 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs @@ -321,7 +321,9 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == "#{block.number}" + # TODO: migrate to the following format in the next release + # assert response["result"] == "#{block.number}" + assert response["result"] == %{"blockNumber" => "#{block.number}"} assert response["status"] == "1" assert response["message"] == "OK" schema = resolve_getblocknobytime_schema() @@ -349,7 +351,9 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == "#{block.number}" + # TODO: migrate to the following format in the next release + # assert response["result"] == "#{block.number}" + assert response["result"] == %{"blockNumber" => "#{block.number}"} assert response["status"] == "1" assert response["message"] == "OK" schema = resolve_getblocknobytime_schema() @@ -377,7 +381,9 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do }) |> json_response(200) - assert response["result"] == "#{block.number}" + # TODO: migrate to the following format in the next release + # assert response["result"] == "#{block.number}" + assert response["result"] == %{"blockNumber" => "#{block.number}"} assert response["status"] == "1" assert response["message"] == "OK" schema = resolve_getblocknobytime_schema() @@ -441,9 +447,17 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do "properties" => %{ "message" => %{"type" => "string"}, "status" => %{"type" => "string"}, + # TODO: migrate to the following format in the next release + # + # "result" => %{ + # "type" => ["string", "null"], + # "description" => "Block number as a string or null if not found" + # } "result" => %{ - "type" => ["string", "null"], - "description" => "Block number as a string or null if not found" + "type" => ["object", "null"], + "properties" => %{ + "blockNumber" => %{"type" => "string"} + } } } }) From 1106bfa688deea0a3c26750949fad9e5fedf65be Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 11 Aug 2025 18:55:56 +0300 Subject: [PATCH 0210/1006] 9.0.1 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .github/workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .github/workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .../workflows/publish-docker-image-for-optimism-interop.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-suave.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 6 ++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 ++-- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 65 files changed, 71 insertions(+), 65 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 012c8aba7808..cc2f550d6be8 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 74e2854e68d9..255197f7c37e 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index a81ffa8ac494..c8d190512ccf 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 38469927d753..bc764496fc5f 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 019cc25cd563..84cd36ceef10 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 26b227203284..23b3ccf0a3e6 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index eadaf588916d..4c6acfcbf34f 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 72b5c44a4e1f..868fe1a22fda 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index c40b8687420e..cebfd271382f 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 5740a7fff571..89985efb1ec5 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 88c6914dcd04..f8306136d844 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 5c88e2180f99..7f9524919e59 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 6e6fe7960686..6564e9b8b761 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 964a6ae0d0f8..30754308a9d9 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index c3b665807408..8e5736d159fd 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index e12b76391202..96b4d85c8022 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index b7b5e1b5cf1f..c7d7e8e1856f 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index a3ee47350f5a..7a035806f233 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 373db1adf81f..e28a57d2d632 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index df8daa2e1a15..297ead4c5ab6 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index aaad795e0c9c..6049db7ab384 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 3112c2de1a2a..bdec57adb3be 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 8ebca265b809..0cd328c1deb4 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 2d7da3ae0072..3ac7e5d2c45c 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 1f47b8f5f2e8..fc6d74815788 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/publish-docker-image-for-optimism-interop.yml index 7b1f1c0807bf..e958c5aabe7f 100644 --- a/.github/workflows/publish-docker-image-for-optimism-interop.yml +++ b/.github/workflows/publish-docker-image-for-optimism-interop.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index fee395147737..f08b5dbbcc2e 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index bad9afb96f8e..b646fc0bbabd 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 85eef533da0d..c2fac18a15f2 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 2422e75f0739..684bd3cf00f1 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index aabc23cc8e3c..e2eb6901fcfe 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index e9fdd8352a95..46da3970c7d4 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 72c70c073416..318eaea2e284 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 3b8cf65b6730..7a7822009f89 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 5cbc10c0ee97..bba3b58b29db 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -12,7 +12,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 5cefcabb5eef..7a958bcf9147 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 jobs: push_to_registry: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index b4878b64bace..d69bdcc3b5fe 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 jobs: push_to_registry: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 03149dbe4bce..aaa61f128f97 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 46648481d74e..45ff1e247e30 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 26171805ef44..c9155b8b4ab1 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 8534f19f60a6..a68fbdad0a0a 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 971365857c8e..a95572474058 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index e6d92963a38f..c6d69cae5747 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 2697824b035d..d47e14f04471 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 2fafe3eed18b..ab32782db195 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 4ae903b37e04..35b2d1dd974d 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index f7d2763af104..ab54f1d0988e 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index 70490bcad29d..ac7864bf30f9 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 5161890b6fb7..f20229e2f678 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 416e19b8dd29..8cc4239ef91c 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index d1d3845ace5e..88bb533291ef 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9a0f15eb1a4..90626013ae4f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a987ba82f04..110b56c40a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 9.0.1 + +### ⚙️ Miscellaneous Tasks + +- Restore `getblocknobytime` response format to use `blockNumber` key ([#12955](https://github.com/blockscout/blockscout/issues/12955)) + ## 9.0.0 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 66c8b193c681..3edfd1ffc044 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.0", + version: "9.0.1", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 439e50198566..f8698a50f5a2 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.0" + version: "9.0.1" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index c17586b64a4e..16918bc1bf46 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.0.0"}] + @default_headers [{"User-Agent", "blockscout-9.0.1"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 0de75c0871bb..756df92678a6 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.0", + version: "9.0.1", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 5a3ba6ca953b..58ac3ed083ab 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.0.0", + version: "9.0.1", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index d66a28e5e42a..1e07e34d0e7a 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.0.0", + version: "9.0.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 534f2bc3afa7..3d18b6199c1e 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.0.0", + version: "9.0.1", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 5e66c209e11e..55cb97f4a865 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index 186a735ba204..a5c814d8c6cb 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 9.0.0 + RELEASE_VERSION: 9.0.1 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index 83daac83b889..bfb1f9e0f941 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.0.0' +RELEASE_VERSION ?= '9.0.1' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 761421eb0438..6ee49f69689d 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.0.0", + version: "9.0.1", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 0575a39fa86b..2e43fa830431 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.0.0" + set version: "9.0.1" set applications: [ :runtime_tools, block_scout_web: :permanent, From 37bb58f0cb53ebba78ec5e2b146d64ec5bf146ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:25:09 +0300 Subject: [PATCH 0211/1006] chore(deps): bump ex_keccak from 0.7.6 to 0.7.8 (#12963) Bumps [ex_keccak](https://github.com/tzumby/ex_keccak) from 0.7.6 to 0.7.8. - [Release notes](https://github.com/tzumby/ex_keccak/releases) - [Changelog](https://github.com/ExWeb3/ex_keccak/blob/main/CHANGELOG.md) - [Commits](https://github.com/tzumby/ex_keccak/compare/v0.7.6...v0.7.8) --- updated-dependencies: - dependency-name: ex_keccak dependency-version: 0.7.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index c706d04c00ba..dd4dffda14a2 100644 --- a/mix.lock +++ b/mix.lock @@ -62,7 +62,7 @@ "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, - "ex_keccak": {:hex, :ex_keccak, "0.7.6", "110c3ed76b55265975d9ae6628205b8a026f11fe081f3073e00c29aab2e91473", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "9d1568424eb7b995e480d1b7f0c1e914226ee625496600abb922bba6f5cdc5e4"}, + "ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.3", "a2877e7028807a494fec3e5286e82872e10d632ddc885c4e26e64eac9ece5651", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "cbb80889f0bb1154fc694ddb66807ae9b7c96cb6a59ea70021d7beaf3a4a9d08"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, @@ -152,7 +152,7 @@ "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"}, "siwe": {:git, "https://github.com/royal-markets/siwe-ex.git", "51c9c08240eb7eea3c35693011f8d260cd9bb3be", [ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, From 6d5534ea8de80d6eda004f6dd0a3cee749970bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:27:08 +0300 Subject: [PATCH 0212/1006] chore(deps): bump open_api_spex from 3.21.5 to 3.22.0 (#12967) Bumps [open_api_spex](https://github.com/open-api-spex/open_api_spex) from 3.21.5 to 3.22.0. - [Release notes](https://github.com/open-api-spex/open_api_spex/releases) - [Changelog](https://github.com/open-api-spex/open_api_spex/blob/master/CHANGELOG.md) - [Commits](https://github.com/open-api-spex/open_api_spex/compare/v3.21.5...v3.22.0) --- updated-dependencies: - dependency-name: open_api_spex dependency-version: 3.22.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index dd4dffda14a2..77651c456c0a 100644 --- a/mix.lock +++ b/mix.lock @@ -122,7 +122,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, - "open_api_spex": {:hex, :open_api_spex, "3.21.5", "ff0c7fe5ceff9a56b9b0bb5a6dcfb7bc96e8afc563a3bef6ae91927de4d38b8e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "bd83c8f462222236fa85044098ba3bf57f7b7d7fd5286e6bc0060c7916f7c0d8"}, + "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, @@ -178,5 +178,5 @@ "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, - "ymlr": {:hex, :ymlr, "5.1.3", "a8061add5a378e20272a31905be70209a5680fdbe0ad51f40cb1af4bdd0a010b", [:mix], [], "hexpm", "8663444fa85101a117887c170204d4c5a2182567e5f84767f0071cf15f2efb1e"}, + "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"}, } From a0cc66af0d1dd1f5524850774e0fee1b34bacf24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 13:28:53 +0300 Subject: [PATCH 0213/1006] chore(deps): bump hammer_backend_redis from 7.0.2 to 7.1.0 (#12966) Bumps [hammer_backend_redis](https://github.com/ExHammer/hammer-backend-redis) from 7.0.2 to 7.1.0. - [Changelog](https://github.com/ExHammer/hammer-backend-redis/blob/master/CHANGELOG.md) - [Commits](https://github.com/ExHammer/hammer-backend-redis/compare/7.0.2...7.1.0) --- updated-dependencies: - dependency-name: hammer_backend_redis dependency-version: 7.1.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 77651c456c0a..68acb6dc1804 100644 --- a/mix.lock +++ b/mix.lock @@ -81,8 +81,8 @@ "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"}, - "hammer": {:hex, :hammer, "7.0.1", "136edcd81af44becbe6b73a958c109e2364ab0dc026d7b19892037dc2632078c", [:mix], [], "hexpm", "796edf14ab2aa80df72080210fcf944ee5e8868d8ece7a7511264d802f58cc2d"}, - "hammer_backend_redis": {:hex, :hammer_backend_redis, "7.0.2", "b58a2c4b553cd809b4ff059b80fc146f89954f03760b9eca674e0e6db3ed1fa9", [:mix], [{:hammer, "~> 7.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.5", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "827a5ff418f99c7c4b17c1d59a4b0fef099591a75910daa3131005da3223106d"}, + "hammer": {:hex, :hammer, "7.1.0", "9b55acd9951035f330aa1556e575e72d93334e8732daf5f2f602793d16008fe9", [:mix], [], "hexpm", "0ef3f0b9b92ae10a01604ca58adc2bfc8df0af4414a3afcf2dd79e256bc94c17"}, + "hammer_backend_redis": {:hex, :hammer_backend_redis, "7.1.0", "b1e3d5d9cb9a549d90109d17c12eb0c57ffe4658163e597542ad77c3c5464a46", [:mix], [{:hammer, "~> 7.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.5", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "b0c22a0121a293002c09f415fc3a04e153c993db8702b4320b1aa9a6edd6de1a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, From bf5b645ef6dafba936b9ffffbc537433053fce9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:18:45 +0300 Subject: [PATCH 0214/1006] chore(deps): bump hackney from 1.24.1 to 1.25.0 (#12956) Bumps [hackney](https://github.com/benoitc/hackney) from 1.24.1 to 1.25.0. - [Release notes](https://github.com/benoitc/hackney/releases) - [Changelog](https://github.com/benoitc/hackney/blob/master/NEWS.md) - [Commits](https://github.com/benoitc/hackney/compare/1.24.1...1.25.0) --- updated-dependencies: - dependency-name: hackney dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 68acb6dc1804..1ca2a9437fa3 100644 --- a/mix.lock +++ b/mix.lock @@ -80,7 +80,7 @@ "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, - "hackney": {:hex, :hackney, "1.24.1", "f5205a125bba6ed4587f9db3cc7c729d11316fa8f215d3e57ed1c067a9703fa9", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "f4a7392a0b53d8bbc3eb855bdcc919cd677358e65b2afd3840b5b3690c4c8a39"}, + "hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"}, "hammer": {:hex, :hammer, "7.1.0", "9b55acd9951035f330aa1556e575e72d93334e8732daf5f2f602793d16008fe9", [:mix], [], "hexpm", "0ef3f0b9b92ae10a01604ca58adc2bfc8df0af4414a3afcf2dd79e256bc94c17"}, "hammer_backend_redis": {:hex, :hammer_backend_redis, "7.1.0", "b1e3d5d9cb9a549d90109d17c12eb0c57ffe4658163e597542ad77c3c5464a46", [:mix], [{:hammer, "~> 7.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.5", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "b0c22a0121a293002c09f415fc3a04e153c993db8702b4320b1aa9a6edd6de1a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, From 1602ffd3b51aa57ee665aaea3e9207034061eb1e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:48:54 +0300 Subject: [PATCH 0215/1006] chore(deps): bump ton from 0.5.0 to 0.5.1 (#12959) Bumps [ton](https://github.com/ayrat555/ton) from 0.5.0 to 0.5.1. - [Release notes](https://github.com/ayrat555/ton/releases) - [Changelog](https://github.com/ayrat555/ton/blob/master/CHANGELOG.md) - [Commits](https://github.com/ayrat555/ton/compare/0.5.0...0.5.1) --- updated-dependencies: - dependency-name: ton dependency-version: 0.5.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mix.lock b/mix.lock index 1ca2a9437fa3..486a348eb689 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "bureaucrat": {:hex, :bureaucrat, "0.2.10", "b0de157dad540e40007b663b683f716ced21f85ff0591093aadb209ad0d967e1", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "bc7e5162b911c29c8ebefee87a2c16fbf13821a58f448a8fd024eb6c17fae15c"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, - "cafezinho": {:hex, :cafezinho, "0.4.3", "0983519fc9a76d7a54347f563533120b73ba3f2a444a13431149d5c8bcee2d08", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "1437d5de261dde0d94c012aaab48d9ee66f4588280a1ed09ab452c9995028e72"}, + "cafezinho": {:hex, :cafezinho, "0.4.4", "36c31fc5456b1284180d8a9d968c7eaaf474782df5af023a8f8b66937c5b2785", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "3ca334f2a2992ec081868c39ad0487eb97d213292702aae6b197fd5e6f04fd58"}, "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, @@ -48,7 +48,7 @@ "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, - "evil_crc32c": {:hex, :evil_crc32c, "0.2.8", "4a522b010bcf1b11fa4dcfea8e9c7cf8fd4b4d945dbb0c9115afb0a55a5e5419", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "5028fdd9e4fd66ca5fb54d4919904a7ad7ca7a6884c6d334b66e804d04c37c17"}, + "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"}, "evision": {:hex, :evision, "0.2.13", "15ce6e1ba70c2485ff6ea1fba67fa9d32abb7442c9189fa65f8cda20357e079e", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "49ff677cbbeaa1870841b5ac9dc4211e62d32d9585253c02a95dc98e30981e7a"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, @@ -64,7 +64,7 @@ "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, - "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.3", "a2877e7028807a494fec3e5286e82872e10d632ddc885c4e26e64eac9ece5651", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "cbb80889f0bb1154fc694ddb66807ae9b7c96cb6a59ea70021d7beaf3a4a9d08"}, + "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.5", "583733a1134a0c8b4df70c6b3e874d1896bf48f2aaafdf332ec5480c7b5117bf", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f138b2acdc5a4d87880e42742e9bead4d1235fd9b1b2b29be0d1da1e01c3a312"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.5", "46e2de19374585da06d99d39de9c6ef9bd666fdf90930d7963cb7dbf1141af46", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "046207ab26908de8b99fbd799d35b1edc6318c2ccf891a8d58f90b7d2072e6bd"}, "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, @@ -109,7 +109,7 @@ "mimerl": {:hex, :mimerl, "1.4.0", "3882a5ca67fbbe7117ba8947f27643557adec38fa2307490c4c4207624cb213b", [:rebar3], [], "hexpm", "13af15f9f68c65884ecca3a3891d50a7b57d82152792f3e19d88650aa126b144"}, "mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, - "mnemoniac": {:hex, :mnemoniac, "0.1.3", "f2aed9ddc36c82938170d38f8944f79725f95acc1e7c08e223bad38aac9953cf", [:mix], [], "hexpm", "dc2f2da766134c55282b48358eda66133e27815c3eb5ad7ecaa1e72281a0658f"}, + "mnemoniac": {:hex, :mnemoniac, "0.1.5", "d10bf14abb94adc1a98f6cee4d81d197e1b3986cad8ec43b2e21d2cd9f4d99b6", [:mix], [], "hexpm", "518ae4b6ddda5285a5c02bf83e1eb6ce1af7e4d65d3f7f5bff3270689a00bcc3"}, "mock": {:hex, :mock, "0.3.9", "10e44ad1f5962480c5c9b9fa779c6c63de9bd31997c8e04a853ec990a9d841af", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "9e1b244c4ca2551bb17bb8415eed89e40ee1308e0fbaed0a4fdfe3ec8a4adbd3"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "msgpax": {:hex, :msgpax, "2.4.0", "4647575c87cb0c43b93266438242c21f71f196cafa268f45f91498541148c15d", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "ca933891b0e7075701a17507c61642bf6e0407bb244040d5d0a58597a06369d2"}, @@ -166,7 +166,7 @@ "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, - "ton": {:hex, :ton, "0.5.0", "be7f22a06910c00d1b3d18db70b5aec32dbcb0d6ec7bcac076cadc53cc623f0a", [:mix], [{:cafezinho, "~> 0.4.3", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.8", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.3", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.2", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "87e12d175a7144e152111285a8124b9cc75cea429b2cf32ef165396f711e892f"}, + "ton": {:hex, :ton, "0.5.1", "79745434a93e5f7de3572fdcf04feb048620f0edab9794fc412a73528672927d", [:mix], [{:cafezinho, "~> 0.4.4", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.9", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.4", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.4", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "916f656c870902a61690347da9500c5ce27f04c02e02441363bac7b128030f07"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"}, "tzdata": {:hex, :tzdata, "1.1.3", "b1cef7bb6de1de90d4ddc25d33892b32830f907e7fc2fccd1e7e22778ab7dfbc", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d4ca85575a064d29d4e94253ee95912edfb165938743dbf002acdf0dcecb0c28"}, "ueberauth": {:hex, :ueberauth, "0.10.8", "ba78fbcbb27d811a6cd06ad851793aaf7d27c3b30c9e95349c2c362b344cd8f0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f2d3172e52821375bccb8460e5fa5cb91cfd60b19b636b6e57e9759b6f8c10c1"}, From b29c564504a0f27a0bc3bfd79cb0c2c5f2473292 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 14:49:28 +0300 Subject: [PATCH 0216/1006] chore(deps): bump bamboo from 2.4.0 to 2.5.0 (#12961) Bumps [bamboo](https://github.com/thoughtbot/bamboo) from 2.4.0 to 2.5.0. - [Release notes](https://github.com/thoughtbot/bamboo/releases) - [Changelog](https://github.com/beam-community/bamboo/blob/main/CHANGELOG.md) - [Commits](https://github.com/thoughtbot/bamboo/compare/v2.4.0...v2.5.0) --- updated-dependencies: - dependency-name: bamboo dependency-version: 2.5.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/explorer/mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 756df92678a6..ffc3c24f87ed 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -57,7 +57,7 @@ defmodule Explorer.Mixfile do # Type `mix help deps` for examples and options. defp deps do [ - {:bamboo, "~> 2.4.0"}, + {:bamboo, "~> 2.5.0"}, {:mime, "~> 2.0"}, {:bcrypt_elixir, "~> 3.0"}, # benchmark optimizations diff --git a/mix.lock b/mix.lock index 486a348eb689..79580e3facc8 100644 --- a/mix.lock +++ b/mix.lock @@ -4,7 +4,7 @@ "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, - "bamboo": {:hex, :bamboo, "2.4.0", "d26bd9f22a18cfa4448dac9b305debfd533b73064aeaea10d251400e543b472b", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "85e15dfce007faa04ef3baa46fe0737aba955cba5843bad17514519f9cb86592"}, + "bamboo": {:hex, :bamboo, "2.5.0", "973f5cb1471a1d2d7d9da7b8e4f6096afb6a133f85394631184fd40be8adb8ab", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35c8635ff6677a81ab7258944ff15739280f3254a041b6f0229dddeb9b90ad3d"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, From 878e582564fdd40e67f5be4eb7cb2a8807fb9d60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:15:55 +0300 Subject: [PATCH 0217/1006] chore(deps): bump postgrex from 0.20.0 to 0.21.1 (#12964) Bumps [postgrex](https://github.com/elixir-ecto/postgrex) from 0.20.0 to 0.21.1. - [Release notes](https://github.com/elixir-ecto/postgrex/releases) - [Changelog](https://github.com/elixir-ecto/postgrex/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-ecto/postgrex/compare/v0.20.0...v0.21.1) --- updated-dependencies: - dependency-name: postgrex dependency-version: 0.21.1 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 79580e3facc8..f11a43f0c3fd 100644 --- a/mix.lock +++ b/mix.lock @@ -137,7 +137,7 @@ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, + "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, "prometheus": {:hex, :prometheus, "4.11.0", "b95f8de8530f541bd95951e18e355a840003672e5eda4788c5fa6183406ba29a", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "719862351aabf4df7079b05dc085d2bbcbe3ac0ac3009e956671b1d5ab88247d"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]}, From 481973a14e10686a007f5ca53e86c646f75e392f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:20:24 +0300 Subject: [PATCH 0218/1006] chore(deps-dev): bump dialyxir from 1.4.5 to 1.4.6 (#12957) Bumps [dialyxir](https://github.com/jeremyjh/dialyxir) from 1.4.5 to 1.4.6. - [Release notes](https://github.com/jeremyjh/dialyxir/releases) - [Changelog](https://github.com/jeremyjh/dialyxir/blob/master/CHANGELOG.md) - [Commits](https://github.com/jeremyjh/dialyxir/compare/1.4.5...1.4.6) --- updated-dependencies: - dependency-name: dialyxir dependency-version: 1.4.6 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index f11a43f0c3fd..c6c6ee8c2a25 100644 --- a/mix.lock +++ b/mix.lock @@ -39,7 +39,7 @@ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, + "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, From e1887244973021530d4b8d2f2717da0b971cbb8a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:47:13 +0300 Subject: [PATCH 0219/1006] chore(deps): bump evision from 0.2.13 to 0.2.14 (#12968) Bumps [evision](https://github.com/cocoa-xu/evision) from 0.2.13 to 0.2.14. - [Release notes](https://github.com/cocoa-xu/evision/releases) - [Changelog](https://github.com/cocoa-xu/evision/blob/main/CHANGELOG.md) - [Commits](https://github.com/cocoa-xu/evision/compare/v0.2.13...v0.2.14) --- updated-dependencies: - dependency-name: evision dependency-version: 0.2.14 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index c6c6ee8c2a25..c05e5d2d597f 100644 --- a/mix.lock +++ b/mix.lock @@ -16,7 +16,7 @@ "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "cafezinho": {:hex, :cafezinho, "0.4.4", "36c31fc5456b1284180d8a9d968c7eaaf474782df5af023a8f8b66937c5b2785", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "3ca334f2a2992ec081868c39ad0487eb97d213292702aae6b197fd5e6f04fd58"}, - "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, + "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, @@ -49,7 +49,7 @@ "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"}, - "evision": {:hex, :evision, "0.2.13", "15ce6e1ba70c2485ff6ea1fba67fa9d32abb7442c9189fa65f8cda20357e079e", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "49ff677cbbeaa1870841b5ac9dc4211e62d32d9585253c02a95dc98e30981e7a"}, + "evision": {:hex, :evision, "0.2.14", "6005e44b38ea61a33109694c5b097d42e81c767723c55d0c230d9662d6c87987", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "630714395f2c242e488944b4c9c75a5de96cbb95d3cbb69f0525bdb3e0a88775"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"}, From 499d4b487c8c1a77ea8b6fb6cd85219b87844137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 15:53:26 +0300 Subject: [PATCH 0220/1006] chore(deps): bump image from 0.61.1 to 0.62.0 (#12962) Bumps [image](https://github.com/kipcole9/image) from 0.61.1 to 0.62.0. - [Release notes](https://github.com/kipcole9/image/releases) - [Changelog](https://github.com/elixir-image/image/blob/main/CHANGELOG.md) - [Commits](https://github.com/kipcole9/image/compare/v0.61.1...v0.62.0) --- updated-dependencies: - dependency-name: image dependency-version: 0.62.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index c05e5d2d597f..4e4497da3990 100644 --- a/mix.lock +++ b/mix.lock @@ -18,7 +18,7 @@ "cafezinho": {:hex, :cafezinho, "0.4.4", "36c31fc5456b1284180d8a9d968c7eaaf474782df5af023a8f8b66937c5b2785", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "3ca334f2a2992ec081868c39ad0487eb97d213292702aae6b197fd5e6f04fd58"}, "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, - "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, + "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, "cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, @@ -87,7 +87,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.61.1", "029d9e2070288dab4a9c50938b1a7809298b61c23d43cd617d6e92ac9ceebb24", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "88f1fdeaeec36696acb9b261f035bbe53198cd2830f841c3e9cbf9010c16a5f9"}, + "image": {:hex, :image, "0.62.0", "037a09f4300e5eb5a7612b2739648009250e327b4357d94372f82578469067b9", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "3c28dd98b1d14f5408b5d5c1d46258f54119468979286379033cc1efa87155ac"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, From 208e32feb08d6e52dee97776ea9ef52d21307fbb Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:03:58 +0300 Subject: [PATCH 0221/1006] fix: Remove apikey from next_page_params (#12972) --- apps/block_scout_web/lib/block_scout_web/paging_helper.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 29617cdfc969..cfd72b9e216e 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -228,6 +228,8 @@ defmodule BlockScoutWeb.PagingHelper do params |> Map.drop([ :address_hash_param, + :apikey, + "apikey", "block_hash_or_number", "transaction_hash_param", "address_hash_param", From 71623564ad88c4336311a8345e6c529547bd81a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:04:31 +0300 Subject: [PATCH 0222/1006] chore(deps): bump tesla from 1.14.3 to 1.15.3 (#12970) Bumps [tesla](https://github.com/elixir-tesla/tesla) from 1.14.3 to 1.15.3. - [Release notes](https://github.com/elixir-tesla/tesla/releases) - [Changelog](https://github.com/elixir-tesla/tesla/blob/master/CHANGELOG.md) - [Commits](https://github.com/elixir-tesla/tesla/compare/v1.14.3...v1.15.3) --- updated-dependencies: - dependency-name: tesla dependency-version: 1.15.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/mix.exs | 2 +- apps/utils/mix.exs | 2 +- mix.exs | 2 +- mix.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index f8698a50f5a2..e24aad565c5c 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -74,7 +74,7 @@ defmodule EthereumJSONRPC.MixProject do {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog {:spandex_datadog, "~> 1.0"}, - {:tesla, "~> 1.14.2"}, + {:tesla, "~> 1.15.3"}, # Convert unix timestamps in JSONRPC to DateTimes {:timex, "~> 3.7.1"}, # Encode/decode function names and arguments diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index ffc3c24f87ed..aec67f77b0c1 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -115,7 +115,7 @@ defmodule Explorer.Mixfile do # `Timex.Duration` for `Explorer.Chain.Cache.Counters.AverageBlockTime.average_block_time/0` {:timex, "~> 3.7.1"}, {:con_cache, "~> 1.0"}, - {:tesla, "~> 1.14.2"}, + {:tesla, "~> 1.15.3"}, {:cbor, "~> 1.0"}, {:cloak_ecto, "~> 1.3.0"}, {:redix, "~> 1.1"}, diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 3d18b6199c1e..c5869847f8be 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -33,7 +33,7 @@ defmodule Utils.MixProject do {:credo, "~> 1.5", only: [:test, :dev], runtime: false}, {:httpoison, "~> 2.0"}, {:mime, "~> 2.0"}, - {:tesla, "~> 1.14.2"} + {:tesla, "~> 1.15.3"} ] end diff --git a/mix.exs b/mix.exs index 6ee49f69689d..cfa67aaaacdb 100644 --- a/mix.exs +++ b/mix.exs @@ -96,7 +96,7 @@ defmodule BlockScout.Mixfile do [ {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, - {:tesla, "~> 1.14.2"}, + {:tesla, "~> 1.15.3"}, {:mint, "~> 1.7.1"}, # Documentation {:ex_doc, "~> 0.38.1", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 4e4497da3990..86aabd45eca8 100644 --- a/mix.lock +++ b/mix.lock @@ -164,7 +164,7 @@ "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "tesla": {:hex, :tesla, "1.14.3", "b27ba2814cc08b5c4eb5f0245120198542cd023f575c490fa14447ae6763ea8d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "4f419aa2ab908cff43a117d3b5de99edad8fd54690211cbbdc7d2941c03a1458"}, + "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, "ton": {:hex, :ton, "0.5.1", "79745434a93e5f7de3572fdcf04feb048620f0edab9794fc412a73528672927d", [:mix], [{:cafezinho, "~> 0.4.4", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.9", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.4", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.4", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "916f656c870902a61690347da9500c5ce27f04c02e02441363bac7b128030f07"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"}, From d5daaac0aa4fe62fb1a5b50ef828a930df3200cb Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 13 Aug 2025 17:19:40 +0400 Subject: [PATCH 0223/1006] Fix RangesHelper.sanitize_ranges for empty list (#12946) * Fix RangesHelper.sanitize_ranges for empty list * Fix sanitize_ranges/1 for step -1 + refactor --- .../ethereum_jsonrpc/utility/ranges_helper.ex | 90 +++++++++++-------- .../utility/ranges_helper_test.exs | 24 +++++ .../block/catchup/missing_ranges_collector.ex | 30 +------ 3 files changed, 81 insertions(+), 63 deletions(-) create mode 100644 apps/ethereum_jsonrpc/test/ethereum_jsonrpc/utility/ranges_helper_test.exs diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex index df9366b06f71..88d499db1b2d 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex @@ -68,19 +68,43 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do def parse_block_ranges(block_ranges_string) do block_ranges_string |> String.split(",") - |> Enum.map(fn string_range -> + |> Enum.reduce({[], nil}, fn string_range, {ranges, to_latest} -> case String.split(string_range, "..") do [from_string, "latest"] -> - parse_integer(from_string) + {ranges, parse_integer(from_string)} [from_string, to_string] -> - get_from_to(from_string, to_string) + {[get_from_to(from_string, to_string) | ranges], to_latest} _ -> - nil + {ranges, to_latest} end end) - |> sanitize_ranges() + |> then(fn + {ranges, nil} -> + sanitize_ranges(ranges) + + {ranges, to_latest} -> + ranges + |> sanitize_ranges() + |> Enum.reduce([], fn + first..last//1 = range, acc -> + cond do + first >= to_latest -> acc + last >= to_latest -> [first..(to_latest - 1) | acc] + true -> [range | acc] + end + + first..last//-1 = range, acc -> + cond do + last >= to_latest -> acc + first >= to_latest -> [(to_latest - 1)..last | acc] + true -> [range | acc] + end + end) + |> Enum.reverse() + |> Kernel.++([to_latest]) + end) end @doc """ @@ -136,39 +160,11 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do @doc """ Rejects empty ranges and merges adjacent ranges """ - @spec sanitize_ranges([Range.t() | integer()]) :: [Range.t() | integer()] + @spec sanitize_ranges([Range.t() | nil]) :: [Range.t()] def sanitize_ranges(ranges) do ranges |> Enum.reject(&is_nil/1) - |> Enum.sort_by( - fn - from.._to//_ -> from - el -> el - end, - :asc - ) - |> Enum.chunk_while( - nil, - fn - _from.._to//_ = chunk, nil -> - {:cont, chunk} - - _ch_from..ch_to//_ = chunk, acc_from..acc_to//_ = acc -> - if Range.disjoint?(chunk, acc), - do: {:cont, acc, chunk}, - else: {:cont, acc_from..max(ch_to, acc_to)} - - num, nil -> - {:halt, num} - - num, acc_from.._//_ = acc -> - if Range.disjoint?(num..num, acc), do: {:cont, acc, num}, else: {:halt, acc_from} - - _, num -> - {:halt, num} - end, - fn remainder -> {:cont, remainder, nil} end - ) + |> do_sanitize_ranges() end @doc """ @@ -213,6 +209,30 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do end end + defp do_sanitize_ranges([]), do: [] + + defp do_sanitize_ranges([_.._//step | _] = ranges) do + ranges + |> Enum.reject(&is_nil/1) + |> Enum.map(fn first..last//_ -> min(first, last)..max(first, last) end) + |> Enum.sort_by(fn first.._last//_ -> first end) + |> Enum.reduce([], fn + range, [] -> + [range] + + first..last//_ = range, [previous_first..previous_last//_ | rest] = acc -> + if first <= previous_last + 1 do + [previous_first..max(previous_last, last) | rest] + else + [range | acc] + end + end) + |> Enum.reverse() + |> Enum.map(fn first..last//_ -> + if step == 1, do: first..last, else: last..first//-1 + end) + end + defp extract_block_number(%{block_number: block_number}), do: block_number defp extract_block_number(block_number), do: block_number diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/utility/ranges_helper_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/utility/ranges_helper_test.exs new file mode 100644 index 000000000000..d7cb9f8a56f8 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/utility/ranges_helper_test.exs @@ -0,0 +1,24 @@ +defmodule EthereumJSONRPC.Utility.RangesHelperTest do + use ExUnit.Case, async: true + + alias EthereumJSONRPC.Utility.RangesHelper + + describe "sanitize_ranges/1" do + test "list of ranges" do + assert RangesHelper.sanitize_ranges([1..2, 1..4, 3..6, 7..9, 11..12]) == [1..9, 11..12] + assert RangesHelper.sanitize_ranges([10..7//-1, 6..4//-1, 3..1//-1]) == [10..1//-1] + assert RangesHelper.sanitize_ranges([10..7//-1, 5..3//-1]) == [5..3//-1, 10..7//-1] + assert RangesHelper.sanitize_ranges([1..3, 7..9, 5..6]) == [1..3, 5..9] + assert RangesHelper.sanitize_ranges([1..3, 5..7, 4..4]) == [1..7] + assert RangesHelper.sanitize_ranges([]) == [] + end + end + + describe "parse_block_ranges/1" do + test "ranges string" do + assert RangesHelper.parse_block_ranges("100..200,300..400,500..latest") == [100..200, 300..400, 500] + assert RangesHelper.parse_block_ranges("100..200,150..300") == [100..300] + assert RangesHelper.parse_block_ranges("") == [] + end + end +end diff --git a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex index 20a7a7d7b8b9..4888a04c2bc5 100644 --- a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex +++ b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex @@ -62,7 +62,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do use Utils.CompileTimeEnvHelper, future_check_interval: [:indexer, [__MODULE__, :future_check_interval]] alias EthereumJSONRPC.Utility.RangesHelper - alias Explorer.{Chain, Helper, Repo} + alias Explorer.{Chain, Repo} alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Utility.{MissingBlockRange, MissingRangesManipulator} @@ -480,22 +480,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do @spec parse_block_ranges(binary()) :: {:finite_ranges, [Range.t()]} | {:infinite_ranges, [Range.t()], non_neg_integer()} | :no_ranges def parse_block_ranges(block_ranges_string) do - ranges = - block_ranges_string - |> String.split(",") - |> Enum.map(fn string_range -> - case String.split(string_range, "..") do - [from_string, "latest"] -> - Helper.parse_integer(from_string) - - [from_string, to_string] -> - get_from_to(from_string, to_string) - - _ -> - nil - end - end) - |> RangesHelper.sanitize_ranges() + ranges = RangesHelper.parse_block_ranges(block_ranges_string) case List.last(ranges) do _from.._to//_ -> @@ -508,15 +493,4 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do {:infinite_ranges, List.delete_at(ranges, -1), num - 1} end end - - # Creates a range from string boundaries, returning nil if parsing fails or if from > to - @spec get_from_to(binary(), binary()) :: Range.t() | nil - defp get_from_to(from_string, to_string) do - with {from, ""} <- Integer.parse(from_string), - {to, ""} <- Integer.parse(to_string) do - if from <= to, do: from..to, else: nil - else - _ -> nil - end - end end From b390bc09f836e7f79f520555c8b88094b2e42aae Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 09:53:56 +0300 Subject: [PATCH 0224/1006] Increase multichain queues sizes (#12988) --- .../multichain_search_db/balances_export_queue.ex | 2 +- .../multichain_search_db/main_export_queue.ex | 2 +- config/runtime.exs | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex index 1962e92575b4..ce9c23ce5fa4 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -19,7 +19,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do @behaviour BufferedTask @delete_queries_chunk_size 10 - @default_max_batch_size 1000 + @default_max_batch_size 3000 @default_max_concurrency 10 @failed_to_re_export_data_error "Batch balances export retry to the Multichain Search DB failed" diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex index 32c31a7e98b7..d9ced6aea750 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/main_export_queue.ex @@ -18,7 +18,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueue do @behaviour BufferedTask @delete_queries_chunk_size 100 - @default_max_batch_size 1000 + @default_max_batch_size 3000 @default_max_concurrency 10 @failed_to_re_export_data_error "Batch main export retry to the Multichain Search DB failed" diff --git a/config/runtime.exs b/config/runtime.exs index d1bd64a3b3fc..e76d1bc1e42b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1142,21 +1142,21 @@ config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_CONCURRENCY", 10), - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE", 1_000), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_BATCH_SIZE", 3_000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", "1s" ), max_queue_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 1_000), + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_MAX_QUEUE_SIZE", 3_000), init_limit: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_INIT_QUERY_LIMIT", 1_000) + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_INIT_QUERY_LIMIT", 3_000) config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_CONCURRENCY", 10), batch_size: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE", 1_000), + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_BATCH_SIZE", 3_000), enqueue_busy_waiting_timeout: ConfigHelper.parse_time_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", @@ -1165,10 +1165,10 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, max_queue_size: ConfigHelper.parse_integer_env_var( "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_MAX_QUEUE_SIZE", - 1_000 + 3_000 ), init_limit: - ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT", 1_000) + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_INIT_QUERY_LIMIT", 3_000) config :indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, concurrency: From 29c1787aad2790c7222dc2b5519f1474194897d7 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:13:02 +0300 Subject: [PATCH 0225/1006] fix: pagination with atoms in paging_params (#12986) --- apps/block_scout_web/lib/block_scout_web/chain.ex | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 50caebc2f68c..3b64e2722e85 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -147,7 +147,15 @@ defmodule BlockScoutWeb.Chain do def next_page_params(_, list, params, paging_function) do paging_params = paging_function.(List.last(list)) - next_page_params = Map.merge(params, paging_params) + atomized_keys = + paging_params + |> Map.keys() + |> Enum.map(fn + key when is_atom(key) -> Atom.to_string(key) + key -> key + end) + + next_page_params = Map.merge(params |> Map.drop(atomized_keys), paging_params) current_items_count_string = Map.get(next_page_params, "items_count") items_count = From 58ce65e46dd231c4082b4f89206f9b143f118b23 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 10:16:34 +0300 Subject: [PATCH 0226/1006] v9.0.2 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .../workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .../workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .../publish-docker-image-for-optimism-interop.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-suave.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 2 +- CHANGELOG.md | 8 ++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 ++-- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 65 files changed, 73 insertions(+), 65 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index cc2f550d6be8..cdb935b945d7 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 255197f7c37e..e7421f8914ec 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index c8d190512ccf..95a167e15fdf 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index bc764496fc5f..9e26377c9faf 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 84cd36ceef10..628c29703c0e 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 23b3ccf0a3e6..34e1cc2e1820 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 4c6acfcbf34f..00d3faad172b 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 868fe1a22fda..d1ea40766ab5 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index cebfd271382f..ae56fa07e317 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 89985efb1ec5..5ed13cf13eba 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index f8306136d844..90bafe95b43a 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 7f9524919e59..19e9cf2726a1 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 6564e9b8b761..7c1afb792c8d 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 30754308a9d9..a9cac327426c 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 8e5736d159fd..8638adeba6d2 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 96b4d85c8022..5322683b68de 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index c7d7e8e1856f..6bf210a2603f 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 7a035806f233..82a2d9df95df 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index e28a57d2d632..524c9eda1e00 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 297ead4c5ab6..0cd2567056c3 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 6049db7ab384..e8c62998372f 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index bdec57adb3be..74d7f8338469 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 0cd328c1deb4..0686c91c7314 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 3ac7e5d2c45c..c458af09f16b 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index fc6d74815788..ca55f12e88ab 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/publish-docker-image-for-optimism-interop.yml index e958c5aabe7f..cbf21768e33e 100644 --- a/.github/workflows/publish-docker-image-for-optimism-interop.yml +++ b/.github/workflows/publish-docker-image-for-optimism-interop.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index f08b5dbbcc2e..c8908ca4523c 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index b646fc0bbabd..48ea1141f796 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index c2fac18a15f2..60477ee49ea2 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 684bd3cf00f1..76cdd7d8ba92 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index e2eb6901fcfe..e0b883f52232 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 46da3970c7d4..bc66fb8990a7 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 318eaea2e284..5a427b801e80 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -10,7 +10,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 7a7822009f89..0194ac2e99d5 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -9,7 +9,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index bba3b58b29db..1b39414a1bb5 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -12,7 +12,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 7a958bcf9147..7511ba4d4133 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 jobs: push_to_registry: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index d69bdcc3b5fe..a8955490e45e 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 jobs: push_to_registry: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index aaa61f128f97..fa893c7f31d8 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 45ff1e247e30..ce5d3decb555 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index c9155b8b4ab1..f581ea9665b6 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index a68fbdad0a0a..730f5e6e8357 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index a95572474058..636b9258945b 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index c6d69cae5747..61a4185d0144 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index d47e14f04471..d1f721103f61 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index ab32782db195..0ccefe4ea67f 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 35b2d1dd974d..cd77da0bd527 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index ab54f1d0988e..20a288a7ea92 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index ac7864bf30f9..04a743b8100f 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index f20229e2f678..cf7ea582a78c 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 8cc4239ef91c..47155a342883 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 88bb533291ef..d9668d3ef8d5 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 90626013ae4f..a7bcd0a62e6c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 steps: - uses: actions/checkout@v4 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 110b56c40a80..02f8e7c40dd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 9.0.2 + +### 🐛 Bug Fixes + +- Pagination with atoms in paging_params ([#12986](https://github.com/blockscout/blockscout/issues/12986)) +- Fix RangesHelper.sanitize_ranges for empty list ([#12946](https://github.com/blockscout/blockscout/issues/12946)) +- Remove apikey from next_page_params ([#12972](https://github.com/blockscout/blockscout/issues/12972)) + ## 9.0.1 ### ⚙️ Miscellaneous Tasks diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 3edfd1ffc044..2591b865998a 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.1", + version: "9.0.2", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index e24aad565c5c..fcbe2b1dced4 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.1" + version: "9.0.2" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 16918bc1bf46..7d0b0b7ab557 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.0.1"}] + @default_headers [{"User-Agent", "blockscout-9.0.2"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index aec67f77b0c1..806b81b794f4 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.1", + version: "9.0.2", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 58ac3ed083ab..e2ee18ec3de8 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.0.1", + version: "9.0.2", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 1e07e34d0e7a..36eec9350335 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.0.1", + version: "9.0.2", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index c5869847f8be..9818e0db811b 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.0.1", + version: "9.0.2", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 55cb97f4a865..0babc774018b 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index a5c814d8c6cb..99b1ce18a614 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 9.0.1 + RELEASE_VERSION: 9.0.2 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index bfb1f9e0f941..d0838a6db327 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.0.1' +RELEASE_VERSION ?= '9.0.2' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index cfa67aaaacdb..5262998f30fe 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.0.1", + version: "9.0.2", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 2e43fa830431..7944eb621a5d 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.0.1" + set version: "9.0.2" set applications: [ :runtime_tools, block_scout_web: :permanent, From fa0f242126a22a63a7af0741305b751f506db45b Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:17:28 +0300 Subject: [PATCH 0227/1006] fix: Set 5 RPS for api/health/* (#12990) --- apps/block_scout_web/priv/rate_limit_config.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/block_scout_web/priv/rate_limit_config.json b/apps/block_scout_web/priv/rate_limit_config.json index 81d72acd0bc1..1b1b72180b75 100644 --- a/apps/block_scout_web/priv/rate_limit_config.json +++ b/apps/block_scout_web/priv/rate_limit_config.json @@ -107,6 +107,13 @@ }, "isolate_rate_limit?": true }, + "api/health/*": { + "ip": { + "period": "1s", + "limit": 5 + }, + "isolate_rate_limit?": true + }, "api/v2/*": { "static_api_key": true, "account_api_key": true, From 05db9e952c18a4ed1325ddfd32a7210786cbb66f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 12:18:39 +0300 Subject: [PATCH 0228/1006] Add CHANGELOG record --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02f8e7c40dd1..e304043df067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🐛 Bug Fixes +- Set 5 RPS for api/health/* ([#12990]https://github.com/blockscout/blockscout/pull/12990) - Pagination with atoms in paging_params ([#12986](https://github.com/blockscout/blockscout/issues/12986)) - Fix RangesHelper.sanitize_ranges for empty list ([#12946](https://github.com/blockscout/blockscout/issues/12946)) - Remove apikey from next_page_params ([#12972](https://github.com/blockscout/blockscout/issues/12972)) From 6c4001ed48a2894aa3aa8909e058df5b3cabfbbb Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 13:13:42 +0300 Subject: [PATCH 0229/1006] Add todo item --- apps/block_scout_web/lib/block_scout_web/routers/api_router.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 34ff45afd3d5..1286f4486888 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -393,7 +393,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - # deprecate in the next major/minor release after 9.0.0 + # todo: deprecate in the next major/minor release after 9.0.0 scope "/3dparty" do get("/:platform_id", V2.Proxy.UniversalProxyController, :index) From 7cc0bf485770a046ce92b6a6a079dbea84fca85c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 13:21:27 +0300 Subject: [PATCH 0230/1006] Update todo item --- apps/block_scout_web/lib/block_scout_web/routers/api_router.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 1286f4486888..8fbc9266ff3b 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -393,7 +393,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - # todo: deprecate in the next major/minor release after 9.0.0 + # todo: deprecate in the next major/minor release in 10.0.0. Related frontend task https://github.com/blockscout/frontend/issues/2945. scope "/3dparty" do get("/:platform_id", V2.Proxy.UniversalProxyController, :index) From 3116cf4ac9a4f2cf9327d90bc836e89048144beb Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 13:24:24 +0300 Subject: [PATCH 0231/1006] Fix Mud worlds API endpoint (#12991) --- CHANGELOG.md | 1 + .../lib/block_scout_web/views/api/v2/mud_view.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e304043df067..46e38ab0fa6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🐛 Bug Fixes +- Fix Mud worlds API endpoint ([#12991](https://github.com/blockscout/blockscout/pull/12991)) - Set 5 RPS for api/health/* ([#12990]https://github.com/blockscout/blockscout/pull/12990) - Pagination with atoms in paging_params ([#12986](https://github.com/blockscout/blockscout/issues/12986)) - Fix RangesHelper.sanitize_ranges for empty list ([#12946](https://github.com/blockscout/blockscout/issues/12946)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex index 83e5c7622d3e..b92f1f063fe0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/mud_view.ex @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.MudView do defp prepare_world_for_list(%Address{} = address) do %{ - "address_hash" => Helper.address_with_info(address, address.hash), + "address" => Helper.address_with_info(address, address.hash), "transactions_count" => address.transactions_count, "coin_balance" => if(address.fetched_coin_balance, do: address.fetched_coin_balance.value) } From b2c2f892bd1cdfb30b96a9a86fea2e6f95f44b9d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 13:46:03 +0300 Subject: [PATCH 0232/1006] Update todo item --- apps/explorer/lib/explorer/chain/blackfort/validator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index b3169f2862f0..c5140b625afa 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -164,7 +164,7 @@ defmodule Explorer.Chain.Blackfort.Validator do {:ok, validators |> Enum.map( - # todo: remove it in favour `address_hash` when the frontend is bound to the new filed (create the task to frontend repo). + # todo: remove it in favour `address_hash` in 10.0.0 (frontend task https://github.com/blockscout/frontend/issues/2946). fn %{ "address_hash" => address_hash_string, "address" => address_hash_string, From e534702286e2b63f0eaa2f3ebe74abc027116a2e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 14:35:59 +0300 Subject: [PATCH 0233/1006] Remove outdated todo item --- .../lib/explorer/chain/blackfort/validator.ex | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index c5140b625afa..c82fa0c57f92 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -163,35 +163,31 @@ defmodule Explorer.Chain.Blackfort.Validator do defp parse_validators_info({:ok, validators}) do {:ok, validators - |> Enum.map( - # todo: remove it in favour `address_hash` in 10.0.0 (frontend task https://github.com/blockscout/frontend/issues/2946). - fn %{ - "address_hash" => address_hash_string, - "address" => address_hash_string, - "name" => name, - "commission" => commission, - "self_bonded_amount" => self_bonded_amount, - "delegated_amount" => delegated_amount, - "slashing_status" => %{ - "is_slashed" => slashing_status_is_slashed, - "by_block" => slashing_status_by_block, - "multiplier" => slashing_status_multiplier - } - } -> - {:ok, address_hash} = HashAddress.cast(address_hash_string) - - %{ - address_hash: address_hash, - name: name, - commission: parse_number(commission), - self_bonded_amount: parse_number(self_bonded_amount), - delegated_amount: parse_number(delegated_amount), - slashing_status_is_slashed: slashing_status_is_slashed, - slashing_status_by_block: slashing_status_by_block, - slashing_status_multiplier: slashing_status_multiplier - } - end - )} + |> Enum.map(fn %{ + "address" => address_hash_string, + "name" => name, + "commission" => commission, + "self_bonded_amount" => self_bonded_amount, + "delegated_amount" => delegated_amount, + "slashing_status" => %{ + "is_slashed" => slashing_status_is_slashed, + "by_block" => slashing_status_by_block, + "multiplier" => slashing_status_multiplier + } + } -> + {:ok, address_hash} = HashAddress.cast(address_hash_string) + + %{ + address_hash: address_hash, + name: name, + commission: parse_number(commission), + self_bonded_amount: parse_number(self_bonded_amount), + delegated_amount: parse_number(delegated_amount), + slashing_status_is_slashed: slashing_status_is_slashed, + slashing_status_by_block: slashing_status_by_block, + slashing_status_multiplier: slashing_status_multiplier + } + end)} end defp parse_validators_info({:error, error}) do From ee04df69ee64d3946ce2fa5055829abc1e4c7a7d Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 14 Aug 2025 14:43:20 +0300 Subject: [PATCH 0234/1006] fix: atoms in token_transfers_next_page_params (#12992) --- .../lib/block_scout_web/chain.ex | 34 ++++++--- .../test/block_scout_web/chain_test.exs | 74 ++++++++++++++++++- 2 files changed, 97 insertions(+), 11 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 3b64e2722e85..a9c03301bea2 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -147,15 +147,9 @@ defmodule BlockScoutWeb.Chain do def next_page_params(_, list, params, paging_function) do paging_params = paging_function.(List.last(list)) - atomized_keys = - paging_params - |> Map.keys() - |> Enum.map(fn - key when is_atom(key) -> Atom.to_string(key) - key -> key - end) - - next_page_params = Map.merge(params |> Map.drop(atomized_keys), paging_params) + string_keys = map_to_string_keys(paging_params) + + next_page_params = Map.merge(params |> Map.drop(string_keys), paging_params) current_items_count_string = Map.get(next_page_params, "items_count") items_count = @@ -980,7 +974,10 @@ defmodule BlockScoutWeb.Chain do |> last_token_transfer_before_current(current_token_transfer) |> (&if(is_nil(&1), do: %{}, else: paging_params(&1))).() + string_keys = map_to_string_keys(new_params) + params + |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]) |> Map.merge(new_params) |> Map.merge(%{ batch_log_index: current_token_transfer.log_index, @@ -989,7 +986,15 @@ defmodule BlockScoutWeb.Chain do index_in_batch: current_token_transfer.index_in_batch }) else - Map.merge(params, paging_params(List.last(list))) + new_params = paging_params(List.last(list)) + + string_keys = map_to_string_keys(new_params) + + Map.merge( + params + |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]), + new_params + ) end end @@ -1040,4 +1045,13 @@ defmodule BlockScoutWeb.Chain do @spec fetch_scam_token_toggle(Keyword.t(), Plug.Conn.t()) :: Keyword.t() def fetch_scam_token_toggle(params, conn), do: Keyword.put(params, :show_scam_tokens?, conn.cookies["show_scam_tokens"] |> parse_boolean()) + + defp map_to_string_keys(map) do + map + |> Map.keys() + |> Enum.map(fn + key when is_atom(key) -> Atom.to_string(key) + key -> key + end) + end end diff --git a/apps/block_scout_web/test/block_scout_web/chain_test.exs b/apps/block_scout_web/test/block_scout_web/chain_test.exs index efbde82db628..4b941780ed46 100644 --- a/apps/block_scout_web/test/block_scout_web/chain_test.exs +++ b/apps/block_scout_web/test/block_scout_web/chain_test.exs @@ -1,9 +1,81 @@ defmodule BlockScoutWeb.ChainTest do use Explorer.DataCase - alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.{Address, Block, Transaction, TokenTransfer} alias BlockScoutWeb.Chain + describe "next_page_params/4" do + # https://github.com/blockscout/blockscout/issues/12984 + test "does not return duplicated keys" do + assert Chain.next_page_params([nil], [%{id: 123}], %{"id" => 178}, fn x -> x end) == %{ + "items_count" => 1, + id: 123 + } + end + end + + describe "token_transfers_next_page_params/3" do + # https://github.com/blockscout/blockscout/issues/12984 + test "does not return duplicated keys" do + assert Chain.token_transfers_next_page_params( + [%TokenTransfer{block_number: 1, log_index: 3}], + [%TokenTransfer{block_number: 1, log_index: 2}], + %{ + "block_number" => 5, + "index" => 4 + } + ) == %{ + block_number: 1, + index: 2 + } + end + + test "does not return duplicated keys with batch transfer" do + assert Chain.token_transfers_next_page_params( + [ + %TokenTransfer{ + block_number: 1, + log_index: 2, + block_hash: "0x123", + transaction_hash: "0x456", + index_in_batch: 1 + } + ], + [ + %TokenTransfer{ + block_number: 1, + log_index: 2, + block_hash: "0xabc", + transaction_hash: "0xdef", + index_in_batch: 3 + }, + %TokenTransfer{ + block_number: 1, + log_index: 2, + block_hash: "0x123", + transaction_hash: "0x456", + index_in_batch: 2 + } + ], + %{ + "block_number" => 5, + "index" => 4, + "batch_log_index" => 3, + "batch_block_hash" => "0x789", + "batch_transaction_hash" => "0xabc", + "index_in_batch" => 2 + } + ) == %{ + :block_number => 1, + :index => 2, + :batch_block_hash => "0x123", + :batch_log_index => 2, + :batch_transaction_hash => "0x456", + :index_in_batch => 2 + } + end + end + describe "current_filter/1" do test "sets direction based on to filter" do assert [direction: :to] = Chain.current_filter(%{"filter" => "to"}) From ed93c12dc1835ec0e7fb3e4be3504ceb7061610e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 14 Aug 2025 14:44:29 +0300 Subject: [PATCH 0235/1006] Add CHANGELOG record --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e38ab0fa6a..25438d873cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🐛 Bug Fixes +- atoms in token_transfers_next_page_params ([#12992](https://github.com/blockscout/blockscout/pull/12992)) - Fix Mud worlds API endpoint ([#12991](https://github.com/blockscout/blockscout/pull/12991)) - Set 5 RPS for api/health/* ([#12990]https://github.com/blockscout/blockscout/pull/12990) - Pagination with atoms in paging_params ([#12986](https://github.com/blockscout/blockscout/issues/12986)) From fdaed2b7ace689c6dd79dc4c4ed0883297c437f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Aug 2025 21:22:21 +0300 Subject: [PATCH 0236/1006] chore(deps): bump ex_secp256k1 from 0.7.5 to 0.7.6 (#12960) Bumps [ex_secp256k1](https://github.com/ayrat555/ex_secp256k1) from 0.7.5 to 0.7.6. - [Release notes](https://github.com/ayrat555/ex_secp256k1/releases) - [Changelog](https://github.com/ayrat555/ex_secp256k1/blob/master/CHANGELOG.md) - [Commits](https://github.com/ayrat555/ex_secp256k1/compare/v0.7.5...v0.7.6) --- updated-dependencies: - dependency-name: ex_secp256k1 dependency-version: 0.7.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 86aabd45eca8..550245f0ebe2 100644 --- a/mix.lock +++ b/mix.lock @@ -66,7 +66,7 @@ "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.5", "583733a1134a0c8b4df70c6b3e874d1896bf48f2aaafdf332ec5480c7b5117bf", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f138b2acdc5a4d87880e42742e9bead4d1235fd9b1b2b29be0d1da1e01c3a312"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, - "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.5", "46e2de19374585da06d99d39de9c6ef9bd666fdf90930d7963cb7dbf1141af46", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "046207ab26908de8b99fbd799d35b1edc6318c2ccf891a8d58f90b7d2072e6bd"}, + "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.6", "49ea3b07dbc55e2b339e2cd54c6fcb3434620e2de1dfbb4b4f41351384119579", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "b84c9c4d85b47410cd951222b4d2b644cdbff96f4f83bc5ac96ee15d850a113c"}, "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, From d7d46d95db877e1df2f78fc0aeeb233de5bce59e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Aug 2025 20:40:07 +0300 Subject: [PATCH 0237/1006] chore(deps): bump sweetalert2 in /apps/block_scout_web/assets (#12999) Bumps [sweetalert2](https://github.com/sweetalert2/sweetalert2) from 11.22.2 to 11.22.4. - [Release notes](https://github.com/sweetalert2/sweetalert2/releases) - [Changelog](https://github.com/sweetalert2/sweetalert2/blob/main/CHANGELOG.md) - [Commits](https://github.com/sweetalert2/sweetalert2/compare/v11.22.2...v11.22.4) --- updated-dependencies: - dependency-name: sweetalert2 dependency-version: 11.22.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 14 +++++++------- apps/block_scout_web/assets/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index a35554302df0..7f6d5697d4b0 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -61,7 +61,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.22.2", + "sweetalert2": "^11.22.4", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", @@ -14346,9 +14346,9 @@ } }, "node_modules/sweetalert2": { - "version": "11.22.2", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz", - "integrity": "sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==", + "version": "11.22.4", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.4.tgz", + "integrity": "sha512-JwcRODfozxiKmspFp+xctZ2izAmLAKbRPcoLMEW7LdugN/YmNrX1LT7hdBW87qsgupEO1ukBBuB17KzKFKW0tg==", "license": "MIT", "funding": { "type": "individual", @@ -26312,9 +26312,9 @@ } }, "sweetalert2": { - "version": "11.22.2", - "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.2.tgz", - "integrity": "sha512-GFQGzw8ZXF23PO79WMAYXLl4zYmLiaKqYJwcp5eBF07wiI5BYPbZtKi2pcvVmfUQK+FqL1risJAMxugcPbGIyg==" + "version": "11.22.4", + "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.22.4.tgz", + "integrity": "sha512-JwcRODfozxiKmspFp+xctZ2izAmLAKbRPcoLMEW7LdugN/YmNrX1LT7hdBW87qsgupEO1ukBBuB17KzKFKW0tg==" }, "symbol-tree": { "version": "3.2.4", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index 20cf223099a0..bf72d727716a 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -73,7 +73,7 @@ "redux": "^5.0.1", "stream-browserify": "^3.0.0", "stream-http": "^3.1.1", - "sweetalert2": "^11.22.2", + "sweetalert2": "^11.22.4", "urijs": "^1.19.11", "url": "^0.11.4", "util": "^0.12.5", From 6c2293b97d44216ce2f63d9e1135a8e22b7de23f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Aug 2025 18:49:36 +0300 Subject: [PATCH 0238/1006] chore(deps): bump floki from 0.37.1 to 0.38.0 (#12605) * chore(deps): bump floki from 0.37.1 to 0.38.0 Bumps [floki](https://github.com/philss/floki) from 0.37.1 to 0.38.0. - [Release notes](https://github.com/philss/floki/releases) - [Changelog](https://github.com/philss/floki/blob/main/CHANGELOG.md) - [Commits](https://github.com/philss/floki/compare/v0.37.1...v0.38.0) --- updated-dependencies: - dependency-name: floki dependency-version: 0.38.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Fix Floki usage in tests --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Baranov --- .../controllers/block_transaction_controller_test.exs | 2 ++ .../controllers/block_withdrawal_controller_test.exs | 2 ++ mix.lock | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs index 494dc0d90923..a4c369877ff1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs @@ -197,6 +197,8 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do defp assert_block_above_tip(conn) do assert conn |> html_response(404) + |> Floki.parse_fragment() + |> elem(1) |> Floki.find(~S|.error-descr|) |> Floki.text() |> String.trim() == "Easy Cowboy! This block does not exist yet!" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs index 831d088286ef..ef1e2222a7e4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs @@ -132,6 +132,8 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do defp assert_block_above_tip(conn) do assert conn |> html_response(404) + |> Floki.parse_fragment() + |> elem(1) |> Floki.find(~S|.error-descr|) |> Floki.text() |> String.trim() == "Easy Cowboy! This block does not exist yet!" diff --git a/mix.lock b/mix.lock index 550245f0ebe2..1847598d05f3 100644 --- a/mix.lock +++ b/mix.lock @@ -76,7 +76,7 @@ "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, - "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, + "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, From 0355d5e8a2232d0030aeeed5d05ecd3b6fc3e39c Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:27:31 +0400 Subject: [PATCH 0239/1006] chore: Move addresses to a separate import stage (#12857) --- apps/explorer/lib/explorer/chain/import.ex | 1 + .../explorer/chain/import/stage/addresses.ex | 26 +++++++++++++++++++ .../lib/explorer/chain/import/stage/main.ex | 17 ++++-------- 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/import/stage/addresses.ex diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 9e7468fca7e5..690c0a2dd806 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -17,6 +17,7 @@ defmodule Explorer.Chain.Import do Import.Stage.Blocks ], [ + Import.Stage.Addresses, Import.Stage.Main ], [ diff --git a/apps/explorer/lib/explorer/chain/import/stage/addresses.ex b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex new file mode 100644 index 000000000000..915de483d0a0 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex @@ -0,0 +1,26 @@ +defmodule Explorer.Chain.Import.Stage.Addresses do + @moduledoc """ + Import addresses. + """ + + alias Explorer.Chain.Import.{Runner, Stage} + + @behaviour Stage + + @runners [ + Runner.Addresses + ] + + @impl Stage + def runners, do: @runners + + @impl Stage + def all_runners, do: runners() + + @addresses_chunk_size 50 + + @impl Stage + def multis(runner_to_changes_list, options) do + Stage.chunk_every(runner_to_changes_list, Runner.Addresses, @addresses_chunk_size, options) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/main.ex b/apps/explorer/lib/explorer/chain/import/stage/main.ex index 7df0f93aecf0..863167db63a6 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/main.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/main.ex @@ -1,15 +1,13 @@ defmodule Explorer.Chain.Import.Stage.Main do @moduledoc """ - Imports main data (addresses, address_coin_balances, address_coin_balances_daily, tokens, transactions). + Imports main data (address_coin_balances, address_coin_balances_daily, tokens, transactions). """ alias Explorer.Chain.Import.{Runner, Stage} @behaviour Stage - @addresses_runner Runner.Addresses - - @rest_runners [ + @runners [ Runner.Tokens, Runner.Address.CoinBalances, Runner.Address.CoinBalancesDaily, @@ -17,21 +15,16 @@ defmodule Explorer.Chain.Import.Stage.Main do ] @impl Stage - def runners, do: [@addresses_runner | @rest_runners] + def runners, do: @runners @impl Stage def all_runners, do: runners() - @addresses_chunk_size 50 - @impl Stage def multis(runner_to_changes_list, options) do - {addresses_multis, remaining_runner_to_changes_list} = - Stage.chunk_every(runner_to_changes_list, Runner.Addresses, @addresses_chunk_size, options) - {final_multi, final_remaining_runner_to_changes_list} = - Stage.single_multi(@rest_runners, remaining_runner_to_changes_list, options) + Stage.single_multi(@runners, runner_to_changes_list, options) - {[final_multi | addresses_multis], final_remaining_runner_to_changes_list} + {[final_multi], final_remaining_runner_to_changes_list} end end From 5397ef286b7f333e270647a1c92d390315e045ef Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:31:26 +0400 Subject: [PATCH 0240/1006] chore: Add disconnect_on_error_codes param to repo config (#12800) --- apps/explorer/config/config.exs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 6eadf5ef5dfd..afd401b39547 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -187,7 +187,9 @@ config :explorer, Explorer.Utility.RateLimiter, enabled: true config :explorer, Explorer.Utility.Hammer.Redis, enabled: true config :explorer, Explorer.Utility.Hammer.ETS, enabled: true -config :explorer, Explorer.Repo, migration_timestamps: [type: :utc_datetime_usec] +config :explorer, Explorer.Repo, + migration_timestamps: [type: :utc_datetime_usec], + disconnect_on_error_codes: [:query_canceled] config :explorer, Explorer.Tracer, service: :explorer, From a5126ee785d9bca15d27a8203546bcb5bce97079 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 18 Aug 2025 20:44:18 +0400 Subject: [PATCH 0241/1006] fix: Expand indexer timeout exception definition (#12748) --- apps/indexer/lib/indexer/block/catchup/fetcher.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 69d7fe184cb9..cea18e777254 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -254,7 +254,7 @@ defmodule Indexer.Block.Catchup.Fetcher do end defp timeout_exception?(%{message: message}) when is_binary(message) do - String.match?(message, ~r/due to a timeout/) + String.match?(message, ~r/due to a timeout/) or String.match?(message, ~r/due to user request/) end defp timeout_exception?(_exception), do: false From 38fd95646bfeaa4fe54945e4867ecef6d8ab72f7 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 18 Aug 2025 20:06:30 +0300 Subject: [PATCH 0242/1006] feat: add celo-cpecific APIv1 `getepoch` action (#12853) * feat: add APIv1 `getepoch` action * fix: cspell --- CHANGELOG.md | 2 +- .../controllers/api/rpc/celo_controller.ex | 45 +++++++++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 29 ++++++++---- .../views/api/rpc/celo_view.ex | 36 +++++++++++++++ .../block_scout_web/views/api/v2/celo_view.ex | 8 +++- cspell.json | 2 +- 6 files changed, 110 insertions(+), 12 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 25438d873cf2..d3b94918808a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,7 +84,7 @@ ### ⚡ Performance -- Api v1 `txlsit`& `txlistinternal` endpoints ([#12774](https://github.com/blockscout/blockscout/issues/12774)) +- Api v1 `txlist`& `txlistinternal` endpoints ([#12774](https://github.com/blockscout/blockscout/issues/12774)) - Optimize Explorer.Chain.Cache.Blocks ([#12402](https://github.com/blockscout/blockscout/issues/12402)) ### ⚙️ Miscellaneous Tasks diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex new file mode 100644 index 000000000000..3680103ed474 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex @@ -0,0 +1,45 @@ +defmodule BlockScoutWeb.API.RPC.CeloController do + use BlockScoutWeb, :controller + alias Explorer.Helper + alias Explorer.Chain.Celo.{ElectionReward, Epoch} + + @max_safe_epoch_number 32_768 + + def getepoch(conn, params) do + options = [ + necessity_by_association: %{ + :distribution => :optional, + :start_processing_block => :optional, + :end_processing_block => :optional + }, + api?: true + ] + + with {:param, {:ok, epoch_number}} <- + {:param, Map.fetch(params, "epochNumber")}, + {:format, {:ok, epoch_number}} <- + {:format, Helper.safe_parse_non_negative_integer(epoch_number, @max_safe_epoch_number)}, + {:epoch, {:ok, epoch}} <- {:epoch, Epoch.from_number(epoch_number, options)} do + aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, options) + + conn + |> render(:celo_epoch, epoch: epoch, aggregated_rewards: aggregated_rewards) + else + {:param, :error} -> + render(conn, :error, error: "Query parameter 'epochNumber' is required") + + {:format, {:error, type}} -> + error = + case type do + :negative_integer -> "Epoch number cannot be negative" + :too_big_integer -> "Epoch number is too big" + :invalid_integer -> "Invalid epoch number" + end + + render(conn, :error, error: error) + + {:epoch, {:error, :not_found}} -> + render(conn, :error, error: "No epoch found") + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 8fbc9266ff3b..43ca2a04cadd 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -575,15 +575,26 @@ defmodule BlockScoutWeb.Routers.ApiRouter do if @reading_enabled do post("/eth-rpc", EthRPC.EthController, :eth_request) - forward("/", RPCTranslatorForwarder, %{ - "block" => {RPC.BlockController, []}, - "account" => {RPC.AddressController, []}, - "logs" => {RPC.LogsController, []}, - "token" => {RPC.TokenController, []}, - "stats" => {RPC.StatsController, []}, - "contract" => {RPC.ContractController, [:verify]}, - "transaction" => {RPC.TransactionController, []} - }) + forward( + "/", + RPCTranslatorForwarder, + %{ + "block" => {RPC.BlockController, []}, + "account" => {RPC.AddressController, []}, + "logs" => {RPC.LogsController, []}, + "token" => {RPC.TokenController, []}, + "stats" => {RPC.StatsController, []}, + "contract" => {RPC.ContractController, [:verify]}, + "transaction" => {RPC.TransactionController, []} + } + |> then(fn options -> + if @chain_type == :celo do + Map.put(options, "epoch", {BlockScoutWeb.API.RPC.CeloController, []}) + else + options + end + end) + ) end end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex new file mode 100644 index 000000000000..5efc953d81c1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex @@ -0,0 +1,36 @@ +defmodule BlockScoutWeb.API.RPC.CeloView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.RPC.RPCView + alias BlockScoutWeb.API.V2.CeloView, as: CeloViewV2 + + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + def render("celo_epoch.json", %{epoch: epoch, aggregated_rewards: aggregated_rewards}) do + distribution = epoch.distribution + + data = %{ + "epochNumber" => to_string(epoch.number), + "type" => CeloViewV2.epoch_type(epoch), + "isFinalized" => epoch.fetched?, + "timestamp" => + epoch.end_processing_block && + epoch.end_processing_block.timestamp |> DateTime.to_unix() |> to_string(), + "startProcessingBlockHash" => epoch.start_processing_block && to_string(epoch.start_processing_block.hash), + "startProcessingBlockNumber" => epoch.start_processing_block && to_string(epoch.start_processing_block.number), + "endProcessingBlockHash" => epoch.end_processing_block && to_string(epoch.end_processing_block.hash), + "endProcessingBlockNumber" => epoch.end_processing_block && to_string(epoch.end_processing_block.number), + "carbonOffsettingTargetEpochRewards" => + distribution.carbon_offsetting_transfer && distribution.carbon_offsetting_transfer.amount, + "communityTargetEpochRewards" => distribution.community_transfer && distribution.community_transfer.amount, + "reserveBolster" => distribution.reserve_bolster_transfer && distribution.reserve_bolster_transfer.amount, + "voterTargetEpochRewards" => aggregated_rewards.voter && aggregated_rewards.voter.total, + "validatorTargetEpochRewards" => aggregated_rewards.validator && aggregated_rewards.validator.total + } + + RPCView.render("show.json", data: data) + end + + def render("error.json", %{error: error}) do + RPCView.render("error.json", error: error) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index 9ce1835d57a9..002d11234ef5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -178,7 +178,13 @@ defmodule BlockScoutWeb.API.V2.CeloView do end) end - defp epoch_type(epoch) do + @doc """ + Returns the type of the epoch based on its number. If the epoch number is less + than the migration epoch number, it returns "L1", otherwise "L2". This is used + to differentiate between the two eras of Celo epochs. + """ + @spec epoch_type(Epoch.t()) :: String.t() + def epoch_type(epoch) do epoch.number |> CeloHelper.pre_migration_epoch_number?() |> if(do: "L1", else: "L2") diff --git a/cspell.json b/cspell.json index 12e32dea2dad..c67cef16cb17 100644 --- a/cspell.json +++ b/cspell.json @@ -272,6 +272,7 @@ "getblocknobytime", "getblockreward", "getcontractcreation", + "getepoch", "getlogs", "getminedblocks", "getsourcecode", @@ -654,7 +655,6 @@ "tsvector", "tsym", "txid", - "txlsit", "txlistinternal", "Txns", "txpool", From 9299628c93c40f196a6b838aa679aca657dd2d90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 09:38:20 +0300 Subject: [PATCH 0243/1006] chore(deps): bump ex_aws_s3 from 2.5.7 to 2.5.8 (#13026) Bumps [ex_aws_s3](https://github.com/ex-aws/ex_aws_s3) from 2.5.7 to 2.5.8. - [Changelog](https://github.com/ex-aws/ex_aws_s3/blob/main/CHANGELOG.md) - [Commits](https://github.com/ex-aws/ex_aws_s3/commits) --- updated-dependencies: - dependency-name: ex_aws_s3 dependency-version: 2.5.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 1847598d05f3..02ce0abe0230 100644 --- a/mix.lock +++ b/mix.lock @@ -51,8 +51,8 @@ "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"}, "evision": {:hex, :evision, "0.2.14", "6005e44b38ea61a33109694c5b097d42e81c767723c55d0c230d9662d6c87987", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "630714395f2c242e488944b4c9c75a5de96cbb95d3cbb69f0525bdb3e0a88775"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, - "ex_aws": {:hex, :ex_aws, "2.5.10", "d3f8ca8959dad6533a2a934dfdf380df1b1bef425feeb215a47a5176dee8736c", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "88fcd9cc1b2e0fcea65106bdaa8340ac56c6e29bf72f46cf7ef174027532d3da"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.7", "e571424d2f345299753382f3a01b005c422b1a460a8bc3ed47659b3d3ef91e9e", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "858e51241e50181e29aa2bc128fef548873a3a9cd580471f57eda5b64dec937f"}, + "ex_aws": {:hex, :ex_aws, "2.5.11", "5646eaad701485505b78246b0cd406fde9b1619459a86e85b53398810d3d0bd3", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e16100ff93a118ef01c916d945969535cbe8d4ab6593fcf01d1cf854eb75345"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"}, From e8d9e461eed79d7962e800c2064e4b807a5ca9ef Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:10:41 +0400 Subject: [PATCH 0244/1006] chore: catchup fetcher various improvements (#12866) * chore: Improve catchup fetcher * Add current token balance comparing test --- .../runner/address/current_token_balances.ex | 2 +- .../chain/import/stage/token_referencing.ex | 18 ++++++--- .../explorer/utility/missing_block_range.ex | 2 +- .../address/current_token_balances_test.exs | 39 +++++++++++++++++++ .../lib/indexer/block/catchup/fetcher.ex | 4 +- 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index ac9a9c6a01de..b7864203e516 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -308,7 +308,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do # new ctb is the same height or older defp should_update?(new_ctb, existing_ctb) do existing_ctb.block_number == new_ctb.block_number and not is_nil(Map.get(new_ctb, :value)) and - (is_nil(existing_ctb.value_fetched_at) or existing_ctb.value_fetched_at < new_ctb.value_fetched_at) + (is_nil(existing_ctb.value_fetched_at) or Timex.before?(existing_ctb.value_fetched_at, new_ctb.value_fetched_at)) end @spec insert(Repo.t(), [map()], %{ diff --git a/apps/explorer/lib/explorer/chain/import/stage/token_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/token_referencing.ex index 18af2e8563b9..de1c15b3069f 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/token_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/token_referencing.ex @@ -7,22 +7,28 @@ defmodule Explorer.Chain.Import.Stage.TokenReferencing do @behaviour Stage - @runners [ - Runner.Address.TokenBalances, - Runner.Address.CurrentTokenBalances + @ctb_runner Runner.Address.CurrentTokenBalances + + @rest_runners [ + Runner.Address.TokenBalances ] @impl Stage - def runners, do: @runners + def runners, do: [@ctb_runner | @rest_runners] @impl Stage def all_runners, do: runners() + @ctb_chunk_size 50 + @impl Stage def multis(runner_to_changes_list, options) do + {ctb_multis, remaining_runner_to_changes_list} = + Stage.chunk_every(runner_to_changes_list, @ctb_runner, @ctb_chunk_size, options) + {final_multi, final_remaining_runner_to_changes_list} = - Stage.single_multi(runners(), runner_to_changes_list, options) + Stage.single_multi(@rest_runners, remaining_runner_to_changes_list, options) - {[final_multi], final_remaining_runner_to_changes_list} + {[final_multi | ctb_multis], final_remaining_runner_to_changes_list} end end diff --git a/apps/explorer/lib/explorer/utility/missing_block_range.ex b/apps/explorer/lib/explorer/utility/missing_block_range.ex index 87d8b59dd9ee..42af949565e9 100644 --- a/apps/explorer/lib/explorer/utility/missing_block_range.ex +++ b/apps/explorer/lib/explorer/utility/missing_block_range.ex @@ -68,7 +68,7 @@ defmodule Explorer.Utility.MissingBlockRange do - A list of `Range` structs, where each range represents a contiguous block range of missing blocks. """ - @spec get_latest_batch(integer()) :: [__MODULE__.t()] + @spec get_latest_batch(integer()) :: [Range.t()] def get_latest_batch(size \\ @default_returning_batch_size) do size |> get_latest_ranges_query() diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs index 5795c32a27f5..25974fb3e2b8 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs @@ -339,6 +339,45 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do assert current_token_balance.value == Decimal.new(200) end + test "ignores when the new value_fetched_at is lower (values compared correctly)", %{ + address: %Address{hash: address_hash} = address, + token: %Token{contract_address_hash: token_contract_address_hash}, + options: options + } do + time_before = ~U[2024-12-31 00:00:00.000000Z] + time_after = ~U[2025-01-01 00:00:00.000000Z] + + assert time_before > time_after + + insert( + :address_current_token_balance, + address: address, + block_number: 1, + token_contract_address_hash: token_contract_address_hash, + value: 200, + value_fetched_at: time_after + ) + + update_holder_count!(token_contract_address_hash, 1) + + assert {:ok, %{address_current_token_balances: [], address_current_token_balances_update_token_holder_counts: []}} = + run_changes( + %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: 1, + value: Decimal.new(100), + value_fetched_at: time_before + }, + options + ) + + current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address_hash) + + assert Decimal.eq?(current_token_balance.value, 200) + assert Timex.equal?(current_token_balance.value_fetched_at, time_after) + end + test "a non-holder updating to a holder increases the holder_count", %{ address: %Address{hash: address_hash} = address, token: %Token{contract_address_hash: token_contract_address_hash}, diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index cea18e777254..a0bb96d027ac 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -29,7 +29,7 @@ defmodule Indexer.Block.Catchup.Fetcher do alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain alias Explorer.Chain.NullRoundHeight - alias Explorer.Utility.{MassiveBlock, MissingRangesManipulator} + alias Explorer.Utility.{MassiveBlock, MissingBlockRange, MissingRangesManipulator} alias Indexer.{Block, Tracer} alias Indexer.Block.Catchup.TaskSupervisor alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @@ -50,7 +50,7 @@ defmodule Indexer.Block.Catchup.Fetcher do Logger.metadata(fetcher: :block_catchup) Process.flag(:trap_exit, true) - case MissingRangesManipulator.get_latest_batch(blocks_batch_size() * blocks_concurrency()) do + case MissingBlockRange.get_latest_batch(blocks_batch_size() * blocks_concurrency()) do [] -> %{ first_block_number: nil, From b246995dfccfb0c6b39d6e9e7974e6dbdccb7e41 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:11:55 +0400 Subject: [PATCH 0245/1006] fix: Fix matching in current token balances import filter (#12930) * fix: Fix matching in current token balances import filter * Add test for empty value_fetched_at ctb param --- .../runner/address/current_token_balances.ex | 2 +- .../address/current_token_balances_test.exs | 39 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index b7864203e516..a6614a599104 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -231,7 +231,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do filtered_ctbs = Enum.filter(changes_list, fn ctb -> existing_ctb = existing_ctb_map[{ctb[:address_hash], ctb[:token_contract_address_hash], ctb[:token_id]}] - should_update?(ctb, existing_ctb) + should_update?(Map.put_new(ctb, :value_fetched_at, nil), existing_ctb) end) {:ok, filtered_ctbs} diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs index 25974fb3e2b8..e61247b327e8 100644 --- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs @@ -307,6 +307,39 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do assert current_token_balance.value == Decimal.new(200) end + test "ignores when the new value_fetched_at not set", %{ + address: %Address{hash: address_hash} = address, + token: %Token{contract_address_hash: token_contract_address_hash}, + options: options + } do + insert( + :address_current_token_balance, + address: address, + block_number: 1, + token_contract_address_hash: token_contract_address_hash, + value: 200, + value_fetched_at: Timex.shift(Timex.now(), minutes: -2) + ) + + update_holder_count!(token_contract_address_hash, 1) + + assert {:ok, %{address_current_token_balances: [], address_current_token_balances_update_token_holder_counts: []}} = + run_changes( + %{ + address_hash: address_hash, + token_contract_address_hash: token_contract_address_hash, + block_number: 2, + value: Decimal.new(100) + }, + options + ) + + current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address_hash) + + assert current_token_balance.block_number == 1 + assert current_token_balance.value == Decimal.new(200) + end + test "ignores when the new block number is lesser", %{ address: %Address{hash: address_hash} = address, token: %Token{contract_address_hash: token_contract_address_hash}, @@ -317,7 +350,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do address: address, block_number: 2, token_contract_address_hash: token_contract_address_hash, - value: 200 + value: 200, + value_fetched_at: Timex.shift(Timex.now(), minutes: -2) ) update_holder_count!(token_contract_address_hash, 1) @@ -328,7 +362,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do address_hash: address_hash, token_contract_address_hash: token_contract_address_hash, block_number: 1, - value: Decimal.new(100) + value: Decimal.new(100), + value_fetched_at: Timex.shift(Timex.now(), minutes: -1) }, options ) From a42d068e243d5182bdb8470858321621486aae6b Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:24:54 +0400 Subject: [PATCH 0246/1006] chore: Move address nonce updating to a separate process (#12941) * chore: Move address nonce updating to a separate process * Process review --- apps/indexer/lib/indexer/block/fetcher.ex | 23 ++++- .../indexer/fetcher/address_nonce_updater.ex | 89 +++++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 2 + 3 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 apps/indexer/lib/indexer/fetcher/address_nonce_updater.ex diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 061c20ce407b..76f16c7b9499 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -29,6 +29,7 @@ defmodule Indexer.Block.Fetcher do alias Indexer.{Prometheus, TokenBalances, Tracer} alias Indexer.Fetcher.{ + AddressNonceUpdater, Beacon.Blob, BlockReward, ContractCode, @@ -237,8 +238,9 @@ defmodule Indexer.Block.Fetcher do Enum.map(transaction_actions, fn action -> Map.put(action, :data, Map.delete(action.data, :block_number)) end), token_instances = TokenInstances.params_set(%{token_transfers_params: token_transfers}), stability_validators = StabilityValidators.parse(blocks), + addresses_without_nonce = process_addresses_nonce(addresses), basic_import_options = %{ - addresses: %{params: addresses}, + addresses: %{params: addresses_without_nonce}, address_coin_balances: %{params: coin_balances_params_set}, address_token_balances: %{params: address_token_balances}, address_current_token_balances: %{ @@ -797,6 +799,25 @@ defmodule Indexer.Block.Fetcher do end) end + defp process_addresses_nonce(addresses) do + {addresses_excluding_nonce_update, addresses_nonce_update_params} = + Enum.reduce(addresses, {[], []}, fn address, + {addresses_excluding_nonce_update_acc, addresses_nonce_update_params_acc} -> + case Map.get(address, :nonce) do + nil -> + {[address | addresses_excluding_nonce_update_acc], addresses_nonce_update_params_acc} + + nonce -> + {[Map.delete(address, :nonce) | addresses_excluding_nonce_update_acc], + [%{hash: address.hash, nonce: nonce} | addresses_nonce_update_params_acc]} + end + end) + + AddressNonceUpdater.add(addresses_nonce_update_params) + + Enum.reverse(addresses_excluding_nonce_update) + end + # Asynchronously schedules matching of Arbitrum L1-to-L2 messages where the message ID is hashed. @spec async_match_arbitrum_messages_to_l2([map()]) :: :ok defp async_match_arbitrum_messages_to_l2([]), do: :ok diff --git a/apps/indexer/lib/indexer/fetcher/address_nonce_updater.ex b/apps/indexer/lib/indexer/fetcher/address_nonce_updater.ex new file mode 100644 index 000000000000..64db707459de --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/address_nonce_updater.ex @@ -0,0 +1,89 @@ +defmodule Indexer.Fetcher.AddressNonceUpdater do + @moduledoc """ + Periodically updates addresses nonce + """ + + use GenServer + + require Logger + + alias Explorer.Chain + + @default_update_interval :timer.seconds(10) + + def child_spec(_) do + %{ + id: __MODULE__, + start: {__MODULE__, :start_link, []}, + type: :worker, + restart: :permanent, + shutdown: Application.get_env(:indexer, :graceful_shutdown_period) + } + end + + def start_link do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(_) do + schedule_next_update() + + {:ok, %{}} + end + + def add(addresses_params) do + GenServer.cast(__MODULE__, {:add, addresses_params}) + end + + def handle_cast({:add, addresses_params}, state) do + params_map = Map.new(addresses_params, fn address -> {address.hash, address} end) + + result_state = + Map.merge(state, params_map, fn _hash, old_address, new_address -> + if new_address.nonce > old_address.nonce, do: new_address, else: old_address + end) + + {:noreply, result_state} + end + + def handle_info(:update, addresses_map) do + addresses_params = Map.values(addresses_map) + + result_state = + case Chain.import(%{addresses: %{params: addresses_params}, timeout: :infinity}) do + {:ok, _} -> + %{} + + error -> + log_error(inspect(error)) + addresses_map + end + + schedule_next_update() + + {:noreply, result_state} + rescue + exception -> + error = Exception.format(:error, exception, __STACKTRACE__) + log_error(error) + schedule_next_update() + + {:noreply, addresses_map} + end + + def handle_info({_ref, _result}, state) do + {:noreply, state} + end + + def handle_info({:DOWN, _ref, :process, _pid, _reason}, state) do + {:noreply, state} + end + + defp schedule_next_update do + Process.send_after(self(), :update, @default_update_interval) + end + + defp log_error(error) do + Logger.error("Failed to update addresses nonce: #{error}, retrying") + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 4c3b9e6281b1..b4fc09736bb8 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -34,6 +34,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.TokenInstance.SanitizeERC721, as: TokenInstanceSanitizeERC721 alias Indexer.Fetcher.{ + AddressNonceUpdater, BlockReward, ContractCode, EmptyBlocksSanitizer, @@ -266,6 +267,7 @@ defmodule Indexer.Supervisor do {EmptyBlocksSanitizer.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, {PendingTransactionsSanitizer, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, {TokenTotalSupplyUpdater, [[]]}, + AddressNonceUpdater, # Notifications cleaner configure(EventNotificationsCleaner, [[]]), From 094ef3ca343c9faba344bf015e933ac2f48797e7 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:25:58 +0400 Subject: [PATCH 0247/1006] Broadcast current token balances from separate fetcher (#12787) --- .../lib/block_scout_web/notifier.ex | 4 +++- .../block_scout_web/realtime_event_handler.ex | 1 + .../lib/indexer/fetcher/token_balance.ex | 16 +++++++++++++++ .../indexer/fetcher/token_balance_test.exs | 20 +++++++++++++++++-- 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index d372f63f3e8a..7cd5f636809f 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -396,8 +396,10 @@ defmodule BlockScoutWeb.Notifier do end @current_token_balances_limit 50 - def handle_event({:chain_event, :address_current_token_balances, :on_demand, address_current_token_balances}) do + def handle_event({:chain_event, :address_current_token_balances, type, address_current_token_balances}) + when type in [:realtime, :on_demand] do address_current_token_balances.address_current_token_balances + |> Repo.preload(:token) |> Enum.group_by(& &1.token_type) |> Enum.each(fn {token_type, balances} -> broadcast_token_balances(address_current_token_balances.address_hash, token_type, balances) diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index 9cfece78f54b..38eadb6fd0f3 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -42,6 +42,7 @@ defmodule BlockScoutWeb.RealtimeEventHandler do Subscriber.to(:addresses, :on_demand) Subscriber.to(:address_coin_balances, :on_demand) Subscriber.to(:address_current_token_balances, :on_demand) + Subscriber.to(:address_current_token_balances, :realtime) Subscriber.to(:address_token_balances, :on_demand) Subscriber.to(:token_total_supply, :on_demand) Subscriber.to(:changed_bytecode, :on_demand) diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index 630303766400..a6f291c7d5a5 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -20,6 +20,7 @@ defmodule Indexer.Fetcher.TokenBalance do alias Explorer.Chain alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} + alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Hash alias Explorer.Utility.MissingBalanceOfToken alias Indexer.{BufferedTask, TokenBalances, Tracer} @@ -220,6 +221,21 @@ defmodule Indexer.Fetcher.TokenBalance do } case Chain.import(import_params) do + {:ok, %{address_current_token_balances: imported_ctbs}} -> + imported_ctbs + |> Enum.group_by(& &1.address_hash) + |> Enum.each(fn {address_hash, ctbs} -> + Publisher.broadcast( + %{ + address_current_token_balances: %{ + address_hash: to_string(address_hash), + address_current_token_balances: ctbs + } + }, + :realtime + ) + end) + {:ok, _} -> :ok diff --git a/apps/indexer/test/indexer/fetcher/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/token_balance_test.exs index 0c4fb0be44f8..7d388c13e4bc 100644 --- a/apps/indexer/test/indexer/fetcher/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_balance_test.exs @@ -5,6 +5,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do import Mox alias Explorer.Chain.{Address, Hash} + alias Explorer.Chain.Events.Subscriber alias Explorer.Repo alias Explorer.Utility.MissingBalanceOfToken alias Indexer.Fetcher.TokenBalance @@ -54,9 +55,11 @@ defmodule Indexer.Fetcher.TokenBalanceTest do end test "imports the given token balances" do + Subscriber.to(:address_current_token_balances, :realtime) + %Address.TokenBalance{ address_hash: %Hash{bytes: address_hash_bytes} = address_hash, - token_contract_address_hash: %Hash{bytes: token_contract_address_hash_bytes}, + token_contract_address_hash: %Hash{bytes: token_contract_address_hash_bytes} = token_contract_address_hash, block_number: block_number } = insert(:token_balance, value_fetched_at: nil, value: nil) @@ -82,8 +85,21 @@ defmodule Indexer.Fetcher.TokenBalanceTest do token_balance_updated = Repo.get_by(Address.TokenBalance, address_hash: address_hash) - assert token_balance_updated.value == Decimal.new(1_000_000_000_000_000_000_000_000) + expected_value = Decimal.new(1_000_000_000_000_000_000_000_000) + assert token_balance_updated.value == expected_value assert token_balance_updated.value_fetched_at != nil + + address_hash_string = to_string(address_hash) + + assert_receive( + {:chain_event, :address_current_token_balances, :realtime, + %{ + address_hash: ^address_hash_string, + address_current_token_balances: [ + %{value: ^expected_value, token_contract_address_hash: ^token_contract_address_hash} + ] + }} + ) end test "fetches duplicate params only once" do From 2b8a0c2e57b085b4b65be279dee224047f03f26b Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:28:50 +0400 Subject: [PATCH 0248/1006] fix: Don't validate address hash for common blocks channels (#13020) * fix: Don't validate address hash for common blocks channels * Add regression test for blocks channels join --- .../lib/block_scout_web/channels/v2/block_channel.ex | 3 ++- .../channels/v2/block_channel_test.exs | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/v2/block_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/v2/block_channel.ex index bcf0a3c1e8d9..8e5202ecd4a6 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/v2/block_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/v2/block_channel.ex @@ -4,7 +4,8 @@ defmodule BlockScoutWeb.V2.BlockChannel do """ use BlockScoutWeb, :channel - def join("blocks:new_block", _params, socket) do + def join("blocks:" <> common, _params, socket) + when common in ["new_block", "indexing", "indexing_internal_transactions"] do {:ok, %{}, socket} end diff --git a/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs index 2610a730baf3..febce4453f38 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/v2/block_channel_test.exs @@ -27,4 +27,15 @@ defmodule BlockScoutWeb.V2.BlockChannelTest do assert false, "Expected message received nothing." end end + + test "user is able to join to common channels" do + common_channels = ["new_block", "indexing", "indexing_internal_transactions"] + + Enum.each(common_channels, fn channel -> + assert {:ok, _reply, _socket} = + BlockScoutWeb.V2.UserSocket + |> socket("no_id", %{}) + |> subscribe_and_join("blocks:#{channel}") + end) + end end From 1b050979271f39a265e75bc6039fe8e5a7c99d72 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 19 Aug 2025 13:30:19 +0300 Subject: [PATCH 0249/1006] feat: runtime config option to disable file logging (#12805) * feat: runtime config option to disable file logging * docs: write doc for `logger_backends/0` --- config/config.exs | 29 ---------------- config/config_helper.exs | 41 +++++++++++++++++++++++ config/runtime.exs | 3 ++ docker-compose/envs/common-blockscout.env | 1 + 4 files changed, 45 insertions(+), 29 deletions(-) diff --git a/config/config.exs b/config/config.exs index c7adb85d85c4..2f45fe9d8ec6 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,35 +13,6 @@ end config :phoenix, :json_library, Jason -config :logger, - backends: [ - # all applications and all levels - :console, - # all applications, but only errors - {LoggerFileBackend, :error}, - # only :ecto, but all levels - {LoggerFileBackend, :ecto}, - # only :block_scout_web, but all levels - {LoggerFileBackend, :block_scout_web}, - # only :ethereum_jsonrpc, but all levels - {LoggerFileBackend, :ethereum_jsonrpc}, - # only :explorer, but all levels - {LoggerFileBackend, :explorer}, - # only :indexer, but all levels - {LoggerFileBackend, :indexer}, - {LoggerFileBackend, :indexer_token_balances}, - {LoggerFileBackend, :token_instances}, - {LoggerFileBackend, :reading_token_functions}, - {LoggerFileBackend, :pending_transactions_to_refetch}, - {LoggerFileBackend, :empty_blocks_to_refetch}, - {LoggerFileBackend, :withdrawal}, - {LoggerFileBackend, :api}, - {LoggerFileBackend, :block_import_timings}, - {LoggerFileBackend, :account}, - {LoggerFileBackend, :api_v2}, - LoggerJSON - ] - config :logger_json, :backend, metadata: ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count diff --git a/config/config_helper.exs b/config/config_helper.exs index 98cff21997b5..af5517b747fe 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -44,6 +44,47 @@ defmodule ConfigHelper do base_repos ++ chain_type_repos ++ ext_repos end + @doc """ + Returns the list of logger backends to be used by the application. + + If the DISABLE_FILE_LOGGING environment variable is set to true, only base + logger backends (:console and LoggerJSON) are returned. Otherwise, returns + both base and file logger backends. + """ + @spec logger_backends() :: list() + def logger_backends do + base_logger_backends = [ + :console, + LoggerJSON + ] + + file_logger_backends = + [ + {LoggerFileBackend, :error}, + {LoggerFileBackend, :ecto}, + {LoggerFileBackend, :block_scout_web}, + {LoggerFileBackend, :ethereum_jsonrpc}, + {LoggerFileBackend, :explorer}, + {LoggerFileBackend, :indexer}, + {LoggerFileBackend, :indexer_token_balances}, + {LoggerFileBackend, :token_instances}, + {LoggerFileBackend, :reading_token_functions}, + {LoggerFileBackend, :pending_transactions_to_refetch}, + {LoggerFileBackend, :empty_blocks_to_refetch}, + {LoggerFileBackend, :withdrawal}, + {LoggerFileBackend, :api}, + {LoggerFileBackend, :block_import_timings}, + {LoggerFileBackend, :account}, + {LoggerFileBackend, :api_v2} + ] + + if parse_bool_env_var("DISABLE_FILE_LOGGING") do + base_logger_backends + else + base_logger_backends ++ file_logger_backends + end + end + @spec http_options(non_neg_integer()) :: list() def http_options(default_timeout \\ 1) do http_timeout = timeout(default_timeout) diff --git a/config/runtime.exs b/config/runtime.exs index e76d1bc1e42b..b2d9dc4dfbae 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -4,6 +4,9 @@ import Config |> Path.join() |> Code.eval_file() +config :logger, + backends: ConfigHelper.logger_backends() + ###################### ### BlockScout Web ### ###################### diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 0c604e6ca006..4d6539313a13 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -1,5 +1,6 @@ ETHEREUM_JSONRPC_VARIANT=geth ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:8545/ +DISABLE_FILE_LOGGING=false DATABASE_URL=postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout # DATABASE_EVENT_URL= From 0b5f44f1fa65c72ef44a874c081ff8be07589773 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:42:40 +0300 Subject: [PATCH 0250/1006] chore(deps-dev): bump ex_doc from 0.38.2 to 0.38.3 (#13025) Bumps [ex_doc](https://github.com/elixir-lang/ex_doc) from 0.38.2 to 0.38.3. - [Release notes](https://github.com/elixir-lang/ex_doc/releases) - [Changelog](https://github.com/elixir-lang/ex_doc/blob/main/CHANGELOG.md) - [Commits](https://github.com/elixir-lang/ex_doc/compare/v0.38.2...v0.38.3) --- updated-dependencies: - dependency-name: ex_doc dependency-version: 0.38.3 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 02ce0abe0230..63ade0804537 100644 --- a/mix.lock +++ b/mix.lock @@ -59,7 +59,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, - "ex_doc": {:hex, :ex_doc, "0.38.2", "504d25eef296b4dec3b8e33e810bc8b5344d565998cd83914ffe1b8503737c02", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "732f2d972e42c116a70802f9898c51b54916e542cc50968ac6980512ec90f42b"}, + "ex_doc": {:hex, :ex_doc, "0.38.3", "ddafe36b8e9fe101c093620879f6604f6254861a95133022101c08e75e6c759a", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "ecaa785456a67f63b4e7d7f200e8832fa108279e7eb73fd9928e7e66215a01f9"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"}, From 3820192ff0ca4b20f54cab8792550740aafa9ab0 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 19 Aug 2025 13:49:27 +0300 Subject: [PATCH 0251/1006] refactor: remove public tags request functionality (#13006) * refactor: remove public tags request functionality * refactor: continue removing public tags requests functionality * fix: translations * chore: process review comments * fix: remove trash --- apps/block_scout_web/API blueprint.md | 1176 ---------------- apps/block_scout_web/API.md | 383 ------ .../account/api/v2/user_controller.ex | 78 +- .../account/public_tags_request_controller.ex | 114 -- .../block_scout_web/routers/account_router.ex | 10 - .../templates/account/common/_nav.html.eex | 3 - .../address_field.html.eex | 8 - .../account/public_tags_request/form.html.eex | 72 - .../public_tags_request/index.html.eex | 44 - .../account/public_tags_request/row.html.eex | 20 - .../layout/_account_menu_item.html.eex | 1 - .../views/account/api/v2/user_view.ex | 31 - .../views/account/public_tags_request_view.ex | 70 - apps/block_scout_web/priv/gettext/default.pot | 1220 +++++++++++++++-- .../priv/gettext/en/LC_MESSAGES/default.po | 1220 +++++++++++++++-- .../account/api/v2/user_controller_test.exs | 163 --- apps/explorer/lib/explorer/account.ex | 2 - .../explorer/account/public_tags_request.ex | 334 ----- .../chain/smart_contract/audit_report.ex | 4 +- .../{airtable.ex => airtable_audit_report.ex} | 41 +- ...5250_drop_account_public_tags_requests.exs | 7 + apps/explorer/test/support/factory.ex | 13 - config/runtime.exs | 8 +- docker-compose/envs/common-blockscout.env | 2 - 24 files changed, 2216 insertions(+), 2808 deletions(-) delete mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex delete mode 100644 apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex delete mode 100644 apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex delete mode 100644 apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex delete mode 100644 apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex delete mode 100644 apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex delete mode 100644 apps/explorer/lib/explorer/account/public_tags_request.ex rename apps/explorer/lib/explorer/third_party_integrations/{airtable.ex => airtable_audit_report.ex} (54%) create mode 100644 apps/explorer/priv/account/migrations/20250815115250_drop_account_public_tags_requests.exs diff --git a/apps/block_scout_web/API blueprint.md b/apps/block_scout_web/API blueprint.md index d473b194a930..8e5de5f06ab2 100644 --- a/apps/block_scout_web/API blueprint.md +++ b/apps/block_scout_web/API blueprint.md @@ -2100,1179 +2100,3 @@ HOST:http://blockscout.com/poa/core { "message": "OK" } -### BlockScoutWeb.Account.Api.V1.UserController create_public_tags_request [POST /api/account/v1/user/public_tags] - - - - - -+ Request Submit request to add a public tag -**POST**  `/api/account/v1/user/public_tags` - - + Headers - - content-type: multipart/mixed; boundary=plug_conn_test - + Body - - { - "website": "website11", - "tags": "Tag17", - "is_owner": false, - "full_name": "full name11", - "email": "test_user-24@blockscout.com", - "company": "company11", - "addresses": [ - "0x0000000000000000000000000000000000000067", - "0x0000000000000000000000000000000000000068", - "0x0000000000000000000000000000000000000069", - "0x000000000000000000000000000000000000006a", - "0x000000000000000000000000000000000000006b", - "0x000000000000000000000000000000000000006c", - "0x000000000000000000000000000000000000006d" - ], - "additional_comment": "additional_comment11" - } - -+ Response 200 - - + Headers - - set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMTJkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTIzQGJsb2Nrc2NvdXQuY29tZAACaWRiAAABEWQABG5hbWVtAAAAC1VzZXIgVGVzdDEyZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjEyZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDEyZAAMd2F0Y2hsaXN0X2lkYgAAARE.NJjO7QbBKV5g6_hGxLxBb5wlGDmJMKp-bpgLhhrFjLM; path=/; SameSite=Lax - content-type: application/json; charset=utf-8 - cache-control: max-age=0, private, must-revalidate - x-request-id: Fy1W2Zt2e1-7YrQAABVh - access-control-allow-credentials: true - access-control-allow-origin: * - access-control-expose-headers: - + Body - - { - "website": "website11", - "tags": "Tag17", - "submission_date": "2022-12-03T16:55:29.441979Z", - "is_owner": false, - "id": 202, - "full_name": "full name11", - "email": "test_user-24@blockscout.com", - "company": "company11", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000067" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000068" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000069" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000006a" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000006b" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000006C" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000006D" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000067", - "0x0000000000000000000000000000000000000068", - "0x0000000000000000000000000000000000000069", - "0x000000000000000000000000000000000000006a", - "0x000000000000000000000000000000000000006b", - "0x000000000000000000000000000000000000006c", - "0x000000000000000000000000000000000000006d" - ], - "additional_comment": "additional_comment11" - } -### BlockScoutWeb.Account.Api.V1.UserController public_tags_requests [GET /api/account/v1/user/public_tags] - - - - - -+ Request Get list of requests to add a public tag -**GET**  `/api/account/v1/user/public_tags` - - -+ Response 200 - - + Headers - - set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYgAAAQhkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGIAAAEI.-a6kcGlCbsFgQtwPNaGA4yaOOpSpyG_54rEROF3a6E0; path=/; SameSite=Lax - content-type: application/json; charset=utf-8 - cache-control: max-age=0, private, must-revalidate - x-request-id: Fy1W2YJiDacnhiAAAA9h - access-control-allow-credentials: true - access-control-allow-origin: * - access-control-expose-headers: - + Body - - [ - { - "website": "website9", - "tags": "Tag14", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": false, - "id": 200, - "full_name": "full name9", - "email": "test_user-13@blockscout.com", - "company": "company9", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003D" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003e" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003f" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000040" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000041" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000042" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000043" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000044" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000045" - } - ], - "addresses": [ - "0x000000000000000000000000000000000000003d", - "0x000000000000000000000000000000000000003e", - "0x000000000000000000000000000000000000003f", - "0x0000000000000000000000000000000000000040", - "0x0000000000000000000000000000000000000041", - "0x0000000000000000000000000000000000000042", - "0x0000000000000000000000000000000000000043", - "0x0000000000000000000000000000000000000044", - "0x0000000000000000000000000000000000000045" - ], - "additional_comment": "additional_comment9" - }, - { - "website": "website8", - "tags": "Tag13", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": false, - "id": 199, - "full_name": "full name8", - "email": "test_user-12@blockscout.com", - "company": "company8", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003a" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003b" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000003c" - } - ], - "addresses": [ - "0x000000000000000000000000000000000000003a", - "0x000000000000000000000000000000000000003b", - "0x000000000000000000000000000000000000003c" - ], - "additional_comment": "additional_comment8" - }, - { - "website": "website7", - "tags": "Tag11;Tag12", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 198, - "full_name": "full name7", - "email": "test_user-11@blockscout.com", - "company": "company7", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000032" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000033" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000034" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000035" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000036" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000037" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000038" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000039" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000032", - "0x0000000000000000000000000000000000000033", - "0x0000000000000000000000000000000000000034", - "0x0000000000000000000000000000000000000035", - "0x0000000000000000000000000000000000000036", - "0x0000000000000000000000000000000000000037", - "0x0000000000000000000000000000000000000038", - "0x0000000000000000000000000000000000000039" - ], - "additional_comment": "additional_comment7" - }, - { - "website": "website6", - "tags": "Tag10", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 197, - "full_name": "full name6", - "email": "test_user-10@blockscout.com", - "company": "company6", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002c" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002D" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002E" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002F" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000030" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000031" - } - ], - "addresses": [ - "0x000000000000000000000000000000000000002c", - "0x000000000000000000000000000000000000002d", - "0x000000000000000000000000000000000000002e", - "0x000000000000000000000000000000000000002f", - "0x0000000000000000000000000000000000000030", - "0x0000000000000000000000000000000000000031" - ], - "additional_comment": "additional_comment6" - }, - { - "website": "website5", - "tags": "Tag9", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 196, - "full_name": "full name5", - "email": "test_user-9@blockscout.com", - "company": "company5", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000028" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000029" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002A" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000002b" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000028", - "0x0000000000000000000000000000000000000029", - "0x000000000000000000000000000000000000002a", - "0x000000000000000000000000000000000000002b" - ], - "additional_comment": "additional_comment5" - }, - { - "website": "website4", - "tags": "Tag7;Tag8", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": false, - "id": 195, - "full_name": "full name4", - "email": "test_user-8@blockscout.com", - "company": "company4", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000020" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000021" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000022" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000023" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000024" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000025" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000026" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000027" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000020", - "0x0000000000000000000000000000000000000021", - "0x0000000000000000000000000000000000000022", - "0x0000000000000000000000000000000000000023", - "0x0000000000000000000000000000000000000024", - "0x0000000000000000000000000000000000000025", - "0x0000000000000000000000000000000000000026", - "0x0000000000000000000000000000000000000027" - ], - "additional_comment": "additional_comment4" - }, - { - "website": "website3", - "tags": "Tag5;Tag6", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 194, - "full_name": "full name3", - "email": "test_user-7@blockscout.com", - "company": "company3", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001a" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001B" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001c" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001D" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001e" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000001F" - } - ], - "addresses": [ - "0x000000000000000000000000000000000000001a", - "0x000000000000000000000000000000000000001b", - "0x000000000000000000000000000000000000001c", - "0x000000000000000000000000000000000000001d", - "0x000000000000000000000000000000000000001e", - "0x000000000000000000000000000000000000001f" - ], - "additional_comment": "additional_comment3" - }, - { - "website": "website2", - "tags": "Tag3;Tag4", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 193, - "full_name": "full name2", - "email": "test_user-6@blockscout.com", - "company": "company2", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000010" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000011" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000012" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000013" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000014" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000015" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000016" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000017" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000018" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000019" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000010", - "0x0000000000000000000000000000000000000011", - "0x0000000000000000000000000000000000000012", - "0x0000000000000000000000000000000000000013", - "0x0000000000000000000000000000000000000014", - "0x0000000000000000000000000000000000000015", - "0x0000000000000000000000000000000000000016", - "0x0000000000000000000000000000000000000017", - "0x0000000000000000000000000000000000000018", - "0x0000000000000000000000000000000000000019" - ], - "additional_comment": "additional_comment2" - }, - { - "website": "website1", - "tags": "Tag2", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": false, - "id": 192, - "full_name": "full name1", - "email": "test_user-5@blockscout.com", - "company": "company1", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000E" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000F" - } - ], - "addresses": [ - "0x000000000000000000000000000000000000000e", - "0x000000000000000000000000000000000000000f" - ], - "additional_comment": "additional_comment1" - }, - { - "website": "website0", - "tags": "Tag0;Tag1", - "submission_date": "2022-12-03T16:55:29.000000Z", - "is_owner": true, - "id": 191, - "full_name": "full name0", - "email": "test_user-4@blockscout.com", - "company": "company0", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000008" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000009" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000A" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000b" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000C" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000000d" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000008", - "0x0000000000000000000000000000000000000009", - "0x000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000c", - "0x000000000000000000000000000000000000000d" - ], - "additional_comment": "additional_comment0" - } - ] -### BlockScoutWeb.Account.Api.V1.UserController delete_public_tags_request [DELETE /api/account/v1/user/public_tags/{id}] - - - - -+ Parameters - + id: `200` - id: 200 - - -+ Request Delete public tags request -**DELETE**  `/api/account/v1/user/public_tags/200` - - + Headers - - content-type: multipart/mixed; boundary=plug_conn_test - + Body - - { - "remove_reason": "reason" - } - -+ Response 200 - - + Headers - - set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyM2QABWVtYWlsbQAAABp0ZXN0X3VzZXItM0BibG9ja3Njb3V0LmNvbWQAAmlkYgAAAQhkAARuYW1lbQAAAApVc2VyIFRlc3QzZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjNkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwM2QADHdhdGNobGlzdF9pZGIAAAEI.-a6kcGlCbsFgQtwPNaGA4yaOOpSpyG_54rEROF3a6E0; path=/; SameSite=Lax - content-type: application/json; charset=utf-8 - cache-control: max-age=0, private, must-revalidate - x-request-id: Fy1W2YdEq9snhiAAAA-h - access-control-allow-credentials: true - access-control-allow-origin: * - access-control-expose-headers: - + Body - - { - "message": "OK" - } -### BlockScoutWeb.Account.Api.V1.UserController update_public_tags_request [PUT /api/account/v1/user/public_tags/{id}] - - - - -+ Parameters - + id: `203` - id: 203 - - -+ Request Edit request to add a public tag -**PUT**  `/api/account/v1/user/public_tags/203` - - + Headers - - content-type: multipart/mixed; boundary=plug_conn_test - + Body - - { - "website": "website13", - "tags": "Tag20;Tag21", - "is_owner": false, - "full_name": "full name13", - "email": "test_user-35@blockscout.com", - "company": "company13", - "addresses": [ - "0x0000000000000000000000000000000000000085", - "0x0000000000000000000000000000000000000086", - "0x0000000000000000000000000000000000000087", - "0x0000000000000000000000000000000000000088", - "0x0000000000000000000000000000000000000089", - "0x000000000000000000000000000000000000008a", - "0x000000000000000000000000000000000000008b" - ], - "additional_comment": "additional_comment13" - } - -+ Response 200 - - + Headers - - set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjFkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTMzQGJsb2Nrc2NvdXQuY29tZAACaWRiAAABGmQABG5hbWVtAAAAC1VzZXIgVGVzdDIxZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIxZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIxZAAMd2F0Y2hsaXN0X2lkYgAAARo.byLDQXd4VuN-Y1kqWEWSxe5Q_ne42ove8xpm5k_GwHc; path=/; SameSite=Lax - content-type: application/json; charset=utf-8 - cache-control: max-age=0, private, must-revalidate - x-request-id: Fy1W2aqpFvr2fxsAABjB - access-control-allow-credentials: true - access-control-allow-origin: * - access-control-expose-headers: - + Body - - { - "website": "website13", - "tags": "Tag20;Tag21", - "submission_date": "2022-12-03T16:55:30.000000Z", - "is_owner": false, - "id": 203, - "full_name": "full name13", - "email": "test_user-35@blockscout.com", - "company": "company13", - "addresses_with_info": [ - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000085" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000086" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000087" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000088" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x0000000000000000000000000000000000000089" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000008A" - }, - { - "watchlist_names": [], - "public_tags": [], - "private_tags": [], - "name": null, - "is_verified": null, - "is_contract": false, - "implementation_name": null, - "hash": "0x000000000000000000000000000000000000008b" - } - ], - "addresses": [ - "0x0000000000000000000000000000000000000085", - "0x0000000000000000000000000000000000000086", - "0x0000000000000000000000000000000000000087", - "0x0000000000000000000000000000000000000088", - "0x0000000000000000000000000000000000000089", - "0x000000000000000000000000000000000000008a", - "0x000000000000000000000000000000000000008b" - ], - "additional_comment": "additional_comment13" - } - diff --git a/apps/block_scout_web/API.md b/apps/block_scout_web/API.md index afc68c19e65f..74ded7812d48 100644 --- a/apps/block_scout_web/API.md +++ b/apps/block_scout_web/API.md @@ -30,10 +30,6 @@ * [custom_abis](#blockscoutweb-account-api-v1-usercontroller-custom_abis) * [update_custom_abi](#blockscoutweb-account-api-v1-usercontroller-update_custom_abi) * [delete_custom_abi](#blockscoutweb-account-api-v1-usercontroller-delete_custom_abi) - * [create_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-create_public_tags_request) - * [public_tags_requests](#blockscoutweb-account-api-v1-usercontroller-public_tags_requests) - * [delete_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-delete_public_tags_request) - * [update_public_tags_request](#blockscoutweb-account-api-v1-usercontroller-update_public_tags_request) ## BlockScoutWeb.Account.Api.V1.UserController ### info @@ -1846,382 +1842,3 @@ access-control-expose-headers: "message": "OK" } ``` - -### create_public_tags_request -#### Submit request to add a public tag - -##### Request -* __Method:__ POST -* __Path:__ /api/account/v1/user/public_tags -* __Request headers:__ -``` -content-type: multipart/mixed; boundary=plug_conn_test -``` -* __Request body:__ -```json -{ - "website": "website0", - "tags": "Tag0", - "is_owner": true, - "full_name": "full name0", - "email": "test_user-6@blockscout.com", - "company": "company0", - "addresses": [ - "0x0000000000000000000000000000000000000009", - "0x000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000c", - "0x000000000000000000000000000000000000000d" - ], - "additional_comment": "additional_comment0" -} -``` - -##### Response -* __Status__: 200 -* __Response headers:__ -``` -set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNWQABWVtYWlsbQAAABp0ZXN0X3VzZXItNUBibG9ja3Njb3V0LmNvbWQAAmlkYcVkAARuYW1lbQAAAApVc2VyIFRlc3Q1ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjVkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNWQADHdhdGNobGlzdF9pZGHF.kXAMBaL9a7aYjPDgZ9Llxe1etUCPH3vEvQe9Fq2May4; path=/; HttpOnly -content-type: application/json; charset=utf-8 -cache-control: max-age=0, private, must-revalidate -x-request-id: FxF1Y2BIESA-ecUAAGgB -access-control-allow-credentials: true -access-control-allow-origin: * -access-control-expose-headers: -``` -* __Response body:__ -```json -{ - "website": "website0", - "tags": "Tag0", - "submission_date": "2022-09-03T21:00:07.156465Z", - "is_owner": true, - "id": 131, - "full_name": "full name0", - "email": "test_user-6@blockscout.com", - "company": "company0", - "addresses": [ - "0x0000000000000000000000000000000000000009", - "0x000000000000000000000000000000000000000a", - "0x000000000000000000000000000000000000000b", - "0x000000000000000000000000000000000000000c", - "0x000000000000000000000000000000000000000d" - ], - "additional_comment": "additional_comment0" -} -``` - -### public_tags_requests -#### Get list of requests to add a public tag - -##### Request -* __Method:__ GET -* __Path:__ /api/account/v1/user/public_tags - -##### Response -* __Status__: 200 -* __Response headers:__ -``` -set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI3QGJsb2Nrc2NvdXQuY29tZAACaWRh12QABG5hbWVtAAAAC1VzZXIgVGVzdDIzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIzZAAMd2F0Y2hsaXN0X2lkYdc._6gJnvzjA6VEztgoIdpp7chhmhsdFrJImlcdrp4-pW0; path=/; HttpOnly -content-type: application/json; charset=utf-8 -cache-control: max-age=0, private, must-revalidate -x-request-id: FxF1Y3SaPVCdkicAAHIi -access-control-allow-credentials: true -access-control-allow-origin: * -access-control-expose-headers: -``` -* __Response body:__ -```json -[ - { - "website": "website13", - "tags": "Tag17", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 143, - "full_name": "full name13", - "email": "test_user-37@blockscout.com", - "company": "company13", - "addresses": [ - "0x000000000000000000000000000000000000007e", - "0x000000000000000000000000000000000000007f", - "0x0000000000000000000000000000000000000080", - "0x0000000000000000000000000000000000000081", - "0x0000000000000000000000000000000000000082", - "0x0000000000000000000000000000000000000083", - "0x0000000000000000000000000000000000000084" - ], - "additional_comment": "additional_comment13" - }, - { - "website": "website12", - "tags": "Tag16", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 142, - "full_name": "full name12", - "email": "test_user-36@blockscout.com", - "company": "company12", - "addresses": [ - "0x0000000000000000000000000000000000000075", - "0x0000000000000000000000000000000000000076", - "0x0000000000000000000000000000000000000077", - "0x0000000000000000000000000000000000000078", - "0x0000000000000000000000000000000000000079", - "0x000000000000000000000000000000000000007a", - "0x000000000000000000000000000000000000007b", - "0x000000000000000000000000000000000000007c", - "0x000000000000000000000000000000000000007d" - ], - "additional_comment": "additional_comment12" - }, - { - "website": "website11", - "tags": "Tag15", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 141, - "full_name": "full name11", - "email": "test_user-35@blockscout.com", - "company": "company11", - "addresses": [ - "0x000000000000000000000000000000000000006d", - "0x000000000000000000000000000000000000006e", - "0x000000000000000000000000000000000000006f", - "0x0000000000000000000000000000000000000070", - "0x0000000000000000000000000000000000000071", - "0x0000000000000000000000000000000000000072", - "0x0000000000000000000000000000000000000073", - "0x0000000000000000000000000000000000000074" - ], - "additional_comment": "additional_comment11" - }, - { - "website": "website10", - "tags": "Tag14", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 140, - "full_name": "full name10", - "email": "test_user-34@blockscout.com", - "company": "company10", - "addresses": [ - "0x0000000000000000000000000000000000000067", - "0x0000000000000000000000000000000000000068", - "0x0000000000000000000000000000000000000069", - "0x000000000000000000000000000000000000006a", - "0x000000000000000000000000000000000000006b", - "0x000000000000000000000000000000000000006c" - ], - "additional_comment": "additional_comment10" - }, - { - "website": "website9", - "tags": "Tag13", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": true, - "id": 139, - "full_name": "full name9", - "email": "test_user-33@blockscout.com", - "company": "company9", - "addresses": [ - "0x0000000000000000000000000000000000000061", - "0x0000000000000000000000000000000000000062", - "0x0000000000000000000000000000000000000063", - "0x0000000000000000000000000000000000000064", - "0x0000000000000000000000000000000000000065", - "0x0000000000000000000000000000000000000066" - ], - "additional_comment": "additional_comment9" - }, - { - "website": "website8", - "tags": "Tag12", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 138, - "full_name": "full name8", - "email": "test_user-32@blockscout.com", - "company": "company8", - "addresses": [ - "0x0000000000000000000000000000000000000060" - ], - "additional_comment": "additional_comment8" - }, - { - "website": "website7", - "tags": "Tag11", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": true, - "id": 137, - "full_name": "full name7", - "email": "test_user-31@blockscout.com", - "company": "company7", - "addresses": [ - "0x000000000000000000000000000000000000005f" - ], - "additional_comment": "additional_comment7" - }, - { - "website": "website6", - "tags": "Tag9;Tag10", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": true, - "id": 136, - "full_name": "full name6", - "email": "test_user-30@blockscout.com", - "company": "company6", - "addresses": [ - "0x000000000000000000000000000000000000005a", - "0x000000000000000000000000000000000000005b", - "0x000000000000000000000000000000000000005c", - "0x000000000000000000000000000000000000005d", - "0x000000000000000000000000000000000000005e" - ], - "additional_comment": "additional_comment6" - }, - { - "website": "website5", - "tags": "Tag8", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": false, - "id": 135, - "full_name": "full name5", - "email": "test_user-29@blockscout.com", - "company": "company5", - "addresses": [ - "0x0000000000000000000000000000000000000051", - "0x0000000000000000000000000000000000000052", - "0x0000000000000000000000000000000000000053", - "0x0000000000000000000000000000000000000054", - "0x0000000000000000000000000000000000000055", - "0x0000000000000000000000000000000000000056", - "0x0000000000000000000000000000000000000057", - "0x0000000000000000000000000000000000000058", - "0x0000000000000000000000000000000000000059" - ], - "additional_comment": "additional_comment5" - }, - { - "website": "website4", - "tags": "Tag6;Tag7", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": true, - "id": 134, - "full_name": "full name4", - "email": "test_user-28@blockscout.com", - "company": "company4", - "addresses": [ - "0x000000000000000000000000000000000000004c", - "0x000000000000000000000000000000000000004d", - "0x000000000000000000000000000000000000004e", - "0x000000000000000000000000000000000000004f", - "0x0000000000000000000000000000000000000050" - ], - "additional_comment": "additional_comment4" - } -] -``` - -### delete_public_tags_request -#### Delete public tags request - -##### Request -* __Method:__ DELETE -* __Path:__ /api/account/v1/user/public_tags/143 -* __Request headers:__ -``` -content-type: multipart/mixed; boundary=plug_conn_test -``` -* __Request body:__ -```json -{ - "remove_reason": "reason" -} -``` - -##### Response -* __Status__: 200 -* __Response headers:__ -``` -set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAmaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyMjNkAAVlbWFpbG0AAAAbdGVzdF91c2VyLTI3QGJsb2Nrc2NvdXQuY29tZAACaWRh12QABG5hbWVtAAAAC1VzZXIgVGVzdDIzZAAIbmlja25hbWVtAAAAC3Rlc3RfdXNlcjIzZAADdWlkbQAAABBibG9ja3Njb3V0fDAwMDIzZAAMd2F0Y2hsaXN0X2lkYdc._6gJnvzjA6VEztgoIdpp7chhmhsdFrJImlcdrp4-pW0; path=/; HttpOnly -content-type: application/json; charset=utf-8 -cache-control: max-age=0, private, must-revalidate -x-request-id: FxF1Y3SwObudkicAAHBB -access-control-allow-credentials: true -access-control-allow-origin: * -access-control-expose-headers: -``` -* __Response body:__ -```json -{ - "message": "OK" -} -``` - -### update_public_tags_request -#### Edit request to add a public tag - -##### Request -* __Method:__ PUT -* __Path:__ /api/account/v1/user/public_tags/132 -* __Request headers:__ -``` -content-type: multipart/mixed; boundary=plug_conn_test -``` -* __Request body:__ -```json -{ - "website": "website2", - "tags": "Tag2;Tag3", - "is_owner": true, - "full_name": "full name2", - "email": "test_user-9@blockscout.com", - "company": "company2", - "addresses": [ - "0x000000000000000000000000000000000000000f", - "0x0000000000000000000000000000000000000010", - "0x0000000000000000000000000000000000000011", - "0x0000000000000000000000000000000000000012", - "0x0000000000000000000000000000000000000013", - "0x0000000000000000000000000000000000000014" - ], - "additional_comment": "additional_comment2" -} -``` - -##### Response -* __Status__: 200 -* __Response headers:__ -``` -set-cookie: _explorer_key=SFMyNTY.g3QAAAABbQAAAAxjdXJyZW50X3VzZXJ0AAAAB2QABmF2YXRhcm0AAAAlaHR0cHM6Ly9leGFtcGxlLmNvbS9hdmF0YXIvdGVzdF91c2VyNmQABWVtYWlsbQAAABp0ZXN0X3VzZXItN0BibG9ja3Njb3V0LmNvbWQAAmlkYcZkAARuYW1lbQAAAApVc2VyIFRlc3Q2ZAAIbmlja25hbWVtAAAACnRlc3RfdXNlcjZkAAN1aWRtAAAAD2Jsb2Nrc2NvdXR8MDAwNmQADHdhdGNobGlzdF9pZGHG.86gruprPiLE-Nf9xkOzjEcW2wfSnCCPly5fHTwHrF6c; path=/; HttpOnly -content-type: application/json; charset=utf-8 -cache-control: max-age=0, private, must-revalidate -x-request-id: FxF1Y2E03jhU4u4AAGSi -access-control-allow-credentials: true -access-control-allow-origin: * -access-control-expose-headers: -``` -* __Response body:__ -```json -{ - "website": "website2", - "tags": "Tag2;Tag3", - "submission_date": "2022-09-03T21:00:07.000000Z", - "is_owner": true, - "id": 132, - "full_name": "full name2", - "email": "test_user-9@blockscout.com", - "company": "company2", - "addresses": [ - "0x000000000000000000000000000000000000000f", - "0x0000000000000000000000000000000000000010", - "0x0000000000000000000000000000000000000011", - "0x0000000000000000000000000000000000000012", - "0x0000000000000000000000000000000000000013", - "0x0000000000000000000000000000000000000014" - ], - "additional_comment": "additional_comment2" -} -``` - diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex index f8e0b6614783..4495234a7bd3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex @@ -15,7 +15,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do alias Explorer.Account.Api.Key, as: ApiKey alias Explorer.Account.CustomABI - alias Explorer.Account.{Identity, PublicTagsRequest, TagAddress, TagTransaction, WatchlistAddress} + alias Explorer.Account.{Identity, TagAddress, TagTransaction, WatchlistAddress} alias Explorer.{Chain, Market, PagingOptions, Repo} alias Plug.CSRFProtection @@ -445,82 +445,6 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do end end - def public_tags_requests(conn, _params) do - with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, - {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, - public_tags_requests <- PublicTagsRequest.get_public_tags_requests_by_identity_id(identity.id) do - conn - |> put_status(200) - |> render(:public_tags_requests, %{public_tags_requests: public_tags_requests}) - end - end - - def delete_public_tags_request(conn, %{"id" => id, "remove_reason" => remove_reason}) do - with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, - {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, - {:public_tag_delete, true} <- - {:public_tag_delete, - PublicTagsRequest.mark_as_deleted_public_tags_request(%{ - id: id, - identity_id: identity.id, - remove_reason: remove_reason - })} do - conn - |> put_status(200) - |> render(:message, %{message: @ok_message}) - end - end - - def create_public_tags_request(conn, params) do - with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, - {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, - {:ok, public_tags_request} <- - PublicTagsRequest.create(%{ - full_name: params["full_name"], - email: params["email"], - tags: params["tags"], - website: params["website"], - additional_comment: params["additional_comment"], - addresses: params["addresses"], - company: params["company"], - is_owner: params["is_owner"], - identity_id: identity.id - }) do - conn - |> put_status(200) - |> render(:public_tags_request, %{public_tags_request: public_tags_request}) - end - end - - def update_public_tags_request( - conn, - %{ - "id" => id - } = params - ) do - with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, - {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, - {:ok, public_tags_request} <- - PublicTagsRequest.update( - reject_nil_map_values(%{ - id: id, - full_name: params["full_name"], - email: params["email"], - tags: params["tags"], - website: params["website"], - additional_comment: params["additional_comment"], - addresses: params["addresses"], - company: params["company"], - is_owner: params["is_owner"], - identity_id: identity.id - }) - ) do - conn - |> put_status(200) - |> render(:public_tags_request, %{public_tags_request: public_tags_request}) - end - end - def get_csrf(conn, _) do with {:auth, %{id: _}} <- {:auth, current_user(conn)} do conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex deleted file mode 100644 index a380a2cc3ca9..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/public_tags_request_controller.ex +++ /dev/null @@ -1,114 +0,0 @@ -defmodule BlockScoutWeb.Account.PublicTagsRequestController do - use BlockScoutWeb, :controller - - alias Ecto.Changeset - alias Explorer.Account.PublicTagsRequest - - import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] - - def index(conn, _params) do - current_user = authenticate!(conn) - - render(conn, "index.html", - public_tags_requests: PublicTagsRequest.get_public_tags_requests_by_identity_id(current_user.id) - ) - end - - def new(conn, _params) do - current_user = authenticate!(conn) - - render(conn, "form.html", - method: :create, - public_tags_request: - PublicTagsRequest.changeset_without_constraints(%PublicTagsRequest{}, %{ - full_name: current_user.name, - email: current_user.email - }) - ) - end - - def create(conn, %{"public_tags_request" => public_tags_request}) do - current_user = authenticate!(conn) - - case PublicTagsRequest.create(%{ - full_name: public_tags_request["full_name"], - email: public_tags_request["email"], - tags: public_tags_request["tags"], - website: public_tags_request["website"], - additional_comment: public_tags_request["additional_comment"], - addresses: public_tags_request["addresses"], - company: public_tags_request["company"], - is_owner: public_tags_request["is_owner"], - identity_id: current_user.id - }) do - {:ok, _} -> - redirect(conn, to: public_tags_request_path(conn, :index)) - - {:error, invalid_public_tags_request} -> - render(conn, "form.html", method: :create, public_tags_request: invalid_public_tags_request) - end - end - - def create(conn, _) do - redirect(conn, to: public_tags_request_path(conn, :index)) - end - - def edit(conn, %{"id" => id}) do - current_user = authenticate!(conn) - - case PublicTagsRequest.get_public_tags_request_by_id_and_identity_id(id, current_user.id) do - nil -> - not_found(conn) - - %PublicTagsRequest{} = public_tags_request -> - render(conn, "form.html", - method: :update, - public_tags_request: PublicTagsRequest.changeset_without_constraints(public_tags_request) - ) - end - end - - def update(conn, %{ - "id" => id, - "public_tags_request" => public_tags_request - }) do - current_user = authenticate!(conn) - - case PublicTagsRequest.update(%{ - id: id, - full_name: public_tags_request["full_name"], - email: public_tags_request["email"], - tags: public_tags_request["tags"], - website: public_tags_request["website"], - additional_comment: public_tags_request["additional_comment"], - addresses: public_tags_request["addresses"], - company: public_tags_request["company"], - is_owner: public_tags_request["is_owner"], - identity_id: current_user.id - }) do - {:error, %Changeset{} = public_tags_request} -> - render(conn, "form.html", method: :update, public_tags_request: public_tags_request) - - _ -> - redirect(conn, to: public_tags_request_path(conn, :index)) - end - end - - def update(conn, _) do - authenticate!(conn) - - redirect(conn, to: public_tags_request_path(conn, :index)) - end - - def delete(conn, %{"id" => id, "remove_reason" => remove_reason}) do - current_user = authenticate!(conn) - - PublicTagsRequest.mark_as_deleted_public_tags_request(%{ - id: id, - identity_id: current_user.id, - remove_reason: remove_reason - }) - - redirect(conn, to: public_tags_request_path(conn, :index)) - end -end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex index 2a915b9d6881..8f5641926cdd 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex @@ -109,11 +109,6 @@ defmodule BlockScoutWeb.Routers.AccountRouter do only: [:new, :create, :edit, :update, :delete, :index], as: :custom_abi ) - - resources("/public_tags_request", Account.PublicTagsRequestController, - only: [:new, :create, :edit, :update, :delete, :index], - as: :public_tags_request - ) end scope "/v2", as: :account_v2 do @@ -151,11 +146,6 @@ defmodule BlockScoutWeb.Routers.AccountRouter do post("/custom_abis", UserController, :create_custom_abi) put("/custom_abis/:id", UserController, :update_custom_abi) - get("/public_tags", UserController, :public_tags_requests) - delete("/public_tags/:id", UserController, :delete_public_tags_request) - post("/public_tags", UserController, :create_public_tags_request) - put("/public_tags/:id", UserController, :update_public_tags_request) - scope "/tags" do get("/address/", UserController, :tags_address) get("/address/:id", UserController, :tags_address) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex index 81667180d7d6..f5397976b145 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/common/_nav.html.eex @@ -18,8 +18,5 @@ - diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex deleted file mode 100644 index a0b78f313da7..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex +++ /dev/null @@ -1,8 +0,0 @@ -
- <%= label @f, :addresses, gettext("Address*"), class: "control-label", style: "font-size: 14px" %> -
- <%= array_input @f, :addresses, maxlength: 42, size: 70, placeholder: gettext "Smart contract / Address (0x...)" %> - <%= array_add_button @f, :addresses, maxlength: 42, size: 70, placeholder: gettext "Smart contract / Address (0x...)" %> -
- <%= error_tag @f, :addresses, class: "text-danger form-error pt-0" %> -
\ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex deleted file mode 100644 index 9a2cec122069..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/form.html.eex +++ /dev/null @@ -1,72 +0,0 @@ -
-
- <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :public_tags %> -
-
-
-

<%=if @method == :update, do: gettext("Request to edit a public tag/label"), else: gettext("Request a public tag/label") %>

- -
-
-
-
- -
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex deleted file mode 100644 index b3a51558682e..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/index.html.eex +++ /dev/null @@ -1,44 +0,0 @@ -
-
- <%= render BlockScoutWeb.Account.CommonView, "_nav.html", conn: @conn, active_item: :public_tags %> -
-
-
-

<%= gettext "Public tags" %>

-
-
-
- <%= gettext "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." %> -
-
-
-
- <%= if @public_tags_requests != [] do %> - - - - - - - - - - - - <%= Enum.map(@public_tags_requests, fn x -> - render("row.html", public_tags_request: x, conn: @conn) - end) %> - -
<%= gettext "Public tag" %><%= gettext "Smart contract / Address" %><%= gettext "Submission date" %>
- <% end %> -
-
- <%= if Enum.count(@public_tags_requests) < PublicTagsRequest.get_max_public_tags_request_count() do %> - <%= gettext "Request to add public tag" %> - <% end %> -
-
-
-
- -
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex deleted file mode 100644 index f7d2772e479c..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/public_tags_request/row.html.eex +++ /dev/null @@ -1,20 +0,0 @@ - - - <%= for tag <- String.split(@public_tags_request.tags, ";") do %> -
<%= tag %>
- <% end %> - - <%= Enum.join(@public_tags_request.addresses, "\n") %> - <%= Calendar.strftime(@public_tags_request.inserted_at, "%b %d, %Y") %> - - <%= link (render BlockScoutWeb.CommonComponentsView, "_svg_pen.html"), to: public_tags_request_path(@conn, :edit, @public_tags_request.id) %> - - -
- - - -
- <%= (render BlockScoutWeb.CommonComponentsView, "_svg_trash.html") %> - - \ No newline at end of file diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex index e7742d231f98..1c7446f20c98 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_account_menu_item.html.eex @@ -17,7 +17,6 @@ <%= gettext "Transaction Tags" %> <%= gettext "API keys" %> <%= gettext "Custom ABI" %> - <%= gettext "Public Tags" %> <% else %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex index 18f4c176d38e..360a188dcab4 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex @@ -84,14 +84,6 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do prepare_custom_abi(custom_abi) end - def render("public_tags_requests.json", %{public_tags_requests: public_tags_requests}) do - Enum.map(public_tags_requests, &prepare_public_tags_request/1) - end - - def render("public_tags_request.json", %{public_tags_request: public_tags_request}) do - prepare_public_tags_request(public_tags_request) - end - def render("changeset_errors.json", %{changeset: changeset}) do %{ "errors" => @@ -194,27 +186,4 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do "name" => transaction_tag.name } end - - defp prepare_public_tags_request(public_tags_request) do - addresses = Address.get_addresses_by_hashes(public_tags_request.addresses) - - addresses_with_info = - Enum.map(addresses, fn address -> - Helper.address_with_info(nil, address, address.hash, false) - end) - - %{ - "id" => public_tags_request.id, - "full_name" => public_tags_request.full_name, - "email" => public_tags_request.email, - "company" => public_tags_request.company, - "website" => public_tags_request.website, - "tags" => public_tags_request.tags, - "addresses" => public_tags_request.addresses, - "addresses_with_info" => addresses_with_info, - "additional_comment" => public_tags_request.additional_comment, - "is_owner" => public_tags_request.is_owner, - "submission_date" => public_tags_request.inserted_at - } - end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex deleted file mode 100644 index 2a13dd8d7ea3..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex +++ /dev/null @@ -1,70 +0,0 @@ -defmodule BlockScoutWeb.Account.PublicTagsRequestView do - use BlockScoutWeb, :view - use Phoenix.HTML - - alias Explorer.Account.PublicTagsRequest - alias Phoenix.HTML.Form - - def array_input(form, field, attrs \\ []) do - values = Form.input_value(form, field) || [""] - id = Form.input_id(form, field) - - content_tag :ul, - id: container_id(id), - data: [index: Enum.count(values), multiple_input_field_container: ""], - class: "multiple-input-fields-container" do - values - |> Enum.map(fn v -> - form_elements(form, field, to_string(v), attrs) - end) - end - end - - def array_add_button(form, field, attrs \\ []) do - id = Form.input_id(form, field) - - content = - form - |> form_elements(field, "", attrs) - |> safe_to_string - - data = [ - prototype: content, - container: container_id(id) - ] - - content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_plus.html"), - data: data, - class: "add-form-field" - ) - end - - defp form_elements(form, field, k, attrs) do - type = Form.input_type(form, field) - id = Form.input_id(form, field) - - input_opts = - [ - name: new_field_name(form, field), - value: k, - id: id, - class: "form-control public-tags-address" - ] ++ attrs - - content_tag :li, class: "public-tags-address form-group" do - [ - apply(Form, type, [form, field, input_opts]), - content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_minus.html"), - data: [container: container_id(id)], - class: "remove-form-field ml-1" - ) - ] - end - end - - defp container_id(id), do: id <> "_container" - - defp new_field_name(form, field) do - Form.input_name(form, field) <> "[]" - end -end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index c298fe3a0fee..a1e52c49bd8c 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -11,47 +11,57 @@ msgid "" msgstr "" +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #, elixir-autogen, elixir-format msgid " - minimal bytecode implementation that delegates all calls to a known address" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #, elixir-autogen, elixir-format msgid " is recommended." msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:3 #: lib/block_scout_web/templates/address/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "%{address} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:12 #: lib/block_scout_web/templates/block/overview.html.eex:12 #, elixir-autogen, elixir-format msgid "%{block_type} Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:55 #: lib/block_scout_web/templates/block/overview.html.eex:55 #, elixir-autogen, elixir-format msgid "%{block_type} Height" msgstr "" +#: lib/block_scout_web/templates/block/index.html.eex:7 #: lib/block_scout_web/templates/block/index.html.eex:7 #, elixir-autogen, elixir-format msgid "%{block_type}s" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:85 #: lib/block_scout_web/templates/block/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "%{count} Transaction" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:87 #: lib/block_scout_web/templates/block/overview.html.eex:87 #: lib/block_scout_web/templates/chain/_block.html.eex:11 +#: lib/block_scout_web/templates/chain/_block.html.eex:11 #, elixir-autogen, elixir-format msgid "%{count} Transactions" msgstr "" +#: lib/block_scout_web/views/address_token_balance_view.ex:10 #: lib/block_scout_web/views/address_token_balance_view.ex:10 #, elixir-autogen, elixir-format msgid "%{count} token" @@ -59,6 +69,7 @@ msgid_plural "%{count} tokens" msgstr[0] "" msgstr[1] "" +#: lib/block_scout_web/templates/block/_tile.html.eex:29 #: lib/block_scout_web/templates/block/_tile.html.eex:29 #, elixir-autogen, elixir-format msgid "%{count} transaction" @@ -66,825 +77,1050 @@ msgid_plural "%{count} transactions" msgstr[0] "" msgstr[1] "" +#: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #, elixir-autogen, elixir-format msgid "%{qty} of Token ID [%{link_to_id}]" msgstr "" +#: lib/block_scout_web/templates/chain/_metatags.html.eex:2 #: lib/block_scout_web/templates/chain/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "%{subnetwork} %{network} Explorer" msgstr "" +#: lib/block_scout_web/templates/layout/_default_title.html.eex:2 #: lib/block_scout_web/templates/layout/_default_title.html.eex:2 #, elixir-autogen, elixir-format msgid "%{subnetwork} Explorer - BlockScout" msgstr "" +#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 #: lib/block_scout_web/templates/withdrawal/index.html.eex:11 #, elixir-autogen, elixir-format msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:375 #: lib/block_scout_web/views/transaction_view.ex:375 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 #, elixir-autogen, elixir-format msgid "(query)" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #, elixir-autogen, elixir-format msgid ") may be added for each contract. Click the Add Library button to add an additional one." msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:93 #: lib/block_scout_web/templates/layout/app.html.eex:93 #, elixir-autogen, elixir-format msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:8 #: lib/block_scout_web/templates/transaction/not_found.html.eex:8 #, elixir-autogen, elixir-format msgid "1. If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:9 #: lib/block_scout_web/templates/transaction/not_found.html.eex:9 #, elixir-autogen, elixir-format msgid "2. It could still be in the TX Pool of a different node, waiting to be broadcasted." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:10 #: lib/block_scout_web/templates/transaction/not_found.html.eex:10 #, elixir-autogen, elixir-format msgid "3. During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:11 #: lib/block_scout_web/templates/transaction/not_found.html.eex:11 #, elixir-autogen, elixir-format msgid "4. If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:197 #: lib/block_scout_web/templates/block/overview.html.eex:197 #, elixir-autogen, elixir-format msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains)." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:97 #: lib/block_scout_web/templates/block/overview.html.eex:97 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 #, elixir-autogen, elixir-format msgid "A block producer who successfully included the block onto the blockchain." msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "A confirmation email was sent to" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #, elixir-autogen, elixir-format msgid "A library name called in the .sol file. Multiple libraries (up to " msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "A string with the name of the action to be invoked." msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 #, elixir-autogen, elixir-format msgid "A string with the name of the module to be invoked." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 #, elixir-autogen, elixir-format msgid "ABI" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "ABI-encoded Constructor Arguments (if required by the contract)" msgstr "" +#: lib/block_scout_web/templates/api_docs/index.html.eex:4 #: lib/block_scout_web/templates/api_docs/index.html.eex:4 #, elixir-autogen, elixir-format msgid "API Documentation" msgstr "" +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #, elixir-autogen, elixir-format msgid "API endpoints for the %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "API for the %{subnetwork} - BlockScout" msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 #: lib/block_scout_web/templates/account/api_key/form.html.eex:13 #: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 #: lib/block_scout_web/templates/account/api_key/index.html.eex:29 #, elixir-autogen, elixir-format msgid "API key" msgstr "" #: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 #: lib/block_scout_web/templates/account/common/_nav.html.eex:16 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 #, elixir-autogen, elixir-format msgid "API keys" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 #: lib/block_scout_web/templates/layout/_topnav.html.eex:106 #, elixir-autogen, elixir-format msgid "APIs" msgstr "" #: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 #, elixir-autogen, elixir-format msgid "Action" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 #, elixir-autogen, elixir-format msgid "Actions" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 #: lib/block_scout_web/templates/transaction/overview.html.eex:484 #, elixir-autogen, elixir-format msgid "Actual gas amount used by the transaction on L2." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 #: lib/block_scout_web/templates/transaction/overview.html.eex:488 #, elixir-autogen, elixir-format msgid "Actual gas amount used by the transaction." msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 +#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 #, elixir-autogen, elixir-format msgid "Add" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 #: lib/block_scout_web/templates/account/api_key/index.html.eex:44 #, elixir-autogen, elixir-format msgid "Add API key" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 #, elixir-autogen, elixir-format msgid "Add Contract Libraries" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 #, elixir-autogen, elixir-format msgid "Add Custom ABI" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 #, elixir-autogen, elixir-format msgid "Add Library" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 #, elixir-autogen, elixir-format msgid "Add address" msgstr "" #: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Add address tag" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #, elixir-autogen, elixir-format msgid "Add address to the Watch list" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Add transaction tag" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/tokens/index.html.eex:34 #: lib/block_scout_web/templates/tokens/index.html.eex:34 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 -#: lib/block_scout_web/views/address_view.ex:109 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 +#: lib/block_scout_web/views/address_view.ex:110 +#: lib/block_scout_web/views/address_view.ex:110 #, elixir-autogen, elixir-format msgid "Address" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 #: lib/block_scout_web/templates/transaction/overview.html.eex:263 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 #: lib/block_scout_web/templates/transaction/overview.html.eex:245 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 #: lib/block_scout_web/templates/account/common/_nav.html.eex:10 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 #, elixir-autogen, elixir-format msgid "Address Tags" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:151 #: lib/block_scout_web/templates/address/overview.html.eex:151 #, elixir-autogen, elixir-format msgid "Address balance in" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #, elixir-autogen, elixir-format msgid "Address of the token contract" msgstr "" +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 #, elixir-autogen, elixir-format msgid "Address used in token mintings and burnings." msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 -#, elixir-autogen, elixir-format -msgid "Address*" -msgstr "" - +#: lib/block_scout_web/templates/address/index.html.eex:5 #: lib/block_scout_web/templates/address/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Addresses" msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:35 #, elixir-autogen, elixir-format msgid "Age" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:22 #: lib/block_scout_web/templates/address_transaction/index.html.eex:22 #: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 #: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_token_transfer_view.ex:12 #: lib/block_scout_web/views/address_token_transfer_view.ex:12 #: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:13 #: lib/block_scout_web/views/verified_contracts_view.ex:13 #, elixir-autogen, elixir-format msgid "All" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 #, elixir-autogen, elixir-format msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:176 #: lib/block_scout_web/templates/address/overview.html.eex:176 #, elixir-autogen, elixir-format msgid "All tokens in the account and total value." msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/withdrawal/index.html.eex:38 #, elixir-autogen, elixir-format msgid "Amount" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:238 #: lib/block_scout_web/templates/block/overview.html.eex:238 #, elixir-autogen, elixir-format msgid "Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees." msgstr "" +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 #, elixir-autogen, elixir-format msgid "An unexpected error has occurred. Try reloading the page, or come back soon and try again." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 #, elixir-autogen, elixir-format msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:134 #: lib/block_scout_web/templates/layout/_topnav.html.eex:134 #, elixir-autogen, elixir-format msgid "Apps" msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #, elixir-autogen, elixir-format msgid "Average" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:102 #: lib/block_scout_web/templates/chain/show.html.eex:102 #, elixir-autogen, elixir-format msgid "Average block time" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #, elixir-autogen, elixir-format msgid "Back to API keys (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Back to Address Tags (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 #, elixir-autogen, elixir-format msgid "Back to Custom ABI (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Back to Transaction Tags (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 #, elixir-autogen, elixir-format msgid "Back to Watch list (Cancel)" msgstr "" #: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 #: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/transaction/not_found.html.eex:13 #: lib/block_scout_web/templates/transaction/not_found.html.eex:13 #, elixir-autogen, elixir-format msgid "Back to home" msgstr "" #: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:152 #: lib/block_scout_web/templates/address/overview.html.eex:152 #: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Balance" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:40 #: lib/block_scout_web/templates/transaction_state/index.html.eex:40 #, elixir-autogen, elixir-format msgid "Balance after" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:37 #: lib/block_scout_web/templates/transaction_state/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Balance before" msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Balances" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:209 #: lib/block_scout_web/templates/block/overview.html.eex:209 #, elixir-autogen, elixir-format msgid "Base Fee per Gas" msgstr "" #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/index.html.eex:5 #: lib/block_scout_web/templates/api_docs/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Base URL:" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "Beacon chain withdrawals - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 #, elixir-autogen, elixir-format msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 #: lib/block_scout_web/templates/transaction/overview.html.eex:545 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/block/overview.html.eex:29 #: lib/block_scout_web/templates/block/overview.html.eex:29 #: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/withdrawal/index.html.eex:29 #, elixir-autogen, elixir-format msgid "Block" msgstr "" #: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 #: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 +#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 #, elixir-autogen, elixir-format msgid "Block #%{number}" msgstr "" +#: lib/block_scout_web/templates/block/_metatags.html.eex:3 #: lib/block_scout_web/templates/block/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "Block %{block_number} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/block_transaction/404.html.eex:7 #: lib/block_scout_web/templates/block_transaction/404.html.eex:7 #, elixir-autogen, elixir-format msgid "Block Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:53 #: lib/block_scout_web/templates/block/overview.html.eex:53 #, elixir-autogen, elixir-format msgid "Block Height" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:47 #: lib/block_scout_web/templates/layout/app.html.eex:47 #, elixir-autogen, elixir-format msgid "Block Mined, awaiting import..." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:34 #: lib/block_scout_web/views/transaction_view.ex:34 #, elixir-autogen, elixir-format msgid "Block Pending" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:158 #: lib/block_scout_web/templates/block/overview.html.eex:158 #, elixir-autogen, elixir-format msgid "Block difficulty for miner, used to calibrate block generation time (Note: constant in POA based networks)." msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:15 #: lib/block_scout_web/views/block_transaction_view.ex:15 #, elixir-autogen, elixir-format msgid "Block not found, please try again later." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 #: lib/block_scout_web/templates/transaction/overview.html.eex:203 #, elixir-autogen, elixir-format msgid "Block number containing the transaction on L1." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:160 #: lib/block_scout_web/templates/transaction/overview.html.eex:160 #, elixir-autogen, elixir-format msgid "Block number containing the transaction." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:259 #: lib/block_scout_web/templates/address/overview.html.eex:259 #, elixir-autogen, elixir-format msgid "Block number in which the address was updated." msgstr "" +#: lib/block_scout_web/templates/chain/_metatags.html.eex:4 #: lib/block_scout_web/templates/chain/_metatags.html.eex:4 #, elixir-autogen, elixir-format msgid "BlockScout provides analytics data, API, and Smart Contract tools for the %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:29 #: lib/block_scout_web/templates/layout/_topnav.html.eex:29 #, elixir-autogen, elixir-format msgid "Blockchain" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #, elixir-autogen, elixir-format msgid "Blocks" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:46 #: lib/block_scout_web/templates/layout/app.html.eex:46 #, elixir-autogen, elixir-format msgid "Blocks Indexed" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:277 +#: lib/block_scout_web/templates/address/overview.html.eex:277 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:361 +#: lib/block_scout_web/templates/address_validation/index.html.eex:11 +#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/views/address_view.ex:362 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:48 #: lib/block_scout_web/templates/layout/app.html.eex:48 #, elixir-autogen, elixir-format msgid "Blocks With Internal Transactions Indexed" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:22 #: lib/block_scout_web/templates/layout/_footer.html.eex:22 #, elixir-autogen, elixir-format msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." msgstr "" +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 #, elixir-autogen, elixir-format msgid "Burn address" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/overview.html.eex:218 #: lib/block_scout_web/templates/block/overview.html.eex:218 #, elixir-autogen, elixir-format msgid "Burnt Fees" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:65 #: lib/block_scout_web/templates/address_token/overview.html.eex:65 #, elixir-autogen, elixir-format msgid "CRC Worth" msgstr "" +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 #: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 #, elixir-autogen, elixir-format msgid "CSV" msgstr "" #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/views/internal_transaction_view.ex:21 #: lib/block_scout_web/views/internal_transaction_view.ex:21 #, elixir-autogen, elixir-format msgid "Call" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:22 #: lib/block_scout_web/views/internal_transaction_view.ex:22 #, elixir-autogen, elixir-format msgid "Call Code" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:43 #: lib/block_scout_web/templates/transaction_state/index.html.eex:43 #, elixir-autogen, elixir-format msgid "Change" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:43 #: lib/block_scout_web/templates/layout/_footer.html.eex:43 #, elixir-autogen, elixir-format msgid "Chat (#blockscout)" msgstr "" +#: lib/block_scout_web/views/block_view.ex:65 #: lib/block_scout_web/views/block_view.ex:65 #, elixir-autogen, elixir-format msgid "Chore Reward" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:38 #: lib/block_scout_web/templates/tokens/index.html.eex:38 #, elixir-autogen, elixir-format msgid "Circulating Market Cap" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 #, elixir-autogen, elixir-format msgid "Clear" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 #, elixir-autogen, elixir-format msgid "Close" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 +#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/views/address_view.ex:356 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 +#: lib/block_scout_web/views/address_view.ex:361 +#: lib/block_scout_web/views/address_view.ex:361 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #, elixir-autogen, elixir-format msgid "Collapse" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Company name" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 -#, elixir-autogen, elixir-format -msgid "Company website" -msgstr "" - +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 #, elixir-autogen, elixir-format msgid "Compiler" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 #: lib/block_scout_web/templates/address_contract/index.html.eex:142 #, elixir-autogen, elixir-format msgid "Compiler Settings" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 #: lib/block_scout_web/templates/address_contract/index.html.eex:71 #, elixir-autogen, elixir-format msgid "Compiler version" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:368 #: lib/block_scout_web/views/transaction_view.ex:368 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:127 #: lib/block_scout_web/templates/transaction/overview.html.eex:127 #, elixir-autogen, elixir-format msgid "Confirmed by " msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 #: lib/block_scout_web/templates/transaction/overview.html.eex:193 #, elixir-autogen, elixir-format msgid "Confirmed within" msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 #, elixir-autogen, elixir-format msgid "Connection Lost" msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 #: lib/block_scout_web/templates/block/index.html.eex:5 +#: lib/block_scout_web/templates/block/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer blocks" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer internal transactions" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 #: lib/block_scout_web/templates/transaction/index.html.eex:22 +#: lib/block_scout_web/templates/transaction/index.html.eex:22 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer transactions" msgstr "" +#: lib/block_scout_web/templates/address_validation/index.html.eex:10 #: lib/block_scout_web/templates/address_validation/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer validations" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 #: lib/block_scout_web/templates/address_contract/index.html.eex:96 #, elixir-autogen, elixir-format msgid "Constructor Arguments" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 #, elixir-autogen, elixir-format msgid "Constructor args" msgstr "" #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 #: lib/block_scout_web/templates/address_contract/index.html.eex:157 #, elixir-autogen, elixir-format msgid "Contract ABI" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:107 +#: lib/block_scout_web/views/address_view.ex:108 +#: lib/block_scout_web/views/address_view.ex:108 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:47 -#: lib/block_scout_web/views/address_view.ex:81 +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 +#: lib/block_scout_web/views/address_view.ex:48 +#: lib/block_scout_web/views/address_view.ex:48 +#: lib/block_scout_web/views/address_view.ex:82 +#: lib/block_scout_web/views/address_view.ex:82 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:497 #: lib/block_scout_web/views/transaction_view.ex:497 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:494 #: lib/block_scout_web/views/transaction_view.ex:494 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 #: lib/block_scout_web/templates/address_contract/index.html.eex:174 #: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 #, elixir-autogen, elixir-format msgid "Contract Creation Code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 #, elixir-autogen, elixir-format msgid "Contract Libraries" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Contract Name" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 #, elixir-autogen, elixir-format msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 #, elixir-autogen, elixir-format msgid "Contract name or address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 #: lib/block_scout_web/templates/address_contract/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Contract name:" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 #: lib/block_scout_web/templates/address_contract/index.html.eex:106 #, elixir-autogen, elixir-format msgid "Contract source code" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:120 #: lib/block_scout_web/templates/address/overview.html.eex:120 #, elixir-autogen, elixir-format msgid "Contract was precompiled and created at genesis or contract creation transaction is missing" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 #, elixir-autogen, elixir-format msgid "Contracts" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 #: lib/block_scout_web/templates/address_contract/index.html.eex:180 #, elixir-autogen, elixir-format msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:42 #: lib/block_scout_web/templates/layout/_footer.html.eex:42 #, elixir-autogen, elixir-format msgid "Contribute" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 #: lib/block_scout_web/templates/address_contract/index.html.eex:159 #, elixir-autogen, elixir-format msgid "Copy ABI" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #, elixir-autogen, elixir-format @@ -893,2843 +1129,3587 @@ msgstr "" #: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 #: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:105 #: lib/block_scout_web/templates/block/overview.html.eex:105 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #, elixir-autogen, elixir-format msgid "Copy Address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 #: lib/block_scout_web/templates/address_contract/index.html.eex:144 #, elixir-autogen, elixir-format msgid "Copy Compiler Settings" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #, elixir-autogen, elixir-format msgid "Copy Contract Address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 #: lib/block_scout_web/templates/address_contract/index.html.eex:176 #: lib/block_scout_web/templates/address_contract/index.html.eex:192 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #, elixir-autogen, elixir-format msgid "Copy Contract Creation Code" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 #: lib/block_scout_web/templates/address_contract/index.html.eex:213 #: lib/block_scout_web/templates/address_contract/index.html.eex:223 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 #: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 #: lib/block_scout_web/templates/transaction/overview.html.eex:254 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:130 #: lib/block_scout_web/templates/block/overview.html.eex:130 #, elixir-autogen, elixir-format msgid "Copy Hash" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 #, elixir-autogen, elixir-format msgid "Copy Metadata" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:149 #: lib/block_scout_web/templates/block/overview.html.eex:149 #: lib/block_scout_web/templates/block/overview.html.eex:150 +#: lib/block_scout_web/templates/block/overview.html.eex:150 #, elixir-autogen, elixir-format msgid "Copy Parent Hash" msgstr "" +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 #, elixir-autogen, elixir-format msgid "Copy Raw Trace" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 #: lib/block_scout_web/templates/address_contract/index.html.eex:132 #, elixir-autogen, elixir-format msgid "Copy Source Code" msgstr "" #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 #: lib/block_scout_web/templates/transaction/overview.html.eex:281 #: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 #, elixir-autogen, elixir-format msgid "Copy Token ID" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 #: lib/block_scout_web/templates/transaction/overview.html.eex:87 #, elixir-autogen, elixir-format msgid "Copy Transaction Hash" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 #: lib/block_scout_web/templates/transaction/overview.html.eex:88 #, elixir-autogen, elixir-format msgid "Copy Txn Hash" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 #: lib/block_scout_web/templates/transaction/overview.html.eex:571 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 #: lib/block_scout_web/templates/transaction/overview.html.eex:577 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 #: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 #, elixir-autogen, elixir-format msgid "Copy Value" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:26 #: lib/block_scout_web/views/internal_transaction_view.ex:26 #, elixir-autogen, elixir-format msgid "Create" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 #, elixir-autogen, elixir-format msgid "Create a Custom ABI to interact with contracts." msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #, elixir-autogen, elixir-format msgid "Create an API key to use with your RPC and EthRPC API requests." msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:27 #: lib/block_scout_web/views/internal_transaction_view.ex:27 #, elixir-autogen, elixir-format msgid "Create2" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:102 #: lib/block_scout_web/templates/address/overview.html.eex:102 #, elixir-autogen, elixir-format msgid "Creator" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 #, elixir-autogen, elixir-format msgid "Curl" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 #: lib/block_scout_web/templates/transaction/overview.html.eex:97 #, elixir-autogen, elixir-format msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Custom" msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 #: lib/block_scout_web/templates/account/common/_nav.html.eex:19 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 #, elixir-autogen, elixir-format msgid "Custom ABI" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 #, elixir-autogen, elixir-format msgid "Custom ABI from account" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:70 #: lib/block_scout_web/templates/chain/show.html.eex:70 #, elixir-autogen, elixir-format msgid "Daily Transactions" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 #, elixir-autogen, elixir-format msgid "Data" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:70 #: lib/block_scout_web/templates/block/overview.html.eex:70 #, elixir-autogen, elixir-format msgid "Date & time at which block was produced." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 #: lib/block_scout_web/templates/transaction/overview.html.eex:179 #, elixir-autogen, elixir-format msgid "Date & time of transaction inclusion, including length of time for confirmation." msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #, elixir-autogen, elixir-format msgid "Decimals" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 #, elixir-autogen, elixir-format msgid "Decoded" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:23 #: lib/block_scout_web/views/internal_transaction_view.ex:23 #, elixir-autogen, elixir-format msgid "Delegate Call" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 #: lib/block_scout_web/templates/address_contract/index.html.eex:219 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 #, elixir-autogen, elixir-format msgid "Description" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 -#, elixir-autogen, elixir-format -msgid "Description*" -msgstr "" - +#: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 #, elixir-autogen, elixir-format msgid "Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:159 #: lib/block_scout_web/templates/block/overview.html.eex:159 #, elixir-autogen, elixir-format msgid "Difficulty" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 #: lib/block_scout_web/templates/address_contract/index.html.eex:181 #, elixir-autogen, elixir-format msgid "Displaying the init data provided of the creating transaction." msgstr "" #: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/csv_export/index.html.eex:33 #: lib/block_scout_web/templates/csv_export/index.html.eex:33 #, elixir-autogen, elixir-format msgid "Download" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Drop all Solidity contract source files into the drop zone." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Drop all Solidity or Yul contract source files into the drop zone." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 #, elixir-autogen, elixir-format msgid "Drop sources and metadata JSON file or click here" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 #, elixir-autogen, elixir-format msgid "Drop sources or click here" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 #, elixir-autogen, elixir-format msgid "Drop the standard input JSON file or click here" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 -#, elixir-autogen, elixir-format -msgid "E-mail*" -msgstr "" - +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #, elixir-autogen, elixir-format msgid "EIP-1167" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:225 #: lib/block_scout_web/views/transaction_view.ex:225 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:223 #: lib/block_scout_web/views/transaction_view.ex:223 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 #, elixir-autogen, elixir-format msgid "ERC-20 tokens (beta)" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:226 #: lib/block_scout_web/views/transaction_view.ex:226 #, elixir-autogen, elixir-format msgid "ERC-404 " msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:224 #: lib/block_scout_web/views/transaction_view.ex:224 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 #, elixir-autogen, elixir-format msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #, elixir-autogen, elixir-format msgid "ETH RPC API Documentation" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 #, elixir-autogen, elixir-format msgid "EVM Version" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #, elixir-autogen, elixir-format msgid "EVM version details" msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:7 #: lib/block_scout_web/views/block_transaction_view.ex:7 #, elixir-autogen, elixir-format msgid "Easy Cowboy! This block does not exist yet!" msgstr "" #: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #, elixir-autogen, elixir-format msgid "Edit Watch list address" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 #, elixir-autogen, elixir-format msgid "Email notifications" msgstr "" +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #, elixir-autogen, elixir-format msgid "Emission Contract" msgstr "" +#: lib/block_scout_web/views/block_view.ex:73 #: lib/block_scout_web/views/block_view.ex:73 #, elixir-autogen, elixir-format msgid "Emission Reward" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Enter the Solidity Contract Code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Enter the Vyper Contract Code" msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 #, elixir-autogen, elixir-format msgid "Error" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #, elixir-autogen, elixir-format msgid "Error in internal transactions" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 #, elixir-autogen, elixir-format msgid "Error rendering value" msgstr "" +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 #: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 #, elixir-autogen, elixir-format msgid "Error trying to fetch balances." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:379 #: lib/block_scout_web/views/transaction_view.ex:379 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:377 #: lib/block_scout_web/views/transaction_view.ex:377 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:120 #: lib/block_scout_web/templates/address/overview.html.eex:120 #, elixir-autogen, elixir-format msgid "Error: Could not determine contract creator." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:120 #: lib/block_scout_web/templates/layout/_topnav.html.eex:120 #, elixir-autogen, elixir-format msgid "Eth RPC" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 #, elixir-autogen, elixir-format msgid "Example Value" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 #, elixir-autogen, elixir-format msgid "Execute" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #, elixir-autogen, elixir-format msgid "Expand" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 #: lib/block_scout_web/templates/csv_export/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Export" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:10 #: lib/block_scout_web/templates/csv_export/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Export Data" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 #: lib/block_scout_web/templates/address_contract/index.html.eex:248 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 #, elixir-autogen, elixir-format msgid "Failed to decode input data." msgstr "" +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 #, elixir-autogen, elixir-format msgid "Failed to decode log data." msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #, elixir-autogen, elixir-format msgid "Fast" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:249 #: lib/block_scout_web/templates/address/overview.html.eex:249 #, elixir-autogen, elixir-format msgid "Fetching gas used..." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #, elixir-autogen, elixir-format msgid "Fetching holders..." msgstr "" +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 #: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 #, elixir-autogen, elixir-format msgid "Fetching tokens..." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:196 #: lib/block_scout_web/templates/address/overview.html.eex:196 #: lib/block_scout_web/templates/address/overview.html.eex:204 +#: lib/block_scout_web/templates/address/overview.html.eex:204 #, elixir-autogen, elixir-format msgid "Fetching transactions..." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:223 #: lib/block_scout_web/templates/address/overview.html.eex:223 #: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #, elixir-autogen, elixir-format msgid "Fetching transfers..." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 #, elixir-autogen, elixir-format msgid "Filter by compiler:" msgstr "" +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 #: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 #, elixir-autogen, elixir-format msgid "For any existing contracts in the database, insert all ABI entries into the contract_methods table. Use this in case you have verified smart contracts before early March 2019 and you want other contracts with the same functions to show those ABI's as candidate matches." msgstr "" +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 #: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 #, elixir-autogen, elixir-format msgid "For contract" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:44 #: lib/block_scout_web/templates/layout/_topnav.html.eex:44 #, elixir-autogen, elixir-format msgid "Forked Blocks (Reorgs)" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:45 #: lib/block_scout_web/templates/layout/_footer.html.eex:45 #, elixir-autogen, elixir-format msgid "Forum" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 #: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:11 #, elixir-autogen, elixir-format msgid "From" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 #, elixir-autogen, elixir-format msgid "GET" msgstr "" +#: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 #: lib/block_scout_web/templates/transaction/overview.html.eex:430 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 #: lib/block_scout_web/templates/transaction/overview.html.eex:404 #, elixir-autogen, elixir-format msgid "Gas Price" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:242 #: lib/block_scout_web/templates/address/overview.html.eex:242 #: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/overview.html.eex:180 #: lib/block_scout_web/templates/block/overview.html.eex:180 #, elixir-autogen, elixir-format msgid "Gas Used" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 #: lib/block_scout_web/templates/transaction/overview.html.eex:489 #, elixir-autogen, elixir-format msgid "Gas Used by Transaction" msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 #, elixir-autogen, elixir-format msgid "Gas tracker" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:241 #: lib/block_scout_web/templates/address/overview.html.eex:241 #, elixir-autogen, elixir-format msgid "Gas used by the address." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:60 #: lib/block_scout_web/templates/block/overview.html.eex:60 #, elixir-autogen, elixir-format msgid "Genesis Block" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:24 #: lib/block_scout_web/templates/layout/_footer.html.eex:24 #, elixir-autogen, elixir-format msgid "Github" msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 #, elixir-autogen, elixir-format msgid "Go to" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:110 #: lib/block_scout_web/templates/layout/_topnav.html.eex:110 #, elixir-autogen, elixir-format msgid "GraphQL" msgstr "" #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/views/block_view.ex:22 #: lib/block_scout_web/views/block_view.ex:22 #: lib/block_scout_web/views/wei_helper.ex:81 +#: lib/block_scout_web/views/wei_helper.ex:81 #, elixir-autogen, elixir-format msgid "Gwei" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:123 #: lib/block_scout_web/templates/block/overview.html.eex:123 #, elixir-autogen, elixir-format msgid "Hash" msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 #: lib/block_scout_web/templates/transaction/overview.html.eex:557 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 #: lib/block_scout_web/templates/transaction/overview.html.eex:224 #, elixir-autogen, elixir-format msgid "Highlighted events of the transaction." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #, elixir-autogen, elixir-format msgid "Holders" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:48 #: lib/block_scout_web/templates/tokens/index.html.eex:48 #, elixir-autogen, elixir-format msgid "Holders Count" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 #, elixir-autogen, elixir-format msgid "However, in general, the" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 #, elixir-autogen, elixir-format msgid "IMPORTANT: This information is a best guess based on similar functions from other verified contracts." msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 #: lib/block_scout_web/templates/transaction/_tile.html.eex:92 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 #, elixir-autogen, elixir-format msgid "IN" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 #, elixir-autogen, elixir-format msgid "If you enabled optimization during compilation, select yes." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:135 #: lib/block_scout_web/templates/address/overview.html.eex:135 #, elixir-autogen, elixir-format msgid "Implementation" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:134 #: lib/block_scout_web/templates/address/overview.html.eex:134 #, elixir-autogen, elixir-format msgid "Implementation address of the proxy contract." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Include nightly builds" msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 #, elixir-autogen, elixir-format msgid "Incoming" msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 #: lib/block_scout_web/templates/withdrawal/index.html.eex:23 #, elixir-autogen, elixir-format msgid "Index" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:251 #: lib/block_scout_web/templates/block/overview.html.eex:251 #, elixir-autogen, elixir-format msgid "Index position(s) of referenced stale blocks." msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 #, elixir-autogen, elixir-format msgid "Indexed?" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 #, elixir-autogen, elixir-format msgid "Input" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 #: lib/block_scout_web/templates/transaction/overview.html.eex:265 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 #, elixir-autogen, elixir-format msgid "Internal Transaction" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 +#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/transaction_view.ex:552 #: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Internal server error" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:25 #: lib/block_scout_web/views/internal_transaction_view.ex:25 #, elixir-autogen, elixir-format msgid "Invalid" msgstr "" #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/views/tokens/overview_view.ex:42 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format msgid "Inventory" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 #, elixir-autogen, elixir-format msgid "Is Yul contract" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 #: lib/block_scout_web/templates/transaction/overview.html.eex:204 #, elixir-autogen, elixir-format msgid "L1 Block" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 #: lib/block_scout_web/templates/transaction/overview.html.eex:523 #: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 #, elixir-autogen, elixir-format msgid "L1 Fee Scalar" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 #: lib/block_scout_web/templates/transaction/overview.html.eex:512 #: lib/block_scout_web/templates/transaction/overview.html.eex:513 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 #, elixir-autogen, elixir-format msgid "L1 Gas Price" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 #: lib/block_scout_web/templates/transaction/overview.html.eex:501 #: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 #, elixir-autogen, elixir-format msgid "L1 Gas Used by Transaction" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 #: lib/block_scout_web/templates/transaction/overview.html.eex:426 #, elixir-autogen, elixir-format msgid "L2 Gas Limit" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 #: lib/block_scout_web/templates/transaction/overview.html.eex:400 #, elixir-autogen, elixir-format msgid "L2 Gas Price" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 #: lib/block_scout_web/templates/transaction/overview.html.eex:485 #, elixir-autogen, elixir-format msgid "L2 Gas Used by Transaction" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 #, elixir-autogen, elixir-format msgid "Last 24h" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:260 #: lib/block_scout_web/templates/address/overview.html.eex:260 #, elixir-autogen, elixir-format msgid "Last Balance Update" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Learn more" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:49 #: lib/block_scout_web/templates/layout/app.html.eex:49 #, elixir-autogen, elixir-format msgid "Less than" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 #, elixir-autogen, elixir-format msgid "Library" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 #, elixir-autogen, elixir-format msgid "License Expires" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 #, elixir-autogen, elixir-format msgid "License ID" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 #: lib/block_scout_web/templates/transaction/overview.html.eex:351 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 #: lib/block_scout_web/templates/transaction/overview.html.eex:335 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 #: lib/block_scout_web/templates/transaction/overview.html.eex:318 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 #: lib/block_scout_web/templates/transaction/overview.html.eex:302 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Loading chart..." msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #, elixir-autogen, elixir-format msgid "Loading..." msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 #, elixir-autogen, elixir-format msgid "Log Data" msgstr "" +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 #, elixir-autogen, elixir-format msgid "Log Index" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/transaction_view.ex:553 #: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:54 #: lib/block_scout_web/templates/layout/_footer.html.eex:54 #, elixir-autogen, elixir-format msgid "Main Networks" msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 -#: lib/block_scout_web/views/address_view.ex:147 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 +#: lib/block_scout_web/views/address_view.ex:148 +#: lib/block_scout_web/views/address_view.ex:148 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 #, elixir-autogen, elixir-format msgid "Market cap" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 #: lib/block_scout_web/templates/transaction/overview.html.eex:440 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 #: lib/block_scout_web/templates/transaction/overview.html.eex:450 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:331 #: lib/block_scout_web/views/transaction_view.ex:331 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 #: lib/block_scout_web/templates/transaction/overview.html.eex:425 #, elixir-autogen, elixir-format msgid "Maximum gas amount approved for the transaction on L2." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 #: lib/block_scout_web/templates/transaction/overview.html.eex:429 #, elixir-autogen, elixir-format msgid "Maximum gas amount approved for the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 #: lib/block_scout_web/templates/transaction/overview.html.eex:439 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 #, elixir-autogen, elixir-format msgid "Metadata" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 #, elixir-autogen, elixir-format msgid "Method Id" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/overview.html.eex:98 #: lib/block_scout_web/templates/block/overview.html.eex:98 #: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 #, elixir-autogen, elixir-format msgid "Miner" msgstr "" #: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:68 #: lib/block_scout_web/views/block_view.ex:68 #, elixir-autogen, elixir-format msgid "Miner Reward" msgstr "" +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 #, elixir-autogen, elixir-format msgid "Minimal Proxy Contract for" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:208 #: lib/block_scout_web/templates/block/overview.html.eex:208 #, elixir-autogen, elixir-format msgid "Minimum fee required per unit of gas. Fee adjusts based on network congestion." msgstr "" +#: lib/block_scout_web/templates/transaction/_actions.html.eex:92 #: lib/block_scout_web/templates/transaction/_actions.html.eex:92 #, elixir-autogen, elixir-format msgid "Mint of %{address} To %{to}" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 #, elixir-autogen, elixir-format msgid "Model" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #, elixir-autogen, elixir-format msgid "Module" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 #, elixir-autogen, elixir-format msgid "More internal transactions have come in" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction/index.html.eex:19 #: lib/block_scout_web/templates/transaction/index.html.eex:19 #, elixir-autogen, elixir-format msgid "More transactions have come in" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 #, elixir-autogen, elixir-format msgid "Must be set to:" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." msgstr "" #: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 #, elixir-autogen, elixir-format msgid "N/A" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:116 #: lib/block_scout_web/templates/block/overview.html.eex:116 #, elixir-autogen, elixir-format msgid "N/A bytes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 #: lib/block_scout_web/templates/account/api_key/form.html.eex:19 #: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 #, elixir-autogen, elixir-format msgid "Name" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 #: lib/block_scout_web/templates/account/api_key/form.html.eex:20 #, elixir-autogen, elixir-format msgid "Name this API key" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 #, elixir-autogen, elixir-format msgid "Name this Custom ABI" msgstr "" #: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 #, elixir-autogen, elixir-format msgid "Name this address" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 #, elixir-autogen, elixir-format msgid "Name this transaction" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:44 #: lib/block_scout_web/templates/address_token/overview.html.eex:44 #, elixir-autogen, elixir-format msgid "Net Worth" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification via Standard input JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification via metadata JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Solidity Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Solidity/Yul Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Vyper Smart Contract Verification" msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 #, elixir-autogen, elixir-format msgid "Next" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 #, elixir-autogen, elixir-format msgid "No" msgstr "" +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 #, elixir-autogen, elixir-format msgid "No trace entries found." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:198 #: lib/block_scout_web/templates/block/overview.html.eex:198 #: lib/block_scout_web/templates/transaction/overview.html.eex:535 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 #, elixir-autogen, elixir-format msgid "Not unique Token" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #, elixir-autogen, elixir-format msgid "Number of accounts holding the token" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:276 #: lib/block_scout_web/templates/address/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "Number of blocks validated by this validator." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #, elixir-autogen, elixir-format msgid "Number of digits that come after the decimal place when displaying token value" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:187 #: lib/block_scout_web/templates/address/overview.html.eex:187 #, elixir-autogen, elixir-format msgid "Number of transactions related to this address." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #, elixir-autogen, elixir-format msgid "Number of transfers for the token" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:214 #: lib/block_scout_web/templates/address/overview.html.eex:214 #, elixir-autogen, elixir-format msgid "Number of transfers to/from this address." msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 #: lib/block_scout_web/templates/transaction/_tile.html.eex:88 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 #, elixir-autogen, elixir-format msgid "OUT" msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "Only the first" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 #, elixir-autogen, elixir-format msgid "Optimization" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 #: lib/block_scout_web/templates/address_contract/index.html.eex:67 #, elixir-autogen, elixir-format msgid "Optimization enabled" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 #, elixir-autogen, elixir-format msgid "Optimization runs" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:78 #: lib/block_scout_web/templates/layout/_footer.html.eex:78 #, elixir-autogen, elixir-format msgid "Other Explorers" msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 #, elixir-autogen, elixir-format msgid "Outgoing" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #, elixir-autogen, elixir-format msgid "Owner Address" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "POA solidity flattener or the" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 #, elixir-autogen, elixir-format msgid "POST" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #, elixir-autogen, elixir-format msgid "Page" msgstr "" +#: lib/block_scout_web/templates/page_not_found/index.html.eex:7 #: lib/block_scout_web/templates/page_not_found/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Page not found" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 #, elixir-autogen, elixir-format msgid "Parameters" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:139 #: lib/block_scout_web/templates/block/overview.html.eex:139 #, elixir-autogen, elixir-format msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/views/transaction_view.ex:374 #: lib/block_scout_web/views/transaction_view.ex:374 #: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 #: lib/block_scout_web/views/transaction_view.ex:427 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Pending Transactions" msgstr "" #: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 #: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 #, elixir-autogen, elixir-format msgid "Play" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:21 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:21 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Please confirm your email address to use the My Account feature." msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 #, elixir-autogen, elixir-format msgid "Please select notification methods:" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Please select what types of notifications you will receive:" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Position" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:256 #: lib/block_scout_web/templates/block/overview.html.eex:256 #, elixir-autogen, elixir-format msgid "Position %{index}" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 #, elixir-autogen, elixir-format msgid "Potential matches from contract method database:" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 #, elixir-autogen, elixir-format msgid "Potential matches from our contract method database:" msgstr "" +#: lib/block_scout_web/templates/layout/_search.html.eex:27 #: lib/block_scout_web/templates/layout/_search.html.eex:27 #, elixir-autogen, elixir-format msgid "Press / and focus will be moved to the search field" msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/layout/app.html.eex:51 #: lib/block_scout_web/templates/layout/app.html.eex:51 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 #, elixir-autogen, elixir-format msgid "Price" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #, elixir-autogen, elixir-format msgid "Price per token on the exchanges" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 #: lib/block_scout_web/templates/transaction/overview.html.eex:399 #, elixir-autogen, elixir-format msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 #: lib/block_scout_web/templates/transaction/overview.html.eex:403 #, elixir-autogen, elixir-format msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 #: lib/block_scout_web/templates/transaction/overview.html.eex:460 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" +#: lib/block_scout_web/templates/block/_tile.html.eex:62 #: lib/block_scout_web/templates/block/_tile.html.eex:62 #, elixir-autogen, elixir-format msgid "Priority Fees" msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 #, elixir-autogen, elixir-format msgid "Profile" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Public Tags" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Public tag" -msgstr "" - -#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Public tags" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 -#, elixir-autogen, elixir-format -msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" -msgstr "" - #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 #, elixir-autogen, elixir-format msgid "QR Code" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #, elixir-autogen, elixir-format msgid "Query" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:115 #: lib/block_scout_web/templates/layout/_topnav.html.eex:115 #, elixir-autogen, elixir-format msgid "RPC" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 #: lib/block_scout_web/templates/transaction/overview.html.eex:546 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 #: lib/block_scout_web/views/transaction_view.ex:554 +#: lib/block_scout_web/views/transaction_view.ex:554 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 +#: lib/block_scout_web/views/address_view.ex:358 +#: lib/block_scout_web/views/address_view.ex:358 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "Records" msgstr "" #: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 #, elixir-autogen, elixir-format msgid "Remove" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 #, elixir-autogen, elixir-format msgid "Remove from Watch list" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #, elixir-autogen, elixir-format msgid "Request URL" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Request a public tag/label" -msgstr "" - +#: lib/block_scout_web/templates/error422/index.html.eex:7 #: lib/block_scout_web/templates/error422/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Request cannot be processed" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 -#, elixir-autogen, elixir-format -msgid "Request to add public tag" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Request to edit a public tag/label" -msgstr "" - +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Resend verification email" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 #, elixir-autogen, elixir-format msgid "Reset" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 #, elixir-autogen, elixir-format msgid "Response Body" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 #, elixir-autogen, elixir-format msgid "Responses" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 #: lib/block_scout_web/templates/transaction/overview.html.eex:98 #, elixir-autogen, elixir-format msgid "Result" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:138 #: lib/block_scout_web/templates/transaction/overview.html.eex:138 #, elixir-autogen, elixir-format msgid "Revert reason" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/chain/_block.html.eex:27 #: lib/block_scout_web/templates/chain/_block.html.eex:27 #: lib/block_scout_web/views/internal_transaction_view.ex:29 +#: lib/block_scout_web/views/internal_transaction_view.ex:29 #, elixir-autogen, elixir-format msgid "Reward" msgstr "" +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 #: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 #, elixir-autogen, elixir-format msgid "Run" msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 #, elixir-autogen, elixir-format msgid "Save" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 #: lib/block_scout_web/templates/transaction/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "Scroll to see more" msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/layout/_search.html.eex:34 #: lib/block_scout_web/templates/layout/_search.html.eex:34 #, elixir-autogen, elixir-format msgid "Search" msgstr "" +#: lib/block_scout_web/templates/search/results.html.eex:17 #: lib/block_scout_web/templates/search/results.html.eex:17 #, elixir-autogen, elixir-format msgid "Search Results" msgstr "" +#: lib/block_scout_web/templates/layout/_search.html.eex:3 #: lib/block_scout_web/templates/layout/_search.html.eex:3 #, elixir-autogen, elixir-format msgid "Search by address, token symbol name, transaction hash, or block number" msgstr "" +#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 #: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 #, elixir-autogen, elixir-format msgid "Search tokens" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 #, elixir-autogen, elixir-format msgid "Select Yes if you want to verify Yul contract." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 #, elixir-autogen, elixir-format msgid "Select yes if you want to show nightly builds." msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:28 #: lib/block_scout_web/views/internal_transaction_view.ex:28 #, elixir-autogen, elixir-format msgid "Self-Destruct" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 -#, elixir-autogen, elixir-format -msgid "Send request" -msgstr "" - #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #, elixir-autogen, elixir-format msgid "Server Response" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 #, elixir-autogen, elixir-format msgid "Show" msgstr "" +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 #, elixir-autogen, elixir-format msgid "Show QR Code" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "Shows the current" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:59 #: lib/block_scout_web/templates/address_token/overview.html.eex:59 #, elixir-autogen, elixir-format msgid "Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)." msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:66 #: lib/block_scout_web/templates/address_token/overview.html.eex:66 #, elixir-autogen, elixir-format msgid "Shows the total CRC balance in the address." msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:45 #: lib/block_scout_web/templates/address_token/overview.html.eex:45 #, elixir-autogen, elixir-format msgid "Shows total assets held in the address" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:33 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:32 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:32 #, elixir-autogen, elixir-format msgid "Sign in" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:24 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:23 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:23 #, elixir-autogen, elixir-format msgid "Sign out" msgstr "" +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 #, elixir-autogen, elixir-format msgid "Signed in as " msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:114 #: lib/block_scout_web/templates/block/overview.html.eex:114 #, elixir-autogen, elixir-format msgid "Size" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:113 #: lib/block_scout_web/templates/block/overview.html.eex:113 #, elixir-autogen, elixir-format msgid "Size of the block in bytes." msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #, elixir-autogen, elixir-format msgid "Slow" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 -#, elixir-autogen, elixir-format -msgid "Smart contract / Address" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 -#, elixir-autogen, elixir-format -msgid "Smart contract / Address (0x...)" -msgstr "" - #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 #: lib/block_scout_web/views/verified_contracts_view.ex:10 +#: lib/block_scout_web/views/verified_contracts_view.ex:10 #, elixir-autogen, elixir-format msgid "Solidity" msgstr "" #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_logs/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/index.html.eex:23 #: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 #: lib/block_scout_web/templates/address_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:14 +#: lib/block_scout_web/templates/block_transaction/index.html.eex:14 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/transaction/index.html.eex:25 +#: lib/block_scout_web/templates/transaction/index.html.eex:25 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:14 #: lib/block_scout_web/templates/withdrawal/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Something went wrong, click to reload." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:225 #: lib/block_scout_web/templates/chain/show.html.eex:225 #, elixir-autogen, elixir-format msgid "Something went wrong, click to retry." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:6 #: lib/block_scout_web/templates/transaction/not_found.html.eex:6 #, elixir-autogen, elixir-format msgid "Sorry, we are unable to locate this transaction hash" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #, elixir-autogen, elixir-format msgid "Sources *.sol files" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #, elixir-autogen, elixir-format msgid "Sources *.sol or *.yul files" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 #, elixir-autogen, elixir-format msgid "Sources and Metadata JSON" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:136 #: lib/block_scout_web/templates/layout/_topnav.html.eex:136 #, elixir-autogen, elixir-format msgid "Stakes" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 #, elixir-autogen, elixir-format msgid "Standard Input JSON" msgstr "" +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/views/transaction_view.ex:555 #: lib/block_scout_web/views/transaction_view.ex:555 #, elixir-autogen, elixir-format msgid "State changes" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:24 #: lib/block_scout_web/views/internal_transaction_view.ex:24 #, elixir-autogen, elixir-format msgid "Static Call" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 #: lib/block_scout_web/templates/transaction/overview.html.eex:110 #, elixir-autogen, elixir-format msgid "Status" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 -#, elixir-autogen, elixir-format -msgid "Submission date" -msgstr "" - +#: lib/block_scout_web/templates/layout/_footer.html.eex:41 #: lib/block_scout_web/templates/layout/_footer.html.eex:41 #, elixir-autogen, elixir-format msgid "Submit an Issue" msgstr "" +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 #: lib/block_scout_web/views/transaction_view.ex:376 +#: lib/block_scout_web/views/transaction_view.ex:376 #, elixir-autogen, elixir-format msgid "Success" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #, elixir-autogen, elixir-format msgid "TX Fee" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 #: lib/block_scout_web/templates/layout/_footer.html.eex:31 #, elixir-autogen, elixir-format msgid "Telegram" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:67 #: lib/block_scout_web/templates/layout/_footer.html.eex:67 #, elixir-autogen, elixir-format msgid "Test Networks" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 #, elixir-autogen, elixir-format msgid "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #, elixir-autogen, elixir-format msgid "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:122 #: lib/block_scout_web/templates/block/overview.html.eex:122 #, elixir-autogen, elixir-format msgid "The SHA256 hash of the block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:51 #: lib/block_scout_web/templates/block/overview.html.eex:51 #, elixir-autogen, elixir-format msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:18 #: lib/block_scout_web/templates/transaction_state/index.html.eex:18 #, elixir-autogen, elixir-format msgid "The changes from this transaction have not yet happened since the transaction is still pending." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 #, elixir-autogen, elixir-format msgid "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #, elixir-autogen, elixir-format msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:138 #: lib/block_scout_web/templates/block/overview.html.eex:138 #, elixir-autogen, elixir-format msgid "The hash of the block from which this block was generated." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:74 #: lib/block_scout_web/templates/address/overview.html.eex:74 #, elixir-autogen, elixir-format msgid "The name found in the source code of the Contract." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:85 #: lib/block_scout_web/templates/address/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "The name of the validator." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:79 #: lib/block_scout_web/templates/block/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "The number of transactions in the block." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #, elixir-autogen, elixir-format msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:137 #: lib/block_scout_web/templates/transaction/overview.html.eex:137 #, elixir-autogen, elixir-format msgid "The revert reason of the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 #: lib/block_scout_web/templates/transaction/overview.html.eex:109 #, elixir-autogen, elixir-format msgid "The status of the transaction: Confirmed or Unconfirmed." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 #, elixir-autogen, elixir-format msgid "The total amount of tokens issued" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:179 #: lib/block_scout_web/templates/block/overview.html.eex:179 #, elixir-autogen, elixir-format msgid "The total gas amount used in the block and its percentage of gas filled in the block." msgstr "" +#: lib/block_scout_web/templates/address_validation/index.html.eex:16 #: lib/block_scout_web/templates/address_validation/index.html.eex:16 #, elixir-autogen, elixir-format msgid "There are no blocks validated by this address." msgstr "" +#: lib/block_scout_web/templates/block/index.html.eex:17 #: lib/block_scout_web/templates/block/index.html.eex:17 #, elixir-autogen, elixir-format msgid "There are no blocks." msgstr "" +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #, elixir-autogen, elixir-format msgid "There are no holders for this Token." msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 #, elixir-autogen, elixir-format msgid "There are no internal transactions for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 #, elixir-autogen, elixir-format msgid "There are no internal transactions for this transaction." msgstr "" +#: lib/block_scout_web/templates/address_logs/index.html.eex:28 #: lib/block_scout_web/templates/address_logs/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no logs for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_log/index.html.eex:20 #: lib/block_scout_web/templates/transaction_log/index.html.eex:20 #, elixir-autogen, elixir-format msgid "There are no logs for this transaction." msgstr "" +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 #, elixir-autogen, elixir-format msgid "There are no pending transactions." msgstr "" +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 #, elixir-autogen, elixir-format msgid "There are no token transfers for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 #, elixir-autogen, elixir-format msgid "There are no token transfers for this transaction" msgstr "" +#: lib/block_scout_web/templates/address_token/index.html.eex:65 #: lib/block_scout_web/templates/address_token/index.html.eex:65 #, elixir-autogen, elixir-format msgid "There are no tokens for this address." msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no tokens." msgstr "" +#: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #, elixir-autogen, elixir-format msgid "There are no transactions for this address." msgstr "" +#: lib/block_scout_web/templates/block_transaction/index.html.eex:19 #: lib/block_scout_web/templates/block_transaction/index.html.eex:19 #, elixir-autogen, elixir-format msgid "There are no transactions for this block." msgstr "" +#: lib/block_scout_web/templates/transaction/index.html.eex:31 #: lib/block_scout_web/templates/transaction/index.html.eex:31 #, elixir-autogen, elixir-format msgid "There are no transactions." msgstr "" +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #, elixir-autogen, elixir-format msgid "There are no transfers for this Token." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 #, elixir-autogen, elixir-format msgid "There are no verified contracts." msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 #, elixir-autogen, elixir-format msgid "There are no withdrawals for this address." msgstr "" +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 #, elixir-autogen, elixir-format msgid "There are no withdrawals for this block." msgstr "" +#: lib/block_scout_web/templates/withdrawal/index.html.eex:53 #: lib/block_scout_web/templates/withdrawal/index.html.eex:53 #, elixir-autogen, elixir-format msgid "There are no withdrawals." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 #, elixir-autogen, elixir-format msgid "There is no coin history for this address." msgstr "" #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/chain/show.html.eex:9 #: lib/block_scout_web/templates/chain/show.html.eex:9 #, elixir-autogen, elixir-format msgid "There was a problem loading the chart." msgstr "" +#: lib/block_scout_web/templates/api_docs/index.html.eex:6 #: lib/block_scout_web/templates/api_docs/index.html.eex:6 #, elixir-autogen, elixir-format msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #, elixir-autogen, elixir-format msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:11 #: lib/block_scout_web/views/block_transaction_view.ex:11 #, elixir-autogen, elixir-format msgid "This block has not been processed yet." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_contract/index.html.eex:47 #, elixir-autogen, elixir-format msgid "This contract has been partially verified via Sourcify." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 #: lib/block_scout_web/templates/address_contract/index.html.eex:51 #, elixir-autogen, elixir-format msgid "This contract has been verified via Sourcify." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 #, elixir-autogen, elixir-format msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." msgstr "" +#: lib/block_scout_web/templates/page_not_found/index.html.eex:8 #: lib/block_scout_web/templates/page_not_found/index.html.eex:8 #, elixir-autogen, elixir-format msgid "This page is no longer explorable! If you are lost, use the search bar to find what you are looking for." msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:22 #: lib/block_scout_web/templates/transaction_state/index.html.eex:22 #, elixir-autogen, elixir-format msgid "This transaction hasn't changed state." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #: lib/block_scout_web/templates/transaction/overview.html.eex:64 #, elixir-autogen, elixir-format msgid "This transaction is pending confirmation." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/transaction/overview.html.eex:180 #: lib/block_scout_web/templates/transaction/overview.html.eex:180 #, elixir-autogen, elixir-format msgid "Timestamp" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 #, elixir-autogen, elixir-format msgid "To" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 #, elixir-autogen, elixir-format msgid "To have guaranteed accuracy, use the link above to verify the contract's source code." msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 #, elixir-autogen, elixir-format msgid "To see accurate decoded input data, the contract must be verified." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:18 #: lib/block_scout_web/templates/layout/_topnav.html.eex:18 #, elixir-autogen, elixir-format msgid "Toggle navigation" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:55 #: lib/block_scout_web/templates/address/overview.html.eex:55 #: lib/block_scout_web/templates/tokens/index.html.eex:31 +#: lib/block_scout_web/templates/tokens/index.html.eex:31 #, elixir-autogen, elixir-format msgid "Token" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 #: lib/block_scout_web/views/transaction_view.ex:488 +#: lib/block_scout_web/views/transaction_view.ex:488 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 #: lib/block_scout_web/views/transaction_view.ex:489 +#: lib/block_scout_web/views/transaction_view.ex:489 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 #, elixir-autogen, elixir-format msgid "Token Details" msgstr "" +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 #: lib/block_scout_web/views/tokens/overview_view.ex:40 #, elixir-autogen, elixir-format msgid "Token Holders" msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 #, elixir-autogen, elixir-format msgid "Token ID" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 #: lib/block_scout_web/views/transaction_view.ex:487 +#: lib/block_scout_web/views/transaction_view.ex:487 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/views/transaction_view.ex:490 #: lib/block_scout_web/views/transaction_view.ex:490 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 +#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/transaction_view.ex:551 #: lib/block_scout_web/views/transaction_view.ex:551 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:54 #: lib/block_scout_web/templates/address/overview.html.eex:54 #, elixir-autogen, elixir-format msgid "Token name and symbol." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #, elixir-autogen, elixir-format msgid "Token type" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/overview.html.eex:177 #: lib/block_scout_web/templates/address/overview.html.eex:177 #: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/tokens/index.html.eex:10 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/views/address_view.ex:352 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 #: lib/block_scout_web/templates/transaction/overview.html.eex:336 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 #: lib/block_scout_web/templates/transaction/overview.html.eex:319 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 #: lib/block_scout_web/templates/transaction/overview.html.eex:303 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:13 #: lib/block_scout_web/templates/address/_metatags.html.eex:13 #, elixir-autogen, elixir-format msgid "Top Accounts - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 #: lib/block_scout_web/templates/address_logs/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Topic" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 #, elixir-autogen, elixir-format msgid "Topics" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 #, elixir-autogen, elixir-format msgid "Total" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:170 #: lib/block_scout_web/templates/block/overview.html.eex:170 #, elixir-autogen, elixir-format msgid "Total Difficulty" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:43 #: lib/block_scout_web/templates/tokens/index.html.eex:43 #, elixir-autogen, elixir-format msgid "Total Supply" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 #, elixir-autogen, elixir-format msgid "Total Supply * Price" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:133 #: lib/block_scout_web/templates/chain/show.html.eex:133 #, elixir-autogen, elixir-format msgid "Total blocks" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:169 #: lib/block_scout_web/templates/block/overview.html.eex:169 #, elixir-autogen, elixir-format msgid "Total difficulty of the chain until this block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:188 #: lib/block_scout_web/templates/block/overview.html.eex:188 #, elixir-autogen, elixir-format msgid "Total gas limit provided by all transactions in the block." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #, elixir-autogen, elixir-format msgid "Total supply" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 #: lib/block_scout_web/templates/transaction/overview.html.eex:383 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:112 #: lib/block_scout_web/templates/chain/show.html.eex:112 #, elixir-autogen, elixir-format msgid "Total transactions" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 #: lib/block_scout_web/views/transaction_view.ex:500 +#: lib/block_scout_web/views/transaction_view.ex:500 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "Transaction %{transaction} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 #, elixir-autogen, elixir-format msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 #: lib/block_scout_web/templates/transaction/overview.html.eex:225 #, elixir-autogen, elixir-format msgid "Transaction Action" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 #: lib/block_scout_web/templates/transaction/overview.html.eex:470 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:50 #: lib/block_scout_web/templates/transaction/overview.html.eex:50 #, elixir-autogen, elixir-format msgid "Transaction Details" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 #: lib/block_scout_web/templates/transaction/overview.html.eex:384 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 #: lib/block_scout_web/templates/transaction/overview.html.eex:80 #, elixir-autogen, elixir-format msgid "Transaction Hash" msgstr "" #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 #, elixir-autogen, elixir-format msgid "Transaction Inputs" msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 #, elixir-autogen, elixir-format msgid "Transaction Tags" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 #: lib/block_scout_web/templates/transaction/overview.html.eex:414 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 #: lib/block_scout_web/templates/transaction/overview.html.eex:534 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 #: lib/block_scout_web/templates/transaction/overview.html.eex:413 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:194 #: lib/block_scout_web/templates/address/overview.html.eex:194 #: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/chain/show.html.eex:216 +#: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:49 +#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/views/address_view.ex:354 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:101 #: lib/block_scout_web/templates/address/overview.html.eex:101 #, elixir-autogen, elixir-format msgid "Transactions and address of creation." msgstr "" #: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:221 #: lib/block_scout_web/templates/address/overview.html.eex:221 #: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 #, elixir-autogen, elixir-format msgid "Transfers" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 #, elixir-autogen, elixir-format msgid "Try it out" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "Try to fetch constructor arguments automatically" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:27 #: lib/block_scout_web/templates/layout/_footer.html.eex:27 #, elixir-autogen, elixir-format msgid "Twitter" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:53 #: lib/block_scout_web/templates/layout/app.html.eex:53 #, elixir-autogen, elixir-format msgid "Tx/day" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 #, elixir-autogen, elixir-format msgid "Txns" msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 #, elixir-autogen, elixir-format msgid "Type" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #, elixir-autogen, elixir-format msgid "Type of the token standard" msgstr "" +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 #: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 #, elixir-autogen, elixir-format msgid "UML diagram" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 #: lib/block_scout_web/templates/transaction/overview.html.eex:560 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" +#: lib/block_scout_web/views/block_view.ex:77 #: lib/block_scout_web/views/block_view.ex:77 #, elixir-autogen, elixir-format msgid "Uncle Reward" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:252 #: lib/block_scout_web/templates/block/overview.html.eex:252 #: lib/block_scout_web/templates/layout/_topnav.html.eex:41 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:41 #, elixir-autogen, elixir-format msgid "Uncles" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:367 #: lib/block_scout_web/views/transaction_view.ex:367 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 #, elixir-autogen, elixir-format msgid "Unique Token" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 #: lib/block_scout_web/templates/transaction/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "Unique character string (TxID) assigned to every verified transaction." msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #, elixir-autogen, elixir-format msgid "Update" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 #: lib/block_scout_web/templates/transaction/overview.html.eex:449 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 #: lib/block_scout_web/templates/transaction/overview.html.eex:459 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:226 #: lib/block_scout_web/templates/block/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "User-defined tips sent to validator for transaction priority/inclusion." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:56 #: lib/block_scout_web/templates/layout/_topnav.html.eex:56 #, elixir-autogen, elixir-format msgid "Validated" msgstr "" +#: lib/block_scout_web/templates/transaction/index.html.eex:12 #: lib/block_scout_web/templates/transaction/index.html.eex:12 #, elixir-autogen, elixir-format msgid "Validated Transactions" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 #, elixir-autogen, elixir-format msgid "Validator Creation Date" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 #, elixir-autogen, elixir-format msgid "Validator Data" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:86 #: lib/block_scout_web/templates/address/overview.html.eex:86 #, elixir-autogen, elixir-format msgid "Validator Name" msgstr "" #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 #: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 #, elixir-autogen, elixir-format msgid "Validator index" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 #: lib/block_scout_web/templates/transaction/overview.html.eex:369 #, elixir-autogen, elixir-format msgid "Value" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 #: lib/block_scout_web/templates/transaction/overview.html.eex:368 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 #, elixir-autogen, elixir-format msgid "Verified" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 #, elixir-autogen, elixir-format msgid "Verified Contracts" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 #: lib/block_scout_web/templates/address_contract/index.html.eex:88 #, elixir-autogen, elixir-format msgid "Verified at" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:69 #: lib/block_scout_web/templates/layout/_topnav.html.eex:69 #, elixir-autogen, elixir-format msgid "Verified contracts" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "Verified contracts - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 #, elixir-autogen, elixir-format msgid "Verified contracts, %{subnetwork}, %{coin}" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 #: lib/block_scout_web/templates/address_contract/index.html.eex:29 #: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 #, elixir-autogen, elixir-format msgid "Verify & publish" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 #, elixir-autogen, elixir-format msgid "Verify the contract " msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:93 #: lib/block_scout_web/templates/layout/_footer.html.eex:93 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 #, elixir-autogen, elixir-format msgid "Version" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 #, elixir-autogen, elixir-format msgid "Via Sourcify: Sources and metadata JSON file" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 #, elixir-autogen, elixir-format msgid "Via Standard Input JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Via flattened source code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 #, elixir-autogen, elixir-format msgid "Via multi-part files" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:155 #: lib/block_scout_web/templates/chain/show.html.eex:155 #, elixir-autogen, elixir-format msgid "View All Blocks" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:215 #: lib/block_scout_web/templates/chain/show.html.eex:215 #, elixir-autogen, elixir-format msgid "View All Transactions" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 #, elixir-autogen, elixir-format msgid "View Contract" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "View Less Transfers" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #, elixir-autogen, elixir-format msgid "View More Transfers" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:39 #, elixir-autogen, elixir-format msgid "View next block" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:23 #: lib/block_scout_web/templates/block/overview.html.eex:23 #, elixir-autogen, elixir-format msgid "View previous block" msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:9 #: lib/block_scout_web/templates/address/_metatags.html.eex:9 #, elixir-autogen, elixir-format msgid "View the account balance, transactions, and other data for %{address} on the %{network}" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 #, elixir-autogen, elixir-format msgid "View the beacon chain withdrawals on %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/block/_metatags.html.eex:10 #: lib/block_scout_web/templates/block/_metatags.html.eex:10 #, elixir-autogen, elixir-format msgid "View the transactions, token transfers, and uncles for block number %{block_number}" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 #, elixir-autogen, elixir-format msgid "View the verified contracts on %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 #, elixir-autogen, elixir-format msgid "View transaction %{transaction} on %{subnetwork}" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 #: lib/block_scout_web/views/verified_contracts_view.ex:11 +#: lib/block_scout_web/views/verified_contracts_view.ex:11 #, elixir-autogen, elixir-format msgid "Vyper" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 #, elixir-autogen, elixir-format msgid "Vyper contract" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 #, elixir-autogen, elixir-format msgid "WEI" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 #: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 #, elixir-autogen, elixir-format msgid "Waiting for transaction's confirmation..." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:141 #: lib/block_scout_web/templates/chain/show.html.eex:141 #, elixir-autogen, elixir-format msgid "Wallet addresses" msgstr "" +#: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 #: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 #, elixir-autogen, elixir-format msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 #, elixir-autogen, elixir-format msgid "Watch list" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" msgstr "" +#: lib/block_scout_web/views/wei_helper.ex:80 #: lib/block_scout_web/views/wei_helper.ex:80 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:29 #: lib/block_scout_web/templates/address/_tabs.html.eex:29 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:5 #: lib/block_scout_web/templates/withdrawal/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Withdrawals" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #, elixir-autogen, elixir-format msgid "Write" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:358 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 +#: lib/block_scout_web/views/address_view.ex:359 +#: lib/block_scout_web/views/address_view.ex:359 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:359 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 +#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/views/address_view.ex:360 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #, elixir-autogen, elixir-format msgid "You can create 3 API keys per account." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 #, elixir-autogen, elixir-format msgid "You can create up to 15 Custom ABIs per account." msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 -#, elixir-autogen, elixir-format -msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." -msgstr "" - +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have address tags yet" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have addresses on you watchlist yet" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have transaction tags yet" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 -#, elixir-autogen, elixir-format -msgid "Your name" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 -#, elixir-autogen, elixir-format -msgid "Your name*" -msgstr "" - +#: lib/block_scout_web/templates/error422/index.html.eex:8 #: lib/block_scout_web/templates/error422/index.html.eex:8 #, elixir-autogen, elixir-format msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 #: lib/block_scout_web/views/verified_contracts_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 #, elixir-autogen, elixir-format msgid "Yul" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:111 #: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "balance of the address" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:217 #: lib/block_scout_web/templates/block/overview.html.eex:217 #, elixir-autogen, elixir-format msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "button" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 #: lib/block_scout_web/templates/transaction/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "created" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 #, elixir-autogen, elixir-format msgid "custom RPC" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:151 #: lib/block_scout_web/templates/address/overview.html.eex:151 #, elixir-autogen, elixir-format msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "elements are displayed" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #, elixir-autogen, elixir-format msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:34 +#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:33 #, elixir-autogen, elixir-format msgid "false" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 #: lib/block_scout_web/templates/csv_export/index.html.eex:14 #, elixir-autogen, elixir-format msgid "for address" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 #, elixir-autogen, elixir-format msgid "here" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 #, elixir-autogen, elixir-format msgid "here." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 #: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 #, elixir-autogen, elixir-format msgid "method Response" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #, elixir-autogen, elixir-format msgid "of" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "on sign up. Didn’t receive?" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 #, elixir-autogen, elixir-format msgid "page" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #, elixir-autogen, elixir-format msgid "receive" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 #, elixir-autogen, elixir-format msgid "required" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #, elixir-autogen, elixir-format msgid "string" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 #: lib/block_scout_web/templates/csv_export/index.html.eex:17 #, elixir-autogen, elixir-format msgid "to CSV file" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:32 +#: lib/block_scout_web/views/address_contract_view.ex:32 #, elixir-autogen, elixir-format msgid "true" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "truffle flattener" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 5dbb1f993aaf..c09d63d939b2 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -11,47 +11,57 @@ msgstr "" "Language: en\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:9 #, elixir-autogen, elixir-format msgid " - minimal bytecode implementation that delegates all calls to a known address" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:14 #, elixir-autogen, elixir-format msgid " is recommended." msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:3 #: lib/block_scout_web/templates/address/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "%{address} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:12 #: lib/block_scout_web/templates/block/overview.html.eex:12 #, elixir-autogen, elixir-format msgid "%{block_type} Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:55 #: lib/block_scout_web/templates/block/overview.html.eex:55 #, elixir-autogen, elixir-format msgid "%{block_type} Height" msgstr "" +#: lib/block_scout_web/templates/block/index.html.eex:7 #: lib/block_scout_web/templates/block/index.html.eex:7 #, elixir-autogen, elixir-format msgid "%{block_type}s" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:85 #: lib/block_scout_web/templates/block/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "%{count} Transaction" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:87 #: lib/block_scout_web/templates/block/overview.html.eex:87 #: lib/block_scout_web/templates/chain/_block.html.eex:11 +#: lib/block_scout_web/templates/chain/_block.html.eex:11 #, elixir-autogen, elixir-format msgid "%{count} Transactions" msgstr "" +#: lib/block_scout_web/views/address_token_balance_view.ex:10 #: lib/block_scout_web/views/address_token_balance_view.ex:10 #, elixir-autogen, elixir-format msgid "%{count} token" @@ -59,6 +69,7 @@ msgid_plural "%{count} tokens" msgstr[0] "" msgstr[1] "" +#: lib/block_scout_web/templates/block/_tile.html.eex:29 #: lib/block_scout_web/templates/block/_tile.html.eex:29 #, elixir-autogen, elixir-format msgid "%{count} transaction" @@ -66,825 +77,1050 @@ msgid_plural "%{count} transactions" msgstr[0] "" msgstr[1] "" +#: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #: lib/block_scout_web/templates/transaction/_actions.html.eex:101 #, elixir-autogen, elixir-format msgid "%{qty} of Token ID [%{link_to_id}]" msgstr "" +#: lib/block_scout_web/templates/chain/_metatags.html.eex:2 #: lib/block_scout_web/templates/chain/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "%{subnetwork} %{network} Explorer" msgstr "" +#: lib/block_scout_web/templates/layout/_default_title.html.eex:2 #: lib/block_scout_web/templates/layout/_default_title.html.eex:2 #, elixir-autogen, elixir-format msgid "%{subnetwork} Explorer - BlockScout" msgstr "" +#: lib/block_scout_web/templates/withdrawal/index.html.eex:11 #: lib/block_scout_web/templates/withdrawal/index.html.eex:11 #, elixir-autogen, elixir-format msgid "%{withdrawals_count} withdrawals processed and %{withdrawals_sum} withdrawn." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:375 #: lib/block_scout_web/views/transaction_view.ex:375 #, elixir-autogen, elixir-format msgid "(Awaiting internal transactions for status)" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:82 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:104 #, elixir-autogen, elixir-format msgid "(query)" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #, elixir-autogen, elixir-format msgid ") may be added for each contract. Click the Add Library button to add an additional one." msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:93 #: lib/block_scout_web/templates/layout/app.html.eex:93 #, elixir-autogen, elixir-format msgid "- We're indexing this chain right now. Some of the counts may be inaccurate." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:8 #: lib/block_scout_web/templates/transaction/not_found.html.eex:8 #, elixir-autogen, elixir-format msgid "1. If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:9 #: lib/block_scout_web/templates/transaction/not_found.html.eex:9 #, elixir-autogen, elixir-format msgid "2. It could still be in the TX Pool of a different node, waiting to be broadcasted." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:10 #: lib/block_scout_web/templates/transaction/not_found.html.eex:10 #, elixir-autogen, elixir-format msgid "3. During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:11 #: lib/block_scout_web/templates/transaction/not_found.html.eex:11 #, elixir-autogen, elixir-format msgid "4. If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:197 #: lib/block_scout_web/templates/block/overview.html.eex:197 #, elixir-autogen, elixir-format msgid "64-bit hash of value verifying proof-of-work (note: null for POA chains)." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:97 #: lib/block_scout_web/templates/block/overview.html.eex:97 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:21 #, elixir-autogen, elixir-format msgid "A block producer who successfully included the block onto the blockchain." msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "A confirmation email was sent to" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:4 #, elixir-autogen, elixir-format msgid "A library name called in the .sol file. Multiple libraries (up to " msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "A string with the name of the action to be invoked." msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:62 #, elixir-autogen, elixir-format msgid "A string with the name of the module to be invoked." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24 #, elixir-autogen, elixir-format msgid "ABI" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "ABI-encoded Constructor Arguments (if required by the contract)" msgstr "" +#: lib/block_scout_web/templates/api_docs/index.html.eex:4 #: lib/block_scout_web/templates/api_docs/index.html.eex:4 #, elixir-autogen, elixir-format msgid "API Documentation" msgstr "" +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:4 #, elixir-autogen, elixir-format msgid "API endpoints for the %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 #: lib/block_scout_web/templates/api_docs/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "API for the %{subnetwork} - BlockScout" msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:13 #: lib/block_scout_web/templates/account/api_key/form.html.eex:13 #: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:14 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:29 #: lib/block_scout_web/templates/account/api_key/index.html.eex:29 #, elixir-autogen, elixir-format msgid "API key" msgstr "" #: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:16 #: lib/block_scout_web/templates/account/common/_nav.html.eex:16 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18 #, elixir-autogen, elixir-format msgid "API keys" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:106 #: lib/block_scout_web/templates/layout/_topnav.html.eex:106 #, elixir-autogen, elixir-format msgid "APIs" msgstr "" #: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 #, elixir-autogen, elixir-format msgid "Action" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:25 #, elixir-autogen, elixir-format msgid "Actions" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:484 #: lib/block_scout_web/templates/transaction/overview.html.eex:484 #, elixir-autogen, elixir-format msgid "Actual gas amount used by the transaction on L2." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:488 #: lib/block_scout_web/templates/transaction/overview.html.eex:488 #, elixir-autogen, elixir-format, fuzzy msgid "Actual gas amount used by the transaction." msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 +#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:10 #, elixir-autogen, elixir-format msgid "Add" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:44 #: lib/block_scout_web/templates/account/api_key/index.html.eex:44 #, elixir-autogen, elixir-format msgid "Add API key" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:86 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:76 #, elixir-autogen, elixir-format msgid "Add Contract Libraries" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44 #, elixir-autogen, elixir-format msgid "Add Custom ABI" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:97 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:87 #, elixir-autogen, elixir-format msgid "Add Library" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:38 #, elixir-autogen, elixir-format msgid "Add address" msgstr "" #: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Add address tag" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #, elixir-autogen, elixir-format msgid "Add address to the Watch list" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Add transaction tag" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:23 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12 +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/tokens/index.html.eex:34 #: lib/block_scout_web/templates/tokens/index.html.eex:34 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:29 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/transaction_state/index.html.eex:34 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 -#: lib/block_scout_web/views/address_view.ex:109 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:60 +#: lib/block_scout_web/views/address_view.ex:110 +#: lib/block_scout_web/views/address_view.ex:110 #, elixir-autogen, elixir-format msgid "Address" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:263 #: lib/block_scout_web/templates/transaction/overview.html.eex:263 #, elixir-autogen, elixir-format msgid "Address (external or contract) receiving the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:245 #: lib/block_scout_web/templates/transaction/overview.html.eex:245 #, elixir-autogen, elixir-format msgid "Address (external or contract) sending the transaction." msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:10 #: lib/block_scout_web/templates/account/common/_nav.html.eex:10 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16 #, elixir-autogen, elixir-format msgid "Address Tags" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:151 #: lib/block_scout_web/templates/address/overview.html.eex:151 #, elixir-autogen, elixir-format msgid "Address balance in" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51 #, elixir-autogen, elixir-format msgid "Address of the token contract" msgstr "" +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:7 #, elixir-autogen, elixir-format msgid "Address used in token mintings and burnings." msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2 -#, elixir-autogen, elixir-format -msgid "Address*" -msgstr "" - +#: lib/block_scout_web/templates/address/index.html.eex:5 #: lib/block_scout_web/templates/address/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Addresses" msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:35 #, elixir-autogen, elixir-format msgid "Age" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:26 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:28 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:22 #: lib/block_scout_web/templates/address_transaction/index.html.eex:22 #: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:88 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:20 #: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:12 +#: lib/block_scout_web/views/address_token_transfer_view.ex:12 #: lib/block_scout_web/views/address_token_transfer_view.ex:12 #: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/address_transaction_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:13 #: lib/block_scout_web/views/verified_contracts_view.ex:13 #, elixir-autogen, elixir-format msgid "All" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:13 #, elixir-autogen, elixir-format msgid "All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "All metadata displayed below is from that contract. In order to verify current contract, click" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:176 #: lib/block_scout_web/templates/address/overview.html.eex:176 #, elixir-autogen, elixir-format msgid "All tokens in the account and total value." msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:41 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:38 #: lib/block_scout_web/templates/withdrawal/index.html.eex:38 #, elixir-autogen, elixir-format, fuzzy msgid "Amount" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "Amount of" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:238 #: lib/block_scout_web/templates/block/overview.html.eex:238 #, elixir-autogen, elixir-format msgid "Amount of distributed reward. Miners receive a static block reward + Tx fees + uncle fees." msgstr "" +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:8 #, elixir-autogen, elixir-format msgid "An unexpected error has occurred. Try reloading the page, or come back soon and try again." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:15 #, elixir-autogen, elixir-format msgid "Anything not in this list is not supported. Click on the method to be taken to the documentation for that method, and check the notes section for any potential differences." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:134 #: lib/block_scout_web/templates/layout/_topnav.html.eex:134 #, elixir-autogen, elixir-format msgid "Apps" msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 #, elixir-autogen, elixir-format msgid "Average" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:102 #: lib/block_scout_web/templates/chain/show.html.eex:102 #, elixir-autogen, elixir-format msgid "Average block time" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #: lib/block_scout_web/templates/account/api_key/form.html.eex:25 #, elixir-autogen, elixir-format msgid "Back to API keys (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Back to Address Tags (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30 #, elixir-autogen, elixir-format msgid "Back to Custom ABI (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Back to Transaction Tags (Cancel)" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81 #, elixir-autogen, elixir-format msgid "Back to Watch list (Cancel)" msgstr "" #: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/error422/index.html.eex:9 +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:9 #: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/page_not_found/index.html.eex:9 +#: lib/block_scout_web/templates/transaction/not_found.html.eex:13 #: lib/block_scout_web/templates/transaction/not_found.html.eex:13 #, elixir-autogen, elixir-format msgid "Back to home" msgstr "" #: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24 +#: lib/block_scout_web/templates/address/overview.html.eex:152 #: lib/block_scout_web/templates/address/overview.html.eex:152 #: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/address_token/overview.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Balance" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:40 #: lib/block_scout_web/templates/transaction_state/index.html.eex:40 #, elixir-autogen, elixir-format msgid "Balance after" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:37 #: lib/block_scout_web/templates/transaction_state/index.html.eex:37 #, elixir-autogen, elixir-format msgid "Balance before" msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Balances" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:209 #: lib/block_scout_web/templates/block/overview.html.eex:209 #, elixir-autogen, elixir-format msgid "Base Fee per Gas" msgstr "" #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:5 +#: lib/block_scout_web/templates/api_docs/index.html.eex:5 #: lib/block_scout_web/templates/api_docs/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Base URL:" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "Beacon chain withdrawals - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:7 #, elixir-autogen, elixir-format msgid "Beacon chain, Withdrawals, %{subnetwork}, %{coin}" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:545 #: lib/block_scout_web/templates/transaction/overview.html.eex:545 #, elixir-autogen, elixir-format msgid "Binary data included with the transaction. See input / logs below for additional info." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:35 +#: lib/block_scout_web/templates/block/overview.html.eex:29 #: lib/block_scout_web/templates/block/overview.html.eex:29 #: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/transaction/overview.html.eex:161 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/withdrawal/index.html.eex:29 #, elixir-autogen, elixir-format msgid "Block" msgstr "" #: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/block/_link.html.eex:2 +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:32 #: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 +#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:43 #, elixir-autogen, elixir-format msgid "Block #%{number}" msgstr "" +#: lib/block_scout_web/templates/block/_metatags.html.eex:3 #: lib/block_scout_web/templates/block/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "Block %{block_number} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/block_transaction/404.html.eex:7 #: lib/block_scout_web/templates/block_transaction/404.html.eex:7 #, elixir-autogen, elixir-format msgid "Block Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:53 #: lib/block_scout_web/templates/block/overview.html.eex:53 #, elixir-autogen, elixir-format msgid "Block Height" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:47 #: lib/block_scout_web/templates/layout/app.html.eex:47 #, elixir-autogen, elixir-format msgid "Block Mined, awaiting import..." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:34 #: lib/block_scout_web/views/transaction_view.ex:34 #, elixir-autogen, elixir-format msgid "Block Pending" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:158 #: lib/block_scout_web/templates/block/overview.html.eex:158 #, elixir-autogen, elixir-format msgid "Block difficulty for miner, used to calibrate block generation time (Note: constant in POA based networks)." msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:15 #: lib/block_scout_web/views/block_transaction_view.ex:15 #, elixir-autogen, elixir-format msgid "Block not found, please try again later." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:203 #: lib/block_scout_web/templates/transaction/overview.html.eex:203 #, elixir-autogen, elixir-format, fuzzy msgid "Block number containing the transaction on L1." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:160 #: lib/block_scout_web/templates/transaction/overview.html.eex:160 #, elixir-autogen, elixir-format msgid "Block number containing the transaction." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:259 #: lib/block_scout_web/templates/address/overview.html.eex:259 #, elixir-autogen, elixir-format msgid "Block number in which the address was updated." msgstr "" +#: lib/block_scout_web/templates/chain/_metatags.html.eex:4 #: lib/block_scout_web/templates/chain/_metatags.html.eex:4 #, elixir-autogen, elixir-format msgid "BlockScout provides analytics data, API, and Smart Contract tools for the %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:29 #: lib/block_scout_web/templates/layout/_topnav.html.eex:29 #, elixir-autogen, elixir-format msgid "Blockchain" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/chain/show.html.eex:156 #: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:34 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #: lib/block_scout_web/templates/layout/_topnav.html.eex:38 #, elixir-autogen, elixir-format msgid "Blocks" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:46 #: lib/block_scout_web/templates/layout/app.html.eex:46 #, elixir-autogen, elixir-format msgid "Blocks Indexed" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/_tabs.html.eex:56 #: lib/block_scout_web/templates/address/overview.html.eex:277 +#: lib/block_scout_web/templates/address/overview.html.eex:277 #: lib/block_scout_web/templates/address_validation/index.html.eex:11 -#: lib/block_scout_web/views/address_view.ex:361 +#: lib/block_scout_web/templates/address_validation/index.html.eex:11 +#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/views/address_view.ex:362 #, elixir-autogen, elixir-format msgid "Blocks Validated" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:48 #: lib/block_scout_web/templates/layout/app.html.eex:48 #, elixir-autogen, elixir-format msgid "Blocks With Internal Transactions Indexed" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:22 #: lib/block_scout_web/templates/layout/_footer.html.eex:22 #, elixir-autogen, elixir-format msgid "Blockscout is a tool for inspecting and analyzing EVM based blockchains. Blockchain explorer for Ethereum Networks." msgstr "" +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:8 #, elixir-autogen, elixir-format msgid "Burn address" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/_tile.html.eex:64 +#: lib/block_scout_web/templates/block/overview.html.eex:218 #: lib/block_scout_web/templates/block/overview.html.eex:218 #, elixir-autogen, elixir-format msgid "Burnt Fees" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:65 #: lib/block_scout_web/templates/address_token/overview.html.eex:65 #, elixir-autogen, elixir-format msgid "CRC Worth" msgstr "" +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 #: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 #, elixir-autogen, elixir-format msgid "CSV" msgstr "" #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/views/internal_transaction_view.ex:21 #: lib/block_scout_web/views/internal_transaction_view.ex:21 #, elixir-autogen, elixir-format msgid "Call" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:22 #: lib/block_scout_web/views/internal_transaction_view.ex:22 #, elixir-autogen, elixir-format msgid "Call Code" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:115 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:107 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:55 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:51 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:47 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:54 #, elixir-autogen, elixir-format msgid "Cancel" msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:43 #: lib/block_scout_web/templates/transaction_state/index.html.eex:43 #, elixir-autogen, elixir-format msgid "Change" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:43 #: lib/block_scout_web/templates/layout/_footer.html.eex:43 #, elixir-autogen, elixir-format msgid "Chat (#blockscout)" msgstr "" +#: lib/block_scout_web/views/block_view.ex:65 #: lib/block_scout_web/views/block_view.ex:65 #, elixir-autogen, elixir-format msgid "Chore Reward" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:38 #: lib/block_scout_web/templates/tokens/index.html.eex:38 #, elixir-autogen, elixir-format msgid "Circulating Market Cap" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:137 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:106 #, elixir-autogen, elixir-format msgid "Clear" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:6 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:14 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:84 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:92 #, elixir-autogen, elixir-format msgid "Close" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/address/_tabs.html.eex:66 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 -#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149 +#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/views/address_view.ex:356 #, elixir-autogen, elixir-format msgid "Code" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:42 -#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 +#: lib/block_scout_web/views/address_view.ex:361 +#: lib/block_scout_web/views/address_view.ex:361 #, elixir-autogen, elixir-format msgid "Coin Balance History" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #, elixir-autogen, elixir-format msgid "Collapse" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Company name" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32 -#, elixir-autogen, elixir-format -msgid "Company website" -msgstr "" - +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:69 #, elixir-autogen, elixir-format msgid "Compiler" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:142 #: lib/block_scout_web/templates/address_contract/index.html.eex:142 #, elixir-autogen, elixir-format msgid "Compiler Settings" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 #: lib/block_scout_web/templates/address_contract/index.html.eex:71 #, elixir-autogen, elixir-format msgid "Compiler version" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:368 #: lib/block_scout_web/views/transaction_view.ex:368 #, elixir-autogen, elixir-format msgid "Confirmed" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:127 #: lib/block_scout_web/templates/transaction/overview.html.eex:127 #, elixir-autogen, elixir-format msgid "Confirmed by " msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:193 #: lib/block_scout_web/templates/transaction/overview.html.eex:193 #, elixir-autogen, elixir-format msgid "Confirmed within" msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:6 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:2 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16 #, elixir-autogen, elixir-format msgid "Connection Lost" msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:12 #: lib/block_scout_web/templates/block/index.html.eex:5 +#: lib/block_scout_web/templates/block/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer blocks" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:15 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer internal transactions" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:11 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:16 #: lib/block_scout_web/templates/transaction/index.html.eex:22 +#: lib/block_scout_web/templates/transaction/index.html.eex:22 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer transactions" msgstr "" +#: lib/block_scout_web/templates/address_validation/index.html.eex:10 #: lib/block_scout_web/templates/address_validation/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Connection Lost, click to load newer validations" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:96 #: lib/block_scout_web/templates/address_contract/index.html.eex:96 #, elixir-autogen, elixir-format msgid "Constructor Arguments" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:78 #, elixir-autogen, elixir-format msgid "Constructor args" msgstr "" #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/transaction/overview.html.eex:273 #: lib/block_scout_web/templates/transaction/overview.html.eex:273 #, elixir-autogen, elixir-format msgid "Contract" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:157 #: lib/block_scout_web/templates/address_contract/index.html.eex:157 #, elixir-autogen, elixir-format msgid "Contract ABI" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3 -#: lib/block_scout_web/views/address_view.ex:107 +#: lib/block_scout_web/views/address_view.ex:108 +#: lib/block_scout_web/views/address_view.ex:108 #, elixir-autogen, elixir-format msgid "Contract Address" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 -#: lib/block_scout_web/views/address_view.ex:47 -#: lib/block_scout_web/views/address_view.ex:81 +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16 +#: lib/block_scout_web/views/address_view.ex:48 +#: lib/block_scout_web/views/address_view.ex:48 +#: lib/block_scout_web/views/address_view.ex:82 +#: lib/block_scout_web/views/address_view.ex:82 #, elixir-autogen, elixir-format msgid "Contract Address Pending" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:497 #: lib/block_scout_web/views/transaction_view.ex:497 #, elixir-autogen, elixir-format msgid "Contract Call" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:494 #: lib/block_scout_web/views/transaction_view.ex:494 #, elixir-autogen, elixir-format msgid "Contract Creation" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:174 #: lib/block_scout_web/templates/address_contract/index.html.eex:174 #: lib/block_scout_web/templates/address_contract/index.html.eex:189 +#: lib/block_scout_web/templates/address_contract/index.html.eex:189 #, elixir-autogen, elixir-format msgid "Contract Creation Code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:90 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:80 #, elixir-autogen, elixir-format msgid "Contract Libraries" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address/overview.html.eex:75 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Contract Name" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_contract/index.html.eex:25 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:11 #, elixir-autogen, elixir-format msgid "Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:47 #, elixir-autogen, elixir-format msgid "Contract name or address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:63 #: lib/block_scout_web/templates/address_contract/index.html.eex:63 #, elixir-autogen, elixir-format msgid "Contract name:" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:106 #: lib/block_scout_web/templates/address_contract/index.html.eex:106 #, elixir-autogen, elixir-format msgid "Contract source code" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:120 #: lib/block_scout_web/templates/address/overview.html.eex:120 #, elixir-autogen, elixir-format msgid "Contract was precompiled and created at genesis or contract creation transaction is missing" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:5 #, elixir-autogen, elixir-format msgid "Contracts" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:180 #: lib/block_scout_web/templates/address_contract/index.html.eex:180 #, elixir-autogen, elixir-format msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:42 #: lib/block_scout_web/templates/layout/_footer.html.eex:42 #, elixir-autogen, elixir-format msgid "Contribute" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:159 #: lib/block_scout_web/templates/address_contract/index.html.eex:159 #, elixir-autogen, elixir-format msgid "Copy ABI" msgstr "" +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #: lib/block_scout_web/templates/account/api_key/row.html.eex:6 #, elixir-autogen, elixir-format @@ -893,2843 +1129,3587 @@ msgstr "" #: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 #: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:38 +#: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/address/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:104 +#: lib/block_scout_web/templates/block/overview.html.eex:105 #: lib/block_scout_web/templates/block/overview.html.eex:105 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44 #, elixir-autogen, elixir-format msgid "Copy Address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:144 #: lib/block_scout_web/templates/address_contract/index.html.eex:144 #, elixir-autogen, elixir-format msgid "Copy Compiler Settings" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6 #, elixir-autogen, elixir-format msgid "Copy Contract Address" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:176 #: lib/block_scout_web/templates/address_contract/index.html.eex:176 #: lib/block_scout_web/templates/address_contract/index.html.eex:192 +#: lib/block_scout_web/templates/address_contract/index.html.eex:192 #, elixir-autogen, elixir-format msgid "Copy Contract Creation Code" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:213 #: lib/block_scout_web/templates/address_contract/index.html.eex:213 #: lib/block_scout_web/templates/address_contract/index.html.eex:223 +#: lib/block_scout_web/templates/address_contract/index.html.eex:223 #, elixir-autogen, elixir-format msgid "Copy Deployed ByteCode" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18 #: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:253 +#: lib/block_scout_web/templates/transaction/overview.html.eex:254 #: lib/block_scout_web/templates/transaction/overview.html.eex:254 #, elixir-autogen, elixir-format msgid "Copy From Address" msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:129 +#: lib/block_scout_web/templates/block/overview.html.eex:130 #: lib/block_scout_web/templates/block/overview.html.eex:130 #, elixir-autogen, elixir-format msgid "Copy Hash" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20 #, elixir-autogen, elixir-format msgid "Copy Metadata" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:149 #: lib/block_scout_web/templates/block/overview.html.eex:149 #: lib/block_scout_web/templates/block/overview.html.eex:150 +#: lib/block_scout_web/templates/block/overview.html.eex:150 #, elixir-autogen, elixir-format msgid "Copy Parent Hash" msgstr "" +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:9 #, elixir-autogen, elixir-format msgid "Copy Raw Trace" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:120 +#: lib/block_scout_web/templates/address_contract/index.html.eex:132 #: lib/block_scout_web/templates/address_contract/index.html.eex:132 #, elixir-autogen, elixir-format msgid "Copy Source Code" msgstr "" #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34 +#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35 #: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:280 +#: lib/block_scout_web/templates/transaction/overview.html.eex:281 #: lib/block_scout_web/templates/transaction/overview.html.eex:281 #: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:288 +#: lib/block_scout_web/templates/transaction/overview.html.eex:289 #: lib/block_scout_web/templates/transaction/overview.html.eex:289 #, elixir-autogen, elixir-format msgid "Copy To Address" msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:32 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:33 #, elixir-autogen, elixir-format msgid "Copy Token ID" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 #: lib/block_scout_web/templates/transaction/overview.html.eex:87 #, elixir-autogen, elixir-format msgid "Copy Transaction Hash" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:88 #: lib/block_scout_web/templates/transaction/overview.html.eex:88 #, elixir-autogen, elixir-format msgid "Copy Txn Hash" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:571 #: lib/block_scout_web/templates/transaction/overview.html.eex:571 #, elixir-autogen, elixir-format msgid "Copy Txn Hex Input" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:577 #: lib/block_scout_web/templates/transaction/overview.html.eex:577 #, elixir-autogen, elixir-format msgid "Copy Txn UTF-8 Input" msgstr "" #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41 #: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:570 +#: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction/overview.html.eex:576 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:8 #, elixir-autogen, elixir-format msgid "Copy Value" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:26 #: lib/block_scout_web/views/internal_transaction_view.ex:26 #, elixir-autogen, elixir-format msgid "Create" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12 #, elixir-autogen, elixir-format msgid "Create a Custom ABI to interact with contracts." msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #, elixir-autogen, elixir-format, fuzzy msgid "Create an API key to use with your RPC and EthRPC API requests." msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:27 #: lib/block_scout_web/views/internal_transaction_view.ex:27 #, elixir-autogen, elixir-format msgid "Create2" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:102 #: lib/block_scout_web/templates/address/overview.html.eex:102 #, elixir-autogen, elixir-format msgid "Creator" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:146 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:116 #, elixir-autogen, elixir-format msgid "Curl" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:97 #: lib/block_scout_web/templates/transaction/overview.html.eex:97 #, elixir-autogen, elixir-format msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:20 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Custom" msgstr "" +#: lib/block_scout_web/templates/account/common/_nav.html.eex:19 #: lib/block_scout_web/templates/account/common/_nav.html.eex:19 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19 #, elixir-autogen, elixir-format msgid "Custom ABI" msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:25 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23 #, elixir-autogen, elixir-format msgid "Custom ABI from account" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:70 #: lib/block_scout_web/templates/chain/show.html.eex:70 #, elixir-autogen, elixir-format msgid "Daily Transactions" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:98 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:7 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:23 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:130 #, elixir-autogen, elixir-format msgid "Data" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:70 #: lib/block_scout_web/templates/block/overview.html.eex:70 #, elixir-autogen, elixir-format msgid "Date & time at which block was produced." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:179 #: lib/block_scout_web/templates/transaction/overview.html.eex:179 #, elixir-autogen, elixir-format msgid "Date & time of transaction inclusion, including length of time for confirmation." msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131 #, elixir-autogen, elixir-format msgid "Decimals" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:32 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:38 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:53 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:43 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:51 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:66 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:82 #, elixir-autogen, elixir-format msgid "Decoded" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:23 #: lib/block_scout_web/views/internal_transaction_view.ex:23 #, elixir-autogen, elixir-format msgid "Delegate Call" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:211 +#: lib/block_scout_web/templates/address_contract/index.html.eex:219 #: lib/block_scout_web/templates/address_contract/index.html.eex:219 #, elixir-autogen, elixir-format msgid "Deployed ByteCode" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:53 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:188 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:60 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:150 #, elixir-autogen, elixir-format msgid "Description" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56 -#, elixir-autogen, elixir-format -msgid "Description*" -msgstr "" - +#: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/address/overview.html.eex:30 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127 #, elixir-autogen, elixir-format msgid "Details" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:159 #: lib/block_scout_web/templates/block/overview.html.eex:159 #, elixir-autogen, elixir-format msgid "Difficulty" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:181 #: lib/block_scout_web/templates/address_contract/index.html.eex:181 #, elixir-autogen, elixir-format msgid "Displaying the init data provided of the creating transaction." msgstr "" #: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/common_components/_csv_export_button.html.eex:4 +#: lib/block_scout_web/templates/csv_export/index.html.eex:33 #: lib/block_scout_web/templates/csv_export/index.html.eex:33 #, elixir-autogen, elixir-format msgid "Download" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Drop all Solidity contract source files into the drop zone." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Drop all Solidity or Yul contract source files into the drop zone." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:18 #, elixir-autogen, elixir-format msgid "Drop sources and metadata JSON file or click here" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:67 #, elixir-autogen, elixir-format msgid "Drop sources or click here" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:28 #, elixir-autogen, elixir-format msgid "Drop the standard input JSON file or click here" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27 -#, elixir-autogen, elixir-format -msgid "E-mail*" -msgstr "" - +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6 #, elixir-autogen, elixir-format msgid "EIP-1167" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:225 #: lib/block_scout_web/views/transaction_view.ex:225 #, elixir-autogen, elixir-format msgid "ERC-1155 " msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:223 #: lib/block_scout_web/views/transaction_view.ex:223 #, elixir-autogen, elixir-format msgid "ERC-20 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40 #, elixir-autogen, elixir-format msgid "ERC-20 tokens (beta)" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:226 #: lib/block_scout_web/views/transaction_view.ex:226 #, elixir-autogen, elixir-format, fuzzy msgid "ERC-404 " msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:224 #: lib/block_scout_web/views/transaction_view.ex:224 #, elixir-autogen, elixir-format msgid "ERC-721 " msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53 #, elixir-autogen, elixir-format msgid "ERC-721, ERC-1155 tokens (NFT) (beta)" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:4 #, elixir-autogen, elixir-format msgid "ETH RPC API Documentation" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract/index.html.eex:82 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:30 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:22 #, elixir-autogen, elixir-format msgid "EVM Version" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #, elixir-autogen, elixir-format msgid "EVM version details" msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:7 #: lib/block_scout_web/views/block_transaction_view.ex:7 #, elixir-autogen, elixir-format msgid "Easy Cowboy! This block does not exist yet!" msgstr "" #: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:16 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16 #: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 +#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27 #, elixir-autogen, elixir-format msgid "Edit" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7 #, elixir-autogen, elixir-format msgid "Edit Watch list address" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71 #, elixir-autogen, elixir-format msgid "Email notifications" msgstr "" +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5 #, elixir-autogen, elixir-format msgid "Emission Contract" msgstr "" +#: lib/block_scout_web/views/block_view.ex:73 #: lib/block_scout_web/views/block_view.ex:73 #, elixir-autogen, elixir-format msgid "Emission Reward" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:72 #, elixir-autogen, elixir-format msgid "Enter the Solidity Contract Code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Enter the Vyper Contract Code" msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:10 #, elixir-autogen, elixir-format msgid "Error" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #: lib/block_scout_web/templates/transaction/_tile.html.eex:11 #, elixir-autogen, elixir-format msgid "Error in internal transactions" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:33 #, elixir-autogen, elixir-format msgid "Error rendering value" msgstr "" +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 #: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:10 #, elixir-autogen, elixir-format msgid "Error trying to fetch balances." msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:379 #: lib/block_scout_web/views/transaction_view.ex:379 #, elixir-autogen, elixir-format msgid "Error: %{reason}" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:377 #: lib/block_scout_web/views/transaction_view.ex:377 #, elixir-autogen, elixir-format msgid "Error: (Awaiting internal transactions for reason)" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:120 #: lib/block_scout_web/templates/address/overview.html.eex:120 #, elixir-autogen, elixir-format msgid "Error: Could not determine contract creator." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:120 #: lib/block_scout_web/templates/layout/_topnav.html.eex:120 #, elixir-autogen, elixir-format msgid "Eth RPC" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:211 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:164 #, elixir-autogen, elixir-format msgid "Example Value" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:128 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:99 #, elixir-autogen, elixir-format msgid "Execute" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:54 #, elixir-autogen, elixir-format msgid "Expand" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 #: lib/block_scout_web/templates/csv_export/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Export" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:10 #: lib/block_scout_web/templates/csv_export/index.html.eex:10 #, elixir-autogen, elixir-format msgid "Export Data" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:248 #: lib/block_scout_web/templates/address_contract/index.html.eex:248 #, elixir-autogen, elixir-format msgid "External libraries" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:40 #, elixir-autogen, elixir-format msgid "Failed to decode input data." msgstr "" +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:35 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:46 #, elixir-autogen, elixir-format msgid "Failed to decode log data." msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #, elixir-autogen, elixir-format msgid "Fast" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:249 #: lib/block_scout_web/templates/address/overview.html.eex:249 #, elixir-autogen, elixir-format msgid "Fetching gas used..." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112 #, elixir-autogen, elixir-format msgid "Fetching holders..." msgstr "" +#: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 #: lib/block_scout_web/templates/address/_balance_dropdown.html.eex:7 #, elixir-autogen, elixir-format msgid "Fetching tokens..." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:196 #: lib/block_scout_web/templates/address/overview.html.eex:196 #: lib/block_scout_web/templates/address/overview.html.eex:204 +#: lib/block_scout_web/templates/address/overview.html.eex:204 #, elixir-autogen, elixir-format msgid "Fetching transactions..." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:223 #: lib/block_scout_web/templates/address/overview.html.eex:223 #: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/address/overview.html.eex:231 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123 #, elixir-autogen, elixir-format msgid "Fetching transfers..." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:15 #, elixir-autogen, elixir-format msgid "Filter by compiler:" msgstr "" +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 #: lib/block_scout_web/templates/admin/dashboard/index.html.eex:16 #, elixir-autogen, elixir-format msgid "For any existing contracts in the database, insert all ABI entries into the contract_methods table. Use this in case you have verified smart contracts before early March 2019 and you want other contracts with the same functions to show those ABI's as candidate matches." msgstr "" +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 #: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:7 #, elixir-autogen, elixir-format msgid "For contract" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:44 #: lib/block_scout_web/templates/layout/_topnav.html.eex:44 #, elixir-autogen, elixir-format msgid "Forked Blocks (Reorgs)" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:45 #: lib/block_scout_web/templates/layout/_footer.html.eex:45 #, elixir-autogen, elixir-format msgid "Forum" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:34 #: lib/block_scout_web/templates/address_transaction/index.html.eex:34 #: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/templates/transaction/overview.html.eex:246 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_token_transfer_view.ex:11 +#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:11 #, elixir-autogen, elixir-format msgid "From" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:18 #, elixir-autogen, elixir-format msgid "GET" msgstr "" +#: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/_tile.html.eex:67 #: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/block/overview.html.eex:189 +#: lib/block_scout_web/templates/transaction/overview.html.eex:430 #: lib/block_scout_web/templates/transaction/overview.html.eex:430 #, elixir-autogen, elixir-format msgid "Gas Limit" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:404 #: lib/block_scout_web/templates/transaction/overview.html.eex:404 #, elixir-autogen, elixir-format, fuzzy msgid "Gas Price" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:242 #: lib/block_scout_web/templates/address/overview.html.eex:242 #: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/_tile.html.eex:73 +#: lib/block_scout_web/templates/block/overview.html.eex:180 #: lib/block_scout_web/templates/block/overview.html.eex:180 #, elixir-autogen, elixir-format msgid "Gas Used" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:489 #: lib/block_scout_web/templates/transaction/overview.html.eex:489 #, elixir-autogen, elixir-format, fuzzy msgid "Gas Used by Transaction" msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:3 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:18 #, elixir-autogen, elixir-format msgid "Gas tracker" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:241 #: lib/block_scout_web/templates/address/overview.html.eex:241 #, elixir-autogen, elixir-format msgid "Gas used by the address." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:60 #: lib/block_scout_web/templates/block/overview.html.eex:60 #, elixir-autogen, elixir-format msgid "Genesis Block" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:24 #: lib/block_scout_web/templates/layout/_footer.html.eex:24 #, elixir-autogen, elixir-format msgid "Github" msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:8 #, elixir-autogen, elixir-format msgid "Go to" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:110 #: lib/block_scout_web/templates/layout/_topnav.html.eex:110 #, elixir-autogen, elixir-format msgid "GraphQL" msgstr "" #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:11 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:21 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:22 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:38 +#: lib/block_scout_web/views/block_view.ex:22 #: lib/block_scout_web/views/block_view.ex:22 #: lib/block_scout_web/views/wei_helper.ex:81 +#: lib/block_scout_web/views/wei_helper.ex:81 #, elixir-autogen, elixir-format msgid "Gwei" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:123 #: lib/block_scout_web/templates/block/overview.html.eex:123 #, elixir-autogen, elixir-format msgid "Hash" msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:553 +#: lib/block_scout_web/templates/transaction/overview.html.eex:557 #: lib/block_scout_web/templates/transaction/overview.html.eex:557 #, elixir-autogen, elixir-format msgid "Hex (Default)" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:224 #: lib/block_scout_web/templates/transaction/overview.html.eex:224 #, elixir-autogen, elixir-format msgid "Highlighted events of the transaction." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108 #, elixir-autogen, elixir-format msgid "Holders" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:48 #: lib/block_scout_web/templates/tokens/index.html.eex:48 #, elixir-autogen, elixir-format msgid "Holders Count" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:11 #, elixir-autogen, elixir-format msgid "However, in general, the" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:19 #, elixir-autogen, elixir-format msgid "IMPORTANT: This information is a best guess based on similar functions from other verified contracts." msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42 #: lib/block_scout_web/templates/transaction/_tile.html.eex:92 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:92 #, elixir-autogen, elixir-format msgid "IN" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:56 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:48 #, elixir-autogen, elixir-format msgid "If you enabled optimization during compilation, select yes." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:135 #: lib/block_scout_web/templates/address/overview.html.eex:135 #, elixir-autogen, elixir-format msgid "Implementation" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:134 #: lib/block_scout_web/templates/address/overview.html.eex:134 #, elixir-autogen, elixir-format msgid "Implementation address of the proxy contract." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:3 #, elixir-autogen, elixir-format msgid "Include nightly builds" msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56 #, elixir-autogen, elixir-format msgid "Incoming" msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:23 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:23 #: lib/block_scout_web/templates/withdrawal/index.html.eex:23 #, elixir-autogen, elixir-format, fuzzy msgid "Index" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Index position of Transaction in the block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:251 #: lib/block_scout_web/templates/block/overview.html.eex:251 #, elixir-autogen, elixir-format msgid "Index position(s) of referenced stale blocks." msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:6 #, elixir-autogen, elixir-format msgid "Indexed?" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3 #, elixir-autogen, elixir-format msgid "Input" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:265 #: lib/block_scout_web/templates/transaction/overview.html.eex:265 #, elixir-autogen, elixir-format msgid "Interacted With (To)" msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:7 #, elixir-autogen, elixir-format msgid "Internal Transaction" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address/_tabs.html.eex:36 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 -#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6 +#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/views/transaction_view.ex:552 #: lib/block_scout_web/views/transaction_view.ex:552 #, elixir-autogen, elixir-format msgid "Internal Transactions" msgstr "" +#: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 #: lib/block_scout_web/templates/internal_server_error/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Internal server error" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:25 #: lib/block_scout_web/views/internal_transaction_view.ex:25 #, elixir-autogen, elixir-format msgid "Invalid" msgstr "" #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19 #: lib/block_scout_web/views/tokens/overview_view.ex:42 +#: lib/block_scout_web/views/tokens/overview_view.ex:42 #, elixir-autogen, elixir-format msgid "Inventory" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:3 #, elixir-autogen, elixir-format msgid "Is Yul contract" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:204 #: lib/block_scout_web/templates/transaction/overview.html.eex:204 #, elixir-autogen, elixir-format, fuzzy msgid "L1 Block" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:523 #: lib/block_scout_web/templates/transaction/overview.html.eex:523 #: lib/block_scout_web/templates/transaction/overview.html.eex:524 +#: lib/block_scout_web/templates/transaction/overview.html.eex:524 #, elixir-autogen, elixir-format msgid "L1 Fee Scalar" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:512 #: lib/block_scout_web/templates/transaction/overview.html.eex:512 #: lib/block_scout_web/templates/transaction/overview.html.eex:513 +#: lib/block_scout_web/templates/transaction/overview.html.eex:513 #, elixir-autogen, elixir-format, fuzzy msgid "L1 Gas Price" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:501 #: lib/block_scout_web/templates/transaction/overview.html.eex:501 #: lib/block_scout_web/templates/transaction/overview.html.eex:502 +#: lib/block_scout_web/templates/transaction/overview.html.eex:502 #, elixir-autogen, elixir-format msgid "L1 Gas Used by Transaction" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:426 #: lib/block_scout_web/templates/transaction/overview.html.eex:426 #, elixir-autogen, elixir-format, fuzzy msgid "L2 Gas Limit" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:400 #: lib/block_scout_web/templates/transaction/overview.html.eex:400 #, elixir-autogen, elixir-format, fuzzy msgid "L2 Gas Price" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:485 #: lib/block_scout_web/templates/transaction/overview.html.eex:485 #, elixir-autogen, elixir-format msgid "L2 Gas Used by Transaction" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:13 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:26 #, elixir-autogen, elixir-format msgid "Last 24h" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:260 #: lib/block_scout_web/templates/address/overview.html.eex:260 #, elixir-autogen, elixir-format msgid "Last Balance Update" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:12 #: lib/block_scout_web/templates/account/api_key/index.html.eex:18 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Learn more" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:49 #: lib/block_scout_web/templates/layout/app.html.eex:49 #, elixir-autogen, elixir-format msgid "Less than" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_address.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 #, elixir-autogen, elixir-format msgid "Library" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:24 #, elixir-autogen, elixir-format msgid "License Expires" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:10 #, elixir-autogen, elixir-format msgid "License ID" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:351 #: lib/block_scout_web/templates/transaction/overview.html.eex:351 #, elixir-autogen, elixir-format msgid "List of ERC-1155 tokens created in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:335 #: lib/block_scout_web/templates/transaction/overview.html.eex:335 #, elixir-autogen, elixir-format msgid "List of token burnt in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:318 #: lib/block_scout_web/templates/transaction/overview.html.eex:318 #, elixir-autogen, elixir-format msgid "List of token minted in the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:302 #: lib/block_scout_web/templates/transaction/overview.html.eex:302 #, elixir-autogen, elixir-format msgid "List of token transferred in the transaction." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:18 #, elixir-autogen, elixir-format msgid "Loading chart..." msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:77 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:109 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:35 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:99 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41 +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:49 #: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12 +#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #: lib/block_scout_web/templates/tokens/contract/index.html.eex:17 #, elixir-autogen, elixir-format msgid "Loading..." msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:2 #, elixir-autogen, elixir-format msgid "Log Data" msgstr "" +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:140 #, elixir-autogen, elixir-format msgid "Log Index" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address/_tabs.html.eex:49 +#: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/address_logs/index.html.eex:10 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 -#: lib/block_scout_web/views/address_view.ex:362 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/address_view.ex:363 +#: lib/block_scout_web/views/transaction_view.ex:553 #: lib/block_scout_web/views/transaction_view.ex:553 #, elixir-autogen, elixir-format msgid "Logs" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:54 #: lib/block_scout_web/templates/layout/_footer.html.eex:54 #, elixir-autogen, elixir-format msgid "Main Networks" msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/chain/show.html.eex:53 +#: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/layout/app.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 -#: lib/block_scout_web/views/address_view.ex:147 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:85 +#: lib/block_scout_web/views/address_view.ex:148 +#: lib/block_scout_web/views/address_view.ex:148 #, elixir-autogen, elixir-format msgid "Market Cap" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:84 #, elixir-autogen, elixir-format msgid "Market cap" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:440 #: lib/block_scout_web/templates/transaction/overview.html.eex:440 #, elixir-autogen, elixir-format msgid "Max Fee per Gas" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:450 #: lib/block_scout_web/templates/transaction/overview.html.eex:450 #, elixir-autogen, elixir-format msgid "Max Priority Fee per Gas" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:331 #: lib/block_scout_web/views/transaction_view.ex:331 #, elixir-autogen, elixir-format msgid "Max of" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:425 #: lib/block_scout_web/templates/transaction/overview.html.eex:425 #, elixir-autogen, elixir-format msgid "Maximum gas amount approved for the transaction on L2." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:429 #: lib/block_scout_web/templates/transaction/overview.html.eex:429 #, elixir-autogen, elixir-format, fuzzy msgid "Maximum gas amount approved for the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:439 #: lib/block_scout_web/templates/transaction/overview.html.eex:439 #, elixir-autogen, elixir-format msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee." msgstr "" +#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 #: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:75 #, elixir-autogen, elixir-format msgid "Metadata" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:5 #, elixir-autogen, elixir-format msgid "Method Id" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/_tile.html.eex:41 +#: lib/block_scout_web/templates/block/overview.html.eex:98 #: lib/block_scout_web/templates/block/overview.html.eex:98 #: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/chain/_block.html.eex:16 +#: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 #: lib/block_scout_web/templates/transaction_state/_state_change.html.eex:22 #, elixir-autogen, elixir-format msgid "Miner" msgstr "" #: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:63 +#: lib/block_scout_web/views/block_view.ex:68 #: lib/block_scout_web/views/block_view.ex:68 #, elixir-autogen, elixir-format msgid "Miner Reward" msgstr "" +#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 #: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:3 #, elixir-autogen, elixir-format msgid "Minimal Proxy Contract for" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:208 #: lib/block_scout_web/templates/block/overview.html.eex:208 #, elixir-autogen, elixir-format msgid "Minimum fee required per unit of gas. Fee adjusts based on network congestion." msgstr "" +#: lib/block_scout_web/templates/transaction/_actions.html.eex:92 #: lib/block_scout_web/templates/transaction/_actions.html.eex:92 #, elixir-autogen, elixir-format msgid "Mint of %{address} To %{to}" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:223 #, elixir-autogen, elixir-format msgid "Model" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #, elixir-autogen, elixir-format msgid "Module" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:12 #, elixir-autogen, elixir-format msgid "More internal transactions have come in" msgstr "" #: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:46 +#: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/chain/show.html.eex:219 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction/index.html.eex:19 #: lib/block_scout_web/templates/transaction/index.html.eex:19 #, elixir-autogen, elixir-format msgid "More transactions have come in" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:63 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:74 #, elixir-autogen, elixir-format msgid "Must be set to:" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Must match the name specified in the code. For example, in contract MyContract {..} MyContract is the contract name." msgstr "" #: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/tokens/_tile.html.eex:40 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:21 #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:61 #, elixir-autogen, elixir-format msgid "N/A" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:116 #: lib/block_scout_web/templates/block/overview.html.eex:116 #, elixir-autogen, elixir-format msgid "N/A bytes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:19 #: lib/block_scout_web/templates/account/api_key/form.html.eex:19 #: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/api_key/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:22 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:22 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_name.html.eex:4 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:21 #, elixir-autogen, elixir-format msgid "Name" msgstr "" +#: lib/block_scout_web/templates/account/api_key/form.html.eex:20 #: lib/block_scout_web/templates/account/api_key/form.html.eex:20 #, elixir-autogen, elixir-format msgid "Name this API key" msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14 #, elixir-autogen, elixir-format msgid "Name this Custom ABI" msgstr "" #: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20 #, elixir-autogen, elixir-format msgid "Name this address" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19 #, elixir-autogen, elixir-format msgid "Name this transaction" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:44 #: lib/block_scout_web/templates/address_token/overview.html.eex:44 #, elixir-autogen, elixir-format msgid "Net Worth" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:5 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:9 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification via Standard input JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:5 #, elixir-autogen, elixir-format msgid "New Smart Contract Verification via metadata JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Solidity Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Solidity/Yul Smart Contract Verification" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:7 #, elixir-autogen, elixir-format msgid "New Vyper Smart Contract Verification" msgstr "" #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:80 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:87 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:95 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:103 +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:111 #, elixir-autogen, elixir-format msgid "Next" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:9 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:46 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:38 #, elixir-autogen, elixir-format msgid "No" msgstr "" +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:15 #, elixir-autogen, elixir-format msgid "No trace entries found." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:198 #: lib/block_scout_web/templates/block/overview.html.eex:198 #: lib/block_scout_web/templates/transaction/overview.html.eex:535 +#: lib/block_scout_web/templates/transaction/overview.html.eex:535 #, elixir-autogen, elixir-format msgid "Nonce" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:11 #, elixir-autogen, elixir-format msgid "Not unique Token" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107 #, elixir-autogen, elixir-format msgid "Number of accounts holding the token" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:276 #: lib/block_scout_web/templates/address/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "Number of blocks validated by this validator." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130 #, elixir-autogen, elixir-format msgid "Number of digits that come after the decimal place when displaying token value" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:187 #: lib/block_scout_web/templates/address/overview.html.eex:187 #, elixir-autogen, elixir-format msgid "Number of transactions related to this address." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118 #, elixir-autogen, elixir-format msgid "Number of transfers for the token" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:214 #: lib/block_scout_web/templates/address/overview.html.eex:214 #, elixir-autogen, elixir-format msgid "Number of transfers to/from this address." msgstr "" +#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 #: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40 #: lib/block_scout_web/templates/transaction/_tile.html.eex:88 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:88 #, elixir-autogen, elixir-format msgid "OUT" msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "Only the first" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:32 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:75 #, elixir-autogen, elixir-format msgid "Optimization" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:67 #: lib/block_scout_web/templates/address_contract/index.html.eex:67 #, elixir-autogen, elixir-format msgid "Optimization enabled" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:62 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:54 #, elixir-autogen, elixir-format msgid "Optimization runs" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:78 #: lib/block_scout_web/templates/layout/_footer.html.eex:78 #, elixir-autogen, elixir-format msgid "Other Explorers" msgstr "" #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61 #, elixir-autogen, elixir-format msgid "Outgoing" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24 #, elixir-autogen, elixir-format msgid "Owner Address" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "POA solidity flattener or the" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:19 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:26 #, elixir-autogen, elixir-format msgid "POST" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #, elixir-autogen, elixir-format msgid "Page" msgstr "" +#: lib/block_scout_web/templates/page_not_found/index.html.eex:7 #: lib/block_scout_web/templates/page_not_found/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Page not found" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:33 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:40 #, elixir-autogen, elixir-format msgid "Parameters" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:139 #: lib/block_scout_web/templates/block/overview.html.eex:139 #, elixir-autogen, elixir-format msgid "Parent Hash" msgstr "" #: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:63 +#: lib/block_scout_web/views/transaction_view.ex:374 #: lib/block_scout_web/views/transaction_view.ex:374 #: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:419 +#: lib/block_scout_web/views/transaction_view.ex:427 #: lib/block_scout_web/views/transaction_view.ex:427 #, elixir-autogen, elixir-format msgid "Pending" msgstr "" +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Pending Transactions" msgstr "" #: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:9 +#: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 #: lib/block_scout_web/templates/address/_custom_view_df_title.html.eex:13 #, elixir-autogen, elixir-format msgid "Play" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:22 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:21 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:21 +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Please confirm your email address to use the My Account feature." msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68 #, elixir-autogen, elixir-format msgid "Please select notification methods:" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24 #, elixir-autogen, elixir-format msgid "Please select what types of notifications you will receive:" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:537 #: lib/block_scout_web/templates/transaction/overview.html.eex:537 #, elixir-autogen, elixir-format msgid "Position" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:256 #: lib/block_scout_web/templates/block/overview.html.eex:256 #, elixir-autogen, elixir-format msgid "Position %{index}" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:18 #, elixir-autogen, elixir-format msgid "Potential matches from contract method database:" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:32 #, elixir-autogen, elixir-format msgid "Potential matches from our contract method database:" msgstr "" +#: lib/block_scout_web/templates/layout/_search.html.eex:27 #: lib/block_scout_web/templates/layout/_search.html.eex:27 #, elixir-autogen, elixir-format msgid "Press / and focus will be moved to the search field" msgstr "" #: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/chain/show.html.eex:42 +#: lib/block_scout_web/templates/layout/app.html.eex:51 #: lib/block_scout_web/templates/layout/app.html.eex:51 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:96 #, elixir-autogen, elixir-format msgid "Price" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95 #, elixir-autogen, elixir-format msgid "Price per token on the exchanges" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:399 #: lib/block_scout_web/templates/transaction/overview.html.eex:399 #, elixir-autogen, elixir-format msgid "Price per unit of gas specified by the sender on L2. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:403 #: lib/block_scout_web/templates/transaction/overview.html.eex:403 #, elixir-autogen, elixir-format, fuzzy msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/block/overview.html.eex:227 +#: lib/block_scout_web/templates/transaction/overview.html.eex:460 #: lib/block_scout_web/templates/transaction/overview.html.eex:460 #, elixir-autogen, elixir-format msgid "Priority Fee / Tip" msgstr "" +#: lib/block_scout_web/templates/block/_tile.html.eex:62 #: lib/block_scout_web/templates/block/_tile.html.eex:62 #, elixir-autogen, elixir-format msgid "Priority Fees" msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:4 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14 #, elixir-autogen, elixir-format msgid "Profile" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Public Tags" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20 -#, elixir-autogen, elixir-format -msgid "Public tag" -msgstr "" - -#: lib/block_scout_web/templates/account/common/_nav.html.eex:22 -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Public tags" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50 -#, elixir-autogen, elixir-format -msgid "Public tags* (2 tags maximum, please use \";\" as a divider)" -msgstr "" - #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10 +#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83 #, elixir-autogen, elixir-format msgid "QR Code" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #, elixir-autogen, elixir-format msgid "Query" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:115 #: lib/block_scout_web/templates/layout/_topnav.html.eex:115 #, elixir-autogen, elixir-format msgid "RPC" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:546 #: lib/block_scout_web/templates/transaction/overview.html.eex:546 #, elixir-autogen, elixir-format msgid "Raw Input" msgstr "" #: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24 +#: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 #: lib/block_scout_web/templates/transaction_raw_trace/_card_body.html.eex:1 #: lib/block_scout_web/views/transaction_view.ex:554 +#: lib/block_scout_web/views/transaction_view.ex:554 #, elixir-autogen, elixir-format msgid "Raw Trace" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/address/_tabs.html.eex:81 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 -#: lib/block_scout_web/views/address_view.ex:356 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/views/tokens/overview_view.ex:41 #: lib/block_scout_web/views/tokens/overview_view.ex:41 #, elixir-autogen, elixir-format msgid "Read Contract" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/address/_tabs.html.eex:88 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 -#: lib/block_scout_web/views/address_view.ex:357 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41 +#: lib/block_scout_web/views/address_view.ex:358 +#: lib/block_scout_web/views/address_view.ex:358 #, elixir-autogen, elixir-format msgid "Read Proxy" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "Records" msgstr "" #: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/api_key/row.html.eex:13 +#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 #: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13 #, elixir-autogen, elixir-format msgid "Remove" msgstr "" +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77 #, elixir-autogen, elixir-format msgid "Remove from Watch list" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155 #, elixir-autogen, elixir-format msgid "Request URL" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Request a public tag/label" -msgstr "" - +#: lib/block_scout_web/templates/error422/index.html.eex:7 #: lib/block_scout_web/templates/error422/index.html.eex:7 #, elixir-autogen, elixir-format msgid "Request cannot be processed" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37 -#, elixir-autogen, elixir-format -msgid "Request to add public tag" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7 -#, elixir-autogen, elixir-format -msgid "Request to edit a public tag/label" -msgstr "" - +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "Resend verification email" msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:112 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:104 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:52 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:48 #, elixir-autogen, elixir-format msgid "Reset" msgstr "" #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:173 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:134 #, elixir-autogen, elixir-format msgid "Response Body" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:185 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:147 #, elixir-autogen, elixir-format msgid "Responses" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 #: lib/block_scout_web/templates/transaction/overview.html.eex:98 #, elixir-autogen, elixir-format msgid "Result" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:138 #: lib/block_scout_web/templates/transaction/overview.html.eex:138 #, elixir-autogen, elixir-format msgid "Revert reason" msgstr "" #: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/block/_tile.html.eex:52 +#: lib/block_scout_web/templates/chain/_block.html.eex:27 #: lib/block_scout_web/templates/chain/_block.html.eex:27 #: lib/block_scout_web/views/internal_transaction_view.ex:29 +#: lib/block_scout_web/views/internal_transaction_view.ex:29 #, elixir-autogen, elixir-format msgid "Reward" msgstr "" +#: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 #: lib/block_scout_web/templates/admin/dashboard/index.html.eex:21 #, elixir-autogen, elixir-format msgid "Run" msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:26 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31 #: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25 +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25 #: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 +#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83 #, elixir-autogen, elixir-format msgid "Save" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:226 #: lib/block_scout_web/templates/transaction/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "Scroll to see more" msgstr "" #: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/address_logs/index.html.eex:16 +#: lib/block_scout_web/templates/layout/_search.html.eex:34 #: lib/block_scout_web/templates/layout/_search.html.eex:34 #, elixir-autogen, elixir-format msgid "Search" msgstr "" +#: lib/block_scout_web/templates/search/results.html.eex:17 #: lib/block_scout_web/templates/search/results.html.eex:17 #, elixir-autogen, elixir-format msgid "Search Results" msgstr "" +#: lib/block_scout_web/templates/layout/_search.html.eex:3 #: lib/block_scout_web/templates/layout/_search.html.eex:3 #, elixir-autogen, elixir-format msgid "Search by address, token symbol name, transaction hash, or block number" msgstr "" +#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 #: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:47 #, elixir-autogen, elixir-format msgid "Search tokens" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:19 #, elixir-autogen, elixir-format msgid "Select Yes if you want to verify Yul contract." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:19 #, elixir-autogen, elixir-format msgid "Select yes if you want to show nightly builds." msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:28 #: lib/block_scout_web/views/internal_transaction_view.ex:28 #, elixir-autogen, elixir-format msgid "Self-Destruct" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63 -#, elixir-autogen, elixir-format -msgid "Send request" -msgstr "" - #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124 #, elixir-autogen, elixir-format msgid "Server Response" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:7 #, elixir-autogen, elixir-format msgid "Show" msgstr "" +#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 #: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:11 #, elixir-autogen, elixir-format msgid "Show QR Code" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "Shows the current" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:59 #: lib/block_scout_web/templates/address_token/overview.html.eex:59 #, elixir-autogen, elixir-format msgid "Shows the tokens held in the address (includes ERC-20, ERC-721 and ERC-1155)." msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:66 #: lib/block_scout_web/templates/address_token/overview.html.eex:66 #, elixir-autogen, elixir-format msgid "Shows the total CRC balance in the address." msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:45 #: lib/block_scout_web/templates/address_token/overview.html.eex:45 #, elixir-autogen, elixir-format msgid "Shows total assets held in the address" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:33 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:32 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:32 #, elixir-autogen, elixir-format msgid "Sign in" msgstr "" -#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:24 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:23 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:23 #, elixir-autogen, elixir-format msgid "Sign out" msgstr "" +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:11 #, elixir-autogen, elixir-format msgid "Signed in as " msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:114 #: lib/block_scout_web/templates/block/overview.html.eex:114 #, elixir-autogen, elixir-format msgid "Size" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:113 #: lib/block_scout_web/templates/block/overview.html.eex:113 #, elixir-autogen, elixir-format msgid "Size of the block in bytes." msgstr "" +#: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #: lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex:20 #, elixir-autogen, elixir-format msgid "Slow" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21 -#, elixir-autogen, elixir-format -msgid "Smart contract / Address" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4 -#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5 -#, elixir-autogen, elixir-format -msgid "Smart contract / Address (0x...)" -msgstr "" - #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:26 #: lib/block_scout_web/views/verified_contracts_view.ex:10 +#: lib/block_scout_web/views/verified_contracts_view.ex:10 #, elixir-autogen, elixir-format msgid "Solidity" msgstr "" #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30 +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_logs/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/index.html.eex:23 #: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token/index.html.eex:60 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:58 #: lib/block_scout_web/templates/address_transaction/index.html.eex:50 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:50 #: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_validation/index.html.eex:20 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:20 #: lib/block_scout_web/templates/block_transaction/index.html.eex:14 +#: lib/block_scout_web/templates/block_transaction/index.html.eex:14 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:14 +#: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/chain/show.html.eex:160 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23 #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22 #: lib/block_scout_web/templates/transaction/index.html.eex:25 +#: lib/block_scout_web/templates/transaction/index.html.eex:25 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_log/index.html.eex:15 #: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:13 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:14 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:51 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:14 #: lib/block_scout_web/templates/withdrawal/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Something went wrong, click to reload." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:225 #: lib/block_scout_web/templates/chain/show.html.eex:225 #, elixir-autogen, elixir-format msgid "Something went wrong, click to retry." msgstr "" +#: lib/block_scout_web/templates/transaction/not_found.html.eex:6 #: lib/block_scout_web/templates/transaction/not_found.html.eex:6 #, elixir-autogen, elixir-format msgid "Sorry, we are unable to locate this transaction hash" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #, elixir-autogen, elixir-format msgid "Sources *.sol files" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:63 #, elixir-autogen, elixir-format msgid "Sources *.sol or *.yul files" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:14 #, elixir-autogen, elixir-format msgid "Sources and Metadata JSON" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:136 #: lib/block_scout_web/templates/layout/_topnav.html.eex:136 #, elixir-autogen, elixir-format msgid "Stakes" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:24 #, elixir-autogen, elixir-format msgid "Standard Input JSON" msgstr "" +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:29 #: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/templates/transaction_state/index.html.eex:6 +#: lib/block_scout_web/views/transaction_view.ex:555 #: lib/block_scout_web/views/transaction_view.ex:555 #, elixir-autogen, elixir-format msgid "State changes" msgstr "" +#: lib/block_scout_web/views/internal_transaction_view.ex:24 #: lib/block_scout_web/views/internal_transaction_view.ex:24 #, elixir-autogen, elixir-format msgid "Static Call" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 #: lib/block_scout_web/templates/transaction/overview.html.eex:110 #, elixir-autogen, elixir-format msgid "Status" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22 -#, elixir-autogen, elixir-format -msgid "Submission date" -msgstr "" - +#: lib/block_scout_web/templates/layout/_footer.html.eex:41 #: lib/block_scout_web/templates/layout/_footer.html.eex:41 #, elixir-autogen, elixir-format msgid "Submit an Issue" msgstr "" +#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 #: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8 #: lib/block_scout_web/views/transaction_view.ex:376 +#: lib/block_scout_web/views/transaction_view.ex:376 #, elixir-autogen, elixir-format msgid "Success" msgstr "" #: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #: lib/block_scout_web/templates/transaction/_tile.html.eex:52 #, elixir-autogen, elixir-format msgid "TX Fee" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:31 #: lib/block_scout_web/templates/layout/_footer.html.eex:31 #, elixir-autogen, elixir-format msgid "Telegram" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:67 #: lib/block_scout_web/templates/layout/_footer.html.eex:67 #, elixir-autogen, elixir-format msgid "Test Networks" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_library_first.html.eex:9 #, elixir-autogen, elixir-format msgid "The 0x library address. This can be found in the generated json file or Truffle output (if using truffle)." msgstr "" #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:34 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:26 #, elixir-autogen, elixir-format msgid "The EVM version the contract is written for. If the bytecode does not match the version, we try to verify using the latest EVM version." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:122 #: lib/block_scout_web/templates/block/overview.html.eex:122 #, elixir-autogen, elixir-format msgid "The SHA256 hash of the block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:51 #: lib/block_scout_web/templates/block/overview.html.eex:51 #, elixir-autogen, elixir-format msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain." msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:18 #: lib/block_scout_web/templates/transaction_state/index.html.eex:18 #, elixir-autogen, elixir-format msgid "The changes from this transaction have not yet happened since the transaction is still pending." msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:26 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:18 #, elixir-autogen, elixir-format msgid "The compiler version is specified in pragma solidity X.X.X. Use the compiler version rather than the nightly build. If using the Solidity compiler, run solc —version to check." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #, elixir-autogen, elixir-format msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:138 #: lib/block_scout_web/templates/block/overview.html.eex:138 #, elixir-autogen, elixir-format msgid "The hash of the block from which this block was generated." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:74 #: lib/block_scout_web/templates/address/overview.html.eex:74 #, elixir-autogen, elixir-format msgid "The name found in the source code of the Contract." msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:85 #: lib/block_scout_web/templates/address/overview.html.eex:85 #, elixir-autogen, elixir-format msgid "The name of the validator." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:79 #: lib/block_scout_web/templates/block/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "The number of transactions in the block." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #, elixir-autogen, elixir-format msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:137 #: lib/block_scout_web/templates/transaction/overview.html.eex:137 #, elixir-autogen, elixir-format msgid "The revert reason of the transaction." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:109 #: lib/block_scout_web/templates/transaction/overview.html.eex:109 #, elixir-autogen, elixir-format msgid "The status of the transaction: Confirmed or Unconfirmed." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67 #, elixir-autogen, elixir-format msgid "The total amount of tokens issued" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:179 #: lib/block_scout_web/templates/block/overview.html.eex:179 #, elixir-autogen, elixir-format msgid "The total gas amount used in the block and its percentage of gas filled in the block." msgstr "" +#: lib/block_scout_web/templates/address_validation/index.html.eex:16 #: lib/block_scout_web/templates/address_validation/index.html.eex:16 #, elixir-autogen, elixir-format msgid "There are no blocks validated by this address." msgstr "" +#: lib/block_scout_web/templates/block/index.html.eex:17 #: lib/block_scout_web/templates/block/index.html.eex:17 #, elixir-autogen, elixir-format msgid "There are no blocks." msgstr "" +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:29 #, elixir-autogen, elixir-format msgid "There are no holders for this Token." msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:54 #, elixir-autogen, elixir-format msgid "There are no internal transactions for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:17 #, elixir-autogen, elixir-format msgid "There are no internal transactions for this transaction." msgstr "" +#: lib/block_scout_web/templates/address_logs/index.html.eex:28 #: lib/block_scout_web/templates/address_logs/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no logs for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_log/index.html.eex:20 #: lib/block_scout_web/templates/transaction_log/index.html.eex:20 #, elixir-autogen, elixir-format msgid "There are no logs for this transaction." msgstr "" +#: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:22 #, elixir-autogen, elixir-format msgid "There are no pending transactions." msgstr "" +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:53 #, elixir-autogen, elixir-format msgid "There are no token transfers for this address." msgstr "" +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:19 #, elixir-autogen, elixir-format msgid "There are no token transfers for this transaction" msgstr "" +#: lib/block_scout_web/templates/address_token/index.html.eex:65 #: lib/block_scout_web/templates/address_token/index.html.eex:65 #, elixir-autogen, elixir-format msgid "There are no tokens for this address." msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28 #, elixir-autogen, elixir-format msgid "There are no tokens." msgstr "" +#: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #: lib/block_scout_web/templates/address_transaction/index.html.eex:55 #, elixir-autogen, elixir-format msgid "There are no transactions for this address." msgstr "" +#: lib/block_scout_web/templates/block_transaction/index.html.eex:19 #: lib/block_scout_web/templates/block_transaction/index.html.eex:19 #, elixir-autogen, elixir-format msgid "There are no transactions for this block." msgstr "" +#: lib/block_scout_web/templates/transaction/index.html.eex:31 #: lib/block_scout_web/templates/transaction/index.html.eex:31 #, elixir-autogen, elixir-format msgid "There are no transactions." msgstr "" +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27 #, elixir-autogen, elixir-format msgid "There are no transfers for this Token." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:99 #, elixir-autogen, elixir-format msgid "There are no verified contracts." msgstr "" +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:54 #, elixir-autogen, elixir-format msgid "There are no withdrawals for this address." msgstr "" +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:45 #, elixir-autogen, elixir-format msgid "There are no withdrawals for this block." msgstr "" +#: lib/block_scout_web/templates/withdrawal/index.html.eex:53 #: lib/block_scout_web/templates/withdrawal/index.html.eex:53 #, elixir-autogen, elixir-format msgid "There are no withdrawals." msgstr "" +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:35 #, elixir-autogen, elixir-format msgid "There is no coin history for this address." msgstr "" #: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:21 +#: lib/block_scout_web/templates/chain/show.html.eex:9 #: lib/block_scout_web/templates/chain/show.html.eex:9 #, elixir-autogen, elixir-format msgid "There was a problem loading the chart." msgstr "" +#: lib/block_scout_web/templates/api_docs/index.html.eex:6 #: lib/block_scout_web/templates/api_docs/index.html.eex:6 #, elixir-autogen, elixir-format msgid "This API is provided for developers transitioning their applications from Etherscan to BlockScout. It supports GET and POST requests." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:7 #, elixir-autogen, elixir-format msgid "This API is provided to support some rpc methods in the exact format specified for ethereum nodes, which can be found " msgstr "" +#: lib/block_scout_web/views/block_transaction_view.ex:11 #: lib/block_scout_web/views/block_transaction_view.ex:11 #, elixir-autogen, elixir-format msgid "This block has not been processed yet." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:47 #: lib/block_scout_web/templates/address_contract/index.html.eex:47 #, elixir-autogen, elixir-format msgid "This contract has been partially verified via Sourcify." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:51 #: lib/block_scout_web/templates/address_contract/index.html.eex:51 #, elixir-autogen, elixir-format msgid "This contract has been verified via Sourcify." msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:10 #, elixir-autogen, elixir-format msgid "This is useful to allow sending requests to blockscout without having to change anything about the request." msgstr "" +#: lib/block_scout_web/templates/page_not_found/index.html.eex:8 #: lib/block_scout_web/templates/page_not_found/index.html.eex:8 #, elixir-autogen, elixir-format msgid "This page is no longer explorable! If you are lost, use the search bar to find what you are looking for." msgstr "" +#: lib/block_scout_web/templates/transaction_state/index.html.eex:22 #: lib/block_scout_web/templates/transaction_state/index.html.eex:22 #, elixir-autogen, elixir-format msgid "This transaction hasn't changed state." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #: lib/block_scout_web/templates/transaction/overview.html.eex:64 #, elixir-autogen, elixir-format msgid "This transaction is pending confirmation." msgstr "" #: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/block/overview.html.eex:71 +#: lib/block_scout_web/templates/transaction/overview.html.eex:180 #: lib/block_scout_web/templates/transaction/overview.html.eex:180 #, elixir-autogen, elixir-format msgid "Timestamp" msgstr "" +#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/address_transaction/index.html.eex:28 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:29 #: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/transaction/overview.html.eex:267 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/templates/withdrawal/index.html.eex:32 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10 +#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_token_transfer_view.ex:10 +#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10 #, elixir-autogen, elixir-format msgid "To" msgstr "" +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:20 #, elixir-autogen, elixir-format msgid "To have guaranteed accuracy, use the link above to verify the contract's source code." msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:8 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:6 #, elixir-autogen, elixir-format msgid "To see accurate decoded input data, the contract must be verified." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:18 #: lib/block_scout_web/templates/layout/_topnav.html.eex:18 #, elixir-autogen, elixir-format msgid "Toggle navigation" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:55 #: lib/block_scout_web/templates/address/overview.html.eex:55 #: lib/block_scout_web/templates/tokens/index.html.eex:31 +#: lib/block_scout_web/templates/tokens/index.html.eex:31 #, elixir-autogen, elixir-format msgid "Token" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3 #: lib/block_scout_web/views/transaction_view.ex:488 +#: lib/block_scout_web/views/transaction_view.ex:488 #, elixir-autogen, elixir-format msgid "Token Burning" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7 #: lib/block_scout_web/views/transaction_view.ex:489 +#: lib/block_scout_web/views/transaction_view.ex:489 #, elixir-autogen, elixir-format msgid "Token Creation" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:34 #, elixir-autogen, elixir-format msgid "Token Details" msgstr "" +#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11 +#: lib/block_scout_web/views/tokens/overview_view.ex:40 #: lib/block_scout_web/views/tokens/overview_view.ex:40 #, elixir-autogen, elixir-format msgid "Token Holders" msgstr "" #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:38 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:18 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:37 #, elixir-autogen, elixir-format msgid "Token ID" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5 #: lib/block_scout_web/views/transaction_view.ex:487 +#: lib/block_scout_web/views/transaction_view.ex:487 #, elixir-autogen, elixir-format msgid "Token Minting" msgstr "" +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9 #: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11 +#: lib/block_scout_web/views/transaction_view.ex:490 #: lib/block_scout_web/views/transaction_view.ex:490 #, elixir-autogen, elixir-format msgid "Token Transfer" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address/_tabs.html.eex:13 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:19 #: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3 +#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5 +#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 +#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 -#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7 +#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/address_view.ex:355 +#: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/instance/overview_view.ex:74 #: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/tokens/overview_view.ex:39 +#: lib/block_scout_web/views/transaction_view.ex:551 #: lib/block_scout_web/views/transaction_view.ex:551 #, elixir-autogen, elixir-format msgid "Token Transfers" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:54 #: lib/block_scout_web/templates/address/overview.html.eex:54 #, elixir-autogen, elixir-format msgid "Token name and symbol." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142 #, elixir-autogen, elixir-format msgid "Token type" msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/_tabs.html.eex:21 +#: lib/block_scout_web/templates/address/overview.html.eex:177 #: lib/block_scout_web/templates/address/overview.html.eex:177 #: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token/overview.html.eex:58 +#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:84 +#: lib/block_scout_web/templates/tokens/index.html.eex:10 #: lib/block_scout_web/templates/tokens/index.html.eex:10 -#: lib/block_scout_web/views/address_view.ex:351 +#: lib/block_scout_web/views/address_view.ex:352 +#: lib/block_scout_web/views/address_view.ex:352 #, elixir-autogen, elixir-format msgid "Tokens" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:336 #: lib/block_scout_web/templates/transaction/overview.html.eex:336 #, elixir-autogen, elixir-format msgid "Tokens Burnt" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:352 #: lib/block_scout_web/templates/transaction/overview.html.eex:352 #, elixir-autogen, elixir-format msgid "Tokens Created" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:319 #: lib/block_scout_web/templates/transaction/overview.html.eex:319 #, elixir-autogen, elixir-format msgid "Tokens Minted" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:303 #: lib/block_scout_web/templates/transaction/overview.html.eex:303 #, elixir-autogen, elixir-format msgid "Tokens Transferred" msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:13 #: lib/block_scout_web/templates/address/_metatags.html.eex:13 #, elixir-autogen, elixir-format msgid "Top Accounts - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/address_logs/index.html.eex:14 #: lib/block_scout_web/templates/address_logs/index.html.eex:14 #, elixir-autogen, elixir-format msgid "Topic" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:68 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:100 #, elixir-autogen, elixir-format msgid "Topics" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:9 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:22 #, elixir-autogen, elixir-format msgid "Total" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:170 #: lib/block_scout_web/templates/block/overview.html.eex:170 #, elixir-autogen, elixir-format msgid "Total Difficulty" msgstr "" +#: lib/block_scout_web/templates/tokens/index.html.eex:43 #: lib/block_scout_web/templates/tokens/index.html.eex:43 #, elixir-autogen, elixir-format msgid "Total Supply" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84 #, elixir-autogen, elixir-format msgid "Total Supply * Price" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:133 #: lib/block_scout_web/templates/chain/show.html.eex:133 #, elixir-autogen, elixir-format msgid "Total blocks" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:169 #: lib/block_scout_web/templates/block/overview.html.eex:169 #, elixir-autogen, elixir-format msgid "Total difficulty of the chain until this block." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:188 #: lib/block_scout_web/templates/block/overview.html.eex:188 #, elixir-autogen, elixir-format msgid "Total gas limit provided by all transactions in the block." msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68 #, elixir-autogen, elixir-format msgid "Total supply" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:383 #: lib/block_scout_web/templates/transaction/overview.html.eex:383 #, elixir-autogen, elixir-format msgid "Total transaction fee." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:112 #: lib/block_scout_web/templates/chain/show.html.eex:112 #, elixir-autogen, elixir-format msgid "Total transactions" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 #: lib/block_scout_web/templates/address_logs/_logs.html.eex:19 #: lib/block_scout_web/views/transaction_view.ex:500 +#: lib/block_scout_web/views/transaction_view.ex:500 #, elixir-autogen, elixir-format msgid "Transaction" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:3 #, elixir-autogen, elixir-format msgid "Transaction %{transaction} - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:11 #, elixir-autogen, elixir-format msgid "Transaction %{transaction}, %{subnetwork} %{transaction}" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:225 #: lib/block_scout_web/templates/transaction/overview.html.eex:225 #, elixir-autogen, elixir-format, fuzzy msgid "Transaction Action" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:470 #: lib/block_scout_web/templates/transaction/overview.html.eex:470 #, elixir-autogen, elixir-format msgid "Transaction Burnt Fee" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:50 #: lib/block_scout_web/templates/transaction/overview.html.eex:50 #, elixir-autogen, elixir-format msgid "Transaction Details" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:384 #: lib/block_scout_web/templates/transaction/overview.html.eex:384 #, elixir-autogen, elixir-format msgid "Transaction Fee" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:80 #: lib/block_scout_web/templates/transaction/overview.html.eex:80 #, elixir-autogen, elixir-format msgid "Transaction Hash" msgstr "" #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:2 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:19 #, elixir-autogen, elixir-format msgid "Transaction Inputs" msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:13 +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17 #, elixir-autogen, elixir-format msgid "Transaction Tags" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:414 #: lib/block_scout_web/templates/transaction/overview.html.eex:414 #, elixir-autogen, elixir-format msgid "Transaction Type" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:534 #: lib/block_scout_web/templates/transaction/overview.html.eex:534 #, elixir-autogen, elixir-format msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:413 #: lib/block_scout_web/templates/transaction/overview.html.eex:413 #, elixir-autogen, elixir-format msgid "Transaction type, introduced in EIP-2718." msgstr "" #: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tabs.html.eex:7 +#: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/_tile.html.eex:31 #: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:188 +#: lib/block_scout_web/templates/address/overview.html.eex:194 #: lib/block_scout_web/templates/address/overview.html.eex:194 #: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address/overview.html.eex:202 +#: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/address_transaction/index.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/_tabs.html.eex:4 +#: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/block/overview.html.eex:80 #: lib/block_scout_web/templates/chain/show.html.eex:216 +#: lib/block_scout_web/templates/chain/show.html.eex:216 #: lib/block_scout_web/templates/layout/_topnav.html.eex:49 -#: lib/block_scout_web/views/address_view.ex:353 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:49 +#: lib/block_scout_web/views/address_view.ex:354 +#: lib/block_scout_web/views/address_view.ex:354 #, elixir-autogen, elixir-format msgid "Transactions" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:101 #: lib/block_scout_web/templates/address/overview.html.eex:101 #, elixir-autogen, elixir-format msgid "Transactions and address of creation." msgstr "" #: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:215 +#: lib/block_scout_web/templates/address/overview.html.eex:221 #: lib/block_scout_web/templates/address/overview.html.eex:221 #: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/address/overview.html.eex:229 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119 #, elixir-autogen, elixir-format msgid "Transfers" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47 #, elixir-autogen, elixir-format msgid "Try it out" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:3 #, elixir-autogen, elixir-format msgid "Try to fetch constructor arguments automatically" msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:27 #: lib/block_scout_web/templates/layout/_footer.html.eex:27 #, elixir-autogen, elixir-format msgid "Twitter" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:53 #: lib/block_scout_web/templates/layout/app.html.eex:53 #, elixir-autogen, elixir-format msgid "Tx/day" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:66 #, elixir-autogen, elixir-format msgid "Txns" msgstr "" +#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 #: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:5 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 +#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:22 #, elixir-autogen, elixir-format msgid "Type" msgstr "" +#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141 #, elixir-autogen, elixir-format msgid "Type of the token standard" msgstr "" +#: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 #: lib/block_scout_web/templates/visualize_sol2uml/index.html.eex:5 #, elixir-autogen, elixir-format msgid "UML diagram" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:560 #: lib/block_scout_web/templates/transaction/overview.html.eex:560 #, elixir-autogen, elixir-format msgid "UTF-8" msgstr "" +#: lib/block_scout_web/views/block_view.ex:77 #: lib/block_scout_web/views/block_view.ex:77 #, elixir-autogen, elixir-format msgid "Uncle Reward" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:252 #: lib/block_scout_web/templates/block/overview.html.eex:252 #: lib/block_scout_web/templates/layout/_topnav.html.eex:41 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:41 #, elixir-autogen, elixir-format msgid "Uncles" msgstr "" +#: lib/block_scout_web/views/transaction_view.ex:367 #: lib/block_scout_web/views/transaction_view.ex:367 #, elixir-autogen, elixir-format msgid "Unconfirmed" msgstr "" +#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 #: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:9 #, elixir-autogen, elixir-format msgid "Unique Token" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:79 #: lib/block_scout_web/templates/transaction/overview.html.eex:79 #, elixir-autogen, elixir-format msgid "Unique character string (TxID) assigned to every verified transaction." msgstr "" #: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/api_key/form.html.eex:7 +#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8 #, elixir-autogen, elixir-format msgid "Update" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:449 #: lib/block_scout_web/templates/transaction/overview.html.eex:449 #, elixir-autogen, elixir-format msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization." msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:459 #: lib/block_scout_web/templates/transaction/overview.html.eex:459 #, elixir-autogen, elixir-format msgid "User-defined tip sent to validator for transaction priority/inclusion." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:226 #: lib/block_scout_web/templates/block/overview.html.eex:226 #, elixir-autogen, elixir-format msgid "User-defined tips sent to validator for transaction priority/inclusion." msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:56 #: lib/block_scout_web/templates/layout/_topnav.html.eex:56 #, elixir-autogen, elixir-format msgid "Validated" msgstr "" +#: lib/block_scout_web/templates/transaction/index.html.eex:12 #: lib/block_scout_web/templates/transaction/index.html.eex:12 #, elixir-autogen, elixir-format msgid "Validated Transactions" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30 #, elixir-autogen, elixir-format msgid "Validator Creation Date" msgstr "" +#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:5 #, elixir-autogen, elixir-format msgid "Validator Data" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:86 #: lib/block_scout_web/templates/address/overview.html.eex:86 #, elixir-autogen, elixir-format msgid "Validator Name" msgstr "" #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:32 +#: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 #: lib/block_scout_web/templates/block_withdrawal/index.html.eex:26 #: lib/block_scout_web/templates/withdrawal/index.html.eex:26 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:26 #, elixir-autogen, elixir-format, fuzzy msgid "Validator index" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:369 #: lib/block_scout_web/templates/transaction/overview.html.eex:369 #, elixir-autogen, elixir-format msgid "Value" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:368 #: lib/block_scout_web/templates/transaction/overview.html.eex:368 #, elixir-autogen, elixir-format msgid "Value sent in the native token (and USD) if applicable." msgstr "" +#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:81 #, elixir-autogen, elixir-format msgid "Verified" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/_stats.html.eex:18 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:6 #, elixir-autogen, elixir-format msgid "Verified Contracts" msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:88 #: lib/block_scout_web/templates/address_contract/index.html.eex:88 #, elixir-autogen, elixir-format msgid "Verified at" msgstr "" +#: lib/block_scout_web/templates/layout/_topnav.html.eex:69 #: lib/block_scout_web/templates/layout/_topnav.html.eex:69 #, elixir-autogen, elixir-format msgid "Verified contracts" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:2 #, elixir-autogen, elixir-format msgid "Verified contracts - %{subnetwork} Explorer" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:7 #, elixir-autogen, elixir-format msgid "Verified contracts, %{subnetwork}, %{coin}" msgstr "" #: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:29 #: lib/block_scout_web/templates/address_contract/index.html.eex:29 #: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:197 +#: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/address_contract/index.html.eex:228 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14 #, elixir-autogen, elixir-format msgid "Verify & Publish" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:111 #: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:37 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:103 #: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 #: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:47 #, elixir-autogen, elixir-format msgid "Verify & publish" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 #, elixir-autogen, elixir-format msgid "Verify the contract " msgstr "" +#: lib/block_scout_web/templates/layout/_footer.html.eex:93 #: lib/block_scout_web/templates/layout/_footer.html.eex:93 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:72 #, elixir-autogen, elixir-format msgid "Version" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:33 #, elixir-autogen, elixir-format msgid "Via Sourcify: Sources and metadata JSON file" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:27 #, elixir-autogen, elixir-format msgid "Via Standard Input JSON" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:22 #, elixir-autogen, elixir-format msgid "Via flattened source code" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:40 #, elixir-autogen, elixir-format msgid "Via multi-part files" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:155 #: lib/block_scout_web/templates/chain/show.html.eex:155 #, elixir-autogen, elixir-format msgid "View All Blocks" msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:215 #: lib/block_scout_web/templates/chain/show.html.eex:215 #, elixir-autogen, elixir-format msgid "View All Transactions" msgstr "" +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 +#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20 #, elixir-autogen, elixir-format msgid "View Contract" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #: lib/block_scout_web/templates/transaction/_tile.html.eex:73 #, elixir-autogen, elixir-format msgid "View Less Transfers" msgstr "" +#: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #: lib/block_scout_web/templates/transaction/_tile.html.eex:72 #, elixir-autogen, elixir-format msgid "View More Transfers" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:39 #: lib/block_scout_web/templates/block/overview.html.eex:39 #, elixir-autogen, elixir-format msgid "View next block" msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:23 #: lib/block_scout_web/templates/block/overview.html.eex:23 #, elixir-autogen, elixir-format msgid "View previous block" msgstr "" +#: lib/block_scout_web/templates/address/_metatags.html.eex:9 #: lib/block_scout_web/templates/address/_metatags.html.eex:9 #, elixir-autogen, elixir-format msgid "View the account balance, transactions, and other data for %{address} on the %{network}" msgstr "" +#: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 #: lib/block_scout_web/templates/withdrawal/_metatags.html.eex:8 #, elixir-autogen, elixir-format msgid "View the beacon chain withdrawals on %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/block/_metatags.html.eex:10 #: lib/block_scout_web/templates/block/_metatags.html.eex:10 #, elixir-autogen, elixir-format msgid "View the transactions, token transfers, and uncles for block number %{block_number}" msgstr "" +#: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 #: lib/block_scout_web/templates/verified_contracts/_metatags.html.eex:8 #, elixir-autogen, elixir-format msgid "View the verified contracts on %{subnetwork}" msgstr "" +#: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 #: lib/block_scout_web/templates/transaction/_metatags.html.eex:10 #, elixir-autogen, elixir-format msgid "View transaction %{transaction} on %{subnetwork}" msgstr "" #: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/_contract.html.eex:28 +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:32 #: lib/block_scout_web/views/verified_contracts_view.ex:11 +#: lib/block_scout_web/views/verified_contracts_view.ex:11 #, elixir-autogen, elixir-format msgid "Vyper" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 #: lib/block_scout_web/templates/address_contract_verification/new.html.eex:46 #, elixir-autogen, elixir-format msgid "Vyper contract" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:142 #, elixir-autogen, elixir-format msgid "WEI" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 #: lib/block_scout_web/templates/smart_contract/_pending_contract_write.html.eex:9 #, elixir-autogen, elixir-format msgid "Waiting for transaction's confirmation..." msgstr "" +#: lib/block_scout_web/templates/chain/show.html.eex:141 #: lib/block_scout_web/templates/chain/show.html.eex:141 #, elixir-autogen, elixir-format msgid "Wallet addresses" msgstr "" +#: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 #: lib/block_scout_web/templates/common_components/_changed_bytecode_warning.html.eex:3 #, elixir-autogen, elixir-format msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky." msgstr "" #: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/common/_nav.html.eex:7 +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:7 #: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 +#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15 #, elixir-autogen, elixir-format msgid "Watch list" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "We recommend using flattened code. This is necessary if your code utilizes a library or inherits dependencies. Use the" msgstr "" +#: lib/block_scout_web/views/wei_helper.ex:80 #: lib/block_scout_web/views/wei_helper.ex:80 #, elixir-autogen, elixir-format msgid "Wei" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:29 #: lib/block_scout_web/templates/address/_tabs.html.eex:29 #: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/address_withdrawal/index.html.eex:13 +#: lib/block_scout_web/templates/block/_tabs.html.eex:13 #: lib/block_scout_web/templates/block/_tabs.html.eex:13 #: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:73 +#: lib/block_scout_web/templates/withdrawal/index.html.eex:5 #: lib/block_scout_web/templates/withdrawal/index.html.eex:5 #, elixir-autogen, elixir-format msgid "Withdrawals" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:106 #, elixir-autogen, elixir-format msgid "Write" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/address/_tabs.html.eex:95 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 -#: lib/block_scout_web/views/address_view.ex:358 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34 +#: lib/block_scout_web/views/address_view.ex:359 +#: lib/block_scout_web/views/address_view.ex:359 #, elixir-autogen, elixir-format msgid "Write Contract" msgstr "" +#: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/address/_tabs.html.eex:102 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 -#: lib/block_scout_web/views/address_view.ex:359 +#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48 +#: lib/block_scout_web/views/address_view.ex:360 +#: lib/block_scout_web/views/address_view.ex:360 #, elixir-autogen, elixir-format msgid "Write Proxy" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_fetch_constructor_args.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_include_nightly_builds_field.html.eex:14 +#: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_common_fields/_yul_contracts_switcher.html.eex:14 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:51 +#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 #: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:43 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" +#: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #: lib/block_scout_web/templates/account/api_key/index.html.eex:18 #, elixir-autogen, elixir-format msgid "You can create 3 API keys per account." msgstr "" +#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 #: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18 #, elixir-autogen, elixir-format msgid "You can create up to 15 Custom ABIs per account." msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11 -#, elixir-autogen, elixir-format -msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days." -msgstr "" - +#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 #: lib/block_scout_web/templates/account/tag_address/index.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have address tags yet" msgstr "" +#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 #: lib/block_scout_web/templates/account/watchlist/show.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have addresses on you watchlist yet" msgstr "" +#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 #: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14 #, elixir-autogen, elixir-format msgid "You don't have transaction tags yet" msgstr "" -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15 -#, elixir-autogen, elixir-format -msgid "Your name" -msgstr "" - -#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14 -#, elixir-autogen, elixir-format -msgid "Your name*" -msgstr "" - +#: lib/block_scout_web/templates/error422/index.html.eex:8 #: lib/block_scout_web/templates/error422/index.html.eex:8 #, elixir-autogen, elixir-format msgid "Your request contained an error, perhaps a mistyped tx/block/address hash. Try again, and check the developer tools console for more info." msgstr "" +#: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 #: lib/block_scout_web/templates/verified_contracts/index.html.eex:38 #: lib/block_scout_web/views/verified_contracts_view.ex:12 +#: lib/block_scout_web/views/verified_contracts_view.ex:12 #, elixir-autogen, elixir-format msgid "Yul" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:111 #: lib/block_scout_web/templates/address/overview.html.eex:111 #, elixir-autogen, elixir-format msgid "at" msgstr "" +#: lib/block_scout_web/templates/address_token/overview.html.eex:52 #: lib/block_scout_web/templates/address_token/overview.html.eex:52 #, elixir-autogen, elixir-format msgid "balance of the address" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:469 #: lib/block_scout_web/templates/transaction/overview.html.eex:469 #, elixir-autogen, elixir-format msgid "burnt for this transaction. Equals Block Base Fee per Gas * Gas Used." msgstr "" +#: lib/block_scout_web/templates/block/overview.html.eex:217 #: lib/block_scout_web/templates/block/overview.html.eex:217 #, elixir-autogen, elixir-format msgid "burnt from transactions included in the block (Base fee (per unit of gas) * Gas Used)." msgstr "" +#: lib/block_scout_web/templates/address_contract/index.html.eex:27 #: lib/block_scout_web/templates/address_contract/index.html.eex:27 #, elixir-autogen, elixir-format msgid "button" msgstr "" +#: lib/block_scout_web/templates/transaction/overview.html.eex:276 #: lib/block_scout_web/templates/transaction/overview.html.eex:276 #, elixir-autogen, elixir-format msgid "created" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:12 #, elixir-autogen, elixir-format msgid "custom RPC" msgstr "" +#: lib/block_scout_web/templates/address/overview.html.eex:151 #: lib/block_scout_web/templates/address/overview.html.eex:151 #, elixir-autogen, elixir-format msgid "doesn't include ERC20, ERC721, ERC1155 tokens)." msgstr "" +#: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #: lib/block_scout_web/templates/common_components/_rap_pagination_container.html.eex:13 #, elixir-autogen, elixir-format msgid "elements are displayed" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:44 #, elixir-autogen, elixir-format msgid "fallback" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:34 +#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:33 #, elixir-autogen, elixir-format msgid "false" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:14 #: lib/block_scout_web/templates/csv_export/index.html.eex:14 #, elixir-autogen, elixir-format msgid "for address" msgstr "" #: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/address_logs/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:12 #: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 +#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:10 #, elixir-autogen, elixir-format msgid "here" msgstr "" +#: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 #: lib/block_scout_web/templates/api_docs/eth_rpc.html.eex:9 #, elixir-autogen, elixir-format msgid "here." msgstr "" +#: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 #: lib/block_scout_web/templates/smart_contract/_function_response.html.eex:3 #, elixir-autogen, elixir-format msgid "method Response" msgstr "" +#: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:41 #, elixir-autogen, elixir-format msgid "of" msgstr "" +#: lib/block_scout_web/templates/layout/app.html.eex:100 #: lib/block_scout_web/templates/layout/app.html.eex:100 #, elixir-autogen, elixir-format msgid "on sign up. Didn’t receive?" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:16 #, elixir-autogen, elixir-format msgid "page" msgstr "" +#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #: lib/block_scout_web/templates/smart_contract/_functions.html.eex:46 #, elixir-autogen, elixir-format msgid "receive" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:81 #: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:70 #, elixir-autogen, elixir-format msgid "required" msgstr "" +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:59 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 +#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:70 #, elixir-autogen, elixir-format msgid "string" msgstr "" +#: lib/block_scout_web/templates/csv_export/index.html.eex:17 #: lib/block_scout_web/templates/csv_export/index.html.eex:17 #, elixir-autogen, elixir-format msgid "to CSV file" msgstr "" -#: lib/block_scout_web/views/address_contract_view.ex:33 +#: lib/block_scout_web/views/address_contract_view.ex:32 +#: lib/block_scout_web/views/address_contract_view.ex:32 #, elixir-autogen, elixir-format msgid "true" msgstr "" +#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:77 #, elixir-autogen, elixir-format msgid "truffle flattener" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs index 339be0b0a2ae..25b1861470e1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -1052,169 +1052,6 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do end end - describe "public tags" do - test "create public tags request", %{conn: conn} do - public_tags_request = build(:public_tags_request) - - post_public_tags_request_response = - conn - |> post( - "/api/account/v2/user/public_tags", - public_tags_request - ) - |> doc(description: "Submit request to add a public tag") - |> json_response(200) - - assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] - assert post_public_tags_request_response["email"] == public_tags_request["email"] - assert post_public_tags_request_response["tags"] == public_tags_request["tags"] - assert post_public_tags_request_response["website"] == public_tags_request["website"] - assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] - assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] - assert post_public_tags_request_response["company"] == public_tags_request["company"] - assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] - assert post_public_tags_request_response["id"] - end - - test "get one public tags requests", %{conn: conn} do - public_tags_request = build(:public_tags_request) - - post_public_tags_request_response = - conn - |> post( - "/api/account/v2/user/public_tags", - public_tags_request - ) - |> json_response(200) - - assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] - assert post_public_tags_request_response["email"] == public_tags_request["email"] - assert post_public_tags_request_response["tags"] == public_tags_request["tags"] - assert post_public_tags_request_response["website"] == public_tags_request["website"] - assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] - assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] - assert post_public_tags_request_response["company"] == public_tags_request["company"] - assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] - assert post_public_tags_request_response["id"] - - assert conn - |> get("/api/account/v2/user/public_tags") - |> json_response(200) - |> Enum.map(&convert_date/1) == - [post_public_tags_request_response] - |> Enum.map(&convert_date/1) - end - - test "get and delete several public tags requests", %{conn: conn} do - public_tags_list = build_list(10, :public_tags_request) - - final_list = - public_tags_list - |> Enum.map(fn request -> - response = - conn - |> post( - "/api/account/v2/user/public_tags", - request - ) - |> json_response(200) - - assert response["full_name"] == request["full_name"] - assert response["email"] == request["email"] - assert response["tags"] == request["tags"] - assert response["website"] == request["website"] - assert response["additional_comment"] == request["additional_comment"] - assert response["addresses"] == request["addresses"] - assert response["company"] == request["company"] - assert response["is_owner"] == request["is_owner"] - assert response["id"] - - convert_date(response) - end) - |> Enum.reverse() - - assert conn - |> get("/api/account/v2/user/public_tags") - |> doc(description: "Get list of requests to add a public tag") - |> json_response(200) - |> Enum.map(&convert_date/1) == final_list - - %{"id" => id} = Enum.at(final_list, 0) - - assert conn - |> delete("/api/account/v2/user/public_tags/#{id}", %{"remove_reason" => "reason"}) - |> doc(description: "Delete public tags request") - |> json_response(200) == %{"message" => "OK"} - - Enum.each(Enum.drop(final_list, 1), fn request -> - assert conn - |> delete("/api/account/v2/user/public_tags/#{request["id"]}", %{"remove_reason" => "reason"}) - |> json_response(200) == %{"message" => "OK"} - end) - - assert conn - |> get("/api/account/v2/user/public_tags") - |> json_response(200) == [] - end - - test "edit public tags request", %{conn: conn} do - public_tags_request = build(:public_tags_request) - - post_public_tags_request_response = - conn - |> post( - "/api/account/v2/user/public_tags", - public_tags_request - ) - |> json_response(200) - - assert post_public_tags_request_response["full_name"] == public_tags_request["full_name"] - assert post_public_tags_request_response["email"] == public_tags_request["email"] - assert post_public_tags_request_response["tags"] == public_tags_request["tags"] - assert post_public_tags_request_response["website"] == public_tags_request["website"] - assert post_public_tags_request_response["additional_comment"] == public_tags_request["additional_comment"] - assert post_public_tags_request_response["addresses"] == public_tags_request["addresses"] - assert post_public_tags_request_response["company"] == public_tags_request["company"] - assert post_public_tags_request_response["is_owner"] == public_tags_request["is_owner"] - assert post_public_tags_request_response["id"] - - assert conn - |> get("/api/account/v2/user/public_tags") - |> json_response(200) - |> Enum.map(&convert_date/1) == - [post_public_tags_request_response] - |> Enum.map(&convert_date/1) - - new_public_tags_request = build(:public_tags_request) - - put_public_tags_request_response = - conn - |> put( - "/api/account/v2/user/public_tags/#{post_public_tags_request_response["id"]}", - new_public_tags_request - ) - |> doc(description: "Edit request to add a public tag") - |> json_response(200) - - assert put_public_tags_request_response["full_name"] == new_public_tags_request["full_name"] - assert put_public_tags_request_response["email"] == new_public_tags_request["email"] - assert put_public_tags_request_response["tags"] == new_public_tags_request["tags"] - assert put_public_tags_request_response["website"] == new_public_tags_request["website"] - assert put_public_tags_request_response["additional_comment"] == new_public_tags_request["additional_comment"] - assert put_public_tags_request_response["addresses"] == new_public_tags_request["addresses"] - assert put_public_tags_request_response["company"] == new_public_tags_request["company"] - assert put_public_tags_request_response["is_owner"] == new_public_tags_request["is_owner"] - assert put_public_tags_request_response["id"] == post_public_tags_request_response["id"] - - assert conn - |> get("/api/account/v2/user/public_tags") - |> json_response(200) - |> Enum.map(&convert_date/1) == - [put_public_tags_request_response] - |> Enum.map(&convert_date/1) - end - end - def convert_date(request) do {:ok, time, _} = DateTime.from_iso8601(request["submission_date"]) %{request | "submission_date" => Calendar.strftime(time, "%b %d, %Y")} diff --git a/apps/explorer/lib/explorer/account.ex b/apps/explorer/lib/explorer/account.ex index ecbfdda5f42f..aa9cd6e79ca7 100644 --- a/apps/explorer/lib/explorer/account.ex +++ b/apps/explorer/lib/explorer/account.ex @@ -9,7 +9,6 @@ defmodule Explorer.Account do alias Explorer.Account.{ CustomABI, Identity, - PublicTagsRequest, TagAddress, TagTransaction, Watchlist, @@ -72,7 +71,6 @@ defmodule Explorer.Account do {Multi.new() |> Key.merge(primary_identity_id, identities_to_merge_ids) |> CustomABI.merge(primary_identity_id, identities_to_merge_ids) - |> PublicTagsRequest.merge(primary_identity_id, identities_to_merge_ids) |> TagAddress.merge(primary_identity_id, identities_to_merge_ids) |> TagTransaction.merge(primary_identity_id, identities_to_merge_ids) |> Watchlist.acquire_for_merge(primary_identity_id, identities_to_merge_ids) diff --git a/apps/explorer/lib/explorer/account/public_tags_request.ex b/apps/explorer/lib/explorer/account/public_tags_request.ex deleted file mode 100644 index a0e37a5ead7c..000000000000 --- a/apps/explorer/lib/explorer/account/public_tags_request.ex +++ /dev/null @@ -1,334 +0,0 @@ -defmodule Explorer.Account.PublicTagsRequest do - @moduledoc """ - Module is responsible for requests for public tags - """ - use Explorer.Schema - - alias Ecto.{Changeset, Multi} - alias Explorer.Account.Identity - alias Explorer.Chain.Hash - alias Explorer.{Helper, Repo} - alias Explorer.ThirdPartyIntegrations.AirTable - - import Ecto.Changeset - - @distance_between_same_addresses 24 * 3600 - - @max_public_tags_request_per_account 15 - @max_addresses_per_request 10 - @max_tags_per_request 2 - @max_tag_length 35 - - @user_not_found "User not found" - - typed_schema "account_public_tags_requests" do - field(:company, :string) - field(:website, :string) - field(:tags, :string, null: false) - field(:addresses, {:array, Hash.Address}, null: false) - field(:description, :string) - field(:additional_comment, :string, null: false) - field(:request_type, :string, null: false) - field(:is_owner, :boolean, default: true, null: false) - field(:remove_reason, :string) - field(:request_id, :string) - field(:full_name, Explorer.Encrypted.Binary, null: false) - field(:email, Explorer.Encrypted.Binary, null: false) - - belongs_to(:identity, Identity, null: false) - - timestamps() - end - - @local_fields [:__meta__, :inserted_at, :updated_at, :id, :request_id] - - def to_map(%__MODULE__{} = request) do - association_fields = request.__struct__.__schema__(:associations) - waste_fields = association_fields ++ @local_fields - - network = Helper.get_app_host() <> Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] - - request |> Map.from_struct() |> Map.drop(waste_fields) |> Map.put(:network, network) - end - - @attrs ~w(company website description remove_reason request_id)a - @required_attrs ~w(full_name email tags addresses additional_comment request_type is_owner identity_id)a - - def changeset(%__MODULE__{} = public_tags_request, attrs \\ %{}) do - public_tags_request - |> cast(trim_empty_addresses(attrs), @attrs ++ @required_attrs) - |> validate_tags() - |> validate_required(@required_attrs, message: "Required") - |> validate_format(:email, ~r/^[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}$/i, message: "is invalid") - |> validate_length(:addresses, min: 1, max: @max_addresses_per_request) - |> extract_and_validate_addresses() - |> foreign_key_constraint(:identity_id, message: @user_not_found) - |> public_tags_request_count_constraint() - |> public_tags_request_time_interval_uniqueness() - end - - def changeset_without_constraints(%__MODULE__{} = public_tags_request \\ %__MODULE__{}, attrs \\ %{}) do - public_tags_request - |> cast(attrs, @attrs ++ @required_attrs) - end - - @doc """ - Creates a new public tags request within a database transaction. - - The creation process involves verifying the existence of the associated identity - and ensuring data consistency through a database lock. The transaction prevents - concurrent modifications of the same identity record. - - ## Parameters - - `attrs`: Map containing the following fields: - - `:identity_id`: Required. The ID of the identity associated with the request - - `:company`: Optional. The company name - - `:website`: Optional. The company's website - - `:tags`: Required. The requested tags - - `:addresses`: Required. List of blockchain addresses - - `:description`: Optional. Description of the request - - `:additional_comment`: Required. Additional information about the request - - `:request_type`: Required. The type of the request - - `:is_owner`: Optional. Boolean indicating ownership (defaults to true) - - `:remove_reason`: Optional. Reason for tag removal if applicable - - `:request_id`: Optional. External request identifier - - `:full_name`: Required. Encrypted full name of the requester - - `:email`: Required. Encrypted email of the requester - - ## Returns - - `{:ok, public_tags_request}` - Returns the created public tags request - - `{:error, changeset}` - Returns a changeset with errors if: - - The identity doesn't exist - - The provided data is invalid - - Required fields are missing - """ - @spec create(map()) :: {:ok, t()} | {:error, Changeset.t()} - def create(%{identity_id: identity_id} = attrs) do - Multi.new() - |> Identity.acquire_with_lock(identity_id) - |> Multi.insert(:public_tags_request, fn _ -> - %__MODULE__{} - |> changeset(Map.put(attrs, :request_type, "add")) - end) - |> Repo.account_repo().transaction() - |> case do - {:ok, %{public_tags_request: public_tags_request}} -> - {:ok, public_tags_request} |> AirTable.submit() - - {:error, :acquire_identity, :not_found, _changes} -> - {:error, - %__MODULE__{} - |> changeset(Map.put(attrs, :request_type, "add")) - |> add_error(:identity_id, @user_not_found, - constraint: :foreign, - constraint_name: "account_public_tags_requests_identity_id_fkey" - )} - end - end - - def create(attrs) do - {:error, - %__MODULE__{} - |> changeset(Map.put(attrs, :request_type, "add"))} - end - - defp trim_empty_addresses(%{addresses: addresses} = attrs) when is_list(addresses) do - filtered_addresses = Enum.filter(addresses, fn addr -> addr != "" and !is_nil(addr) end) - Map.put(attrs, :addresses, if(filtered_addresses == [], do: [""], else: filtered_addresses)) - end - - defp trim_empty_addresses(attrs), do: attrs - - def public_tags_request_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = request) do - if identity_id - |> public_tags_requests_by_identity_id_query() - |> limit(@max_public_tags_request_per_account) - |> Repo.account_repo().aggregate(:count, :id) >= @max_public_tags_request_per_account do - request - |> add_error(:tags, "Max #{@max_public_tags_request_per_account} public tags requests per account") - else - request - end - end - - def public_tags_request_count_constraint(changeset), do: changeset - - defp public_tags_request_time_interval_uniqueness(%Changeset{changes: %{addresses: addresses}} = request) do - prepared_addresses = - if request.data && request.data.addresses, do: addresses -- request.data.addresses, else: addresses - - public_tags_request = - request - |> fetch_field!(:identity_id) - |> public_tags_requests_by_identity_id_query() - |> where( - [public_tags_request], - fragment("? && ?", public_tags_request.addresses, ^Enum.map(prepared_addresses, fn x -> x.bytes end)) - ) - |> limit(1) - |> Repo.account_repo().one() - - now = DateTime.utc_now() - - if !is_nil(public_tags_request) && - public_tags_request.inserted_at - |> DateTime.add(@distance_between_same_addresses, :second) - |> DateTime.compare(now) == :gt do - request - |> add_error(:addresses, "You have already submitted the same public tag address in the last 24 hours") - else - request - end - end - - defp public_tags_request_time_interval_uniqueness(changeset), do: changeset - - defp extract_and_validate_addresses(%Changeset{} = changeset) do - with {:fetch, {_src, addresses}} <- {:fetch, fetch_field(changeset, :addresses)}, - false <- is_nil(addresses), - {:uniqueness, true} <- {:uniqueness, Enum.count(Enum.uniq(addresses)) == Enum.count(addresses)} do - changeset - else - {:uniqueness, false} -> - add_error(changeset, :addresses, "All addresses should be unique") - - _ -> - add_error(changeset, :addresses, "No addresses") - end - end - - defp validate_tags(%Changeset{} = changeset) do - with {:fetch, {_src, tags}} <- {:fetch, fetch_field(changeset, :tags)}, - false <- is_nil(tags), - trimmed_tags <- String.trim(tags), - tags_list <- String.split(trimmed_tags, ";"), - {:filter_empty, [_ | _] = filtered_tags} <- {:filter_empty, Enum.filter(tags_list, fn tag -> tag != "" end)}, - trimmed_spaces_tags <- Enum.map(filtered_tags, fn tag -> String.trim(tag) end), - {:validate, false} <- {:validate, Enum.any?(tags_list, fn tag -> String.length(tag) > @max_tag_length end)}, - {:uniqueness, true} <- - {:uniqueness, - Enum.count(Enum.uniq_by(trimmed_spaces_tags, &String.downcase(&1))) == Enum.count(trimmed_spaces_tags)}, - trimmed_tags_list <- Enum.take(trimmed_spaces_tags, @max_tags_per_request) do - force_change(changeset, :tags, Enum.join(trimmed_tags_list, ";")) - else - {:uniqueness, false} -> - add_error(changeset, :tags, "All tags should be unique") - - {:filter_empty, _} -> - add_error(changeset, :tags, "All tags are empty strings") - - {:validate, _} -> - add_error(changeset, :tags, "Tags should contain less than #{@max_tag_length} characters") - - _ -> - add_error(changeset, :tags, "No tags") - end - end - - def public_tags_requests_by_identity_id_query(id) when not is_nil(id) do - __MODULE__ - |> where( - [request], - request.identity_id == ^id and request.request_type != "delete" and not is_nil(request.request_id) - ) - |> order_by([request], desc: request.id) - end - - def public_tags_requests_by_identity_id_query(_), do: nil - - def public_tags_request_by_id_and_identity_id_query(id, identity_id) - when not is_nil(id) and not is_nil(identity_id) do - __MODULE__ - |> where([public_tags_request], public_tags_request.identity_id == ^identity_id and public_tags_request.id == ^id) - end - - def public_tags_request_by_id_and_identity_id_query(_, _), do: nil - - def get_public_tags_request_by_id_and_identity_id(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do - id |> public_tags_request_by_id_and_identity_id_query(identity_id) |> Repo.account_repo().one() - end - - def get_public_tags_request_by_id_and_identity_id(_, _), do: nil - - def get_public_tags_requests_by_identity_id(id) when not is_nil(id) do - id - |> public_tags_requests_by_identity_id_query() - |> Repo.account_repo().all() - end - - def get_public_tags_requests_by_identity_id(_), do: nil - - def delete_public_tags_request(identity_id, id) when not is_nil(id) and not is_nil(identity_id) do - id - |> public_tags_request_by_id_and_identity_id_query(identity_id) - |> Repo.account_repo().delete_all() - end - - def delete_public_tags_request(_, _), do: nil - - def update(%{id: id, identity_id: identity_id} = attrs) do - with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id), - false <- is_nil(public_tags_request), - {:ok, changeset} <- - public_tags_request |> changeset(Map.put(attrs, :request_type, "edit")) |> Repo.account_repo().update() do - AirTable.submit({:ok, changeset}) - else - true -> - {:error, %{reason: :item_not_found}} - - other -> - other - end - end - - def mark_as_deleted_public_tags_request(%{id: id, identity_id: identity_id, remove_reason: remove_reason}) do - with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id), - false <- is_nil(public_tags_request), - {:ok, changeset} <- - public_tags_request - |> changeset_without_constraints(%{request_type: "delete", remove_reason: remove_reason}) - |> Repo.account_repo().update() do - case AirTable.submit({:ok, changeset}) do - {:error, changeset} -> - changeset - - _ -> - true - end - else - {:error, changeset} -> - changeset - - _ -> - false - end - end - - def get_max_public_tags_request_count, do: @max_public_tags_request_per_account - - @doc """ - Merges public tags requests from multiple identities into a primary identity. - - This function updates the `identity_id` of all public tags requests belonging to the - identities specified in `ids_to_merge` to the `primary_id`. It's designed to - be used as part of an Ecto.Multi transaction. - - ## Parameters - - `multi`: An Ecto.Multi struct to which this operation will be added. - - `primary_id`: The ID of the primary identity that will own the merged keys. - - `ids_to_merge`: A list of identity IDs whose public tags requests will be merged. - - ## Returns - - An updated Ecto.Multi struct with the merge operation added. - """ - @spec merge(Multi.t(), integer(), [integer()]) :: Multi.t() - def merge(multi, primary_id, ids_to_merge) do - Multi.run(multi, :merge_public_tags_requests, fn repo, _ -> - {:ok, - repo.update_all( - from(key in __MODULE__, where: key.identity_id in ^ids_to_merge), - set: [identity_id: primary_id] - )} - end) - end -end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex index 124255b920c9..f383540e49ea 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.SmartContract.AuditReport do alias Explorer.{Chain, Helper, Repo} alias Explorer.Chain.Hash - alias Explorer.ThirdPartyIntegrations.AirTable + alias Explorer.ThirdPartyIntegrations.AirTableAuditReport @max_reports_per_day_for_contract 5 @@ -108,7 +108,7 @@ defmodule Explorer.Chain.SmartContract.AuditReport do def create(attrs) do %__MODULE__{} |> changeset(attrs) - |> AirTable.submit() + |> AirTableAuditReport.submit() |> Repo.insert() end diff --git a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex b/apps/explorer/lib/explorer/third_party_integrations/airtable_audit_report.ex similarity index 54% rename from apps/explorer/lib/explorer/third_party_integrations/airtable.ex rename to apps/explorer/lib/explorer/third_party_integrations/airtable_audit_report.ex index 401e9674ac88..3e0c7ce5ae9f 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/airtable_audit_report.ex @@ -1,49 +1,18 @@ -defmodule Explorer.ThirdPartyIntegrations.AirTable do +defmodule Explorer.ThirdPartyIntegrations.AirTableAuditReport do @moduledoc """ - Module is responsible for submitting requests for public tags and audit reports to AirTable + Module is responsible for submitting audit reports to AirTable """ require Logger alias Ecto.Changeset - alias Explorer.Account.PublicTagsRequest alias Explorer.Chain.SmartContract.AuditReport - alias Explorer.{HttpClient, Repo} + alias Explorer.HttpClient @doc """ Submits a public tags request or audit report to AirTable """ - @spec submit({:ok, PublicTagsRequest.t()} | {:error, Changeset.t()} | Changeset.t()) :: - {:ok, PublicTagsRequest.t()} | {:error, Changeset.t()} | Changeset.t() - def submit({:ok, %PublicTagsRequest{} = new_request} = input) do - if Mix.env() == :test do - new_request - |> PublicTagsRequest.changeset(%{request_id: "123"}) - |> Repo.account_repo().update() - - input - else - submit_entry( - PublicTagsRequest.to_map(new_request), - :air_table_public_tags, - fn request_id -> - new_request - |> PublicTagsRequest.changeset(%{request_id: request_id}) - |> Repo.account_repo().update() - - input - end, - fn -> - {:error, - %{ - (%PublicTagsRequest{} - |> PublicTagsRequest.changeset_without_constraints(PublicTagsRequest.to_map(new_request)) - |> Changeset.add_error(:full_name, "AirTable error. Please try again later")) - | action: :insert - }} - end - ) - end - end + @spec submit(Changeset.t()) :: Changeset.t() + @spec submit({:ok, AuditReport.t()} | {:error, Changeset.t()}, Changeset.t()) :: Changeset.t() def submit(%Changeset{} = changeset), do: submit(Changeset.apply_action(changeset, :insert), changeset) diff --git a/apps/explorer/priv/account/migrations/20250815115250_drop_account_public_tags_requests.exs b/apps/explorer/priv/account/migrations/20250815115250_drop_account_public_tags_requests.exs new file mode 100644 index 000000000000..28386f20bde3 --- /dev/null +++ b/apps/explorer/priv/account/migrations/20250815115250_drop_account_public_tags_requests.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Account.Migrations.DropAccountPublicTagsRequests do + use Ecto.Migration + + def change do + drop(table(:account_public_tags_requests)) + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 204699f8f70f..73aad9512c26 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -196,19 +196,6 @@ defmodule Explorer.Factory do %{"contract_address_hash" => contract_address_hash, "name" => sequence("test"), "abi" => contract_code_info().abi} end - def public_tags_request_factory do - %{ - "full_name" => sequence("full name"), - "email" => sequence(:email, &"test_user-#{&1}@blockscout.com"), - "tags" => Enum.join(Enum.map(1..Enum.random(1..2), fn _ -> sequence("Tag") end), ";"), - "website" => sequence("website"), - "additional_comment" => sequence("additional_comment"), - "addresses" => Enum.map(1..Enum.random(1..10), fn _ -> to_string(build(:address).hash) end), - "company" => sequence("company"), - "is_owner" => random_bool() - } - end - def account_watchlist_factory do %Watchlist{ identity: build(:account_identity) diff --git a/config/runtime.exs b/config/runtime.exs index b2d9dc4dfbae..2939fc524566 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -665,14 +665,8 @@ config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), service_url: System.get_env("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") -config :explorer, :air_table_public_tags, - table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"), - api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") - audit_reports_table_url = System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_URL") - -audit_reports_api_key = - System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY") || System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY") +audit_reports_api_key = System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY") config :explorer, :air_table_audit_reports, table_url: audit_reports_table_url, diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 4d6539313a13..38ccf8567457 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -454,8 +454,6 @@ DECODE_NOT_A_CONTRACT_CALLS=true # ACCOUNT_AUTH0_CLIENT_ID= # ACCOUNT_AUTH0_CLIENT_SECRET= # ACCOUNT_AUTH0_APPLICATION_ID= -# ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL= -# ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY= # ACCOUNT_SENDGRID_API_KEY= # ACCOUNT_SENDGRID_SENDER= # ACCOUNT_SENDGRID_TEMPLATE= From 5a888983c7fb76c6fb2e157a94d591fbf4458923 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 19 Aug 2025 17:30:13 +0300 Subject: [PATCH 0252/1006] chore: Refactor usage of delete_parameters_from_next_page_params/1 (#13005) * chore: Refactor usage of delete_parameters_from_next_page_params/1 * Process review comments --- .../lib/block_scout_web/chain.ex | 19 ++++++++++----- .../account/api/v2/user_controller.ex | 8 +++---- .../controllers/api/v2/address_controller.ex | 24 +++++++++---------- .../controllers/api/v2/block_controller.ex | 15 ++++++------ .../controllers/api/v2/celo_controller.ex | 2 -- .../api/v2/internal_transaction_controller.ex | 7 +----- .../controllers/api/v2/optimism_controller.ex | 7 +----- .../controllers/api/v2/scroll_controller.ex | 7 +----- .../api/v2/smart_contract_controller.ex | 3 +-- .../controllers/api/v2/token_controller.ex | 17 +++++++------ .../api/v2/token_transfer_controller.ex | 3 +-- .../api/v2/transaction_controller.ex | 19 +++++++-------- .../api/v2/validator_controller.ex | 7 +++--- .../api/v2/withdrawal_controller.ex | 3 +-- 14 files changed, 60 insertions(+), 81 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index a9c03301bea2..032e1274bce9 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -19,6 +19,8 @@ defmodule BlockScoutWeb.Chain do page_size: 0 ] + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + import Explorer.Helper, only: [parse_boolean: 1, parse_integer: 1] alias BlockScoutWeb.PagingHelper @@ -149,7 +151,12 @@ defmodule BlockScoutWeb.Chain do string_keys = map_to_string_keys(paging_params) - next_page_params = Map.merge(params |> Map.drop(string_keys), paging_params) + next_page_params = + params + |> delete_parameters_from_next_page_params() + |> Map.drop(string_keys) + |> Map.merge(paging_params) + current_items_count_string = Map.get(next_page_params, "items_count") items_count = @@ -977,6 +984,7 @@ defmodule BlockScoutWeb.Chain do string_keys = map_to_string_keys(new_params) params + |> delete_parameters_from_next_page_params() |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]) |> Map.merge(new_params) |> Map.merge(%{ @@ -990,11 +998,10 @@ defmodule BlockScoutWeb.Chain do string_keys = map_to_string_keys(new_params) - Map.merge( - params - |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]), - new_params - ) + params + |> delete_parameters_from_next_page_params() + |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]) + |> Map.merge(new_params) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex index 4495234a7bd3..d3b82cf0263a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex @@ -11,8 +11,6 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do split_list_by_page: 1 ] - import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] - alias Explorer.Account.Api.Key, as: ApiKey alias Explorer.Account.CustomABI alias Explorer.Account.{Identity, TagAddress, TagTransaction, WatchlistAddress} @@ -53,7 +51,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do {watchlist_addresses, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> next_page_params(watchlist_addresses, delete_parameters_from_next_page_params(params)) + next_page |> next_page_params(watchlist_addresses, params) watchlist_addresses_prepared = Enum.map(watchlist_addresses, fn wa -> @@ -230,7 +228,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do {tags, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(tags, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(tags, params) conn |> put_status(200) @@ -289,7 +287,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do {tags, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(tags, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(tags, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index e516a813e1c7..6f9b620443ef 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -19,7 +19,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do import BlockScoutWeb.PagingHelper, only: [ addresses_sorting: 1, - delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1, address_transactions_sorting: 1, nft_types_options: 1 @@ -374,7 +373,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page |> next_page_params( transactions, - delete_parameters_from_next_page_params(params), + params, &Transaction.address_transactions_next_page_params/1 ) @@ -476,7 +475,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page_params = next_page - |> token_transfers_next_page_params(token_transfers, delete_parameters_from_next_page_params(params)) + |> token_transfers_next_page_params(token_transfers, params) conn |> put_status(200) @@ -561,7 +560,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {internal_transactions, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> next_page_params(internal_transactions, delete_parameters_from_next_page_params(params)) + next_page |> next_page_params(internal_transactions, params) conn |> put_status(200) @@ -636,7 +635,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {logs, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(logs, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(logs, params) conn |> put_status(200) @@ -710,7 +709,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) {blocks, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(blocks, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(blocks, params) conn |> put_status(200) @@ -769,7 +768,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {coin_balances, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> next_page_params(coin_balances, delete_parameters_from_next_page_params(params)) + next_page |> next_page_params(coin_balances, params) conn |> put_status(200) @@ -904,7 +903,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page |> next_page_params( tokens, - delete_parameters_from_next_page_params(params), + params, &paging_params_with_fiat_value/1 ) @@ -961,7 +960,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do withdrawals_plus_one = address_hash |> Chain.address_hash_to_withdrawals(options) {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) - next_page_params = next_page |> next_page_params(withdrawals, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(withdrawals, params) conn |> put_status(200) @@ -1040,7 +1039,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> Address.list_top_addresses() |> split_list_by_page() - next_page_params = next_page_params(next_page, addresses, delete_parameters_from_next_page_params(params)) + next_page_params = next_page_params(next_page, addresses, params) exchange_rate = Market.get_coin_exchange_rate() total_supply = Chain.total_supply() @@ -1190,7 +1189,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page |> next_page_params( nfts, - delete_parameters_from_next_page_params(params), + params, &Instance.nft_list_next_page_params/1 ) @@ -1266,7 +1265,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page |> next_page_params( collections, - delete_parameters_from_next_page_params(params), + params, &Instance.nft_collections_next_page_params/1 ) @@ -1326,7 +1325,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do filtered_params = params - |> delete_parameters_from_next_page_params() |> Map.drop([ "epoch_number", "amount", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index d26ed059132b..08dc3deda324 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -15,7 +15,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do import BlockScoutWeb.PagingHelper, only: [ - delete_parameters_from_next_page_params: 1, select_block_type: 1, type_filter_options: 1, internal_transaction_type_options: 1, @@ -173,7 +172,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do {blocks, next_page} = split_list_by_page(blocks_plus_one) - next_page_params = next_page |> next_page_params(blocks, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(blocks, params) conn |> put_status(200) @@ -199,7 +198,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> ArbitrumSettlementReader.batch_blocks(full_options) |> split_list_by_page() - next_page_params = next_page |> next_page_params(blocks, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(blocks, params) conn |> put_status(200) @@ -226,7 +225,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> OptimismTransactionBatch.batch_blocks(full_options) |> split_list_by_page() - next_page_params = next_page |> next_page_params(blocks, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(blocks, params) conn |> put_status(200) @@ -253,7 +252,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> ScrollReader.batch_blocks(full_options) |> split_list_by_page() - next_page_params = next_page |> next_page_params(blocks, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(blocks, params) conn |> put_status(200) @@ -284,7 +283,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params = next_page - |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + |> next_page_params(transactions, params) conn |> put_status(200) @@ -324,7 +323,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page |> next_page_params( internal_transactions, - delete_parameters_from_next_page_params(params), + params, &InternalTransaction.internal_transaction_to_block_paging_options/1 ) @@ -360,7 +359,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do withdrawals_plus_one = Chain.block_to_withdrawals(block.hash, full_options) {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) - next_page_params = next_page |> next_page_params(withdrawals, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(withdrawals, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index cbf7e152de16..1d99c7ab76b9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -10,7 +10,6 @@ defmodule BlockScoutWeb.API.V2.CeloController do ] import Explorer.PagingOptions, only: [default_paging_options: 0] - import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] alias Explorer.Chain.Hash alias Explorer.Chain.Celo.{ElectionReward, Epoch} @@ -47,7 +46,6 @@ defmodule BlockScoutWeb.API.V2.CeloController do filtered_params = params - |> delete_parameters_from_next_page_params() |> Map.drop(["number"]) next_page_params = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index f936c03e135b..39e9df1bb035 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -11,11 +11,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do next_page_params: 3 ] - import BlockScoutWeb.PagingHelper, - only: [ - delete_parameters_from_next_page_params: 1 - ] - import Explorer.PagingOptions, only: [default_paging_options: 0] action_fallback(BlockScoutWeb.API.V2.FallbackController) @@ -42,7 +37,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do {internal_transactions, next_page} = result next_page_params = - next_page |> next_page_params(internal_transactions, delete_parameters_from_next_page_params(params)) + next_page |> next_page_params(internal_transactions, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 69fe84d2e9cb..090b76ba1766 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -10,11 +10,6 @@ defmodule BlockScoutWeb.API.V2.OptimismController do split_list_by_page: 1 ] - import BlockScoutWeb.PagingHelper, - only: [ - delete_parameters_from_next_page_params: 1 - ] - import Explorer.Helper, only: [hash_to_binary: 1] alias BlockScoutWeb.API.V2.ApiView @@ -54,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do |> TransactionBatch.list() |> split_list_by_page() - next_page_params = next_page_params(next_page, batches, delete_parameters_from_next_page_params(params)) + next_page_params = next_page_params(next_page, batches, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/scroll_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/scroll_controller.ex index 1374369284ad..e62b0ebd4dca 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/scroll_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/scroll_controller.ex @@ -8,11 +8,6 @@ defmodule BlockScoutWeb.API.V2.ScrollController do split_list_by_page: 1 ] - import BlockScoutWeb.PagingHelper, - only: [ - delete_parameters_from_next_page_params: 1 - ] - alias Explorer.Chain.Scroll.Reader @api_true [api?: true] @@ -55,7 +50,7 @@ defmodule BlockScoutWeb.API.V2.ScrollController do |> Reader.batches() |> split_list_by_page() - next_page_params = next_page_params(next_page, batches, delete_parameters_from_next_page_params(params)) + next_page_params = next_page_params(next_page, batches, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 9c770fa66d51..0d984065f948 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -14,7 +14,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do import BlockScoutWeb.PagingHelper, only: [ current_filter: 1, - delete_parameters_from_next_page_params: 1, search_query: 1 ] @@ -144,7 +143,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do next_page |> next_page_params( addresses, - delete_parameters_from_next_page_params(params), + params, pager ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index dc16a127dee1..9a895ff3c191 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -25,7 +25,6 @@ defmodule BlockScoutWeb.API.V2.TokenController do import BlockScoutWeb.PagingHelper, only: [ chain_ids_filter_options: 1, - delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1, tokens_sorting: 1 ] @@ -100,7 +99,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do next_page_params = next_page - |> token_transfers_next_page_params(token_transfers, delete_parameters_from_next_page_params(params)) + |> token_transfers_next_page_params(token_transfers, params) conn |> put_status(200) @@ -122,7 +121,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {token_balances, next_page} = split_list_by_page(results_plus_one) - next_page_params = next_page |> next_page_params(token_balances, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(token_balances, params) conn |> put_status(200) @@ -160,7 +159,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {token_instances, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> unique_tokens_next_page(token_instances, delete_parameters_from_next_page_params(params)) + next_page |> unique_tokens_next_page(token_instances, params) conn |> put_status(200) @@ -191,7 +190,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {token_instances, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> unique_tokens_next_page(token_instances, delete_parameters_from_next_page_params(params)) + next_page |> unique_tokens_next_page(token_instances, params) conn |> put_status(200) @@ -269,7 +268,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do next_page_params = next_page - |> token_transfers_next_page_params(token_transfers, delete_parameters_from_next_page_params(params)) + |> token_transfers_next_page_params(token_transfers, params) conn |> put_status(200) @@ -300,7 +299,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do next_page_params = next_page - |> next_page_params(token_holders, delete_parameters_from_next_page_params(params)) + |> next_page_params(token_holders, params) conn |> put_status(200) @@ -347,7 +346,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {tokens, next_page} = filter |> Token.list_top(options) |> split_list_by_page() - next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(tokens, params) conn |> put_status(200) @@ -366,7 +365,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {tokens, next_page} = filter |> BridgedToken.list_top_bridged_tokens(options) |> split_list_by_page() - next_page_params = next_page |> next_page_params(tokens, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(tokens, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index 3141ff298331..a1176bf90bdb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -13,7 +13,6 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do import BlockScoutWeb.PagingHelper, only: [ - delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1 ] @@ -72,7 +71,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do |> Enum.into(%{}, fn {%{hash: hash}, decoded_input} -> {hash, decoded_input} end) next_page_params = - next_page |> token_transfers_next_page_params(token_transfers, delete_parameters_from_next_page_params(params)) + next_page |> token_transfers_next_page_params(token_transfers, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index ac6f140048a1..7d1731cd867c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -17,7 +17,6 @@ defmodule BlockScoutWeb.API.V2.TransactionController do import BlockScoutWeb.PagingHelper, only: [ - delete_parameters_from_next_page_params: 1, paging_options: 2, filter_options: 2, method_filter_options: 1, @@ -209,7 +208,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(transactions, params) conn |> put_status(200) @@ -359,7 +358,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(transactions, params) conn |> put_status(200) @@ -403,7 +402,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do |> Chain.hashes_to_transactions(full_options) {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(transactions, params) conn |> put_status(200) @@ -426,7 +425,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + |> next_page_params(transactions, params) conn |> put_status(200) @@ -489,7 +488,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> token_transfers_next_page_params(token_transfers, delete_parameters_from_next_page_params(params)) + |> token_transfers_next_page_params(token_transfers, params) conn |> put_status(200) @@ -519,7 +518,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> next_page_params(internal_transactions, delete_parameters_from_next_page_params(params)) + |> next_page_params(internal_transactions, params) conn |> put_status(200) @@ -551,7 +550,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> next_page_params(logs, delete_parameters_from_next_page_params(params)) + |> next_page_params(logs, params) conn |> put_status(200) @@ -582,7 +581,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> next_page_params(state_changes, delete_parameters_from_next_page_params(params)) + |> next_page_params(state_changes, params) conn |> put_status(200) @@ -607,7 +606,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {transactions, next_page} = split_list_by_page(transactions_plus_one) - next_page_params = next_page |> next_page_params(transactions, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(transactions, params) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex index 66a6ed81398b..63674c20080e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex @@ -13,7 +13,6 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do import BlockScoutWeb.PagingHelper, only: [ - delete_parameters_from_next_page_params: 1, stability_validators_state_options: 1, validators_blackfort_sorting: 1, validators_stability_sorting: 1 @@ -52,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do next_page |> next_page_params( validators, - delete_parameters_from_next_page_params(params), + params, &ValidatorStability.next_page_params/1 ) @@ -102,7 +101,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do next_page |> next_page_params( validators, - delete_parameters_from_next_page_params(params), + params, &ValidatorBlackfort.next_page_params/1 ) @@ -171,7 +170,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do next_page |> next_page_params( validators, - delete_parameters_from_next_page_params(params), + params, &ValidatorZilliqa.next_page_params/1 ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index b374a84a4ae7..83b009cee4bc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -4,7 +4,6 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] - import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] @@ -24,7 +23,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do withdrawals_plus_one = Chain.list_withdrawals(full_options) {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) - next_page_params = next_page |> next_page_params(withdrawals, delete_parameters_from_next_page_params(params)) + next_page_params = next_page |> next_page_params(withdrawals, params) conn |> put_status(200) From 092b00fbaef11353e9ae69cbbac3e09241bdc0a1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 19 Aug 2025 18:06:04 +0300 Subject: [PATCH 0253/1006] chore: Route left API DB requests from master to read DB replica (#12896) * chore: Route left API DB requests from master to read DB replica * Process review comments --- .../controllers/api/v2/mud_controller.ex | 4 +- .../views/api/v2/optimism_view.ex | 6 +- apps/explorer/lib/explorer/chain.ex | 75 +--------------- .../csv_export/address/token_transfers.ex | 2 +- apps/explorer/lib/explorer/chain/mud.ex | 4 +- .../lib/explorer/chain/optimism/withdrawal.ex | 27 +++--- apps/explorer/lib/explorer/etherscan.ex | 10 ++- apps/explorer/test/explorer/chain_test.exs | 88 ------------------- ...epochs_test.ex => celo_l2_epochs_test.exs} | 0 9 files changed, 31 insertions(+), 185 deletions(-) rename apps/explorer/test/explorer/migrator/{celo_l2_epochs_test.ex => celo_l2_epochs_test.exs} (100%) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex index bff0df4e29b1..4e998ba106ad 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex @@ -156,7 +156,7 @@ defmodule BlockScoutWeb.API.V2.MudController do {records, next_page} = world |> Mud.world_table_records(table_id, options) |> split_list_by_page() - blocks = Mud.preload_records_timestamps(records) + blocks = Mud.preload_records_timestamps(records, @api_true) next_page_params = next_page_params(next_page, records, conn.query_params, fn item -> @@ -207,7 +207,7 @@ defmodule BlockScoutWeb.API.V2.MudController do {:format, {:ok, record_id}} <- {:format, Data.cast(record_id_param)}, {:ok, schema} <- Mud.world_table_schema(world, table_id), {:ok, record} <- Mud.world_table_record(world, table_id, record_id) do - blocks = Mud.preload_records_timestamps([record]) + blocks = Mud.preload_records_timestamps([record], @api_true) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 830765ae1965..d1a7d4df2987 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -8,6 +8,8 @@ defmodule BlockScoutWeb.API.V2.OptimismView do alias Explorer.Chain.{Block, Transaction} alias Explorer.Chain.Optimism.{DisputeGame, FrameSequence, FrameSequenceBlob, InteropMessage, Withdrawal} + @api_true [api?: true] + @doc """ Function to render GET requests to `/api/v2/optimism/txn-batches` endpoint. """ @@ -179,7 +181,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do next_page_params: next_page_params, conn: conn }) do - respected_games = Withdrawal.respected_games() + respected_games = Withdrawal.respected_games(@api_true) %{ items: @@ -209,7 +211,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do _ -> {nil, nil} end - {status, challenge_period_end} = Withdrawal.status(w, respected_games) + {status, challenge_period_end} = Withdrawal.status(w, respected_games, @api_true) %{ "msg_nonce_raw" => Decimal.to_string(w.msg_nonce, :normal), diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 0a2e72d9e6bf..ffd57417f398 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -24,7 +24,7 @@ defmodule Explorer.Chain do where: 3 ] - import EthereumJSONRPC, only: [integer_to_quantity: 1, fetch_block_internal_transactions: 2] + import EthereumJSONRPC, only: [fetch_block_internal_transactions: 2] require Logger @@ -787,30 +787,6 @@ defmodule Explorer.Chain do end end - @spec token_contract_address_from_token_name(String.t()) :: {:ok, Hash.Address.t()} | {:error, :not_found} - def token_contract_address_from_token_name(name) when is_binary(name) do - query = - from(token in Token, - where: ilike(token.symbol, ^name), - or_where: ilike(token.name, ^name), - select: token.contract_address_hash - ) - - query - |> Repo.all() - |> case do - [] -> - {:error, :not_found} - - hashes -> - if Enum.count(hashes) == 1 do - {:ok, List.first(hashes)} - else - {:error, :not_found} - end - end - end - @doc """ Converts `t:Explorer.Chain.Address.t/0` `hash` to the `t:Explorer.Chain.Address.t/0` with that `hash`. @@ -2607,42 +2583,6 @@ defmodule Explorer.Chain do end end - @doc """ - Checks if an address is a contract - """ - @spec contract_address?(String.t(), non_neg_integer(), Keyword.t()) :: boolean() | :json_rpc_error - def contract_address?(address_hash, block_number, json_rpc_named_arguments \\ []) do - {:ok, binary_hash} = Explorer.Chain.Hash.Address.cast(address_hash) - - query = Address.address_query(binary_hash) - - address = Repo.one(query) - - cond do - is_nil(address) -> - block_quantity = integer_to_quantity(block_number) - - case EthereumJSONRPC.fetch_codes( - [%{block_quantity: block_quantity, address: address_hash}], - json_rpc_named_arguments - ) do - {:ok, %EthereumJSONRPC.FetchedCodes{params_list: fetched_codes}} -> - result = List.first(fetched_codes) - - result && !(is_nil(result[:code]) || result[:code] == "" || result[:code] == "0x") - - _ -> - :json_rpc_error - end - - is_nil(address.contract_code) -> - false - - true -> - true - end - end - @doc """ Fetches contract creation input data from the transaction (not internal transaction). """ @@ -3025,13 +2965,6 @@ defmodule Explorer.Chain do Repo.exists?(query) end - @spec address_tokens_with_balance(Hash.Address.t(), [any()]) :: [] - def address_tokens_with_balance(address_hash, paging_options \\ []) do - address_hash - |> Address.Token.list_address_tokens_with_balance(paging_options) - |> Repo.all() - end - @spec find_and_update_replaced_transactions([ %{ required(:nonce) => non_neg_integer, @@ -3252,12 +3185,6 @@ defmodule Explorer.Chain do end end - def get_token_ids_1155(contract_address_hash) do - contract_address_hash - |> CurrentTokenBalance.token_ids_query() - |> Repo.all() - end - @spec data() :: Dataloader.Ecto.t() def data, do: DataloaderEcto.new(Repo) diff --git a/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex b/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex index b347cc3e9b09..f70f4696d9e3 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex @@ -132,7 +132,7 @@ defmodule Explorer.Chain.CsvExport.Address.TokenTransfers do |> handle_token_transfer_paging_options(paging_options) |> preload(^DenormalizationHelper.extend_transaction_preload([:transaction])) |> preload(:token) - |> Repo.all() + |> Repo.replica().all() end end diff --git a/apps/explorer/lib/explorer/chain/mud.ex b/apps/explorer/lib/explorer/chain/mud.ex index 77f3cfee69d8..dc70e0467285 100644 --- a/apps/explorer/lib/explorer/chain/mud.ex +++ b/apps/explorer/lib/explorer/chain/mud.ex @@ -361,13 +361,13 @@ defmodule Explorer.Chain.Mud do Returns a map of block numbers to timestamps. """ @spec preload_records_timestamps([Mud.t()]) :: %{non_neg_integer() => DateTime.t()} - def preload_records_timestamps(records) do + def preload_records_timestamps(records, options \\ []) do block_numbers = records |> Enum.map(&(&1.block_number |> Decimal.to_integer())) |> Enum.uniq() Block |> where([b], b.number in ^block_numbers) |> select([b], {b.number, b.timestamp}) - |> Repo.all() + |> Chain.select_repo(options).all() |> Enum.into(%{}) end diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex index 5850d1d8d88d..c2eff65dfdad 100644 --- a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex @@ -6,6 +6,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] alias Explorer.Application.Constants + alias Explorer.Chain alias Explorer.Chain.{Block, Hash, Transaction} alias Explorer.Chain.Cache.OptimismFinalizationPeriod alias Explorer.Chain.Optimism.{DisputeGame, OutputRoot, WithdrawalEvent} @@ -27,6 +28,8 @@ defmodule Explorer.Chain.Optimism.Withdrawal do @required_attrs ~w(msg_nonce hash l2_transaction_hash l2_block_number)a @game_fields ~w(created_at resolved_at status)a + @api_true [api?: true] + @typedoc """ * `msg_nonce` - A nonce of the withdrawal message. * `hash` - A withdrawal hash. @@ -158,7 +161,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF ) - {status, _} = status(w) + {status, _} = status(w, nil, @api_true) {msg_nonce, status, w.l1_transaction_hash} end) end @@ -173,9 +176,9 @@ defmodule Explorer.Chain.Optimism.Withdrawal do ## Parameters - `w`: A map with the withdrawal info. - - `respected_games`: A list of games returned by the `respected_games()` function. + - `respected_games`: A list of games returned by the `respected_games(options)` function. Used to avoid duplicated SQL requests when the `status` function - is called in a loop. If `nil`, the `respected_games()` function + is called in a loop. If `nil`, the `respected_games(options)` function is called internally. ## Returns @@ -183,15 +186,15 @@ defmodule Explorer.Chain.Optimism.Withdrawal do `datetime` is the point of time when the challenge period ends. (only for `In challenge period` status). """ - @spec status(map(), list() | nil) :: {String.t(), DateTime.t() | nil} - def status(w, respected_games \\ nil) + @spec status(map(), list() | nil, [Chain.api?()]) :: {String.t(), DateTime.t() | nil} + def status(w, respected_games \\ nil, options \\ []) - def status(w, respected_games) when is_nil(w.l1_transaction_hash) do + def status(w, respected_games, options) when is_nil(w.l1_transaction_hash) do proven_events = proven_events_by_hash(w.hash) respected_games = if is_nil(respected_games) do - respected_games() + respected_games(options) else respected_games end @@ -212,7 +215,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do end end - def status(_w, _respected_games) do + def status(_w, _respected_games, _options) do {@withdrawal_status_relayed, nil} end @@ -220,8 +223,8 @@ defmodule Explorer.Chain.Optimism.Withdrawal do Returns the list of games which type is equal to the current respected game type received from OptimismPortal contract. """ - @spec respected_games() :: list() - def respected_games do + @spec respected_games([Chain.api?()]) :: list() + def respected_games(options) do case Helper.parse_integer(Constants.get_constant_value("optimism_respected_game_type")) do nil -> [] @@ -234,7 +237,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do limit: 100 ) - Repo.all(query, timeout: :infinity) + Chain.select_repo(options).all(query, timeout: :infinity) end end @@ -313,7 +316,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do # # ## Parameters # - `proven_events`: A list of WithdrawalProven events. Each item is `{l1_timestamp, game_address_hash, game_index}` tuple. - # - `respected_games`: A list of games returned by the `respected_games()` function. + # - `respected_games`: A list of games returned by the `respected_games(options)` function. # # ## Returns # - `{status, datetime}` tuple where the `status` is the current withdrawal status, diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 5cf7fdfcb867..2a99d1d6ea5b 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -532,11 +532,11 @@ defmodule Explorer.Etherscan do end defp list_erc20_token_transfers(address_hash, contract_address_hash, options) do - "ERC-20" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + "ERC-20" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.replica().all() end defp list_nft_transfers(address_hash, contract_address_hash, options) do - "ERC-721" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + "ERC-721" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.replica().all() end defp list_erc1155_token_transfers(address_hash, contract_address_hash, options) do @@ -564,11 +564,13 @@ defmodule Explorer.Etherscan do [unnest: unnest], {^options.order_by_direction, unnest.index_in_batch} ) - |> Repo.all() + |> Repo.replica().all() end defp list_erc404_token_transfers(address_hash, contract_address_hash, options) do - "ERC-404" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.all() + "ERC-404" + |> base_token_transfers_query(address_hash, contract_address_hash, options) + |> Repo.replica().all() end defp base_token_transfers_query(transfers_type, address_hash, contract_address_hash, options) do diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 24086a04bcd5..dede80fdfea0 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -731,30 +731,6 @@ defmodule Explorer.ChainTest do end end - describe "token_contract_address_from_token_name/1" do - test "return not found if token doesn't exist" do - name = "AYR" - - assert {:error, :not_found} = Chain.token_contract_address_from_token_name(name) - end - - test "return the correct token if it exists" do - name = "AYR" - insert(:token, symbol: name) - - assert {:ok, _} = Chain.token_contract_address_from_token_name(name) - end - - test "return not found if multiple records are in the results" do - name = "TOKEN" - - insert(:token, symbol: name) - insert(:token, symbol: name) - - assert {:error, :not_found} = Chain.token_contract_address_from_token_name(name) - end - end - describe "find_or_insert_address_from_hash/1" do test "returns an address if it already exists" do address = insert(:address) @@ -2787,70 +2763,6 @@ defmodule Explorer.ChainTest do end end - describe "contract_address?/2" do - test "returns true if address has contract code" do - code = %Data{ - bytes: <<1, 2, 3, 4, 5>> - } - - address = insert(:address, contract_code: code) - - assert Chain.contract_address?(to_string(address.hash), 1) - end - - test "returns false if address has not contract code" do - address = insert(:address) - - refute Chain.contract_address?(to_string(address.hash), 1) - end - - @tag :no_nethermind - @tag :no_geth - test "returns true if fetched code from json rpc", %{ - json_rpc_named_arguments: json_rpc_named_arguments - } do - hash = "0x71300d93a8CdF93385Af9635388cF2D00b95a480" - - if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn _arguments, _options -> - {:ok, - [ - %{ - id: 0, - result: "0x0102030405" - } - ]} - end) - end - - assert Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments) - end - - @tag :no_nethermind - @tag :no_geth - test "returns false if no fetched code from json rpc", %{ - json_rpc_named_arguments: json_rpc_named_arguments - } do - hash = "0x71300d93a8CdF93385Af9635388cF2D00b95a480" - - if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn _arguments, _options -> - {:ok, - [ - %{ - id: 0, - result: "0x" - } - ]} - end) - end - - refute Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments) - end - end - describe "fetch_first_trace/2" do test "fetched first trace", %{ json_rpc_named_arguments: json_rpc_named_arguments diff --git a/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex b/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs similarity index 100% rename from apps/explorer/test/explorer/migrator/celo_l2_epochs_test.ex rename to apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs From 07c0217d134bb3cf1dce45482a168df91a27fd62 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 Aug 2025 20:04:50 +0300 Subject: [PATCH 0254/1006] fix: token type filtering to support multiple types with OR logic (#13008) * Initial plan * Fix token type filtering to handle multiple types with OR logic Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * fix: format * Remove redundant empty list clause in last_token_balances/2 Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Remove unused expected_combined variable from token types test Co-authored-by: vbaranov <4341812+vbaranov@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> Co-authored-by: Fedor Ivanov Co-authored-by: vbaranov <4341812+vbaranov@users.noreply.github.com> --- .../api/v2/address_controller_test.exs | 15 +++++++++++++++ .../chain/address/current_token_balance.ex | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 4adf9032e60d..9a6da7ac62d6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -3101,6 +3101,21 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do check_paginated_response(response, response_2nd_page, ctbs_erc_1155) assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + # Test multiple token types (the fix for the original issue) + filter = %{"type" => "ERC-721,ERC-1155"} + request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) + assert response = json_response(request, 200) + + # Verify we get tokens from both types + response_token_types = + response["items"] + |> Enum.map(fn item -> item["token"]["type"] end) + |> Enum.uniq() + |> Enum.sort() + + assert response_token_types == ["ERC-1155", "ERC-721"] + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index d2f996eedabf..a692f5d7af62 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -192,7 +192,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do """ def last_token_balances(address_hash, type \\ []) - def last_token_balances(address_hash, [type | _]) do + def last_token_balances(address_hash, types) when is_list(types) and types != [] do fiat_balance = fiat_value_query() from( @@ -202,7 +202,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do left_join: t in assoc(ctb, :token), on: ctb.token_contract_address_hash == t.contract_address_hash, preload: [token: t], - where: t.type == ^type, + where: t.type in ^types, select: ctb, select_merge: ^%{fiat_value: fiat_balance}, order_by: ^[desc_nulls_last: fiat_balance], From 192b11464b812423021912f90151b37b68c58a67 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Wed, 20 Aug 2025 12:57:39 +0300 Subject: [PATCH 0255/1006] fix: address api spec for `filecoin` and `zilliqa` chain types (#12996) * fix: address api spec for `filecoin` and `zilliqa` chain types * fix: update required fields in filecoin schema * fix: update type for `filecoin.id` * chore: process review comments * fix: disable warning --- .../lib/block_scout_web/schemas/api/v2/address.ex | 12 ++++++++++-- .../schemas/api/v2/address/response.ex | 12 +++++++++++- .../lib/block_scout_web/views/api/v2/zilliqa_view.ex | 7 +++---- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index a4ac085aab9e..90ae066a1422 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do @filecoin_robust_address_schema %Schema{ type: :string, + description: "Robust f0/f1/f2/f3/f4 Filecoin address", example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", nullable: true } @@ -19,14 +20,21 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do |> put_in([:properties, :filecoin], %Schema{ type: :object, properties: %{ - id: %Schema{type: :string, example: "f03248220", nullable: true}, + id: %Schema{ + type: :string, + description: "Short f0 Filecoin address that may change during chain reorgs", + example: "f03248220", + nullable: true + }, robust: @filecoin_robust_address_schema, actor_type: %Schema{ type: :string, + description: "Type of actor associated with the Filecoin address", enum: EctoEnum.values(Address, :filecoin_actor_type), nullable: true } - } + }, + required: [:id, :robust, :actor_type] }) _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index 90ec30950ed8..8bbd8b707a8e 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -6,10 +6,19 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations @filecoin_robust_address_schema %Schema{ type: :string, + description: "Robust f0/f1/f2/f3/f4 Filecoin address", example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", nullable: true } + @zilliqa_schema %Schema{ + type: :object, + properties: %{ + is_scilla_contract: %Schema{type: :boolean, nullable: false} + }, + required: [:is_scilla_contract] + } + @doc """ Applies chain-specific field customizations to the given schema based on the configured chain type. @@ -29,7 +38,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations :zilliqa -> schema - |> put_in([:properties, :is_scilla_contract], %Schema{type: :boolean, nullable: false}) + |> put_in([:properties, :zilliqa], @zilliqa_schema) + |> update_in([:required], &[:zilliqa | &1]) _ -> schema diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex index 2b9ad89ea1c2..ecbc419cc542 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex @@ -7,6 +7,7 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do if @chain_type == :zilliqa do # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release import Explorer.Chain.Zilliqa.Helper, only: [scilla_transaction?: 1], warn: false + alias Ecto.Association.NotLoaded, warn: false alias Explorer.Chain.{Address, Block, Transaction}, warn: false alias Explorer.Chain.Zilliqa.{AggregateQuorumCertificate, QuorumCertificate}, warn: false @@ -64,8 +65,8 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do ## Returns - A map extended with data related to Zilliqa. """ - @spec extend_address_json_response(map(), Address.t()) :: map() - def extend_address_json_response(out_json, %Address{} = address) do + @spec extend_address_json_response(map(), Address.t() | nil | NotLoaded.t()) :: map() + def extend_address_json_response(out_json, address) do is_scilla_contract = case address do %Address{ @@ -82,8 +83,6 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do }) end - def extend_address_json_response(out_json, _address), do: out_json - @spec add_quorum_certificate(map(), Block.t()) :: map() defp add_quorum_certificate( zilliqa_json, From dd40ea57401a513e31a7e1cd95b9cf2bf4f64bef Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 20 Aug 2025 19:32:35 +0300 Subject: [PATCH 0256/1006] perf: Optimize maybe_hide_scam_addresses/3 query (#12927) * fix: Optimize maybe_hide_scam_addresses/3 query * Introduce left join * Fix credo --- apps/explorer/lib/explorer/helper.ex | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index bccbba1779e4..d25037b78252 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -5,9 +5,9 @@ defmodule Explorer.Helper do alias ABI.TypeDecoder alias Explorer.Chain - alias Explorer.Chain.{Data, Hash, Wei} + alias Explorer.Chain.{Address.ScamBadgeToAddress, Data, Hash, Wei} - import Ecto.Query, only: [where: 3] + import Ecto.Query import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @max_safe_integer round(:math.pow(2, 63)) - 1 @@ -253,13 +253,8 @@ defmodule Explorer.Helper do def maybe_hide_scam_addresses(query, address_hash_key, options) do if Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] do query - |> where( - [q], - fragment( - "NOT EXISTS (SELECT 1 FROM scam_address_badge_mappings sabm WHERE sabm.address_hash=?)", - field(q, ^address_hash_key) - ) - ) + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> where([sabm: sabm], is_nil(sabm.address_hash)) else query end From c70fde9f1ef88bd77e6b5e9641c6b554929a34ca Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 20 Aug 2025 20:35:45 +0400 Subject: [PATCH 0257/1006] feat: Add methodId to txlist rpc method (#13043) --- .../block_scout_web/lib/block_scout_web/etherscan.ex | 12 ++++++++++-- .../block_scout_web/views/api/rpc/address_view.ex | 3 ++- .../controllers/api/rpc/address_controller_test.exs | 3 ++- apps/explorer/lib/explorer/chain/transaction.ex | 11 +++++++++++ 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 2df0f79bd252..bb8ff20b47cd 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -82,7 +82,8 @@ defmodule BlockScoutWeb.Etherscan do "contractAddress" => "", "cumulativeGasUsed" => "122207", "gasUsed" => "122207", - "confirmations" => "5994246" + "confirmations" => "5994246", + "methodId" => "0xf00d4b5d" } ] } @@ -665,6 +666,12 @@ defmodule BlockScoutWeb.Etherscan do example: ~s("6005998") } + @method_id_type %{ + type: "string", + definition: "Method signature used in transaction (0x for simple coin transfers)", + example: ~s("0xf00d4b5d") + } + @transaction_index_type %{ type: "transaction index", definition: "Index of the transaction in it's block.", @@ -786,7 +793,8 @@ defmodule BlockScoutWeb.Etherscan do contractAddress: @address_hash_type, cumulativeGasUsed: @gas_type, gasUsed: @gas_type, - confirmations: @confirmation_type + confirmations: @confirmation_type, + methodId: @method_id_type } } diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index c2c4cf3ce3bc..a6eba00e8b1d 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -129,7 +129,8 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "contractAddress" => "#{transaction.created_contract_address_hash}", "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}", "gasUsed" => "#{transaction.gas_used}", - "confirmations" => "#{transaction.confirmations}" + "confirmations" => "#{transaction.confirmations}", + "methodId" => Transaction.method_id(transaction) } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index a57bc991634f..d0a85492ad02 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -670,7 +670,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do "contractAddress" => "#{transaction.created_contract_address_hash}", "cumulativeGasUsed" => "#{transaction.cumulative_gas_used}", "gasUsed" => "#{transaction.gas_used}", - "confirmations" => "0" + "confirmations" => "0", + "methodId" => "0x" } ] diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 02ae71978daa..80fa0b893e50 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -2239,4 +2239,15 @@ defmodule Explorer.Chain.Transaction do def method_name(_, _, _) do nil end + + @doc """ + Return method id used in transaction + """ + def method_id(%__MODULE__{ + created_contract_address_hash: nil, + input: %{bytes: <>} + }), + do: "0x" <> Base.encode16(method_id, case: :lower) + + def method_id(_transaction), do: "0x" end From bbaf8d367f90da9bad566cfec20727545ed56219 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:51:06 +0400 Subject: [PATCH 0258/1006] fix: Change signed_authorizations chain_id type to numeric (#13042) --- .../lib/explorer/chain/signed_authorization.ex | 10 +++++----- ...ange_chain_id_type_in_signed_authorization.exs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20250820121833_change_chain_id_type_in_signed_authorization.exs diff --git a/apps/explorer/lib/explorer/chain/signed_authorization.ex b/apps/explorer/lib/explorer/chain/signed_authorization.ex index 0b6665099158..ad40466b0792 100644 --- a/apps/explorer/lib/explorer/chain/signed_authorization.ex +++ b/apps/explorer/lib/explorer/chain/signed_authorization.ex @@ -24,9 +24,9 @@ defmodule Explorer.Chain.SignedAuthorization do @type to_import :: %{ transaction_hash: binary(), index: non_neg_integer(), - chain_id: non_neg_integer(), + chain_id: non_neg_integer() | Decimal.t(), address: binary(), - nonce: non_neg_integer(), + nonce: non_neg_integer() | Decimal.t(), r: non_neg_integer(), s: non_neg_integer(), v: non_neg_integer(), @@ -52,7 +52,7 @@ defmodule Explorer.Chain.SignedAuthorization do @primary_key false typed_schema "signed_authorizations" do field(:index, :integer, primary_key: true, null: false) - field(:chain_id, :integer, null: false) + field(:chain_id, :decimal, null: false) field(:address, Hash.Address, null: false) field(:nonce, :decimal, null: false) field(:r, :decimal, null: false) @@ -136,10 +136,10 @@ defmodule Explorer.Chain.SignedAuthorization do chain_id = ChainId.get_id() cond do - struct.chain_id != 0 and !is_nil(chain_id) and struct.chain_id != chain_id -> + not Decimal.eq?(struct.chain_id, 0) and !is_nil(chain_id) and not Decimal.eq?(struct.chain_id, chain_id) -> :invalid_chain_id - struct.chain_id != 0 and is_nil(chain_id) -> + not Decimal.eq?(struct.chain_id, 0) and is_nil(chain_id) -> nil struct.nonce |> Decimal.gte?(2 ** 64 - 1) -> diff --git a/apps/explorer/priv/repo/migrations/20250820121833_change_chain_id_type_in_signed_authorization.exs b/apps/explorer/priv/repo/migrations/20250820121833_change_chain_id_type_in_signed_authorization.exs new file mode 100644 index 000000000000..3be90d80a771 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250820121833_change_chain_id_type_in_signed_authorization.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Migrations.ChangeChainIdTypeInSignedAuthorization do + use Ecto.Migration + + def up do + alter table(:signed_authorizations) do + modify(:chain_id, :numeric, precision: 78, scale: 0) + end + end + + def down do + alter table(:signed_authorizations) do + modify(:chain_id, :bigint) + end + end +end From cbae116be4897ee6269140eee235c83d8e20bdca Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 20 Aug 2025 22:56:34 +0400 Subject: [PATCH 0259/1006] chore: Cover token info export to Multichain service by unit tests (#12899) * Extend unit tests for Explorer.MicroserviceInterfaces.MultichainSearch * Add unit tests for Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../multichain_search_test.exs | 933 +++++++++++++----- .../token_info_export_queue_test.exs | 223 +++++ 2 files changed, 915 insertions(+), 241 deletions(-) create mode 100644 apps/indexer/test/indexer/fetcher/multichain_search_db/token_info_export_queue_test.exs diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index da5ed73469fc..f14d9c279340 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -5,253 +5,13 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.MultichainSearchDb.{MainExportQueue, TokenInfoExportQueue} - alias Explorer.Chain.Wei + alias Explorer.Chain.{Token, Wei} alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.{Repo, TestHelper} alias Plug.Conn setup :verify_on_exit! - describe "batch_export_token_info/1" do - setup do - Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) - Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) - - on_exit(fn -> - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) - - :ok - end - - test "returns {:ok, :service_disabled} when the service is disabled" do - items_from_db_queue = [insert(:multichain_search_db_export_token_info_queue)] - assert MultichainSearch.batch_export_token_info(items_from_db_queue) == {:ok, :service_disabled} - end - - test "processes chunks and returns {:ok, {:chunks_processed, _}} when the service is enabled" do - bypass = Bypass.open() - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Application.put_env(:explorer, MultichainSearch, - service_url: "http://localhost:#{bypass.port}", - api_key: "12345", - token_info_chunk_size: 1000 - ) - - on_exit(fn -> - Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) - Bypass.down(bypass) - end) - - TestHelper.get_chain_id_mock() - - Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> - Conn.resp( - conn, - 200, - Jason.encode!(%{"status" => "ok"}) - ) - end) - - token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) - token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) - items_from_db_queue = [token_info_item_1, token_info_item_2] - - items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_export_token_info(items_from_db_queue) - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - end - - test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_export_token_info_queue' table is populated" do - bypass = Bypass.open() - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Application.put_env(:explorer, MultichainSearch, - service_url: "http://localhost:#{bypass.port}", - api_key: "12345", - token_info_chunk_size: 1000 - ) - - on_exit(fn -> - Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) - Bypass.down(bypass) - end) - - TestHelper.get_chain_id_mock() - - Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> - Conn.resp( - conn, - 500, - Jason.encode!(%{"code" => 0, "message" => "Error"}) - ) - end) - - token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) - token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) - items_from_db_queue = [token_info_item_1, token_info_item_2] - - items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - - assert {:error, - %{ - tokens: [ - MultichainSearch.token_info_queue_item_to_http_item(token_info_item_1), - MultichainSearch.token_info_queue_item_to_http_item(token_info_item_2) - ] - }} == MultichainSearch.batch_export_token_info(items_from_db_queue) - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 2 - records = Repo.all(TokenInfoExportQueue) - - assert Enum.all?(records, fn record -> - (record.address_hash == token_info_item_1.address_hash && record.data_type == token_info_item_1.data_type) || - (record.address_hash == token_info_item_2.address_hash && - record.data_type == token_info_item_2.data_type) - end) - end - - test "returns {:error, data_to_retry} when at least one chunk is failed" do - Application.put_env(:explorer, MultichainSearch, - service_url: "http://localhost:1234", - api_key: "12345", - token_info_chunk_size: 1000 - ) - - on_exit(fn -> - Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) - end) - - TestHelper.get_chain_id_mock() - - # 1002 addresses (1000 in the first chunk and 2 in the second) - items_from_db_queue = - for _ <- 0..1001 do - insert(:multichain_search_db_export_token_info_queue) - end - - items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: "http://localhost:1234/api/v1/import:batch"}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(%{"status" => "ok"}) - }} - end - ) - - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: "http://localhost:1234/api/v1/import:batch", headers: [{"Content-Type", "application/json"}]}, - _opts -> - {:ok, - %Tesla.Env{ - status: 500, - body: Jason.encode!(%{"code" => 0, "message" => "Error"}) - }} - end - ) - - assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) - assert Enum.count(results.tokens) == 1000 - assert Repo.aggregate(TokenInfoExportQueue, :count) == 1000 - end - - test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_export_token_info_queue' table is populated with all the input data" do - bypass = Bypass.open() - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Application.put_env(:explorer, MultichainSearch, - service_url: "http://localhost:#{bypass.port}", - api_key: "12345", - token_info_chunk_size: 2 - ) - - on_exit(fn -> - Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) - Bypass.down(bypass) - end) - - TestHelper.get_chain_id_mock() - - Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> - Conn.resp( - conn, - 500, - Jason.encode!(%{"code" => 0, "message" => "Error"}) - ) - end) - - items_from_db_queue = 10 |> insert_list(:multichain_search_db_export_token_info_queue) - - items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) - assert Enum.count(results.tokens) == 10 - assert Repo.aggregate(TokenInfoExportQueue, :count) == 10 - end - - test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_token_info_queue' table is populated" do - bypass = Bypass.open() - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Application.put_env(:explorer, MultichainSearch, - service_url: "http://localhost:#{bypass.port}", - api_key: "12345", - token_info_chunk_size: 2 - ) - - on_exit(fn -> - Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) - Bypass.down(bypass) - end) - - TestHelper.get_chain_id_mock() - - Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> - Conn.resp( - conn, - 500, - Jason.encode!(%{"code" => 0, "message" => "Error"}) - ) - end) - - items_from_db_queue = 15 |> insert_list(:multichain_search_db_export_token_info_queue) - - items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() - - assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 - assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) - assert Enum.count(results.tokens) == 15 - assert Repo.aggregate(TokenInfoExportQueue, :count) == 15 - end - end - describe "batch_import/1" do setup do Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) @@ -553,6 +313,697 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do end end + describe "batch_export_token_info/1" do + setup do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + + on_exit(fn -> + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + :ok + end + + test "returns {:ok, :service_disabled} when the service is disabled" do + items_from_db_queue = [insert(:multichain_search_db_export_token_info_queue)] + assert MultichainSearch.batch_export_token_info(items_from_db_queue) == {:ok, :service_disabled} + end + + test "processes chunks and returns {:ok, {:chunks_processed, _}} when the service is enabled" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + items_from_db_queue = [token_info_item_1, token_info_item_2] + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + end + + test "returns {:error, data_to_retry} when an error occurs during processing and 'multichain_search_db_export_token_info_queue' table is populated" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + items_from_db_queue = [token_info_item_1, token_info_item_2] + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + assert {:error, + %{ + tokens: [ + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_1), + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_2) + ] + }} == MultichainSearch.batch_export_token_info(items_from_db_queue) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 2 + records = Repo.all(TokenInfoExportQueue) + + assert Enum.all?(records, fn record -> + (record.address_hash == token_info_item_1.address_hash && record.data_type == token_info_item_1.data_type) || + (record.address_hash == token_info_item_2.address_hash && + record.data_type == token_info_item_2.data_type) + end) + end + + test "returns {:error, data_to_retry} when an error occurs during processing and retries_number is increased" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + items_from_db_queue = [token_info_item_1, token_info_item_2] + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 2 + assert is_nil(token_info_item_1.retries_number) + assert is_nil(token_info_item_2.retries_number) + + assert {:error, + %{ + tokens: [ + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_1), + MultichainSearch.token_info_queue_item_to_http_item(token_info_item_2) + ] + }} == MultichainSearch.batch_export_token_info(items_from_db_queue) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 2 + records = Repo.all(TokenInfoExportQueue) + + assert Enum.all?(records, fn record -> + (record.address_hash == token_info_item_1.address_hash && record.data_type == token_info_item_1.data_type && + record.retries_number == 1) || + (record.address_hash == token_info_item_2.address_hash && + record.data_type == token_info_item_2.data_type && record.retries_number == 1) + end) + end + + test "returns {:error, data_to_retry} when at least one chunk is failed" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + TestHelper.get_chain_id_mock() + + # 1002 addresses (1000 in the first chunk and 2 in the second) + items_from_db_queue = + for _ <- 0..1001 do + insert(:multichain_search_db_export_token_info_queue) + end + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(%{"status" => "ok"}) + }} + end + ) + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", headers: [{"Content-Type", "application/json"}]}, + _opts -> + {:ok, + %Tesla.Env{ + status: 500, + body: Jason.encode!(%{"code" => 0, "message" => "Error"}) + }} + end + ) + + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 1000 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 1000 + end + + test "returns {:error, data_to_retry} when an error occurs in all chunks during processing and 'multichain_search_db_export_token_info_queue' table is populated with all the input data" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + items_from_db_queue = 10 |> insert_list(:multichain_search_db_export_token_info_queue) + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 10 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 10 + end + + test "returns {:error, data_to_retry} when an error occurs in all chunks (and number of chunks more than @max_concurrency) during processing and 'multichain_search_db_export_token_info_queue' table is populated" do + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Bypass.down(bypass) + end) + + TestHelper.get_chain_id_mock() + + Bypass.expect(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 500, + Jason.encode!(%{"code" => 0, "message" => "Error"}) + ) + end) + + items_from_db_queue = 15 |> insert_list(:multichain_search_db_export_token_info_queue) + + items_from_db_queue + |> TokenInfoExportQueue.delete_query() + |> Repo.transaction() + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) + assert Enum.count(results.tokens) == 15 + assert Repo.aggregate(TokenInfoExportQueue, :count) == 15 + end + end + + describe "token_info_queue_item_to_http_item/1" do + test "returns correct map to send to multichain service" do + address_hash_string = "0x000102030405060708090a0b0c0d0e0f10111213" + address_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + + assert MultichainSearch.token_info_queue_item_to_http_item(%{ + address_hash: address_hash_binary, + data_type: :metadata, + data: %{token_type: "ERC-20", name: "TestToken", symbol: "TEST", decimals: 18, total_supply: "1000"} + }) == %{ + address_hash: address_hash_string, + metadata: %{token_type: "ERC-20", name: "TestToken", symbol: "TEST", decimals: 18, total_supply: "1000"} + } + + assert MultichainSearch.token_info_queue_item_to_http_item(%{ + address_hash: address_hash_binary, + data_type: :total_supply, + data: %{total_supply: "1000"} + }) == %{ + address_hash: address_hash_string, + metadata: %{total_supply: "1000"} + } + + assert MultichainSearch.token_info_queue_item_to_http_item(%{ + address_hash: address_hash_binary, + data_type: :counters, + data: %{holders_count: "123", transfers_count: "456"} + }) == %{ + address_hash: address_hash_string, + counters: %{holders_count: "123", transfers_count: "456"} + } + + assert MultichainSearch.token_info_queue_item_to_http_item(%{ + address_hash: address_hash_binary, + data_type: :market_data, + data: %{fiat_value: "123.456", circulating_market_cap: "1000.0001"} + }) == %{ + address_hash: address_hash_string, + price_data: %{fiat_value: "123.456", circulating_market_cap: "1000.0001"} + } + end + end + + describe "token_info_http_item_to_queue_item/1" do + test "returns correct map to add to queue" do + address_hash_string = "0x000102030405060708090a0b0c0d0e0f10111213" + address_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + + assert MultichainSearch.token_info_http_item_to_queue_item(%{ + address_hash: address_hash_string, + metadata: %{token_type: "ERC-20", name: "TestToken", symbol: "TEST", decimals: 18, total_supply: "1000"} + }) == %{ + address_hash: address_hash_binary, + data_type: :metadata, + data: %{token_type: "ERC-20", name: "TestToken", symbol: "TEST", decimals: 18, total_supply: "1000"} + } + + assert MultichainSearch.token_info_http_item_to_queue_item(%{ + address_hash: address_hash_string, + metadata: %{token_type: "ERC-20"} + }) == %{ + address_hash: address_hash_binary, + data_type: :metadata, + data: %{token_type: "ERC-20"} + } + + assert MultichainSearch.token_info_http_item_to_queue_item(%{ + address_hash: address_hash_string, + metadata: %{total_supply: "1000"} + }) == %{ + address_hash: address_hash_binary, + data_type: :total_supply, + data: %{total_supply: "1000"} + } + + assert MultichainSearch.token_info_http_item_to_queue_item(%{ + address_hash: address_hash_string, + counters: %{holders_count: "123", transfers_count: "456"} + }) == %{ + address_hash: address_hash_binary, + data_type: :counters, + data: %{holders_count: "123", transfers_count: "456"} + } + + assert MultichainSearch.token_info_http_item_to_queue_item(%{ + address_hash: address_hash_string, + price_data: %{fiat_value: "123.456", circulating_market_cap: "1000.0001"} + }) == %{ + address_hash: address_hash_binary, + data_type: :market_data, + data: %{fiat_value: "123.456", circulating_market_cap: "1000.0001"} + } + end + end + + describe "prepare_token_metadata_for_queue/2" do + test "returns an empty map when the service is disabled" do + assert MultichainSearch.prepare_token_metadata_for_queue(%Token{type: "ERC-20"}, %{name: "TestToken"}) == %{} + end + + test "returns correct map to add to queue" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{ + type: "ERC-20", + icon_url: "http://localhost:1235/test.png", + name: "TestToken", + symbol: "TST" + }, + %{ + name: "TestToken2", + symbol: "TST2", + decimals: 18, + total_supply: 123 + } + ) == %{ + token_type: "ERC-20", + icon_url: "http://localhost:1235/test.png", + name: "TestToken2", + symbol: "TST2", + decimals: 18, + total_supply: "123" + } + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{ + type: "ERC-20", + name: "TestToken", + symbol: "TST" + }, + %{ + name: "TestToken2", + symbol: "TST2", + decimals: 18 + } + ) == %{ + token_type: "ERC-20", + name: "TestToken2", + symbol: "TST2", + decimals: 18 + } + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{ + type: "ERC-1155", + name: "TestToken", + symbol: "TST" + }, + %{ + name: "TestToken2", + symbol: "TST2" + } + ) == %{ + token_type: "ERC-1155", + name: "TestToken2", + symbol: "TST2" + } + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{ + type: "ERC-1155", + name: "TestToken", + symbol: "TST" + }, + %{ + name: "TestToken2" + } + ) == %{ + token_type: "ERC-1155", + name: "TestToken2" + } + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{ + type: "ERC-1155", + name: "TestToken", + symbol: "TST" + }, + %{} + ) == %{ + token_type: "ERC-1155" + } + + assert MultichainSearch.prepare_token_metadata_for_queue( + %Token{type: "ERC-1155"}, + %{} + ) == %{ + token_type: "ERC-1155" + } + end + end + + describe "prepare_token_total_supply_for_queue/1" do + test "returns nil when the service is disabled" do + assert is_nil(MultichainSearch.prepare_token_total_supply_for_queue(1000)) + end + + test "returns correct map to add to queue" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + assert MultichainSearch.prepare_token_total_supply_for_queue(1000) == %{total_supply: "1000"} + end + end + + describe "prepare_token_market_data_for_queue/1" do + test "returns an empty map when the service is disabled" do + assert MultichainSearch.prepare_token_market_data_for_queue(%{ + fiat_value: Decimal.new("100.5"), + circulating_market_cap: Decimal.new("2000.28") + }) == %{} + end + + test "returns correct map to add to queue" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + assert MultichainSearch.prepare_token_market_data_for_queue(%{ + fiat_value: Decimal.new("100.5"), + circulating_market_cap: Decimal.new("2000.28") + }) == %{fiat_value: "100.5", circulating_market_cap: "2000.28"} + + assert MultichainSearch.prepare_token_market_data_for_queue(%{ + fiat_value: Decimal.new("100.5"), + circulating_market_cap: Decimal.new("2000.28"), + name: "TestToken" + }) == %{fiat_value: "100.5", circulating_market_cap: "2000.28"} + + assert MultichainSearch.prepare_token_market_data_for_queue(%{fiat_value: Decimal.new("100.5")}) == %{ + fiat_value: "100.5" + } + + assert MultichainSearch.prepare_token_market_data_for_queue(%{circulating_market_cap: Decimal.new("2000.28")}) == + %{circulating_market_cap: "2000.28"} + + assert MultichainSearch.prepare_token_market_data_for_queue(%{name: "TestToken"}) == %{} + assert MultichainSearch.prepare_token_market_data_for_queue(%{}) == %{} + end + end + + describe "prepare_token_counters_for_queue/2" do + test "returns an empty map when the service is disabled" do + assert MultichainSearch.prepare_token_counters_for_queue(456, 123) == %{} + end + + test "returns correct map to add to queue" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + assert MultichainSearch.prepare_token_counters_for_queue(456, 123) == %{ + transfers_count: "456", + holders_count: "123" + } + + assert MultichainSearch.prepare_token_counters_for_queue(0, 0) == %{transfers_count: "0", holders_count: "0"} + end + end + + describe "send_token_info_to_queue/2" do + test "does nothing and returns :ignore when the service is disabled" do + address_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + assert MultichainSearch.send_token_info_to_queue(%{address_hash_binary => %{total_supply: "123"}}, :total_supply) == + :ignore + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + end + + test "adds an item to db queue and returns :ok" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + address_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + assert MultichainSearch.send_token_info_to_queue(%{address_hash_binary => %{total_supply: "123"}}, :total_supply) == + :ok + + [record] = Repo.all(TokenInfoExportQueue) + + assert record.address_hash == address_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "123"} + end + + test "adds all items to db queue" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 2 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + address1_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + address2_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111214", case: :mixed) + address3_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111215", case: :mixed) + address4_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111216", case: :mixed) + + entries = %{ + address1_hash_binary => %{total_supply: "123"}, + address2_hash_binary => %{total_supply: "124"}, + address3_hash_binary => %{total_supply: "125"}, + address4_hash_binary => %{total_supply: "126"} + } + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + assert MultichainSearch.send_token_info_to_queue(entries, :total_supply) == :ok + + records = Repo.all(TokenInfoExportQueue) + + assert Enum.all?(records, fn record -> + (record.address_hash == address1_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "123"}) || + (record.address_hash == address2_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "124"}) || + (record.address_hash == address3_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "125"}) || + (record.address_hash == address4_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "126"}) + end) + end + + test "replaces an existing item in the db queue and updates `updated_at` field" do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + address_hash_binary = Base.decode16!("000102030405060708090a0b0c0d0e0f10111213", case: :mixed) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + assert MultichainSearch.send_token_info_to_queue(%{address_hash_binary => %{total_supply: "123"}}, :total_supply) == + :ok + + [record] = Repo.all(TokenInfoExportQueue) + + assert record.address_hash == address_hash_binary && record.data_type == :total_supply && + record.data == %{"total_supply" => "123"} + + assert MultichainSearch.send_token_info_to_queue(%{address_hash_binary => %{total_supply: "124"}}, :total_supply) == + :ok + + [record_new] = Repo.all(TokenInfoExportQueue) + + assert record_new.address_hash == address_hash_binary && record_new.data_type == :total_supply && + record_new.data == %{"total_supply" => "124"} && + DateTime.compare(record_new.updated_at, record.updated_at) == :gt + end + end + describe "extract_batch_import_params_into_chunks/1" do setup do TestHelper.get_chain_id_mock() diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/token_info_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/token_info_export_queue_test.exs new file mode 100644 index 000000000000..879d32df8d35 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/token_info_export_queue_test.exs @@ -0,0 +1,223 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueueTest do + use ExUnit.Case + use Explorer.DataCase + + import ExUnit.CaptureLog, only: [capture_log: 1] + + alias Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.TestHelper + + alias Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, + as: MultichainSearchDbTokenInfoExportQueue + + alias Plug.Conn + + @moduletag :capture_log + + setup do + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + Application.put_env(:indexer, MultichainSearchDbTokenInfoExportQueue.Supervisor, disabled?: false) + + on_exit(fn -> + Application.put_env(:indexer, MultichainSearchDbTokenInfoExportQueue.Supervisor, disabled?: true) + end) + + :ok + end + + describe "init/3" do + setup do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + end + + test "initializes with data from the retry queue" do + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_3 = insert(:multichain_search_db_export_token_info_queue) + + {:ok, token_info_item_1_data} = Jason.decode(Jason.encode!(token_info_item_1.data)) + {:ok, token_info_item_2_data} = Jason.decode(Jason.encode!(token_info_item_2.data)) + {:ok, token_info_item_3_data} = Jason.decode(Jason.encode!(token_info_item_3.data)) + + reducer = fn data, acc -> [data | acc] end + + pid = + [] + |> MultichainSearchDbTokenInfoExportQueue.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + + results = MultichainSearchDbTokenInfoExportQueue.init([], reducer, nil) + + assert Enum.count(results) == 3 + + assert Enum.member?(results, %{ + address_hash: token_info_item_1.address_hash, + data_type: token_info_item_1.data_type, + data: token_info_item_1_data + }) + + assert Enum.member?(results, %{ + address_hash: token_info_item_2.address_hash, + data_type: token_info_item_2.data_type, + data: token_info_item_2_data + }) + + assert Enum.member?(results, %{ + address_hash: token_info_item_3.address_hash, + data_type: token_info_item_3.data_type, + data: token_info_item_3_data + }) + + :timer.sleep(100) + GenServer.stop(pid) + end + end + + describe "run/2" do + setup do + bypass = Bypass.open() + + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + api_key: "12345", + token_info_chunk_size: 1000 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + Bypass.down(bypass) + end) + + {:ok, bypass: bypass} + end + + test "successfully processes multichain search db export retry queue data", %{bypass: bypass} do + export_data = [ + insert(:multichain_search_db_export_token_info_queue), + insert(:multichain_search_db_export_token_info_queue), + insert(:multichain_search_db_export_token_info_queue) + ] + + TestHelper.get_chain_id_mock() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + Bypass.expect_once(bypass, "POST", "/api/v1/import:batch", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{"status" => "ok"}) + ) + end) + + assert :ok = MultichainSearchDbTokenInfoExportQueue.run(export_data, nil) + end + + test "returns {:retry, failed_data} on error where failed_data is only chunks that failed to export", %{ + bypass: _bypass + } do + Application.put_env(:explorer, MultichainSearch, + service_url: "http://localhost:1234", + api_key: "12345", + token_info_chunk_size: 1 + ) + + on_exit(fn -> + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end) + + token_info_item_1 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_2 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_3 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_4 = insert(:multichain_search_db_export_token_info_queue) + token_info_item_4_address_hash_string = "0x" <> Base.encode16(token_info_item_4.address_hash, case: :lower) + token_info_item_5 = insert(:multichain_search_db_export_token_info_queue) + + export_data = [ + token_info_item_1, + token_info_item_2, + token_info_item_3, + token_info_item_4, + token_info_item_5 + ] + + TestHelper.get_chain_id_mock() + + tesla_expectations(token_info_item_4_address_hash_string) + + log = + capture_log(fn -> + assert {:retry, [item_to_retry]} = MultichainSearchDbTokenInfoExportQueue.run(export_data, nil) + + assert item_to_retry.address_hash == token_info_item_4.address_hash and + item_to_retry.data_type == token_info_item_4.data_type and + item_to_retry.data == token_info_item_4.data + end) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 1 + results = Repo.all(TokenInfoExportQueue) + assert Enum.all?(results, &(&1.retries_number == 1)) + assert log =~ "Batch token info export attempt to the Multichain Search DB failed" + + # Check that `retries_number` is incrementing + + TestHelper.get_chain_id_mock() + + tesla_expectations(token_info_item_4_address_hash_string) + + MultichainSearchDbTokenInfoExportQueue.run(export_data, nil) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 1 + results = Repo.all(TokenInfoExportQueue) + assert Enum.all?(results, &(&1.retries_number == 2)) + + export_data_2 = [ + token_info_item_2, + token_info_item_3, + token_info_item_4, + token_info_item_5 + ] + + Tesla.Test.expect_tesla_call( + times: 4, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + + MultichainSearchDbTokenInfoExportQueue.run(export_data_2, nil) + + assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 + + Application.put_env(:explorer, MultichainSearch, service_url: nil, api_key: nil, token_info_chunk_size: 1000) + end + end + + defp tesla_expectations(address_4_hash_string) do + Tesla.Test.expect_tesla_call( + times: 5, + returns: fn %{url: "http://localhost:1234/api/v1/import:batch", body: body}, _opts -> + case Jason.decode(body) do + {:ok, %{"tokens" => [%{"address_hash" => ^address_4_hash_string}]}} -> + {:ok, %Tesla.Env{status: 500, body: Jason.encode!(%{"code" => 0, "message" => "Error"})}} + + _ -> + {:ok, %Tesla.Env{status: 200, body: Jason.encode!(%{"status" => "ok"})}} + end + end + ) + end +end From baa2a6900b34c30147adf7369b840b122e041155 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 21 Aug 2025 13:06:22 +0400 Subject: [PATCH 0260/1006] Don't broadcast current token balances from realtime fetcher (#13039) --- .../lib/block_scout_web/notifier.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 7cd5f636809f..4c7fa9f98de7 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -396,16 +396,24 @@ defmodule BlockScoutWeb.Notifier do end @current_token_balances_limit 50 - def handle_event({:chain_event, :address_current_token_balances, type, address_current_token_balances}) + def handle_event( + {:chain_event, :address_current_token_balances, type, + %{address_current_token_balances: address_current_token_balances, address_hash: address_hash}} + ) when type in [:realtime, :on_demand] do - address_current_token_balances.address_current_token_balances + address_current_token_balances |> Repo.preload(:token) |> Enum.group_by(& &1.token_type) |> Enum.each(fn {token_type, balances} -> - broadcast_token_balances(address_current_token_balances.address_hash, token_type, balances) + broadcast_token_balances(address_hash, token_type, balances) end) end + def handle_event({:chain_event, :address_current_token_balances, :realtime, _empty_balances_params}) do + # Don't broadcast empty balances params from realtime block fetcher + :ok + end + case @chain_type do :arbitrum -> def handle_event({:chain_event, topic, _, _} = event) when topic in @chain_type_specific_events, From 23d864213c3989ee6ece3724a90fc4685a018652 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 21 Aug 2025 13:14:15 +0300 Subject: [PATCH 0261/1006] fix: csv export for celo l2 epoch rewards on address (#12815) * refactor: celo election rewards csv export path * fix: csv export for celo l2 epoch rewards on address * fix: handle `nil`s in `from_epoch` and `to_epoch` * refactor: apply suggestion by @vbaranov --- .../lib/block_scout_web/routers/api_router.ex | 21 +++------- .../explorer/chain/celo/election_reward.ex | 32 +++++++-------- .../explorer/lib/explorer/chain/celo/epoch.ex | 35 +++++++++++++++++ .../chain/csv_export/celo/election_rewards.ex | 39 ++++++++++++------- 4 files changed, 80 insertions(+), 47 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 43ca2a04cadd..5c93384c3d8e 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -212,11 +212,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/arbitrum-batch/:batch_number", V2.BlockController, :arbitrum_batch) end - if @chain_type == :celo do - get("/:block_hash_or_number/epoch", V2.BlockController, :celo_epoch) - get("/:block_hash_or_number/election-rewards/:reward_type", V2.BlockController, :celo_election_rewards) - end - chain_scope :optimism do get("/optimism-batch/:batch_number", V2.BlockController, :optimism_batch) end @@ -315,13 +310,11 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - scope "/celo" do - chain_scope :celo do - scope "/epochs" do - get("/", V2.CeloController, :epochs) - get("/:number", V2.CeloController, :epoch) - get("/:number/election-rewards/:type", V2.CeloController, :election_rewards) - end + chain_scope :celo do + scope "/celo/epochs" do + get("/", V2.CeloController, :epochs) + get("/:number", V2.CeloController, :epoch) + get("/:number/election-rewards/:type", V2.CeloController, :election_rewards) end end @@ -533,10 +526,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do # leave the same endpoint in v1 in order to keep backward compatibility get("/search", SearchController, :search) - if @chain_type == :celo do - get("/celo-election-rewards-csv", V2.CsvExportController, :celo_election_rewards_csv) - end - get("/gas-price-oracle", GasPriceOracleController, :gas_price_oracle) if @reading_enabled do diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index 7102621f98cd..1afbc4fd5855 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -318,9 +318,12 @@ defmodule Explorer.Chain.Celo.ElectionReward do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, default_paging_options()) sorting_options = Keyword.get(options, :sorting, []) + from_epoch = Keyword.get(options, :from_epoch) + to_epoch = Keyword.get(options, :to_epoch) address_hash |> address_hash_to_rewards_query() + |> where_epoch_number_in_period(from_epoch, to_epoch) |> join_token() |> SortingHelper.apply_sorting(sorting_options, default_sorting) |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) @@ -395,34 +398,27 @@ defmodule Explorer.Chain.Celo.ElectionReward do @doc """ Custom filter for `ElectionReward`, inspired by - `Chain.where_block_number_in_period/3`. - - TODO: Consider reusing `Chain.where_block_number_in_period/3`. This would - require storing or making `merge_select` of `block_number`. + `Explorer.Chain.Block.Reader.General.where_block_number_in_period/3` """ - @spec where_block_number_in_period( + @spec where_epoch_number_in_period( Ecto.Query.t(), String.t() | integer() | nil, String.t() | integer() | nil ) :: Ecto.Query.t() - def where_block_number_in_period(base_query, from_block, to_block) - when is_nil(from_block) and not is_nil(to_block), - do: where(base_query, [_, block], block.number <= ^to_block) + def where_epoch_number_in_period(base_query, nil, nil), + do: base_query - def where_block_number_in_period(base_query, from_block, to_block) - when not is_nil(from_block) and is_nil(to_block), - do: where(base_query, [_, block], block.number > ^from_block) + def where_epoch_number_in_period(base_query, nil, to_epoch), + do: where(base_query, [reward], reward.epoch_number < ^to_epoch) - def where_block_number_in_period(base_query, from_block, to_block) - when is_nil(from_block) and is_nil(to_block), - do: base_query + def where_epoch_number_in_period(base_query, from_epoch, nil), + do: where(base_query, [reward], reward.epoch_number >= ^from_epoch) - def where_block_number_in_period(base_query, from_block, to_block), + def where_epoch_number_in_period(base_query, from_epoch, to_epoch), do: where( base_query, - [_, block], - block.number > ^from_block and - block.number <= ^to_block + [reward], + reward.epoch_number >= ^from_epoch and reward.epoch_number < ^to_epoch ) end diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex index 3089581a30b0..d851c0f1c849 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -329,4 +329,39 @@ defmodule Explorer.Chain.Celo.Epoch do limit: 1 ) end + + @doc """ + Converts a block number range to epoch number range by finding epochs whose + end block numbers fall within the specified block range. + + ## Parameters + - `from_block` (`integer()`): The starting block number. + - `to_block` (`integer()`): The ending block number. + - `options` (`Keyword.t()`): Options for selecting the repository. + + ## Returns + - `{integer(), integer()} | nil`: A tuple containing the minimum and maximum + epoch numbers or nil. + + ## Examples + + iex> Explorer.Chain.Celo.Epoch.block_range_to_epoch_range(123400, 125000) + {42, 43} + + iex> Explorer.Chain.Celo.Epoch.block_range_to_epoch_range(999999, 1000000) + nil + """ + @spec block_range_to_epoch_range(integer(), integer(), Keyword.t()) :: {integer(), integer()} | nil + def block_range_to_epoch_range(from_block, to_block, options \\ []) do + query = + from(e in __MODULE__, + where: e.end_block_number >= ^from_block and e.end_block_number < ^to_block, + select: {min(e.number), max(e.number)} + ) + + case Chain.select_repo(options).one(query) do + {nil, nil} -> nil + res -> res + end + end end diff --git a/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex b/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex index 867eafbf0073..4acf9f44bfcc 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/celo/election_rewards.ex @@ -2,27 +2,38 @@ defmodule Explorer.Chain.CsvExport.Address.Celo.ElectionRewards do @moduledoc """ Exports Celo election rewards to a csv file. """ - import Explorer.Chain.Celo.Helper, - only: [ - block_number_to_epoch_number: 1 - ] - - alias Explorer.Chain.Celo.ElectionReward + alias Explorer.Chain.Celo.{ElectionReward, Epoch} alias Explorer.Chain.CsvExport.Helper alias Explorer.Chain.{Hash, Wei} @spec export(Hash.Address.t(), String.t() | nil, String.t() | nil, Keyword.t(), any(), any()) :: Enumerable.t() def export(address_hash, from_period, to_period, _options, _filter_type, _filter_value) do {from_block, to_block} = Helper.block_from_period(from_period, to_period) + epoch_range = Epoch.block_range_to_epoch_range(from_block, to_block) options = [ + necessity_by_association: %{ + :account_address => :optional, + :associated_account_address => :optional, + [epoch: [:end_processing_block]] => :optional + }, paging_options: Helper.paging_options(), - from_block: from_block, - to_block: to_block + api?: true ] - address_hash - |> ElectionReward.address_hash_to_rewards(options) + epoch_range + |> case do + nil -> + [] + + {from_epoch, to_epoch} -> + full_options = + options + |> Keyword.put(:from_epoch, from_epoch) + |> Keyword.put(:to_epoch, to_epoch) + + address_hash |> ElectionReward.address_hash_to_rewards(full_options) + end |> to_csv_format() |> Helper.dump_to_stream() end @@ -54,13 +65,15 @@ defmodule Explorer.Chain.CsvExport.Address.Celo.ElectionRewards do rows = election_rewards |> Stream.map(fn reward -> + block = reward.epoch.end_processing_block + [ # EpochNumber - reward.block.number |> block_number_to_epoch_number(), + reward.epoch_number, # BlockNumber - reward.block.number, + block.number, # TimestampUTC - reward.block.timestamp, + block.timestamp, # EpochTxType Map.get(reward_type_to_human_readable, reward.type, "N/A"), # ValidatorAddress From 1d235f5d087ff1c6fbe78c84a171d69db694d9c4 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 21 Aug 2025 16:34:39 +0300 Subject: [PATCH 0262/1006] fix: Fix multichain search queue export bug processing (#13049) --- .../microservice_interfaces/multichain_search.ex | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 6ad7160985b9..0c1b1bf4687c 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -867,14 +867,17 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do block_ranges_in_chunk = if index == 0, do: block_ranges, else: [] address_token_balances_in_chunk = address_token_balances_chunk_by_index(address_token_balances, index) + address_coin_balances_chunk = + case Enum.fetch(indexed_address_coin_balances_chunks, index) do + {:ok, {chunk, ^index}} when is_list(chunk) -> chunk + _ -> [] + end + base_data_chunk |> Map.put(:addresses, addresses_chunk) |> Map.put( :address_coin_balances, - if(Enum.empty?(indexed_address_coin_balances_chunks), - do: [], - else: indexed_address_coin_balances_chunks |> Enum.at(index) |> elem(0) || [] - ) + address_coin_balances_chunk ) |> Map.put(:hashes, hashes_in_chunk) |> Map.put(:block_ranges, block_ranges_in_chunk) From 2753503ac2eda990aad8847e0e7e831c6448b0bf Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 21 Aug 2025 19:09:55 +0400 Subject: [PATCH 0263/1006] Add INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT env variable (#13051) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- config/runtime.exs | 4 +++- docker-compose/envs/common-blockscout.env | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/config/runtime.exs b/config/runtime.exs index 2939fc524566..83e5237e35a0 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1204,7 +1204,9 @@ config :indexer, Indexer.Fetcher.Optimism.Interop.MessageQueue.Supervisor, disabled?: ConfigHelper.chain_type() != :optimism config :indexer, Indexer.Fetcher.Optimism.Interop.MultichainExport.Supervisor, - disabled?: ConfigHelper.chain_type() != :optimism + disabled?: + ConfigHelper.chain_type() != :optimism || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT", "true") config :indexer, Indexer.Fetcher.Optimism, optimism_l1_rpc: System.get_env("INDEXER_OPTIMISM_L1_RPC"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 38ccf8567457..66a47ff4f179 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -328,6 +328,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_OPTIMISM_INTEROP_RECV_TIMEOUT= # INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS= # INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE= +# INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT= # INDEXER_SCROLL_L1_RPC= # INDEXER_SCROLL_L1_MESSENGER_CONTRACT= # INDEXER_SCROLL_L1_MESSENGER_START_BLOCK= From ba457afa57deba4bc3d34b576194825d48b2f961 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Fri, 22 Aug 2025 15:06:46 +0400 Subject: [PATCH 0264/1006] feat: Export main page counters to Multichain service (#13007) * Add main page counters fetcher * Add main page counters queue * Add specs and docs * Update config * Remove batches for DELETE queries to avoid deadlocks * Fix tests and small refactoring --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../counters_export_queue.ex | 156 ++++++++++++ .../token_info_export_queue.ex | 24 +- .../chain/transaction/history/historian.ex | 19 ++ .../multichain_search.ex | 231 +++++++++++++++++- ..._search_db_counters_export_queue_table.exs | 19 ++ .../multichain_search_test.exs | 35 ++- .../counters_export_queue.ex | 136 +++++++++++ .../multichain_search_db/counters_fetcher.ex | 131 ++++++++++ .../token_info_export_queue.ex | 9 +- apps/indexer/lib/indexer/supervisor.ex | 4 + config/runtime.exs | 45 +++- docker-compose/envs/common-blockscout.env | 7 + 12 files changed, 777 insertions(+), 39 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/multichain_search_db/counters_export_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250814065910_add_multichain_search_db_counters_export_queue_table.exs create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_export_queue.ex create mode 100644 apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/counters_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/counters_export_queue.ex new file mode 100644 index 000000000000..e00e79fbd72a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/counters_export_queue.ex @@ -0,0 +1,156 @@ +defmodule Explorer.Chain.MultichainSearchDb.CountersExportQueue do + @moduledoc """ + Tracks counters data, pending for export to the Multichain Service database. + """ + + use Explorer.Schema + + import Ecto.Query + + alias Ecto.Multi + alias Explorer.Repo + + @required_attrs ~w(timestamp counter_type data)a + @optional_attrs ~w(retries_number)a + @allowed_attrs @optional_attrs ++ @required_attrs + + @typedoc """ + * `timestamp` - The timestamp of the counters. The counters in `data` are only relevant at the moment of the timestamp. + * `counter_type` - The type of the counters in `data`. Currently only `global` type is implemented which includes the following counters: + - daily transactions number + - total transactions number + - total addresses number + * `data` - The map containing the counters relevant to the timestamp. + * `retries_number` - A number of retries to send the counters to Multichain service. + Equals to `nil` if the counters haven't been sent to the service yet. + """ + @primary_key false + typed_schema "multichain_search_db_export_counters_queue" do + field(:timestamp, :utc_datetime_usec, primary_key: true) + + field(:counter_type, Ecto.Enum, + values: [:global], + null: false, + primary_key: true + ) + + field(:data, :map) + field(:retries_number, :integer) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() + def changeset(%__MODULE__{} = queue, attrs) do + queue + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + end + + @doc """ + Streams a batch of multichain database counter entries that need to be retried for export. + + This function selects specific fields from the export records and applies a reducer function + to each entry in the stream, accumulating the result. + Optionally, the stream can be limited based on the `limited?` flag. + + ## Parameters + - `initial`: The initial accumulator value. + - `reducer`: A function that takes an entry (as a map) and the current accumulator, returning the updated accumulator. + - `limited?` (optional): A boolean indicating whether to apply a fetch limit to the stream. Defaults to `false`. + + ## Returns + - `{:ok, accumulator}`: A tuple containing `:ok` and the final accumulator after processing the stream. + """ + @spec stream_multichain_db_counters_batch( + initial :: accumulator, + reducer :: (entry :: map(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_multichain_db_counters_batch(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + __MODULE__ + |> select([export], %{ + timestamp: export.timestamp, + counter_type: export.counter_type, + data: export.data + }) + |> add_queue_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + # Limits the SELECT query if needed. The limit is defined in `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT` env variable. + # + # ## Parameters + # - `query`: The query to add the limit to. + # - `true or false`: If `true`, add the limit. If `false`, leave the query as it is. + # + # ## Returns + # - The modified query with the limit or the source query without changes. + @spec add_queue_fetcher_limit(Ecto.Query.t(), boolean()) :: Ecto.Query.t() + defp add_queue_fetcher_limit(query, false), do: query + + defp add_queue_fetcher_limit(query, true) do + limit = Application.get_env(:indexer, Indexer.Fetcher.MultichainSearchDb.CountersExportQueue)[:init_limit] + limit(query, ^limit) + end + + @doc """ + Constructs DELETE FROM queries for the counter items to be deleted from the queue. + + ## Parameters + - `queue_items`: A list of items to be deleted from the queue. Each item is identified by its primary key. + + ## Returns + - An `Ecto.Multi` struct containing the delete operations. + """ + @spec delete_query([%{:timestamp => DateTime.t(), :counter_type => atom(), optional(:data) => map()}]) :: Multi.t() + def delete_query(queue_items) do + queue_items + |> Enum.reduce(Multi.new(), fn queue_item, multi_acc -> + Multi.delete_all( + multi_acc, + {queue_item.timestamp, queue_item.counter_type}, + from(q in __MODULE__, + where: q.timestamp == ^queue_item.timestamp and q.counter_type == ^queue_item.counter_type + ) + ) + end) + end + + @doc """ + Returns the current number of items in the queue. + + ## Returns + - The current number of items in the queue. + """ + @spec queue_size() :: non_neg_integer() + def queue_size do + Repo.aggregate(__MODULE__, :count) + end + + @doc """ + Returns an Ecto query that defines the conflict resolution strategy for the + `multichain_search_db_export_counters_queue` table. On conflict, it increments the `retries_number` + (by using the db stored value or 0 if not present) and updates the + `updated_at` field to the greatest value between the current and the new timestamp. + + This is typically used in upsert operations to ensure retry counts are tracked and + timestamps are properly updated. + """ + @spec increase_retries_on_conflict :: Ecto.Query.t() + def increase_retries_on_conflict do + from( + q in __MODULE__, + update: [ + set: [ + retries_number: fragment("COALESCE(?, 0) + 1", q.retries_number), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", q.updated_at) + ] + ] + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex index 0020607a4c8b..860f6705016c 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/token_info_export_queue.ex @@ -7,7 +7,6 @@ defmodule Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue do import Ecto.Query - alias Ecto.Multi alias Explorer.Repo @required_attrs ~w(address_hash data_type data)a @@ -79,26 +78,19 @@ defmodule Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue do end @doc """ - Constructs DELETE FROM queries for the token info items to be deleted from the queue. + Constructs query for DELETE FROM query for the token info item to be deleted from the queue. ## Parameters - - `queue_items`: A list of items to be deleted from the queue. Each item is identified by its primary key. + - `queue_item`: An item to be deleted from the queue. The item is identified by its primary key. ## Returns - - An `Ecto.Multi` struct containing the delete operations. + - An `Ecto.Query` struct containing the delete operation. """ - @spec delete_query([%{:address_hash => binary(), :data_type => atom(), optional(:data) => map()}]) :: Multi.t() - def delete_query(queue_items) do - queue_items - |> Enum.reduce(Multi.new(), fn queue_item, multi_acc -> - Multi.delete_all( - multi_acc, - {queue_item.address_hash, queue_item.data_type}, - from(q in __MODULE__, - where: q.address_hash == ^queue_item.address_hash and q.data_type == ^queue_item.data_type - ) - ) - end) + @spec delete_query(%{:address_hash => binary(), :data_type => atom(), optional(:data) => map()}) :: Ecto.Query.t() + def delete_query(queue_item) do + from(q in __MODULE__, + where: q.address_hash == ^queue_item.address_hash and q.data_type == ^queue_item.data_type + ) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex index d454af6717a0..b570bb93c02b 100644 --- a/apps/explorer/lib/explorer/chain/transaction/history/historian.ex +++ b/apps/explorer/lib/explorer/chain/transaction/history/historian.ex @@ -7,6 +7,7 @@ defmodule Explorer.Chain.Transaction.History.Historian do alias Explorer.Chain.{Block, DenormalizationHelper, Transaction} alias Explorer.Chain.Block.Reader.General, as: BlockGeneralReader + alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.History.Process, as: HistoryProcess @@ -253,6 +254,12 @@ defmodule Explorer.Chain.Transaction.History.Historian do Logger.info("tx/per day chart: number of inserted #{num_inserted}") + # we need to store the last timestamp of success to use it in Indexer.Fetcher.MultichainSearchDb.CountersFetcher + LastFetchedCounter.upsert(%{ + counter_type: transaction_stats_last_save_records_timestamp(), + value: DateTime.to_unix(DateTime.utc_now()) + }) + Publisher.broadcast(:transaction_stats) num_inserted end @@ -269,4 +276,16 @@ defmodule Explorer.Chain.Transaction.History.Historian do defp date_today do HistoryProcess.config_or_default(:utc_today, Date.utc_today(), __MODULE__) end + + @doc """ + Defines a name of counter type for LastFetchedCounter row + storing the last timestamp of when the `save_records` function was called. + + ## Returns + - The counter type name. + """ + @spec transaction_stats_last_save_records_timestamp() :: String.t() + def transaction_stats_last_save_records_timestamp do + "transaction_stats_last_save_records_timestamp" + end end diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 0c1b1bf4687c..6d2e80422739 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -7,7 +7,14 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Block, Hash, Token, Transaction, Wei} alias Explorer.Chain.Block.Range - alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue, TokenInfoExportQueue} + + alias Explorer.Chain.MultichainSearchDb.{ + BalancesExportQueue, + CountersExportQueue, + MainExportQueue, + TokenInfoExportQueue + } + alias Explorer.{Helper, HttpClient, Repo} alias Explorer.Utility.Microservice @@ -41,6 +48,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do """ @spec batch_import(map()) :: {:error, map()} | {:ok, any()} def batch_import(params) do + # todo: rename this function to `batch_export` (and all related places in code & comments) if enabled?() do params_chunks = extract_batch_import_params_into_chunks(params) url = batch_import_url() @@ -198,6 +206,93 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end end + @doc """ + Processes a batch export of counters by splitting the items from db queue into chunks + and sending each chunk as an HTTP POST request to a configured microservice endpoint. + + If the microservice is enabled, the function: + - Splits the input `items_from_db_queue` into manageable chunks. + - Sends each chunk concurrently using `Task.async_stream/5` with a maximum concurrency and timeout. + - Collects the results, merging any errors and accumulating data that needs to be retried. + - Returns `{:ok, {:chunks_processed, chunks}}` if all chunks are processed successfully. + - Returns `{:error, data_to_retry}` if any chunk fails, where `data_to_retry` contains counters that need to be retried. + + If the microservice is disabled, returns `{:ok, :service_disabled}`. + + ## Parameters + - `items_from_db_queue`: The queue items to be exported, which will be split into chunks for processing. + + ## Returns + - `{:ok, any()}`: If all chunks are processed successfully or the service is disabled. + - `{:error, map()}`: If one or more chunks fail, with details about the data that needs to be retried. + """ + @spec batch_export_counters([ + %{ + :timestamp => DateTime.t(), + :counter_type => :global, + :data => map() + } + ]) :: {:ok, any()} | {:error, map()} + def batch_export_counters(items_from_db_queue) do + if enabled?() do + url = batch_import_url() + api_key = api_key() + chain_id = to_string(ChainId.get_id()) + + chunks = + items_from_db_queue + |> Enum.chunk_every(counters_chunk_size()) + |> Enum.map(fn chunk_items -> + %{ + api_key: api_key, + chain_id: chain_id, + counters: Enum.map(chunk_items, &counter_queue_item_to_http_item(&1)) + } + end) + + chunks + |> Task.async_stream( + fn export_body -> http_post_request(url, export_body) end, + max_concurrency: @max_concurrency, + timeout: @post_timeout, + zip_input_on_exit: true + ) + |> Enum.reduce({:ok, {:chunks_processed, chunks}}, fn + {:ok, {:ok, _result}}, acc -> + acc + + {:ok, {:error, error}}, acc -> + counter_on_error(error) + + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, %{counters: error.data_to_retry.counters}} + + {:error, data_to_retry} -> + {:error, %{counters: data_to_retry.counters ++ error.data_to_retry.counters}} + end + + {:exit, {export_body, reason}}, acc -> + counter_on_error(%{ + url: url, + data_to_retry: export_body, + reason: reason + }) + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + case acc do + {:ok, {:chunks_processed, _}} -> + {:error, %{counters: export_body.counters}} + + {:error, data_to_retry} -> + {:error, %{counters: data_to_retry.counters ++ export_body.counters}} + end + end) + else + {:ok, :service_disabled} + end + end + defp log_error(%{ url: url, data_to_retry: data_to_retry, @@ -296,6 +391,36 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp token_info_on_error(_), do: :ignore + # Logs error when trying to send counter from the queue to Multichain service + # and increments `retries_number` counter of the corresponding queue items. + # + # ## Parameters + # - `error`: A map with the queue items. + # + # ## Returns + # - Nothing. + @spec counter_on_error(%{ + :data_to_retry => %{:counters => [map()], optional(any()) => any()}, + optional(any()) => any() + }) :: any() + defp counter_on_error(%{data_to_retry: data_to_retry} = error) do + log_error(error) + + prepared_counters_data = + data_to_retry.counters + |> Enum.map(&counter_http_item_to_queue_item(&1)) + |> Helper.add_timestamps() + + Repo.insert_all( + CountersExportQueue, + prepared_counters_data, + on_conflict: CountersExportQueue.increase_retries_on_conflict(), + conflict_target: [:timestamp, :counter_type] + ) + end + + defp counter_on_error(_), do: :ignore + @doc """ Converts database queue item with token info to the item ready to send to Multichain service via HTTP. @@ -370,6 +495,53 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do } end + @doc """ + Converts database queue item with counters to the item ready to send to Multichain service via HTTP. + + ## Parameters + - `item_from_db_queue`: The queue item map from database. + + ## Returns + - A map ready to send to Multichain service via HTTP. + """ + @spec counter_queue_item_to_http_item(%{ + :timestamp => DateTime.t(), + :counter_type => :global, + :data => map() + }) :: %{:timestamp => String.t(), :global_counters => map()} + def counter_queue_item_to_http_item(item_from_db_queue) do + counter = %{timestamp: to_string(DateTime.to_unix(item_from_db_queue.timestamp))} + + case item_from_db_queue.counter_type do + :global -> Map.put(counter, :global_counters, item_from_db_queue.data) + end + end + + @doc """ + Converts queue item (containing counter data) ready to send to Multichain service via HTTP + to the queue item ready to be written to the database. + + ## Parameters + - `http_item`: The queue item for HTTP. + + ## Returns + - A map ready to write to the database. + """ + @spec counter_http_item_to_queue_item(%{:timestamp => String.t(), :global_counters => map()}) :: %{ + :timestamp => DateTime.t(), + :counter_type => :global, + :data => map() + } + def counter_http_item_to_queue_item(%{timestamp: timestamp_string} = http_item) do + timestamp_integer = String.to_integer(timestamp_string) + + %{ + timestamp: DateTime.from_unix!(timestamp_integer * 1_000_000, :microsecond), + counter_type: :global, + data: http_item[:global_counters] + } + end + @doc """ Sends provided blockchain data to the appropriate export queues for further processing. @@ -588,6 +760,59 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do |> Enum.chunk_every(token_info_chunk_size()) end + @doc """ + Writes counters to database queue to send those to Multichain service later. + + ## Parameters + - `entries`: A map of counter entries. + - `entries_type`: A type of the counter entries. + + ## Returns + # - `:ok` if the data is accepted for insertion. + # - `:ignore` if the Multichain service is not used. + """ + @spec send_counters_to_queue(%{DateTime.t() => map()}, :global) :: :ok | :ignore + def send_counters_to_queue(entries, entries_type) do + if enabled?() do + entries + |> extract_counter_entries_into_chunks(entries_type) + |> Enum.each(fn chunk -> + Repo.insert_all( + CountersExportQueue, + Helper.add_timestamps(chunk), + on_conflict: {:replace, [:data, :updated_at]}, + conflict_target: [:timestamp, :counter_type] + ) + end) + + :ok + else + :ignore + end + end + + # Takes a map of counter entries and makes a list of the entries divided into chunks. + # The chunk max size is defined by `MICROSERVICE_MULTICHAIN_SEARCH_COUNTERS_CHUNK_SIZE` env variable. + # + # ## Parameters + # - `entries`: A map of the counter entries. + # - `entries_type`: A type of the counter entries. + # + # ## Returns + # - A list of chunks with the entries. + @spec extract_counter_entries_into_chunks(%{DateTime.t() => map()}, :global) :: list() + defp extract_counter_entries_into_chunks(entries, entries_type) do + entries + |> Enum.map(fn {timestamp, data} -> + %{ + timestamp: timestamp, + counter_type: entries_type, + data: data + } + end) + |> Enum.chunk_every(counters_chunk_size()) + end + # sobelow_skip ["DOS.StringToAtom"] defp prepare_export_data_for_queue(%{ addresses: addresses, @@ -1074,4 +1299,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do defp token_info_chunk_size do Application.get_env(:explorer, __MODULE__)[:token_info_chunk_size] end + + defp counters_chunk_size do + Application.get_env(:explorer, __MODULE__)[:counters_chunk_size] + end end diff --git a/apps/explorer/priv/repo/migrations/20250814065910_add_multichain_search_db_counters_export_queue_table.exs b/apps/explorer/priv/repo/migrations/20250814065910_add_multichain_search_db_counters_export_queue_table.exs new file mode 100644 index 000000000000..bbe3243f05eb --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250814065910_add_multichain_search_db_counters_export_queue_table.exs @@ -0,0 +1,19 @@ +defmodule Explorer.Repo.Migrations.AddMultichainSearchDbCountersExportQueueTable do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE multichain_search_counter_type AS ENUM ('global')", + "DROP TYPE multichain_search_counter_type" + ) + + create table(:multichain_search_db_export_counters_queue, primary_key: false) do + add(:timestamp, :utc_datetime_usec, primary_key: true) + add(:counter_type, :multichain_search_counter_type, null: false, primary_key: true) + add(:data, :jsonb, null: false) + add(:retries_number, :smallint, null: true) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index f14d9c279340..94460e13d99e 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -361,8 +361,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do items_from_db_queue = [token_info_item_1, token_info_item_2] items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Enum.each(fn item -> + item + |> TokenInfoExportQueue.delete_query() + |> Repo.delete_all() + end) assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 assert {:ok, {:chunks_processed, _}} = MultichainSearch.batch_export_token_info(items_from_db_queue) @@ -400,8 +403,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do items_from_db_queue = [token_info_item_1, token_info_item_2] items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Enum.each(fn item -> + item + |> TokenInfoExportQueue.delete_query() + |> Repo.delete_all() + end) assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 @@ -496,8 +502,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do end items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Enum.each(fn item -> + item + |> TokenInfoExportQueue.delete_query() + |> Repo.delete_all() + end) assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 @@ -558,8 +567,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do items_from_db_queue = 10 |> insert_list(:multichain_search_db_export_token_info_queue) items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Enum.each(fn item -> + item + |> TokenInfoExportQueue.delete_query() + |> Repo.delete_all() + end) assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) @@ -596,8 +608,11 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do items_from_db_queue = 15 |> insert_list(:multichain_search_db_export_token_info_queue) items_from_db_queue - |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Enum.each(fn item -> + item + |> TokenInfoExportQueue.delete_query() + |> Repo.delete_all() + end) assert Repo.aggregate(TokenInfoExportQueue, :count) == 0 assert {:error, results} = MultichainSearch.batch_export_token_info(items_from_db_queue) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_export_queue.ex new file mode 100644 index 000000000000..42a64451c9ea --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_export_queue.ex @@ -0,0 +1,136 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.CountersExportQueue do + @moduledoc """ + Exports blockchain data to Multichain Search DB service from the queue. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + alias Explorer.Chain.MultichainSearchDb.CountersExportQueue + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Repo + + alias Indexer.{BufferedTask, Helper} + + @behaviour BufferedTask + + @default_max_batch_size 1000 + @default_max_concurrency 10 + @delete_queries_chunk_size 10 + @failed_to_export_data_error "Batch counters export attempt to the Multichain Search DB failed" + @fetcher_name :multichain_search_db_counters_export_queue + @queue_size_info "Queue size" + @successfully_sent_info "Successfully sent" + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.merge(state: []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + CountersExportQueue.stream_multichain_db_counters_batch( + initial_acc, + fn data, acc -> + Helper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end, + true + ) + + acc + end + + @impl BufferedTask + def run(items_from_db_queue, _) when is_list(items_from_db_queue) do + case MultichainSearch.batch_export_counters(items_from_db_queue) do + {:ok, {:chunks_processed, chunks}} -> + chunks + |> Enum.map(fn chunk -> chunk.counters end) + |> List.flatten() + |> Enum.map(&MultichainSearch.counter_http_item_to_queue_item(&1)) + |> delete_queue_items() + |> log_queue_size() + + {:error, data_to_retry} -> + Logger.error(fn -> + ["#{@failed_to_export_data_error}", "#{inspect(data_to_retry.counters)}"] + end) + + queue_items_to_retry = + data_to_retry.counters + |> Enum.map(&MultichainSearch.counter_http_item_to_queue_item(&1)) + + items_from_db_queue + |> Enum.reject(fn item_to_export -> + Enum.any?( + queue_items_to_retry, + &(&1.timestamp == item_to_export.timestamp and &1.counter_type == item_to_export.counter_type) + ) + end) + |> delete_queue_items() + |> log_queue_size() + + {:retry, queue_items_to_retry} + end + end + + # Removes items successfully sent to Multichain service from db queue. + # The list is split into small chunks to prevent db deadlocks. + # + # ## Parameters + # - `items`: The list of queue items to delete from the queue. + # + # ## Returns + # - The `items` list. + @spec delete_queue_items([map()]) :: [map()] + defp delete_queue_items(items) do + items + |> Enum.chunk_every(@delete_queries_chunk_size) + |> Enum.each(fn chunk_items -> + chunk_items + |> CountersExportQueue.delete_query() + |> Repo.transaction() + end) + + items + end + + # Logs the number of the current queue size and the number of successfully sent items. + # + # ## Parameters + # - `items_successful`: The list of items successfully sent to Multichain service. + # + # ## Returns + # - `:ok` + @spec log_queue_size(list()) :: any() + defp log_queue_size(items_successful) do + Logger.info( + fn -> + [ + "#{@queue_size_info}: ", + "#{CountersExportQueue.queue_size()}, ", + "#{@successfully_sent_info}: ", + "#{Enum.count(items_successful)}" + ] + end, + fetcher: @fetcher_name + ) + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex new file mode 100644 index 000000000000..7535335bf933 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex @@ -0,0 +1,131 @@ +defmodule Indexer.Fetcher.MultichainSearchDb.CountersFetcher do + @moduledoc """ + Fetches counters and adds them to a queue to send to Multichain Search DB service. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query, + only: [ + from: 2 + ] + + alias Explorer.Chain.{Address, Transaction} + alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.Transaction.History.{Historian, TransactionStats} + alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Repo + + @fetcher_name :multichain_search_db_counters_fetcher + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, nil}} + end + + @impl GenServer + def handle_continue(_, _state) do + Logger.metadata(fetcher: @fetcher_name) + + # two-second pause needed to avoid exceeding Supervisor restart intensity when DB issues + :timer.sleep(2000) + + Process.send(self(), :try_to_fetch_yesterday_counters, []) + + {:noreply, %{}} + end + + @impl GenServer + def handle_info(:try_to_fetch_yesterday_counters, _state) do + today = Date.utc_today() + yesterday = Date.add(today, -1) + + Logger.info("Waiting for transaction stats to be collected for #{yesterday}...") + + last_save_records_date = + Historian.transaction_stats_last_save_records_timestamp() + |> LastFetchedCounter.get() + |> Decimal.to_integer() + |> DateTime.from_unix!() + |> DateTime.to_date() + + if last_save_records_date == today do + # Historian module worked today, so we can use its results + # for yesterday's number of transactions + number_of_transactions = + yesterday + |> TransactionStats.by_date_range(yesterday) + |> Enum.at(0, %{}) + |> Map.get(:number_of_transactions, 0) + + Process.send(self(), :fetch_yesterday_counters, []) + + {:noreply, %{number_of_transactions: number_of_transactions, yesterday: yesterday}} + else + # the stats is not ready yet, so wait for 1 minute and try again + Process.send_after(self(), :try_to_fetch_yesterday_counters, 60_000) + {:noreply, %{}} + end + end + + @impl GenServer + def handle_info(:fetch_yesterday_counters, %{number_of_transactions: number_of_transactions, yesterday: yesterday}) do + yesterday_dt = DateTime.new!(yesterday, Time.new!(23, 59, 59, 0)) + + daily_transactions_number = number_of_transactions + + total_transactions_number = + Repo.aggregate(from(t in Transaction, where: t.block_timestamp <= ^yesterday_dt), :count, :hash, + timeout: :infinity + ) + + total_addresses_number = + Repo.aggregate(from(a in Address, where: a.inserted_at <= ^yesterday_dt), :count, timeout: :infinity) + + Logger.info("Transaction stats is now available for #{yesterday}:") + Logger.info("daily_transactions_number = #{daily_transactions_number}") + Logger.info("total_transactions_number = #{total_transactions_number}") + Logger.info("total_addresses_number = #{total_addresses_number}") + + MultichainSearch.send_counters_to_queue( + %{ + yesterday_dt => %{ + daily_transactions_number: to_string(daily_transactions_number), + total_transactions_number: to_string(total_transactions_number), + total_addresses_number: to_string(total_addresses_number) + } + }, + :global + ) + + Logger.info("Waiting for the next day...") + Process.send_after(self(), :try_to_fetch_yesterday_counters, calculate_delay_until_next_midnight()) + + {:noreply, %{}} + end + + defp calculate_delay_until_next_midnight do + now = DateTime.utc_now() + tomorrow = DateTime.new!(Date.add(Date.utc_today(), 1), Time.new!(0, 0, 0, 0), now.time_zone) + + DateTime.diff(tomorrow, now, :millisecond) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex index 1903100383b9..66378b80bdef 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/token_info_export_queue.ex @@ -18,7 +18,6 @@ defmodule Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue do @default_max_batch_size 1000 @default_max_concurrency 10 - @delete_queries_chunk_size 10 @failed_to_export_data_error "Batch token info export attempt to the Multichain Search DB failed" @fetcher_name :multichain_search_db_token_info_export_queue @queue_size_info "Queue size" @@ -83,7 +82,6 @@ defmodule Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue do end # Removes items successfully sent to Multichain service from db queue. - # The list is split into small chunks to prevent db deadlocks. # # ## Parameters # - `items`: The list of queue items to delete from the queue. @@ -93,11 +91,10 @@ defmodule Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue do @spec delete_queue_items([map()]) :: [map()] defp delete_queue_items(items) do items - |> Enum.chunk_every(@delete_queries_chunk_size) - |> Enum.each(fn chunk_items -> - chunk_items + |> Enum.each(fn item -> + item |> TokenInfoExportQueue.delete_query() - |> Repo.transaction() + |> Repo.delete_all() end) items diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index b4fc09736bb8..e226bb3b7eec 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -24,6 +24,8 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime alias Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue + alias Indexer.Fetcher.MultichainSearchDb.CountersExportQueue, as: MultichainSearchDbCountersExportQueue + alias Indexer.Fetcher.MultichainSearchDb.CountersFetcher, as: MultichainSearchDbCountersFetcher alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue alias Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, as: MultichainSearchDbTokenInfoExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability @@ -158,6 +160,8 @@ defmodule Indexer.Supervisor do {MultichainSearchDbMainExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbBalancesExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbTokenInfoExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbCountersExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, + {MultichainSearchDbCountersFetcher.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.RollupL1ReorgMonitor.Supervisor, [[memory_monitor: memory_monitor]]}, configure( Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, diff --git a/config/runtime.exs b/config/runtime.exs index 83e5237e35a0..731b8b2d1a43 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,6 +7,9 @@ import Config config :logger, backends: ConfigHelper.logger_backends() +microservice_multichain_search_url = System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL") +transactions_stats_enabled = ConfigHelper.parse_bool_env_var("TXS_STATS_ENABLED", "true") + ###################### ### BlockScout Web ### ###################### @@ -532,7 +535,7 @@ config :explorer, Explorer.Market.Fetcher.History, config :explorer, Explorer.Chain.Transaction, suave_bid_contracts: System.get_env("SUAVE_BID_CONTRACTS", "") config :explorer, Explorer.Chain.Transaction.History.Historian, - enabled: ConfigHelper.parse_bool_env_var("TXS_STATS_ENABLED", "true"), + enabled: transactions_stats_enabled, init_lag_milliseconds: ConfigHelper.parse_time_env_var("TXS_HISTORIAN_INIT_LAG", "0"), days_to_compile_at_init: ConfigHelper.parse_integer_env_var("TXS_STATS_DAYS_TO_COMPILE_AT_INIT", 40) @@ -655,11 +658,12 @@ config :explorer, Explorer.SmartContract.StylusVerifierInterface, config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), - service_url: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL"), + service_url: microservice_multichain_search_url, addresses_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE", 7_000), token_info_chunk_size: - ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_TOKEN_INFO_CHUNK_SIZE", 1_000) + ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_TOKEN_INFO_CHUNK_SIZE", 1_000), + counters_chunk_size: ConfigHelper.parse_integer_env_var("MICROSERVICE_MULTICHAIN_SEARCH_COUNTERS_CHUNK_SIZE", 1_000) config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), @@ -907,6 +911,9 @@ trace_block_ranges = ranges -> ranges end +disable_multichain_search_db_export_counters_queue_fetcher = + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_FETCHER") + config :indexer, block_transformer: ConfigHelper.block_transformer(), metadata_updater_milliseconds_interval: ConfigHelper.parse_time_env_var("TOKEN_METADATA_UPDATE_INTERVAL", "48h"), @@ -1044,17 +1051,29 @@ config :indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, config :indexer, Indexer.Fetcher.MultichainSearchDb.MainExportQueue.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER") || - is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + is_nil(microservice_multichain_search_url) config :indexer, Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER") || - is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + is_nil(microservice_multichain_search_url) config :indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue.Supervisor, disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_FETCHER") || - is_nil(System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL")) + is_nil(microservice_multichain_search_url) + +config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersExportQueue.Supervisor, + disabled?: + disable_multichain_search_db_export_counters_queue_fetcher || + is_nil(microservice_multichain_search_url) || + !transactions_stats_enabled + +config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersFetcher.Supervisor, + disabled?: + disable_multichain_search_db_export_counters_queue_fetcher || + is_nil(microservice_multichain_search_url) || + !transactions_stats_enabled config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 10), @@ -1182,6 +1201,20 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, init_limit: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_INIT_QUERY_LIMIT", 1_000) +config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersExportQueue, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_CONCURRENCY", 10), + batch_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_BATCH_SIZE", 1_000), + enqueue_busy_waiting_timeout: + ConfigHelper.parse_time_env_var( + "INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT", + "1s" + ), + max_queue_size: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_MAX_QUEUE_SIZE", 1_000), + init_limit: + ConfigHelper.parse_integer_env_var("INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT", 1_000) + config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 66a47ff4f179..7433df1cec1f 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -189,6 +189,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_MAIN_QUEUE_FETCHER= # INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_BALANCES_QUEUE_FETCHER= # INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_FETCHER= +# INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_FETCHER= # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE= # INDEXER_CATCHUP_BLOCKS_CONCURRENCY= # INDEXER_CATCHUP_BLOCK_INTERVAL= @@ -223,6 +224,11 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_MAX_QUEUE_SIZE= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_TOKEN_INFO_QUEUE_INIT_QUERY_LIMIT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_BATCH_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_CONCURRENCY= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_MAX_QUEUE_SIZE= +# INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= @@ -445,6 +451,7 @@ MICROSERVICE_ACCOUNT_ABSTRACTION_URL=http://user-ops-indexer:8050/ # MICROSERVICE_MULTICHAIN_SEARCH_API_KEY= # MICROSERVICE_MULTICHAIN_SEARCH_ADDRESSES_CHUNK_SIZE= # MICROSERVICE_MULTICHAIN_SEARCH_TOKEN_INFO_CHUNK_SIZE= +# MICROSERVICE_MULTICHAIN_SEARCH_COUNTERS_CHUNK_SIZE= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED= # MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL= DECODE_NOT_A_CONTRACT_CALLS=true From 4ccb09a5e7921c86914d372c0477d519434148e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:09:39 +0300 Subject: [PATCH 0265/1006] chore(deps): bump sha.js in /apps/block_scout_web/assets (#13053) Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12. - [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12) --- updated-dependencies: - dependency-name: sha.js dependency-version: 2.4.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 7f6d5697d4b0..2adb6d66fa4f 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -13805,17 +13805,45 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" }, "bin": { "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -25946,12 +25974,20 @@ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" }, "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "shallow-clone": { From a3842f57f3a1dd2b11a83a4a854836a2711d3d0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Aug 2025 14:10:57 +0300 Subject: [PATCH 0266/1006] chore(deps): bump cipher-base in /apps/block_scout_web/assets (#13054) Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6. - [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md) - [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6) --- updated-dependencies: - dependency-name: cipher-base dependency-version: 1.0.6 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/assets/package-lock.json | 51 +++++++++++++++---- 1 file changed, 41 insertions(+), 10 deletions(-) diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 2adb6d66fa4f..1d8216ed3343 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -5685,14 +5685,38 @@ "dev": true }, "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "license": "MIT", "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" } }, + "node_modules/cipher-base/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/cjs-module-lexer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", @@ -20005,12 +20029,19 @@ "dev": true }, "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "cjs-module-lexer": { From cc19fdf3002e59c4ccc94454666b85ee63d7b1b9 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:12:13 +0300 Subject: [PATCH 0267/1006] fix: Fix Postgres errors in Explorer.Migrator.BackfillMetadataURL (#13063) --- .../lib/explorer/metadata_uri_validator.ex | 3 +- .../migrator/backfill_metadata_url.ex | 2 +- .../migrator/backfill_metadata_url_test.exs | 96 +++++++++++++++++++ 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/metadata_uri_validator.ex b/apps/explorer/lib/explorer/metadata_uri_validator.ex index 3320d3d41a2e..44d59135fc8b 100644 --- a/apps/explorer/lib/explorer/metadata_uri_validator.ex +++ b/apps/explorer/lib/explorer/metadata_uri_validator.ex @@ -63,7 +63,8 @@ defmodule Explorer.MetadataURIValidator do """ @spec validate_uri(String.t()) :: :ok | {:error, atom()} def validate_uri(uri) do - with {:empty_host, %URI{host: host, scheme: scheme}} when host not in ["", nil] <- {:empty_host, URI.parse(uri)}, + with {:not_printable, false} <- {:not_printable, not String.printable?(uri)}, + {:empty_host, %URI{host: host, scheme: scheme}} when host not in ["", nil] <- {:empty_host, URI.parse(uri)}, {:disallowed_protocol, false} <- {:disallowed_protocol, scheme not in allowed_uri_protocols()}, {:nxdomain, ip_list} when not is_nil(ip_list) <- {:nxdomain, host_to_ip_list(host)}, {:blacklist, false} <- {:blacklist, not Enum.all?(ip_list, &allowed_ip?/1)} do diff --git a/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex b/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex index eea496c52454..9af61bee3f6e 100644 --- a/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex +++ b/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex @@ -143,7 +143,7 @@ defmodule Explorer.Migrator.BackfillMetadataURL do defp process_common_url(url) do case MetadataURIValidator.validate_uri(url) do :ok -> - %{metadata_url: url, skip_metadata_url: false} + %{metadata_url: String.slice(url, 0, 2048), skip_metadata_url: false} {:error, reason} -> %{ diff --git a/apps/explorer/test/explorer/migrator/backfill_metadata_url_test.exs b/apps/explorer/test/explorer/migrator/backfill_metadata_url_test.exs index 2a6a7a4b8964..1ad32f349dcb 100644 --- a/apps/explorer/test/explorer/migrator/backfill_metadata_url_test.exs +++ b/apps/explorer/test/explorer/migrator/backfill_metadata_url_test.exs @@ -346,5 +346,101 @@ defmodule Explorer.Migrator.BackfillMetadataURLTest do assert MigrationStatus.get_status("backfill_metadata_url") == "completed" end + + test "regression for https://github.com/blockscout/blockscout/issues/12389" do + token = insert(:token, type: "ERC-721") + + insert(:token_instance, + metadata: %{awesome: "metadata"}, + token_contract_address_hash: token.contract_address_hash, + token_id: 0 + ) + + insert(:token_instance, + metadata: %{awesome: "metadata"}, + token_contract_address_hash: token.contract_address_hash, + token_id: 1 + ) + + token_contract_address_hash_string = to_string(token.contract_address_hash) + + encoded_url_2 = + "0x" <> + (ABI.TypeEncoder.encode( + ["http://example.com/api/card/{id}/" <> String.duplicate("a", 3000)], + %ABI.FunctionSelector{ + function: nil, + types: [ + :string + ] + } + ) + |> Base.encode16(case: :lower)) + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: id_1, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000000", + to: ^token_contract_address_hash_string + }, + "latest" + ] + }, + %{ + id: id_2, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000001", + to: ^token_contract_address_hash_string + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: id_1, + jsonrpc: "2.0", + result: + "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004768747470733a2f2f6170692e63727970746f736861636b2e636c75622f676f7068657273000000000000000000000000000000000000000000000000002f6a736f6e2f3235343300000000000000000000000000000000000000000000000000" + }, + %{id: id_2, jsonrpc: "2.0", result: encoded_url_2} + ]} + end + ) + + assert MigrationStatus.get_status("backfill_metadata_url") == nil + + BackfillMetadataURL.start_link([]) + Process.sleep(100) + + [instance_1, instance_2] = + Instance + |> order_by([i], asc: i.token_id) + |> Repo.all() + + assert instance_1.skip_metadata_url == false + assert instance_2.skip_metadata_url == false + + assert is_nil(instance_1.metadata) + assert !is_nil(instance_2.metadata) + + assert instance_2.metadata == %{"awesome" => "metadata"} + assert String.length(instance_2.metadata_url) == 2048 + assert instance_1.error == "not_printable" + + assert MigrationStatus.get_status("backfill_metadata_url") == "completed" + end end end From 395474e86bb2d17d36b1cb9dfffb0f57ef837a5e Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 25 Aug 2025 13:14:30 +0300 Subject: [PATCH 0268/1006] fix: Fix no function clause matching in Explorer.Chain.Transaction.decoded_input_data/5 (#13055) --- .../block_scout_web/schemas/api/v2/address.ex | 2 +- .../api/v2/address_controller_test.exs | 95 +++++++++++++++++++ .../lib/explorer/chain/transaction.ex | 9 ++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index 90ae066a1422..7356f79fd070 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -70,7 +70,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do type: :array, items: General.Implementation }, - is_verified: %Schema{type: :boolean, description: "Has address associated source code?", nullable: false}, + is_verified: %Schema{type: :boolean, description: "Has address associated source code?", nullable: true}, ens_domain_name: %Schema{ type: :string, description: "ENS domain name associated with the address", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 9a6da7ac62d6..944a1cb0a306 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -1192,6 +1192,101 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end + + test "regression test for decoding issue", %{conn: conn} do + from_address = insert(:address) + to_address = build(:address) + + transaction = + insert(:transaction, from_address: from_address, to_address_hash: to_address.hash, to_address: to_address) + + Explorer.Repo.delete(to_address) + + bypass = Bypass.open() + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + old_chain_id = Application.get_env(:block_scout_web, :chain_id) + chain_id = 1 + Application.put_env(:block_scout_web, :chain_id, chain_id) + + old_env_bens = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.BENS) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + old_env_metadata = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.Metadata) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Bypass.expect_once(bypass, "POST", "api/v1/#{chain_id}/addresses:batch_resolve_names", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{ + "names" => %{ + to_string(to_address) => "test.eth" + } + }) + ) + end) + + Bypass.expect_once(bypass, "GET", "api/v1/metadata", fn conn -> + Conn.resp( + conn, + 200, + Jason.encode!(%{ + "addresses" => %{ + to_string(to_address) => %{ + "tags" => [ + %{ + "name" => "Proposer Fee Recipient", + "ordinal" => 0, + "slug" => "proposer-fee-recipient", + "tagType" => "generic", + "meta" => "{\"styles\":\"danger_high\"}" + } + ] + } + } + }) + ) + end) + + request = get(conn, "/api/v2/addresses/#{from_address.hash}/transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + transaction = Enum.at(response["items"], 0) + assert transaction["to"]["ens_domain_name"] == "test.eth" + + assert transaction["to"]["metadata"] == %{ + "tags" => [ + %{ + "slug" => "proposer-fee-recipient", + "name" => "Proposer Fee Recipient", + "ordinal" => 0, + "tagType" => "generic", + "meta" => %{"styles" => "danger_high"} + } + ] + } + + Application.put_env(:block_scout_web, :chain_id, old_chain_id) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.BENS, old_env_bens) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.Metadata, old_env_metadata) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + Bypass.down(bypass) + end end describe "/addresses/{address_hash}/token-transfers" do diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 80fa0b893e50..86774b3748c3 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -966,6 +966,15 @@ defmodule Explorer.Chain.Transaction do end end + def decoded_input_data( + %__MODULE__{to_address: %{metadata: _, ens_domain_name: _}}, + _, + _, + _, + _ + ), + do: {:error, :no_to_address} + defp decode_function_call_via_sig_provider_wrapper(input, hash, skip_sig_provider?) do case decode_function_call_via_sig_provider(input, hash, skip_sig_provider?) do [] -> From 559dd629834b9dd951eebb011fb40989c679cd32 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 25 Aug 2025 15:39:28 +0300 Subject: [PATCH 0269/1006] feat: Add token_type to token transfer API response (#13038) --- .../lib/block_scout_web/schemas/api/v2/token_transfer.ex | 8 +++++--- .../block_scout_web/views/api/v2/token_transfer_view.ex | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index d87d2499754f..9297e9a33a6c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do """ require OpenApiSpex - alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token, Token.Type} alias BlockScoutWeb.Schemas.API.V2.TokenTransfer.{Total, TotalERC1155, TotalERC721} alias OpenApiSpex.Schema @@ -28,7 +28,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do method: General.MethodNameNullable, block_hash: General.FullHash, block_number: %Schema{type: :integer, nullable: false}, - log_index: %Schema{type: :integer, nullable: false} + log_index: %Schema{type: :integer, nullable: false}, + token_type: Type }, required: [ :transaction_hash, @@ -41,7 +42,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do :method, :block_hash, :block_number, - :log_index + :log_index, + :token_type ] }) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex index 81c28e3eca35..7420c4192948 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex @@ -59,7 +59,8 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do "method" => Transaction.method_name(token_transfer.transaction, decoded_input, true), "block_hash" => to_string(token_transfer.block_hash), "block_number" => token_transfer.block_number, - "log_index" => token_transfer.log_index + "log_index" => token_transfer.log_index, + "token_type" => token_transfer.token_type } end From fb8ab44a504d598ea024da33435c99b6d9846171 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Mon, 25 Aug 2025 16:57:18 +0400 Subject: [PATCH 0270/1006] chore: Remove Polygon Edge modules and chain type (#13056) * Remove Polygon Edge modules * Remove an old socket topic * Remove polygon_edge chain type --------- Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .github/workflows/config.yml | 2 +- .github/workflows/generate-swagger.yml | 2 +- .../channels/optimism_channel.ex | 6 - .../channels/v2/user_socket.ex | 3 +- .../api/v2/polygon_edge_controller.ex | 70 -- .../lib/block_scout_web/notifiers/optimism.ex | 6 - .../lib/block_scout_web/routers/api_router.ex | 9 - .../schemas/api/v2/transaction.ex | 7 - .../views/api/v2/polygon_edge_view.ex | 101 --- .../views/api/v2/transaction_view.ex | 12 - apps/explorer/lib/explorer/application.ex | 1 - .../runner/polygon_edge/deposit_executes.ex | 106 --- .../import/runner/polygon_edge/deposits.ex | 110 ---- .../runner/polygon_edge/withdrawal_exits.ex | 106 --- .../import/runner/polygon_edge/withdrawals.ex | 108 --- .../chain/import/stage/chain_type_specific.ex | 6 - .../explorer/chain/polygon_edge/deposit.ex | 44 -- .../chain/polygon_edge/deposit_execute.ex | 34 - .../lib/explorer/chain/polygon_edge/reader.ex | 153 ----- .../explorer/chain/polygon_edge/withdrawal.ex | 53 -- .../chain/polygon_edge/withdrawal_exit.ex | 33 - apps/indexer/lib/indexer/block/fetcher.ex | 20 - .../lib/indexer/block/realtime/fetcher.ex | 8 - .../lib/indexer/fetcher/polygon_edge.ex | 621 ------------------ .../indexer/fetcher/polygon_edge/deposit.ex | 162 ----- .../fetcher/polygon_edge/deposit_execute.ex | 226 ------- .../fetcher/polygon_edge/withdrawal.ex | 241 ------- .../fetcher/polygon_edge/withdrawal_exit.ex | 106 --- .../fetcher/rollup_l1_reorg_monitor.ex | 6 - apps/indexer/lib/indexer/supervisor.ex | 8 - .../polygon_edge/deposit_executes.ex | 55 -- .../transform/polygon_edge/withdrawals.ex | 54 -- config/config_helper.exs | 2 - config/runtime.exs | 37 +- docker-compose/envs/common-blockscout.env | 10 - 35 files changed, 4 insertions(+), 2524 deletions(-) delete mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_edge_controller.ex delete mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex delete mode 100644 apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposit_executes.ex delete mode 100644 apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposits.ex delete mode 100644 apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawal_exits.ex delete mode 100644 apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawals.ex delete mode 100644 apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex delete mode 100644 apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex delete mode 100644 apps/explorer/lib/explorer/chain/polygon_edge/reader.ex delete mode 100644 apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex delete mode 100644 apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/polygon_edge.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex delete mode 100644 apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex delete mode 100644 apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex delete mode 100644 apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index fd26e53e91e0..8597ac7406a9 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -68,7 +68,7 @@ jobs: "neon" ]; - const extraChainTypes = ["suave", "polygon_edge"]; + const extraChainTypes = ["suave"]; // Chain type matrix we use in master branch const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes); diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index cdb935b945d7..29d297f5aeb4 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -51,7 +51,7 @@ jobs: "neon" ]; - const extraChainTypes = ["suave", "polygon_edge"]; + const extraChainTypes = ["suave"]; // Chain type matrix we use in master branch const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes); diff --git a/apps/block_scout_web/lib/block_scout_web/channels/optimism_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/optimism_channel.ex index 6fa69e4ebb3b..64fcb7e64bdb 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/optimism_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/optimism_channel.ex @@ -11,10 +11,4 @@ defmodule BlockScoutWeb.OptimismChannel do def join("optimism:new_deposits", _params, socket) do {:ok, %{}, socket} end - - # todo: the `optimism_deposits:new_deposits` socket topic is for backward compatibility - # for the frontend and should be removed after the frontend starts to use the `optimism:new_deposits` - def join("optimism_deposits:new_deposits", _params, socket) do - {:ok, %{}, socket} - end end diff --git a/apps/block_scout_web/lib/block_scout_web/channels/v2/user_socket.ex b/apps/block_scout_web/lib/block_scout_web/channels/v2/user_socket.ex index 1f21d23fbaa7..dec4a541b74f 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/v2/user_socket.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/v2/user_socket.ex @@ -16,8 +16,7 @@ defmodule BlockScoutWeb.V2.UserSocket do case @chain_type do :arbitrum -> channel("arbitrum:*", BlockScoutWeb.ArbitrumChannel) - # todo: change `optimism*"` to `optimism:*` after the deprecated `optimism_deposits:new_deposits` topic is removed - :optimism -> channel("optimism*", BlockScoutWeb.OptimismChannel) + :optimism -> channel("optimism:*", BlockScoutWeb.OptimismChannel) _ -> nil end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_edge_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_edge_controller.ex deleted file mode 100644 index 53157d08bdbe..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/polygon_edge_controller.ex +++ /dev/null @@ -1,70 +0,0 @@ -defmodule BlockScoutWeb.API.V2.PolygonEdgeController do - use BlockScoutWeb, :controller - - import BlockScoutWeb.Chain, - only: [ - next_page_params: 3, - paging_options: 1, - split_list_by_page: 1 - ] - - alias Explorer.Chain.PolygonEdge.Reader - - action_fallback(BlockScoutWeb.API.V2.FallbackController) - - @spec deposits(Plug.Conn.t(), map()) :: Plug.Conn.t() - def deposits(conn, params) do - {deposits, next_page} = - params - |> paging_options() - |> Keyword.put(:api?, true) - |> Reader.deposits() - |> split_list_by_page() - - next_page_params = next_page_params(next_page, deposits, params) - - conn - |> put_status(200) - |> render(:polygon_edge_deposits, %{ - deposits: deposits, - next_page_params: next_page_params - }) - end - - @spec deposits_count(Plug.Conn.t(), map()) :: Plug.Conn.t() - def deposits_count(conn, _params) do - count = Reader.deposits_count(api?: true) - - conn - |> put_status(200) - |> render(:polygon_edge_items_count, %{count: count}) - end - - @spec withdrawals(Plug.Conn.t(), map()) :: Plug.Conn.t() - def withdrawals(conn, params) do - {withdrawals, next_page} = - params - |> paging_options() - |> Keyword.put(:api?, true) - |> Reader.withdrawals() - |> split_list_by_page() - - next_page_params = next_page_params(next_page, withdrawals, params) - - conn - |> put_status(200) - |> render(:polygon_edge_withdrawals, %{ - withdrawals: withdrawals, - next_page_params: next_page_params - }) - end - - @spec withdrawals_count(Plug.Conn.t(), map()) :: Plug.Conn.t() - def withdrawals_count(conn, _params) do - count = Reader.withdrawals_count(api?: true) - - conn - |> put_status(200) - |> render(:polygon_edge_items_count, %{count: count}) - end -end diff --git a/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex b/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex index 7d9151c9eae7..13941eec610c 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex @@ -24,12 +24,6 @@ defmodule BlockScoutWeb.Notifiers.Optimism do Endpoint.broadcast("optimism:new_deposits", "new_optimism_deposits", %{ deposits: deposits_count }) - - # todo: the `optimism_deposits:new_deposits` socket topic is for backward compatibility - # for the frontend and should be removed after the frontend starts to use the `optimism:new_deposits` - Endpoint.broadcast("optimism_deposits:new_deposits", "deposits", %{ - deposits: deposits_count - }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 5c93384c3d8e..8e1c781e4720 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -318,15 +318,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - scope "/polygon-edge" do - chain_scope :polygon_edge do - get("/deposits", V2.PolygonEdgeController, :deposits) - get("/deposits/count", V2.PolygonEdgeController, :deposits_count) - get("/withdrawals", V2.PolygonEdgeController, :withdrawals) - get("/withdrawals/count", V2.PolygonEdgeController, :withdrawals_count) - end - end - scope "/scroll" do if @chain_type == :scroll do get("/batches", V2.ScrollController, :batches) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex index 804730f15878..8e89f9148989 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -5,8 +5,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do alias BlockScoutWeb.Schemas.API.V2.Transaction.Fee alias OpenApiSpex.Schema - @polygon_edge_deposit_withdrawal_schema %Schema{type: :object, nullable: true} - @zksync_schema %Schema{ type: :object, nullable: false, @@ -145,11 +143,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do # credo:disable-for-next-line def chain_type_fields(schema) do case Application.get_env(:explorer, :chain_type) do - :polygon_edge -> - schema - |> put_in([:properties, :polygon_edge_deposit], @polygon_edge_deposit_withdrawal_schema) - |> put_in([:properties, :polygon_edge_withdrawal], @polygon_edge_deposit_withdrawal_schema) - :polygon_zkevm -> schema |> put_in([:properties, :zkevm_batch_number], %Schema{type: :integer, nullable: true}) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex deleted file mode 100644 index 7bdef247e90b..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/polygon_edge_view.ex +++ /dev/null @@ -1,101 +0,0 @@ -defmodule BlockScoutWeb.API.V2.PolygonEdgeView do - use BlockScoutWeb, :view - - alias BlockScoutWeb.API.V2.Helper - alias Explorer.Chain - alias Explorer.Chain.PolygonEdge.Reader - - @spec render(String.t(), map()) :: map() - def render("polygon_edge_deposits.json", %{ - deposits: deposits, - next_page_params: next_page_params - }) do - %{ - items: - Enum.map(deposits, fn deposit -> - %{ - "msg_id" => deposit.msg_id, - "from" => deposit.from, - "to" => deposit.to, - "l1_transaction_hash" => deposit.l1_transaction_hash, - "l1_timestamp" => deposit.l1_timestamp, - "success" => deposit.success, - "l2_transaction_hash" => deposit.l2_transaction_hash - } - end), - next_page_params: next_page_params - } - end - - def render("polygon_edge_withdrawals.json", %{ - withdrawals: withdrawals, - next_page_params: next_page_params - }) do - %{ - items: - Enum.map(withdrawals, fn withdrawal -> - %{ - "msg_id" => withdrawal.msg_id, - "from" => withdrawal.from, - "to" => withdrawal.to, - "l2_transaction_hash" => withdrawal.l2_transaction_hash, - "l2_timestamp" => withdrawal.l2_timestamp, - "success" => withdrawal.success, - "l1_transaction_hash" => withdrawal.l1_transaction_hash - } - end), - next_page_params: next_page_params - } - end - - def render("polygon_edge_items_count.json", %{count: count}) do - count - end - - def extend_transaction_json_response(out_json, transaction_hash, connection) do - out_json - |> Map.put("polygon_edge_deposit", polygon_edge_deposit(transaction_hash, connection)) - |> Map.put("polygon_edge_withdrawal", polygon_edge_withdrawal(transaction_hash, connection)) - end - - defp polygon_edge_deposit(transaction_hash, conn) do - transaction_hash - |> Reader.deposit_by_transaction_hash() - |> polygon_edge_deposit_or_withdrawal(conn) - end - - defp polygon_edge_withdrawal(transaction_hash, conn) do - transaction_hash - |> Reader.withdrawal_by_transaction_hash() - |> polygon_edge_deposit_or_withdrawal(conn) - end - - defp polygon_edge_deposit_or_withdrawal(item, conn) do - if not is_nil(item) do - {from_address, from_address_hash} = hash_to_address_and_hash(item.from) - {to_address, to_address_hash} = hash_to_address_and_hash(item.to) - - item - |> Map.put(:from, Helper.address_with_info(conn, from_address, from_address_hash, item.from)) - |> Map.put(:to, Helper.address_with_info(conn, to_address, to_address_hash, item.to)) - end - end - - defp hash_to_address_and_hash(hash) do - with false <- is_nil(hash), - {:ok, address} <- - Chain.hash_to_address( - hash, - necessity_by_association: %{ - :names => :optional, - :smart_contract => :optional, - proxy_implementations_association() => :optional - }, - api?: true - ) do - {address, address.hash} - else - _ -> {nil, nil} - end - end -end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index abdc79805038..0baa1dfdb690 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -897,18 +897,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do do_with_chain_type_fields(chain_type, result, transaction, single_transaction?, conn, watchlist_names) end - defp do_with_chain_type_fields( - :polygon_edge, - result, - transaction, - true = _single_transaction?, - conn, - _watchlist_names - ) do - # credo:disable-for-next-line Credo.Check.Design.AliasUsage - BlockScoutWeb.API.V2.PolygonEdgeView.extend_transaction_json_response(result, transaction.hash, conn) - end - defp do_with_chain_type_fields( :polygon_zkevm, result, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index b9dab2491e26..088a01980904 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -172,7 +172,6 @@ defmodule Explorer.Application do configure_chain_type_dependent_process(Explorer.Chain.Cache.Counters.Stability.ValidatorsCount, :stability), configure_chain_type_dependent_process(Explorer.Chain.Cache.LatestL1BlockNumber, [ :optimism, - :polygon_edge, :polygon_zkevm, :scroll, :shibarium diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposit_executes.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposit_executes.ex deleted file mode 100644 index bc20a12567c2..000000000000 --- a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposit_executes.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Explorer.Chain.Import.Runner.PolygonEdge.DepositExecutes do - @moduledoc """ - Bulk imports `t:Explorer.Chain.PolygonEdge.DepositExecute.t/0`. - """ - - require Ecto.Query - - import Ecto.Query, only: [from: 2] - - alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Import - alias Explorer.Chain.PolygonEdge.DepositExecute - alias Explorer.Prometheus.Instrumenter - - @behaviour Import.Runner - - # milliseconds - @timeout 60_000 - - @type imported :: [DepositExecute.t()] - - @impl Import.Runner - def ecto_schema_module, do: DepositExecute - - @impl Import.Runner - def option_key, do: :polygon_edge_deposit_executes - - @impl Import.Runner - @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} - def imported_table_row do - %{ - value_type: "[#{ecto_schema_module()}.t()]", - value_description: "List of `t:#{ecto_schema_module()}.t/0`s" - } - end - - @impl Import.Runner - @spec run(Multi.t(), list(), map()) :: Multi.t() - def run(multi, changes_list, %{timestamps: timestamps} = options) do - insert_options = - options - |> Map.get(option_key(), %{}) - |> Map.take(~w(on_conflict timeout)a) - |> Map.put_new(:timeout, @timeout) - |> Map.put(:timestamps, timestamps) - - Multi.run(multi, :insert_polygon_edge_deposit_executes, fn repo, _ -> - Instrumenter.block_import_stage_runner( - fn -> insert(repo, changes_list, insert_options) end, - :block_referencing, - :polygon_edge_deposit_executes, - :polygon_edge_deposit_executes - ) - end) - end - - @impl Import.Runner - def timeout, do: @timeout - - @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: - {:ok, [DepositExecute.t()]} - | {:error, [Changeset.t()]} - def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do - on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - - # Enforce PolygonEdge.DepositExecute ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.msg_id) - - {:ok, inserted} = - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: :msg_id, - on_conflict: on_conflict, - for: DepositExecute, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - - {:ok, inserted} - end - - defp default_on_conflict do - from( - de in DepositExecute, - update: [ - set: [ - # Don't update `msg_id` as it is a primary key and used for the conflict target - l2_transaction_hash: fragment("EXCLUDED.l2_transaction_hash"), - l2_block_number: fragment("EXCLUDED.l2_block_number"), - success: fragment("EXCLUDED.success"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", de.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", de.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.l2_transaction_hash, EXCLUDED.l2_block_number, EXCLUDED.success) IS DISTINCT FROM (?, ?, ?)", - de.l2_transaction_hash, - de.l2_block_number, - de.success - ) - ) - end -end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposits.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposits.ex deleted file mode 100644 index b4893f86b67d..000000000000 --- a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/deposits.ex +++ /dev/null @@ -1,110 +0,0 @@ -defmodule Explorer.Chain.Import.Runner.PolygonEdge.Deposits do - @moduledoc """ - Bulk imports `t:Explorer.Chain.PolygonEdge.Deposit.t/0`. - """ - - require Ecto.Query - - import Ecto.Query, only: [from: 2] - - alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Import - alias Explorer.Chain.PolygonEdge.Deposit - alias Explorer.Prometheus.Instrumenter - - @behaviour Import.Runner - - # milliseconds - @timeout 60_000 - - @type imported :: [Deposit.t()] - - @impl Import.Runner - def ecto_schema_module, do: Deposit - - @impl Import.Runner - def option_key, do: :polygon_edge_deposits - - @impl Import.Runner - @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} - def imported_table_row do - %{ - value_type: "[#{ecto_schema_module()}.t()]", - value_description: "List of `t:#{ecto_schema_module()}.t/0`s" - } - end - - @impl Import.Runner - @spec run(Multi.t(), list(), map()) :: Multi.t() - def run(multi, changes_list, %{timestamps: timestamps} = options) do - insert_options = - options - |> Map.get(option_key(), %{}) - |> Map.take(~w(on_conflict timeout)a) - |> Map.put_new(:timeout, @timeout) - |> Map.put(:timestamps, timestamps) - - Multi.run(multi, :insert_polygon_edge_deposits, fn repo, _ -> - Instrumenter.block_import_stage_runner( - fn -> insert(repo, changes_list, insert_options) end, - :block_referencing, - :polygon_edge_deposits, - :polygon_edge_deposits - ) - end) - end - - @impl Import.Runner - def timeout, do: @timeout - - @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: - {:ok, [Deposit.t()]} - | {:error, [Changeset.t()]} - def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do - on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - - # Enforce PolygonEdge.Deposit ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.msg_id) - - {:ok, inserted} = - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: :msg_id, - on_conflict: on_conflict, - for: Deposit, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - - {:ok, inserted} - end - - defp default_on_conflict do - from( - d in Deposit, - update: [ - set: [ - # Don't update `msg_id` as it is a primary key and used for the conflict target - from: fragment("EXCLUDED.from"), - to: fragment("EXCLUDED.to"), - l1_transaction_hash: fragment("EXCLUDED.l1_transaction_hash"), - l1_timestamp: fragment("EXCLUDED.l1_timestamp"), - l1_block_number: fragment("EXCLUDED.l1_block_number"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", d.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", d.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.from, EXCLUDED.to, EXCLUDED.l1_transaction_hash, EXCLUDED.l1_timestamp, EXCLUDED.l1_block_number) IS DISTINCT FROM (?, ?, ?, ?, ?)", - d.from, - d.to, - d.l1_transaction_hash, - d.l1_timestamp, - d.l1_block_number - ) - ) - end -end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawal_exits.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawal_exits.ex deleted file mode 100644 index 16c13eadbf1f..000000000000 --- a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawal_exits.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Explorer.Chain.Import.Runner.PolygonEdge.WithdrawalExits do - @moduledoc """ - Bulk imports `t:Explorer.Chain.PolygonEdge.WithdrawalExit.t/0`. - """ - - require Ecto.Query - - import Ecto.Query, only: [from: 2] - - alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Import - alias Explorer.Chain.PolygonEdge.WithdrawalExit - alias Explorer.Prometheus.Instrumenter - - @behaviour Import.Runner - - # milliseconds - @timeout 60_000 - - @type imported :: [WithdrawalExit.t()] - - @impl Import.Runner - def ecto_schema_module, do: WithdrawalExit - - @impl Import.Runner - def option_key, do: :polygon_edge_withdrawal_exits - - @impl Import.Runner - @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} - def imported_table_row do - %{ - value_type: "[#{ecto_schema_module()}.t()]", - value_description: "List of `t:#{ecto_schema_module()}.t/0`s" - } - end - - @impl Import.Runner - @spec run(Multi.t(), list(), map()) :: Multi.t() - def run(multi, changes_list, %{timestamps: timestamps} = options) do - insert_options = - options - |> Map.get(option_key(), %{}) - |> Map.take(~w(on_conflict timeout)a) - |> Map.put_new(:timeout, @timeout) - |> Map.put(:timestamps, timestamps) - - Multi.run(multi, :insert_polygon_edge_withdrawal_exits, fn repo, _ -> - Instrumenter.block_import_stage_runner( - fn -> insert(repo, changes_list, insert_options) end, - :block_referencing, - :polygon_edge_withdrawal_exits, - :polygon_edge_withdrawal_exits - ) - end) - end - - @impl Import.Runner - def timeout, do: @timeout - - @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: - {:ok, [WithdrawalExit.t()]} - | {:error, [Changeset.t()]} - def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do - on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - - # Enforce PolygonEdge.WithdrawalExit ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.msg_id) - - {:ok, inserted} = - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: :msg_id, - on_conflict: on_conflict, - for: WithdrawalExit, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - - {:ok, inserted} - end - - defp default_on_conflict do - from( - we in WithdrawalExit, - update: [ - set: [ - # Don't update `msg_id` as it is a primary key and used for the conflict target - l1_transaction_hash: fragment("EXCLUDED.l1_transaction_hash"), - l1_block_number: fragment("EXCLUDED.l1_block_number"), - success: fragment("EXCLUDED.success"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", we.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", we.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.l1_transaction_hash, EXCLUDED.l1_block_number, EXCLUDED.success) IS DISTINCT FROM (?, ?, ?)", - we.l1_transaction_hash, - we.l1_block_number, - we.success - ) - ) - end -end diff --git a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawals.ex b/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawals.ex deleted file mode 100644 index e728e2cba495..000000000000 --- a/apps/explorer/lib/explorer/chain/import/runner/polygon_edge/withdrawals.ex +++ /dev/null @@ -1,108 +0,0 @@ -defmodule Explorer.Chain.Import.Runner.PolygonEdge.Withdrawals do - @moduledoc """ - Bulk imports `t:Explorer.Chain.PolygonEdge.Withdrawal.t/0`. - """ - - require Ecto.Query - - import Ecto.Query, only: [from: 2] - - alias Ecto.{Changeset, Multi, Repo} - alias Explorer.Chain.Import - alias Explorer.Chain.PolygonEdge.Withdrawal - alias Explorer.Prometheus.Instrumenter - - @behaviour Import.Runner - - # milliseconds - @timeout 60_000 - - @type imported :: [Withdrawal.t()] - - @impl Import.Runner - def ecto_schema_module, do: Withdrawal - - @impl Import.Runner - def option_key, do: :polygon_edge_withdrawals - - @impl Import.Runner - @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} - def imported_table_row do - %{ - value_type: "[#{ecto_schema_module()}.t()]", - value_description: "List of `t:#{ecto_schema_module()}.t/0`s" - } - end - - @impl Import.Runner - @spec run(Multi.t(), list(), map()) :: Multi.t() - def run(multi, changes_list, %{timestamps: timestamps} = options) do - insert_options = - options - |> Map.get(option_key(), %{}) - |> Map.take(~w(on_conflict timeout)a) - |> Map.put_new(:timeout, @timeout) - |> Map.put(:timestamps, timestamps) - - Multi.run(multi, :insert_polygon_edge_withdrawals, fn repo, _ -> - Instrumenter.block_import_stage_runner( - fn -> insert(repo, changes_list, insert_options) end, - :block_referencing, - :polygon_edge_withdrawals, - :polygon_edge_withdrawals - ) - end) - end - - @impl Import.Runner - def timeout, do: @timeout - - @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: - {:ok, [Withdrawal.t()]} - | {:error, [Changeset.t()]} - def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do - on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - - # Enforce PolygonEdge.Withdrawal ShareLocks order (see docs: sharelock.md) - ordered_changes_list = Enum.sort_by(changes_list, & &1.msg_id) - - {:ok, inserted} = - Import.insert_changes_list( - repo, - ordered_changes_list, - conflict_target: :msg_id, - on_conflict: on_conflict, - for: Withdrawal, - returning: true, - timeout: timeout, - timestamps: timestamps - ) - - {:ok, inserted} - end - - defp default_on_conflict do - from( - w in Withdrawal, - update: [ - set: [ - # Don't update `msg_id` as it is a primary key and used for the conflict target - from: fragment("EXCLUDED.from"), - to: fragment("EXCLUDED.to"), - l2_transaction_hash: fragment("EXCLUDED.l2_transaction_hash"), - l2_block_number: fragment("EXCLUDED.l2_block_number"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", w.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", w.updated_at) - ] - ], - where: - fragment( - "(EXCLUDED.from, EXCLUDED.to, EXCLUDED.l2_transaction_hash, EXCLUDED.l2_block_number) IS DISTINCT FROM (?, ?, ?, ?)", - w.from, - w.to, - w.l2_transaction_hash, - w.l2_block_number - ) - ) - end -end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 9259d16f415e..035f573801d8 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -20,12 +20,6 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Runner.Optimism.EIP1559ConfigUpdates, Runner.Optimism.InteropMessages ], - polygon_edge: [ - Runner.PolygonEdge.Deposits, - Runner.PolygonEdge.DepositExecutes, - Runner.PolygonEdge.Withdrawals, - Runner.PolygonEdge.WithdrawalExits - ], polygon_zkevm: [ Runner.PolygonZkevm.LifecycleTransactions, Runner.PolygonZkevm.TransactionBatches, diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex b/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex deleted file mode 100644 index a18900868876..000000000000 --- a/apps/explorer/lib/explorer/chain/polygon_edge/deposit.ex +++ /dev/null @@ -1,44 +0,0 @@ -defmodule Explorer.Chain.PolygonEdge.Deposit do - @moduledoc "Models Polygon Edge deposit." - - use Explorer.Schema - - alias Explorer.Chain.{ - Block, - Hash - } - - @optional_attrs ~w(from to l1_transaction_hash l1_timestamp)a - - @required_attrs ~w(msg_id l1_block_number)a - - @allowed_attrs @optional_attrs ++ @required_attrs - - @typedoc """ - * `msg_id` - id of the message - * `from` - source address of the message - * `to` - target address of the message - * `l1_transaction_hash` - hash of the L1 transaction containing the corresponding StateSynced event - * `l1_timestamp` - timestamp of the L1 transaction block - * `l1_block_number` - block number of the L1 transaction - """ - @primary_key false - typed_schema "polygon_edge_deposits" do - field(:msg_id, :integer, primary_key: true, null: false) - field(:from, Hash.Address) - field(:to, Hash.Address) - field(:l1_transaction_hash, Hash.Full) - field(:l1_timestamp, :utc_datetime_usec) - field(:l1_block_number, :integer) :: Block.block_number() - - timestamps() - end - - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() - def changeset(%__MODULE__{} = module, attrs \\ %{}) do - module - |> cast(attrs, @allowed_attrs) - |> validate_required(@required_attrs) - |> unique_constraint(:msg_id) - end -end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex b/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex deleted file mode 100644 index 7e2ed2c64e5e..000000000000 --- a/apps/explorer/lib/explorer/chain/polygon_edge/deposit_execute.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Explorer.Chain.PolygonEdge.DepositExecute do - @moduledoc "Models Polygon Edge deposit execute." - - use Explorer.Schema - - alias Explorer.Chain.Hash - - @required_attrs ~w(msg_id l2_transaction_hash l2_block_number success)a - - @typedoc """ - * `msg_id` - id of the message - * `l2_transaction_hash` - hash of the L2 transaction containing the corresponding StateSyncResult event - * `l2_block_number` - block number of the L2 transaction - * `success` - a status of onStateReceive internal call (namely internal deposit transaction) - """ - @primary_key false - typed_schema "polygon_edge_deposit_executes" do - field(:msg_id, :integer, primary_key: true, null: false) - field(:l2_transaction_hash, Hash.Full, null: false) - field(:l2_block_number, :integer, null: false) - field(:success, :boolean, null: false) - - timestamps() - end - - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() - def changeset(%__MODULE__{} = module, attrs \\ %{}) do - module - |> cast(attrs, @required_attrs) - |> validate_required(@required_attrs) - |> unique_constraint(:msg_id) - |> unique_constraint(:l2_transaction_hash) - end -end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/reader.ex b/apps/explorer/lib/explorer/chain/polygon_edge/reader.ex deleted file mode 100644 index 7f27ccf6048e..000000000000 --- a/apps/explorer/lib/explorer/chain/polygon_edge/reader.ex +++ /dev/null @@ -1,153 +0,0 @@ -defmodule Explorer.Chain.PolygonEdge.Reader do - @moduledoc "Contains read functions for Polygon Edge modules." - - import Ecto.Query, - only: [ - from: 2, - limit: 2 - ] - - import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] - - alias Explorer.{PagingOptions, Repo} - alias Explorer.Chain.PolygonEdge.{Deposit, DepositExecute, Withdrawal, WithdrawalExit} - alias Explorer.Chain.{Block, Hash} - - @spec deposits(list()) :: list() - def deposits(options \\ []) do - paging_options = Keyword.get(options, :paging_options, default_paging_options()) - - case paging_options do - %PagingOptions{key: {0}} -> - [] - - _ -> - base_query = - from( - de in DepositExecute, - inner_join: d in Deposit, - on: d.msg_id == de.msg_id and not is_nil(d.l1_timestamp), - select: %{ - msg_id: de.msg_id, - from: d.from, - to: d.to, - l1_transaction_hash: d.l1_transaction_hash, - l1_timestamp: d.l1_timestamp, - success: de.success, - l2_transaction_hash: de.l2_transaction_hash - }, - order_by: [desc: de.msg_id] - ) - - base_query - |> page_deposits_or_withdrawals(paging_options) - |> limit(^paging_options.page_size) - |> select_repo(options).all() - end - end - - @spec deposits_count(list()) :: term() | nil - def deposits_count(options \\ []) do - query = - from( - de in DepositExecute, - inner_join: d in Deposit, - on: d.msg_id == de.msg_id and not is_nil(d.l1_timestamp) - ) - - select_repo(options).aggregate(query, :count, timeout: :infinity) - end - - @spec withdrawals(list()) :: list() - def withdrawals(options \\ []) do - paging_options = Keyword.get(options, :paging_options, default_paging_options()) - - case paging_options do - %PagingOptions{key: 0} -> - [] - - _ -> - base_query = - from( - w in Withdrawal, - left_join: we in WithdrawalExit, - on: we.msg_id == w.msg_id, - left_join: b in Block, - on: b.number == w.l2_block_number and b.consensus == true, - select: %{ - msg_id: w.msg_id, - from: w.from, - to: w.to, - l2_transaction_hash: w.l2_transaction_hash, - l2_timestamp: b.timestamp, - success: we.success, - l1_transaction_hash: we.l1_transaction_hash - }, - where: not is_nil(w.from), - order_by: [desc: w.msg_id] - ) - - base_query - |> page_deposits_or_withdrawals(paging_options) - |> limit(^paging_options.page_size) - |> select_repo(options).all() - end - end - - @spec withdrawals_count(list()) :: term() | nil - def withdrawals_count(options \\ []) do - query = - from( - w in Withdrawal, - where: not is_nil(w.from) - ) - - select_repo(options).aggregate(query, :count, timeout: :infinity) - end - - @spec deposit_by_transaction_hash(Hash.t()) :: Ecto.Schema.t() | term() | nil - def deposit_by_transaction_hash(hash) do - query = - from( - de in DepositExecute, - inner_join: d in Deposit, - on: d.msg_id == de.msg_id and not is_nil(d.from), - select: %{ - msg_id: de.msg_id, - from: d.from, - to: d.to, - success: de.success, - l1_transaction_hash: d.l1_transaction_hash - }, - where: de.l2_transaction_hash == ^hash - ) - - Repo.replica().one(query) - end - - @spec withdrawal_by_transaction_hash(Hash.t()) :: Ecto.Schema.t() | term() | nil - def withdrawal_by_transaction_hash(hash) do - query = - from( - w in Withdrawal, - left_join: we in WithdrawalExit, - on: we.msg_id == w.msg_id, - select: %{ - msg_id: w.msg_id, - from: w.from, - to: w.to, - success: we.success, - l1_transaction_hash: we.l1_transaction_hash - }, - where: w.l2_transaction_hash == ^hash and not is_nil(w.from) - ) - - Repo.replica().one(query) - end - - defp page_deposits_or_withdrawals(query, %PagingOptions{key: nil}), do: query - - defp page_deposits_or_withdrawals(query, %PagingOptions{key: {msg_id}}) do - from(item in query, where: item.msg_id < ^msg_id) - end -end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex deleted file mode 100644 index 22b50b1ffa13..000000000000 --- a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal.ex +++ /dev/null @@ -1,53 +0,0 @@ -defmodule Explorer.Chain.PolygonEdge.Withdrawal do - @moduledoc "Models Polygon Edge withdrawal." - - use Explorer.Schema - - alias Explorer.Chain.{ - Address, - Block, - Hash, - Transaction - } - - @optional_attrs ~w(from to)a - - @required_attrs ~w(msg_id l2_transaction_hash l2_block_number)a - - @allowed_attrs @optional_attrs ++ @required_attrs - - @typedoc """ - * `msg_id` - id of the message - * `from` - source address of the message - * `to` - target address of the message - * `l2_transaction_hash` - hash of the L2 transaction containing the corresponding L2StateSynced event - * `l2_block_number` - block number of the L2 transaction - """ - @primary_key false - typed_schema "polygon_edge_withdrawals" do - field(:msg_id, :integer, primary_key: true, null: false) - - belongs_to(:from_address, Address, foreign_key: :from, references: :hash, type: Hash.Address) - belongs_to(:to_address, Address, foreign_key: :to, references: :hash, type: Hash.Address) - - belongs_to(:l2_transaction, Transaction, - foreign_key: :l2_transaction_hash, - references: :hash, - type: Hash.Full, - null: false - ) - - belongs_to(:l2_block, Block, foreign_key: :l2_block_number, references: :number, type: :integer, null: false) - - timestamps() - end - - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() - def changeset(%__MODULE__{} = module, attrs \\ %{}) do - module - |> cast(attrs, @allowed_attrs) - |> validate_required(@required_attrs) - |> unique_constraint(:msg_id) - |> unique_constraint(:l2_transaction_hash) - end -end diff --git a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex b/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex deleted file mode 100644 index 959d3bfc68a3..000000000000 --- a/apps/explorer/lib/explorer/chain/polygon_edge/withdrawal_exit.ex +++ /dev/null @@ -1,33 +0,0 @@ -defmodule Explorer.Chain.PolygonEdge.WithdrawalExit do - @moduledoc "Models Polygon Edge withdrawal exit." - - use Explorer.Schema - - alias Explorer.Chain.{Block, Hash} - - @required_attrs ~w(msg_id l1_transaction_hash l1_block_number success)a - - @typedoc """ - * `msg_id` - id of the message - * `l1_transaction_hash` - hash of the L1 transaction containing the corresponding ExitProcessed event - * `l1_block_number` - block number of the L1 transaction - * `success` - a status of onL2StateReceive internal call (namely internal withdrawal transaction) - """ - @primary_key false - typed_schema "polygon_edge_withdrawal_exits" do - field(:msg_id, :integer, primary_key: true, null: false) - field(:l1_transaction_hash, Hash.Full, null: false) - field(:l1_block_number, :integer) :: Block.block_number() - field(:success, :boolean, null: false) - - timestamps() - end - - @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Schema.t() - def changeset(%__MODULE__{} = module, attrs \\ %{}) do - module - |> cast(attrs, @required_attrs) - |> validate_required(@required_attrs) - |> unique_constraint(:msg_id) - end -end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 76f16c7b9499..e051787bcb86 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -56,8 +56,6 @@ defmodule Indexer.Block.Fetcher do alias Indexer.Transform.Optimism.Withdrawals, as: OptimismWithdrawals - alias Indexer.Transform.PolygonEdge.{DepositExecutes, Withdrawals} - alias Indexer.Transform.Scroll.L1FeeParams, as: ScrollL1FeeParams alias Indexer.Transform.Arbitrum.Messaging, as: ArbitrumMessaging @@ -181,13 +179,6 @@ defmodule Indexer.Block.Fetcher do %{mint_transfers: mint_transfers} = MintTransfers.parse(logs), optimism_withdrawals = if(callback_module == Indexer.Block.Realtime.Fetcher, do: OptimismWithdrawals.parse(logs), else: []), - polygon_edge_withdrawals = - if(callback_module == Indexer.Block.Realtime.Fetcher, do: Withdrawals.parse(logs), else: []), - polygon_edge_deposit_executes = - if(callback_module == Indexer.Block.Realtime.Fetcher, - do: DepositExecutes.parse(logs), - else: [] - ), scroll_l1_fee_params = if(callback_module == Indexer.Block.Realtime.Fetcher, do: ScrollL1FeeParams.parse(logs), @@ -261,8 +252,6 @@ defmodule Indexer.Block.Fetcher do %{ transactions_with_receipts: transactions_with_receipts, optimism_withdrawals: optimism_withdrawals, - polygon_edge_withdrawals: polygon_edge_withdrawals, - polygon_edge_deposit_executes: polygon_edge_deposit_executes, polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations, scroll_l1_fee_params: scroll_l1_fee_params, shibarium_bridge_operations: shibarium_bridge_operations, @@ -318,15 +307,6 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:optimism_withdrawals, %{params: optimism_withdrawals}) end - defp do_import_options(:polygon_edge, basic_import_options, %{ - polygon_edge_withdrawals: polygon_edge_withdrawals, - polygon_edge_deposit_executes: polygon_edge_deposit_executes - }) do - basic_import_options - |> Map.put_new(:polygon_edge_withdrawals, %{params: polygon_edge_withdrawals}) - |> Map.put_new(:polygon_edge_deposit_executes, %{params: polygon_edge_deposit_executes}) - end - defp do_import_options(:polygon_zkevm, basic_import_options, %{ polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations }) do diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 6e2c638e0890..38b80cb147a7 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -398,14 +398,6 @@ defmodule Indexer.Block.Realtime.Fetcher do Indexer.Fetcher.Optimism.Withdrawal.remove(reorg_block_number) end - # Removes all rows from `polygon_edge_withdrawals` and `polygon_edge_deposit_executes` tables - # previously written starting from the reorg block number - defp do_remove_assets_by_number(:polygon_edge, reorg_block) do - # credo:disable-for-lines:2 Credo.Check.Design.AliasUsage - Indexer.Fetcher.PolygonEdge.Withdrawal.remove(reorg_block) - Indexer.Fetcher.PolygonEdge.DepositExecute.remove(reorg_block) - end - # Removes all rows from `polygon_zkevm_bridge` table # previously written starting from the reorg block number defp do_remove_assets_by_number(:polygon_zkevm, reorg_block) do diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge.ex deleted file mode 100644 index cea07427f76e..000000000000 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge.ex +++ /dev/null @@ -1,621 +0,0 @@ -defmodule Indexer.Fetcher.PolygonEdge do - @moduledoc """ - Contains common functions for PolygonEdge.* fetchers. - """ - - # todo: this module is deprecated and should be removed - - use GenServer - use Indexer.Fetcher - - require Logger - - import Ecto.Query - - import EthereumJSONRPC, - only: [json_rpc: 2, integer_to_quantity: 1, request: 1] - - import Explorer.Helper, only: [parse_integer: 1] - - alias Explorer.{Chain, Repo} - alias Indexer.Helper - alias Indexer.Fetcher.PolygonEdge.{Deposit, DepositExecute, Withdrawal, WithdrawalExit} - - @fetcher_name :polygon_edge - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(_args) do - Logger.metadata(fetcher: @fetcher_name) - :ignore - end - - @spec init_l1( - Explorer.Chain.PolygonEdge.Deposit | Explorer.Chain.PolygonEdge.WithdrawalExit, - list(), - pid(), - binary(), - binary(), - binary(), - binary() - ) :: {:ok, map()} | :ignore - def init_l1(table, env, pid, contract_address, contract_name, table_name, entity_name) - when table in [Explorer.Chain.PolygonEdge.Deposit, Explorer.Chain.PolygonEdge.WithdrawalExit] do - with {:start_block_l1_undefined, false} <- {:start_block_l1_undefined, is_nil(env[:start_block_l1])}, - polygon_edge_l1_rpc = Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_l1_rpc], - {:rpc_l1_undefined, false} <- {:rpc_l1_undefined, is_nil(polygon_edge_l1_rpc)}, - {:contract_is_valid, true} <- {:contract_is_valid, Helper.address_correct?(contract_address)}, - start_block_l1 = parse_integer(env[:start_block_l1]), - false <- is_nil(start_block_l1), - true <- start_block_l1 > 0, - {last_l1_block_number, last_l1_transaction_hash} <- get_last_l1_item(table), - {:start_block_l1_valid, true} <- - {:start_block_l1_valid, start_block_l1 <= last_l1_block_number || last_l1_block_number == 0}, - json_rpc_named_arguments = json_rpc_named_arguments(polygon_edge_l1_rpc), - {:ok, last_l1_transaction} <- - Helper.get_transaction_by_hash( - last_l1_transaction_hash, - json_rpc_named_arguments, - Helper.infinite_retries_number() - ), - {:l1_transaction_not_found, false} <- - {:l1_transaction_not_found, !is_nil(last_l1_transaction_hash) && is_nil(last_l1_transaction)}, - {:ok, block_check_interval, last_safe_block} <- - Helper.get_block_check_interval(json_rpc_named_arguments) do - start_block = max(start_block_l1, last_l1_block_number) - - Process.send(pid, :continue, []) - - {:ok, - %{ - contract_address: contract_address, - block_check_interval: block_check_interval, - start_block: start_block, - end_block: last_safe_block, - json_rpc_named_arguments: json_rpc_named_arguments - }} - else - {:start_block_l1_undefined, true} -> - # the process shouldn't start if the start block is not defined - :ignore - - {:rpc_l1_undefined, true} -> - Logger.error("L1 RPC URL is not defined.") - :ignore - - {:contract_is_valid, false} -> - Logger.error("#{contract_name} contract address is invalid or not defined.") - :ignore - - {:start_block_l1_valid, false} -> - Logger.error("Invalid L1 Start Block value. Please, check the value and #{table_name} table.") - - :ignore - - {:error, error_data} -> - Logger.error( - "Cannot get last L1 transaction from RPC by its hash, last safe block, or block timestamp by its number due to RPC error: #{inspect(error_data)}" - ) - - :ignore - - {:l1_transaction_not_found, true} -> - Logger.error( - "Cannot find last L1 transaction from RPC by its hash. Probably, there was a reorg on L1 chain. Please, check #{table_name} table." - ) - - :ignore - - _ -> - Logger.error("#{entity_name} L1 Start Block is invalid or zero.") - :ignore - end - end - - @spec init_l2( - Explorer.Chain.PolygonEdge.DepositExecute | Explorer.Chain.PolygonEdge.Withdrawal, - list(), - pid(), - binary(), - binary(), - binary(), - binary(), - list() - ) :: {:ok, map()} | :ignore - def init_l2(table, env, pid, contract_address, contract_name, table_name, entity_name, json_rpc_named_arguments) - when table in [Explorer.Chain.PolygonEdge.DepositExecute, Explorer.Chain.PolygonEdge.Withdrawal] do - with {:start_block_l2_undefined, false} <- {:start_block_l2_undefined, is_nil(env[:start_block_l2])}, - {:contract_address_valid, true} <- {:contract_address_valid, Helper.address_correct?(contract_address)}, - start_block_l2 = parse_integer(env[:start_block_l2]), - false <- is_nil(start_block_l2), - true <- start_block_l2 > 0, - {last_l2_block_number, last_l2_transaction_hash} <- get_last_l2_item(table), - {safe_block, safe_block_is_latest} = get_safe_block(json_rpc_named_arguments), - {:start_block_l2_valid, true} <- - {:start_block_l2_valid, - (start_block_l2 <= last_l2_block_number || last_l2_block_number == 0) && start_block_l2 <= safe_block}, - {:ok, last_l2_transaction} <- - Helper.get_transaction_by_hash( - last_l2_transaction_hash, - json_rpc_named_arguments, - Helper.infinite_retries_number() - ), - {:l2_transaction_not_found, false} <- - {:l2_transaction_not_found, !is_nil(last_l2_transaction_hash) && is_nil(last_l2_transaction)} do - Process.send(pid, :continue, []) - - {:ok, - %{ - start_block: max(start_block_l2, last_l2_block_number), - start_block_l2: start_block_l2, - safe_block: safe_block, - safe_block_is_latest: safe_block_is_latest, - contract_address: contract_address, - json_rpc_named_arguments: json_rpc_named_arguments - }} - else - {:start_block_l2_undefined, true} -> - # the process shouldn't start if the start block is not defined - :ignore - - {:contract_address_valid, false} -> - Logger.error("#{contract_name} contract address is invalid or not defined.") - :ignore - - {:start_block_l2_valid, false} -> - Logger.error("Invalid L2 Start Block value. Please, check the value and #{table_name} table.") - - :ignore - - {:error, error_data} -> - Logger.error("Cannot get last L2 transaction from RPC by its hash due to RPC error: #{inspect(error_data)}") - - :ignore - - {:l2_transaction_not_found, true} -> - Logger.error( - "Cannot find last L2 transaction from RPC by its hash. Probably, there was a reorg on L2 chain. Please, check #{table_name} table." - ) - - :ignore - - _ -> - Logger.error("#{entity_name} L2 Start Block is invalid or zero.") - :ignore - end - end - - @spec handle_continue(map(), binary(), Deposit | WithdrawalExit) :: {:noreply, map()} - def handle_continue( - %{ - contract_address: contract_address, - block_check_interval: block_check_interval, - start_block: start_block, - end_block: end_block, - json_rpc_named_arguments: json_rpc_named_arguments - } = state, - event_signature, - calling_module - ) - when calling_module in [Deposit, WithdrawalExit] do - time_before = Timex.now() - - eth_get_logs_range_size = - Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_eth_get_logs_range_size] - - chunks_number = ceil((end_block - start_block + 1) / eth_get_logs_range_size) - chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) - - last_written_block = - chunk_range - |> Enum.reduce_while(start_block - 1, fn current_chunk, _ -> - chunk_start = start_block + eth_get_logs_range_size * current_chunk - chunk_end = min(chunk_start + eth_get_logs_range_size - 1, end_block) - - if chunk_end >= chunk_start do - Helper.log_blocks_chunk_handling(chunk_start, chunk_end, start_block, end_block, nil, :L1) - - {:ok, result} = - get_logs( - chunk_start, - chunk_end, - contract_address, - event_signature, - json_rpc_named_arguments, - Helper.infinite_retries_number() - ) - - {events, event_name} = - result - |> calling_module.prepare_events(json_rpc_named_arguments) - |> import_events(calling_module) - - Helper.log_blocks_chunk_handling( - chunk_start, - chunk_end, - start_block, - end_block, - "#{Enum.count(events)} #{event_name} event(s)", - :L1 - ) - end - - {:cont, chunk_end} - end) - - new_start_block = last_written_block + 1 - - {:ok, new_end_block} = - Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - - delay = - if new_end_block == last_written_block do - # there is no new block, so wait for some time to let the chain issue the new block - max(block_check_interval - Timex.diff(Timex.now(), time_before, :milliseconds), 0) - else - 0 - end - - Process.send_after(self(), :continue, delay) - - {:noreply, %{state | start_block: new_start_block, end_block: new_end_block}} - end - - @spec fill_block_range(integer(), integer(), DepositExecute | Withdrawal, binary(), list(), boolean()) :: integer() - def fill_block_range( - l2_block_start, - l2_block_end, - calling_module, - contract_address, - json_rpc_named_arguments, - scan_db - ) - when calling_module in [ - DepositExecute, - Withdrawal - ] do - eth_get_logs_range_size = - Application.get_all_env(:indexer)[Indexer.Fetcher.PolygonEdge][:polygon_edge_eth_get_logs_range_size] - - chunks_number = - if scan_db do - 1 - else - ceil((l2_block_end - l2_block_start + 1) / eth_get_logs_range_size) - end - - chunk_range = Range.new(0, max(chunks_number - 1, 0), 1) - - Enum.reduce(chunk_range, 0, fn current_chunk, count_acc -> - chunk_start = l2_block_start + eth_get_logs_range_size * current_chunk - - chunk_end = - if scan_db do - l2_block_end - else - min(chunk_start + eth_get_logs_range_size - 1, l2_block_end) - end - - Helper.log_blocks_chunk_handling(chunk_start, chunk_end, l2_block_start, l2_block_end, nil, :L2) - - count = - calling_module.find_and_save_entities( - scan_db, - contract_address, - chunk_start, - chunk_end, - json_rpc_named_arguments - ) - - event_name = - if calling_module == Indexer.Fetcher.PolygonEdge.DepositExecute do - "StateSyncResult" - else - "L2StateSynced" - end - - Helper.log_blocks_chunk_handling( - chunk_start, - chunk_end, - l2_block_start, - l2_block_end, - "#{count} #{event_name} event(s)", - :L2 - ) - - count_acc + count - end) - end - - @spec fill_block_range(integer(), integer(), {module(), module()}, binary(), list()) :: any() - def fill_block_range(start_block, end_block, {module, table}, contract_address, json_rpc_named_arguments) do - if start_block <= end_block do - fill_block_range(start_block, end_block, module, contract_address, json_rpc_named_arguments, true) - - fill_msg_id_gaps( - start_block, - table, - module, - contract_address, - json_rpc_named_arguments, - false - ) - - {last_l2_block_number, _} = get_last_l2_item(table) - - fill_block_range( - max(start_block, last_l2_block_number), - end_block, - module, - contract_address, - json_rpc_named_arguments, - false - ) - end - end - - @spec fill_msg_id_gaps(integer(), module(), module(), binary(), list(), boolean()) :: no_return() - def fill_msg_id_gaps( - start_block_l2, - table, - calling_module, - contract_address, - json_rpc_named_arguments, - scan_db \\ true - ) do - id_min = Repo.aggregate(table, :min, :msg_id) - id_max = Repo.aggregate(table, :max, :msg_id) - - with true <- !is_nil(id_min) and !is_nil(id_max), - starts = msg_id_gap_starts(id_max, table), - ends = msg_id_gap_ends(id_min, table), - min_block_l2 = l2_block_number_by_msg_id(id_min, table), - {new_starts, new_ends} = - if(start_block_l2 < min_block_l2, - do: {[start_block_l2 | starts], [min_block_l2 | ends]}, - else: {starts, ends} - ), - true <- Enum.count(new_starts) == Enum.count(new_ends) do - ranges = Enum.zip(new_starts, new_ends) - - invalid_range_exists = Enum.any?(ranges, fn {l2_block_start, l2_block_end} -> l2_block_start > l2_block_end end) - - ranges_final = - with {:ranges_are_invalid, true} <- {:ranges_are_invalid, invalid_range_exists}, - {max_block_l2, _} = get_last_l2_item(table), - {:start_block_l2_is_min, true} <- {:start_block_l2_is_min, start_block_l2 <= max_block_l2} do - [{start_block_l2, max_block_l2}] - else - {:ranges_are_invalid, false} -> ranges - {:start_block_l2_is_min, false} -> [] - end - - ranges_final - |> Enum.each(fn {l2_block_start, l2_block_end} -> - count = - fill_block_range( - l2_block_start, - l2_block_end, - calling_module, - contract_address, - json_rpc_named_arguments, - scan_db - ) - - if count > 0 do - log_fill_msg_id_gaps(scan_db, l2_block_start, l2_block_end, table, count) - end - end) - - if scan_db do - fill_msg_id_gaps(start_block_l2, table, calling_module, contract_address, json_rpc_named_arguments, false) - end - end - end - - defp log_fill_msg_id_gaps(scan_db, l2_block_start, l2_block_end, table, count) do - find_place = if scan_db, do: "in DB", else: "through RPC" - table_name = table.__schema__(:source) - - Logger.info( - "Filled gaps between L2 blocks #{l2_block_start} and #{l2_block_end}. #{count} event(s) were found #{find_place} and written to #{table_name} table." - ) - end - - defp msg_id_gap_starts(id_max, table) - when table in [Explorer.Chain.PolygonEdge.DepositExecute, Explorer.Chain.PolygonEdge.Withdrawal] do - query = - if table == Explorer.Chain.PolygonEdge.DepositExecute do - from(item in table, - select: item.l2_block_number, - order_by: item.msg_id, - where: - fragment( - "NOT EXISTS (SELECT msg_id FROM polygon_edge_deposit_executes WHERE msg_id = (? + 1)) AND msg_id != ?", - item.msg_id, - ^id_max - ) - ) - else - from(item in table, - select: item.l2_block_number, - order_by: item.msg_id, - where: - fragment( - "NOT EXISTS (SELECT msg_id FROM polygon_edge_withdrawals WHERE msg_id = (? + 1)) AND msg_id != ?", - item.msg_id, - ^id_max - ) - ) - end - - Repo.all(query) - end - - defp msg_id_gap_ends(id_min, table) - when table in [Explorer.Chain.PolygonEdge.DepositExecute, Explorer.Chain.PolygonEdge.Withdrawal] do - query = - if table == Explorer.Chain.PolygonEdge.DepositExecute do - from(item in table, - select: item.l2_block_number, - order_by: item.msg_id, - where: - fragment( - "NOT EXISTS (SELECT msg_id FROM polygon_edge_deposit_executes WHERE msg_id = (? - 1)) AND msg_id != ?", - item.msg_id, - ^id_min - ) - ) - else - from(item in table, - select: item.l2_block_number, - order_by: item.msg_id, - where: - fragment( - "NOT EXISTS (SELECT msg_id FROM polygon_edge_withdrawals WHERE msg_id = (? - 1)) AND msg_id != ?", - item.msg_id, - ^id_min - ) - ) - end - - Repo.all(query) - end - - defp get_safe_block(json_rpc_named_arguments) do - case Helper.get_block_number_by_tag("safe", json_rpc_named_arguments) do - {:ok, safe_block} -> - {safe_block, false} - - {:error, :not_found} -> - {:ok, latest_block} = - Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - - {latest_block, true} - end - end - - @spec get_logs( - non_neg_integer() | binary(), - non_neg_integer() | binary(), - binary(), - binary(), - list(), - non_neg_integer() - ) :: {:ok, list()} | {:error, term()} - def get_logs(from_block, to_block, address, topic0, json_rpc_named_arguments, retries) do - # TODO: use the function from the Indexer.Helper module - processed_from_block = if is_integer(from_block), do: integer_to_quantity(from_block), else: from_block - processed_to_block = if is_integer(to_block), do: integer_to_quantity(to_block), else: to_block - - req = - request(%{ - id: 0, - method: "eth_getLogs", - params: [ - %{ - :fromBlock => processed_from_block, - :toBlock => processed_to_block, - :address => address, - :topics => [topic0] - } - ] - }) - - error_message = &"Cannot fetch logs for the block range #{from_block}..#{to_block}. Error: #{inspect(&1)}" - - Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) - end - - defp get_last_l1_item(table) do - query = - from(item in table, - select: {item.l1_block_number, item.l1_transaction_hash}, - order_by: [desc: item.msg_id], - limit: 1 - ) - - query - |> Repo.one() - |> Kernel.||({0, nil}) - end - - @spec get_last_l2_item(module()) :: {non_neg_integer(), binary() | nil} - def get_last_l2_item(table) do - query = - from(item in table, - select: {item.l2_block_number, item.l2_transaction_hash}, - order_by: [desc: item.msg_id], - limit: 1 - ) - - query - |> Repo.one() - |> Kernel.||({0, nil}) - end - - defp json_rpc_named_arguments(polygon_edge_l1_rpc) do - [ - transport: EthereumJSONRPC.HTTP, - transport_options: [ - http: EthereumJSONRPC.HTTP.Tesla, - urls: [polygon_edge_l1_rpc], - http_options: [ - recv_timeout: :timer.minutes(10), - timeout: :timer.minutes(10), - pool: :ethereum_jsonrpc - ] - ] - ] - end - - defp l2_block_number_by_msg_id(id, table) do - Repo.one(from(item in table, select: item.l2_block_number, where: item.msg_id == ^id)) - end - - defp import_events(events, calling_module) do - # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise - {import_data, event_name} = - case Application.get_env(:explorer, :chain_type) == :polygon_edge && calling_module do - Deposit -> - {%{polygon_edge_deposits: %{params: events}, timeout: :infinity}, "StateSynced"} - - WithdrawalExit -> - {%{polygon_edge_withdrawal_exits: %{params: events}, timeout: :infinity}, "ExitProcessed"} - - _ -> - {%{}, ""} - end - - {:ok, _} = Chain.import(import_data) - - {events, event_name} - end - - @spec repeated_request(list(), any(), list(), non_neg_integer()) :: {:ok, any()} | {:error, atom()} - def repeated_request(req, error_message, json_rpc_named_arguments, retries) do - Helper.repeated_call(&json_rpc/2, [req, json_rpc_named_arguments], error_message, retries) - end - - @doc """ - Returns L1 RPC URL for a Polygon Edge module. - """ - @spec l1_rpc_url() :: binary() - def l1_rpc_url do - Application.get_all_env(:indexer)[__MODULE__][:polygon_edge_l1_rpc] - end -end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex deleted file mode 100644 index 0ebade665768..000000000000 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit.ex +++ /dev/null @@ -1,162 +0,0 @@ -defmodule Indexer.Fetcher.PolygonEdge.Deposit do - @moduledoc """ - Fills polygon_edge_deposits DB table. - """ - - # todo: this module is deprecated and should be removed - - use GenServer - use Indexer.Fetcher - - require Logger - - import EthereumJSONRPC, only: [id_to_params: 1, quantity_to_integer: 1] - import Explorer.Helper, only: [decode_data: 2] - - alias ABI.TypeDecoder - alias EthereumJSONRPC.Block.ByNumber - alias EthereumJSONRPC.Blocks - alias Explorer.Chain.PolygonEdge.Deposit - alias Indexer.Fetcher.PolygonEdge - alias Indexer.Helper - - @fetcher_name :polygon_edge_deposit - - # 32-byte signature of the event StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data) - @state_synced_event "0xd1d7f6609674cc5871fdb4b0bcd4f0a214118411de9e38983866514f22659165" - - # 32-byte representation of deposit signature, keccak256("DEPOSIT") - @deposit_signature "87a7811f4bfedea3d341ad165680ae306b01aaeacc205d227629cf157dd9f821" - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(_args) do - {:ok, %{}, {:continue, :ok}} - end - - @impl GenServer - def handle_continue(:ok, state) do - Logger.metadata(fetcher: @fetcher_name) - - env = Application.get_all_env(:indexer)[__MODULE__] - - case PolygonEdge.init_l1( - Deposit, - env, - self(), - env[:state_sender], - "State Sender", - "polygon_edge_deposits", - "Deposits" - ) do - :ignore -> {:stop, :normal, state} - {:ok, new_state} -> {:noreply, new_state} - end - end - - @impl GenServer - def handle_info(:continue, state) do - PolygonEdge.handle_continue(state, @state_synced_event, __MODULE__) - end - - @impl GenServer - def handle_info({ref, _result}, state) do - Process.demonitor(ref, [:flush]) - {:noreply, state} - end - - @spec prepare_events(list(), list()) :: list() - def prepare_events(events, json_rpc_named_arguments) do - Enum.map(events, fn event -> - [data_bytes] = decode_data(event["data"], [:bytes]) - - sig = binary_part(data_bytes, 0, 32) - - l1_block_number = quantity_to_integer(event["blockNumber"]) - - {from, to, l1_timestamp} = - if Base.encode16(sig, case: :lower) == @deposit_signature do - timestamps = get_timestamps_by_events(events, json_rpc_named_arguments) - - [_sig, _root_token, sender, receiver, _amount] = - TypeDecoder.decode_raw(data_bytes, [{:bytes, 32}, :address, :address, :address, {:uint, 256}]) - - {sender, receiver, Map.get(timestamps, l1_block_number)} - else - {nil, nil, nil} - end - - %{ - msg_id: quantity_to_integer(Enum.at(event["topics"], 1)), - from: from, - to: to, - l1_transaction_hash: event["transactionHash"], - l1_timestamp: l1_timestamp, - l1_block_number: l1_block_number - } - end) - end - - @doc """ - Returns L1 RPC URL for this module. - """ - @spec l1_rpc_url() :: binary() | nil - def l1_rpc_url do - PolygonEdge.l1_rpc_url() - end - - @doc """ - Determines if `Indexer.Fetcher.RollupL1ReorgMonitor` module must be up - for this module. - - ## Returns - - `true` if the reorg monitor must be active, `false` otherwise. - """ - @spec requires_l1_reorg_monitor?() :: boolean() - def requires_l1_reorg_monitor? do - module_config = Application.get_all_env(:indexer)[__MODULE__] - not is_nil(module_config[:start_block_l1]) - end - - defp get_blocks_by_events(events, json_rpc_named_arguments, retries) do - request = - events - |> Enum.reduce(%{}, fn event, acc -> - Map.put(acc, event["blockNumber"], 0) - end) - |> Stream.map(fn {block_number, _} -> %{number: block_number} end) - |> id_to_params() - |> Blocks.requests(&ByNumber.request(&1, false, false)) - - error_message = &"Cannot fetch blocks with batch request. Error: #{inspect(&1)}. Request: #{inspect(request)}" - - case PolygonEdge.repeated_request(request, error_message, json_rpc_named_arguments, retries) do - {:ok, results} -> Enum.map(results, fn %{result: result} -> result end) - {:error, _} -> [] - end - end - - defp get_timestamps_by_events(events, json_rpc_named_arguments) do - events - |> get_blocks_by_events(json_rpc_named_arguments, Helper.infinite_retries_number()) - |> Enum.reduce(%{}, fn block, acc -> - block_number = quantity_to_integer(Map.get(block, "number")) - {:ok, timestamp} = DateTime.from_unix(quantity_to_integer(Map.get(block, "timestamp"))) - Map.put(acc, block_number, timestamp) - end) - end -end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex deleted file mode 100644 index 90159b16ccd2..000000000000 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/deposit_execute.ex +++ /dev/null @@ -1,226 +0,0 @@ -defmodule Indexer.Fetcher.PolygonEdge.DepositExecute do - @moduledoc """ - Fills polygon_edge_deposit_executes DB table. - """ - - # todo: this module is deprecated and should be removed - - use GenServer - use Indexer.Fetcher - - require Logger - - import Ecto.Query - - import EthereumJSONRPC, only: [quantity_to_integer: 1] - import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5] - import Indexer.Helper, only: [log_topic_to_string: 1] - - alias Explorer.{Chain, Repo} - alias Explorer.Chain.Log - alias Explorer.Chain.PolygonEdge.DepositExecute - alias Indexer.Fetcher.PolygonEdge - alias Indexer.Helper - - @fetcher_name :polygon_edge_deposit_execute - - # 32-byte signature of the event StateSyncResult(uint256 indexed counter, bool indexed status, bytes message) - @state_sync_result_event "0x31c652130602f3ce96ceaf8a4c2b8b49f049166c6fcf2eb31943a75ec7c936ae" - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(args) do - {:ok, %{}, {:continue, args}} - end - - @impl GenServer - def handle_continue(args, state) do - Logger.metadata(fetcher: @fetcher_name) - - json_rpc_named_arguments = args[:json_rpc_named_arguments] - env = Application.get_all_env(:indexer)[__MODULE__] - - case PolygonEdge.init_l2( - DepositExecute, - env, - self(), - env[:state_receiver], - "StateReceiver", - "polygon_edge_deposit_executes", - "Deposit Executes", - json_rpc_named_arguments - ) do - :ignore -> {:stop, :normal, state} - {:ok, new_state} -> {:noreply, new_state} - end - end - - @impl GenServer - def handle_info( - :continue, - %{ - start_block_l2: start_block_l2, - contract_address: contract_address, - json_rpc_named_arguments: json_rpc_named_arguments - } = state - ) do - PolygonEdge.fill_msg_id_gaps( - start_block_l2, - DepositExecute, - __MODULE__, - contract_address, - json_rpc_named_arguments - ) - - Process.send(self(), :find_new_events, []) - {:noreply, state} - end - - @impl GenServer - def handle_info( - :find_new_events, - %{ - start_block: start_block, - safe_block: safe_block, - safe_block_is_latest: safe_block_is_latest, - contract_address: contract_address, - json_rpc_named_arguments: json_rpc_named_arguments - } = state - ) do - # find and fill all events between start_block and "safe" block - # the "safe" block can be "latest" (when safe_block_is_latest == true) - fill_block_range( - start_block, - safe_block, - {__MODULE__, DepositExecute}, - contract_address, - json_rpc_named_arguments - ) - - if not safe_block_is_latest do - # find and fill all events between "safe" and "latest" block (excluding "safe") - {:ok, latest_block} = - Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - - fill_block_range( - safe_block + 1, - latest_block, - {__MODULE__, DepositExecute}, - contract_address, - json_rpc_named_arguments - ) - end - - {:stop, :normal, state} - end - - @impl GenServer - def handle_info({ref, _result}, state) do - Process.demonitor(ref, [:flush]) - {:noreply, state} - end - - @spec remove(non_neg_integer()) :: no_return() - def remove(starting_block) do - Repo.delete_all(from(de in DepositExecute, where: de.l2_block_number >= ^starting_block)) - end - - @spec event_to_deposit_execute(binary(), binary(), binary(), binary()) :: map() - def event_to_deposit_execute(second_topic, third_topic, l2_transaction_hash, l2_block_number) do - msg_id = - second_topic - |> log_topic_to_string() - |> quantity_to_integer() - - status = - third_topic - |> log_topic_to_string() - |> quantity_to_integer() - - %{ - msg_id: msg_id, - l2_transaction_hash: l2_transaction_hash, - l2_block_number: quantity_to_integer(l2_block_number), - success: status != 0 - } - end - - @spec find_and_save_entities(boolean(), binary(), non_neg_integer(), non_neg_integer(), list()) :: non_neg_integer() - def find_and_save_entities( - scan_db, - state_receiver, - block_start, - block_end, - json_rpc_named_arguments - ) do - executes = - if scan_db do - query = - from(log in Log, - select: {log.second_topic, log.third_topic, log.transaction_hash, log.block_number}, - where: - log.first_topic == ^@state_sync_result_event and log.address_hash == ^state_receiver and - log.block_number >= ^block_start and log.block_number <= ^block_end - ) - - query - |> Repo.all(timeout: :infinity) - |> Enum.map(fn {second_topic, third_topic, l2_transaction_hash, l2_block_number} -> - event_to_deposit_execute(second_topic, third_topic, l2_transaction_hash, l2_block_number) - end) - else - {:ok, result} = - PolygonEdge.get_logs( - block_start, - block_end, - state_receiver, - @state_sync_result_event, - json_rpc_named_arguments, - Helper.infinite_retries_number() - ) - - Enum.map(result, fn event -> - event_to_deposit_execute( - Enum.at(event["topics"], 1), - Enum.at(event["topics"], 2), - event["transactionHash"], - event["blockNumber"] - ) - end) - end - - # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise - import_options = - if Application.get_env(:explorer, :chain_type) == :polygon_edge do - %{ - polygon_edge_deposit_executes: %{params: executes}, - timeout: :infinity - } - else - %{} - end - - {:ok, _} = Chain.import(import_options) - - Enum.count(executes) - end - - @spec state_sync_result_event_signature() :: binary() - def state_sync_result_event_signature do - @state_sync_result_event - end -end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex deleted file mode 100644 index 47f6661830e5..000000000000 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal.ex +++ /dev/null @@ -1,241 +0,0 @@ -defmodule Indexer.Fetcher.PolygonEdge.Withdrawal do - @moduledoc """ - Fills polygon_edge_withdrawals DB table. - """ - - # todo: this module is deprecated and should be removed - - use GenServer - use Indexer.Fetcher - - require Logger - - import Ecto.Query - - import EthereumJSONRPC, only: [quantity_to_integer: 1] - import Explorer.Helper, only: [decode_data: 2] - import Indexer.Fetcher.PolygonEdge, only: [fill_block_range: 5] - import Indexer.Helper, only: [log_topic_to_string: 1] - - alias ABI.TypeDecoder - alias Explorer.{Chain, Repo} - alias Explorer.Chain.Log - alias Explorer.Chain.PolygonEdge.Withdrawal - alias Indexer.Fetcher.PolygonEdge - alias Indexer.Helper - - @fetcher_name :polygon_edge_withdrawal - - # 32-byte signature of the event L2StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data) - @l2_state_synced_event "0xedaf3c471ebd67d60c29efe34b639ede7d6a1d92eaeb3f503e784971e67118a5" - - # 32-byte representation of withdrawal signature, keccak256("WITHDRAW") - @withdrawal_signature "7a8dc26796a1e50e6e190b70259f58f6a4edd5b22280ceecc82b687b8e982869" - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(args) do - {:ok, %{}, {:continue, args}} - end - - @impl GenServer - def handle_continue(args, state) do - Logger.metadata(fetcher: @fetcher_name) - - json_rpc_named_arguments = args[:json_rpc_named_arguments] - env = Application.get_all_env(:indexer)[__MODULE__] - - case PolygonEdge.init_l2( - Withdrawal, - env, - self(), - env[:state_sender], - "L2StateSender", - "polygon_edge_withdrawals", - "Withdrawals", - json_rpc_named_arguments - ) do - :ignore -> {:stop, :normal, state} - {:ok, new_state} -> {:noreply, new_state} - end - end - - @impl GenServer - def handle_info( - :continue, - %{ - start_block_l2: start_block_l2, - contract_address: contract_address, - json_rpc_named_arguments: json_rpc_named_arguments - } = state - ) do - PolygonEdge.fill_msg_id_gaps( - start_block_l2, - Withdrawal, - __MODULE__, - contract_address, - json_rpc_named_arguments - ) - - Process.send(self(), :find_new_events, []) - {:noreply, state} - end - - @impl GenServer - def handle_info( - :find_new_events, - %{ - start_block: start_block, - safe_block: safe_block, - safe_block_is_latest: safe_block_is_latest, - contract_address: contract_address, - json_rpc_named_arguments: json_rpc_named_arguments - } = state - ) do - # find and fill all events between start_block and "safe" block - # the "safe" block can be "latest" (when safe_block_is_latest == true) - fill_block_range( - start_block, - safe_block, - {__MODULE__, Withdrawal}, - contract_address, - json_rpc_named_arguments - ) - - if not safe_block_is_latest do - # find and fill all events between "safe" and "latest" block (excluding "safe") - {:ok, latest_block} = - Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()) - - fill_block_range( - safe_block + 1, - latest_block, - {__MODULE__, Withdrawal}, - contract_address, - json_rpc_named_arguments - ) - end - - {:stop, :normal, state} - end - - @impl GenServer - def handle_info({ref, _result}, state) do - Process.demonitor(ref, [:flush]) - {:noreply, state} - end - - @spec remove(non_neg_integer()) :: no_return() - def remove(starting_block) do - Repo.delete_all(from(w in Withdrawal, where: w.l2_block_number >= ^starting_block)) - end - - @spec event_to_withdrawal(binary(), map(), binary(), binary()) :: map() - def event_to_withdrawal(second_topic, data, l2_transaction_hash, l2_block_number) do - msg_id = - second_topic - |> log_topic_to_string() - |> quantity_to_integer() - - [data_bytes] = decode_data(data, [:bytes]) - - sig = binary_part(data_bytes, 0, 32) - - {from, to} = - if Base.encode16(sig, case: :lower) == @withdrawal_signature do - [_sig, _root_token, sender, receiver, _amount] = - TypeDecoder.decode_raw(data_bytes, [{:bytes, 32}, :address, :address, :address, {:uint, 256}]) - - {sender, receiver} - else - {nil, nil} - end - - %{ - msg_id: msg_id, - from: from, - to: to, - l2_transaction_hash: l2_transaction_hash, - l2_block_number: quantity_to_integer(l2_block_number) - } - end - - @spec find_and_save_entities(boolean(), binary(), non_neg_integer(), non_neg_integer(), list()) :: non_neg_integer() - def find_and_save_entities( - scan_db, - state_sender, - block_start, - block_end, - json_rpc_named_arguments - ) do - withdrawals = - if scan_db do - query = - from(log in Log, - select: {log.second_topic, log.data, log.transaction_hash, log.block_number}, - where: - log.first_topic == ^@l2_state_synced_event and log.address_hash == ^state_sender and - log.block_number >= ^block_start and log.block_number <= ^block_end - ) - - query - |> Repo.all(timeout: :infinity) - |> Enum.map(fn {second_topic, data, l2_transaction_hash, l2_block_number} -> - event_to_withdrawal(second_topic, data, l2_transaction_hash, l2_block_number) - end) - else - {:ok, result} = - PolygonEdge.get_logs( - block_start, - block_end, - state_sender, - @l2_state_synced_event, - json_rpc_named_arguments, - Helper.infinite_retries_number() - ) - - Enum.map(result, fn event -> - event_to_withdrawal( - Enum.at(event["topics"], 1), - event["data"], - event["transactionHash"], - event["blockNumber"] - ) - end) - end - - # here we explicitly check CHAIN_TYPE as Dialyzer throws an error otherwise - import_options = - if Application.get_env(:explorer, :chain_type) == :polygon_edge do - %{ - polygon_edge_withdrawals: %{params: withdrawals}, - timeout: :infinity - } - else - %{} - end - - {:ok, _} = Chain.import(import_options) - - Enum.count(withdrawals) - end - - @spec l2_state_synced_event_signature() :: binary() - def l2_state_synced_event_signature do - @l2_state_synced_event - end -end diff --git a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex b/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex deleted file mode 100644 index 64d82b394b3b..000000000000 --- a/apps/indexer/lib/indexer/fetcher/polygon_edge/withdrawal_exit.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Indexer.Fetcher.PolygonEdge.WithdrawalExit do - @moduledoc """ - Fills polygon_edge_withdrawal_exits DB table. - """ - - # todo: this module is deprecated and should be removed - - use GenServer - use Indexer.Fetcher - - require Logger - - import EthereumJSONRPC, only: [quantity_to_integer: 1] - - alias Explorer.Chain.PolygonEdge.WithdrawalExit - alias Indexer.Fetcher.PolygonEdge - - @fetcher_name :polygon_edge_withdrawal_exit - - # 32-byte signature of the event ExitProcessed(uint256 indexed id, bool indexed success, bytes returnData) - @exit_processed_event "0x8bbfa0c9bee3785c03700d2a909592286efb83fc7e7002be5764424b9842f7ec" - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(_args) do - {:ok, %{}, {:continue, :ok}} - end - - @impl GenServer - def handle_continue(:ok, state) do - Logger.metadata(fetcher: @fetcher_name) - - env = Application.get_all_env(:indexer)[__MODULE__] - - case PolygonEdge.init_l1( - WithdrawalExit, - env, - self(), - env[:exit_helper], - "Exit Helper", - "polygon_edge_withdrawal_exits", - "Withdrawals" - ) do - :ignore -> {:stop, :normal, state} - {:ok, new_state} -> {:noreply, new_state} - end - end - - @impl GenServer - def handle_info(:continue, state) do - PolygonEdge.handle_continue(state, @exit_processed_event, __MODULE__) - end - - @impl GenServer - def handle_info({ref, _result}, state) do - Process.demonitor(ref, [:flush]) - {:noreply, state} - end - - @spec prepare_events(list(), list()) :: list() - def prepare_events(events, _) do - Enum.map(events, fn event -> - %{ - msg_id: quantity_to_integer(Enum.at(event["topics"], 1)), - l1_transaction_hash: event["transactionHash"], - l1_block_number: quantity_to_integer(event["blockNumber"]), - success: quantity_to_integer(Enum.at(event["topics"], 2)) != 0 - } - end) - end - - @doc """ - Returns L1 RPC URL for this module. - """ - @spec l1_rpc_url() :: binary() | nil - def l1_rpc_url do - PolygonEdge.l1_rpc_url() - end - - @doc """ - Determines if `Indexer.Fetcher.RollupL1ReorgMonitor` module must be up - for this module. - - ## Returns - - `true` if the reorg monitor must be active, `false` otherwise. - """ - @spec requires_l1_reorg_monitor?() :: boolean() - def requires_l1_reorg_monitor? do - module_config = Application.get_all_env(:indexer)[__MODULE__] - not is_nil(module_config[:start_block_l1]) - end -end diff --git a/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex index f8bdaf1bfcd0..426fb6a9a911 100644 --- a/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex +++ b/apps/indexer/lib/indexer/fetcher/rollup_l1_reorg_monitor.ex @@ -32,12 +32,6 @@ defmodule Indexer.Fetcher.RollupL1ReorgMonitor do Indexer.Fetcher.Optimism.WithdrawalEvent ] - :polygon_edge -> - [ - Indexer.Fetcher.PolygonEdge.Deposit, - Indexer.Fetcher.PolygonEdge.WithdrawalExit - ] - :polygon_zkevm -> [ Indexer.Fetcher.PolygonZkevm.BridgeL1 diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index e226bb3b7eec..5cf8549cb8b5 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -195,14 +195,6 @@ defmodule Indexer.Supervisor do Indexer.Fetcher.Optimism.Interop.MultichainExport.Supervisor, [[memory_monitor: memory_monitor]] }, - configure(Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, [[memory_monitor: memory_monitor]]), - configure(Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, [ - [memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments] - ]), - configure(Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, [ - [memory_monitor: memory_monitor, json_rpc_named_arguments: json_rpc_named_arguments] - ]), - configure(Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, [[memory_monitor: memory_monitor]]), configure(Indexer.Fetcher.Shibarium.L2.Supervisor, [ [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] ]), diff --git a/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex b/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex deleted file mode 100644 index 0e3ddeae5520..000000000000 --- a/apps/indexer/lib/indexer/transform/polygon_edge/deposit_executes.ex +++ /dev/null @@ -1,55 +0,0 @@ -defmodule Indexer.Transform.PolygonEdge.DepositExecutes do - @moduledoc """ - Helper functions for transforming data for Polygon Edge deposit executes. - """ - - require Logger - - alias Indexer.Fetcher.PolygonEdge.DepositExecute - alias Indexer.Helper - - @doc """ - Returns a list of deposit executes given a list of logs. - """ - @spec parse(list()) :: list() - def parse(logs) do - prev_metadata = Logger.metadata() - Logger.metadata(fetcher: :polygon_edge_deposit_executes_realtime) - - items = - with false <- - is_nil(Application.get_env(:indexer, DepositExecute)[:start_block_l2]), - state_receiver = Application.get_env(:indexer, DepositExecute)[:state_receiver], - true <- Helper.address_correct?(state_receiver) do - state_receiver = String.downcase(state_receiver) - state_sync_result_event_signature = DepositExecute.state_sync_result_event_signature() - - logs - |> Enum.filter(fn log -> - !is_nil(log.first_topic) && String.downcase(log.first_topic) == state_sync_result_event_signature && - String.downcase(Helper.address_hash_to_string(log.address_hash)) == state_receiver - end) - |> Enum.map(fn log -> - Logger.info("Deposit Execute (StateSyncResult) message found, id: #{log.second_topic}.") - - DepositExecute.event_to_deposit_execute( - log.second_topic, - log.third_topic, - log.transaction_hash, - log.block_number - ) - end) - else - true -> - [] - - false -> - Logger.error("StateReceiver contract address is incorrect. Cannot use #{__MODULE__} for parsing logs.") - [] - end - - Logger.reset_metadata(prev_metadata) - - items - end -end diff --git a/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex b/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex deleted file mode 100644 index 02ffcbaaa71e..000000000000 --- a/apps/indexer/lib/indexer/transform/polygon_edge/withdrawals.ex +++ /dev/null @@ -1,54 +0,0 @@ -defmodule Indexer.Transform.PolygonEdge.Withdrawals do - @moduledoc """ - Helper functions for transforming data for Polygon Edge withdrawals. - """ - - require Logger - - alias Indexer.Fetcher.PolygonEdge.Withdrawal - alias Indexer.Helper - - @doc """ - Returns a list of withdrawals given a list of logs. - """ - @spec parse(list()) :: list() - def parse(logs) do - prev_metadata = Logger.metadata() - Logger.metadata(fetcher: :polygon_edge_withdrawals_realtime) - - items = - with false <- is_nil(Application.get_env(:indexer, Withdrawal)[:start_block_l2]), - state_sender = Application.get_env(:indexer, Withdrawal)[:state_sender], - true <- Helper.address_correct?(state_sender) do - state_sender = String.downcase(state_sender) - l2_state_synced_event_signature = Withdrawal.l2_state_synced_event_signature() - - logs - |> Enum.filter(fn log -> - !is_nil(log.first_topic) && String.downcase(log.first_topic) == l2_state_synced_event_signature && - String.downcase(Helper.address_hash_to_string(log.address_hash)) == state_sender - end) - |> Enum.map(fn log -> - Logger.info("Withdrawal message found, id: #{log.second_topic}.") - - Withdrawal.event_to_withdrawal( - log.second_topic, - log.data, - log.transaction_hash, - log.block_number - ) - end) - else - true -> - [] - - false -> - Logger.error("L2StateSender contract address is incorrect. Cannot use #{__MODULE__} for parsing logs.") - [] - end - - Logger.reset_metadata(prev_metadata) - - items - end -end diff --git a/config/config_helper.exs b/config/config_helper.exs index af5517b747fe..bf38736b1fd7 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -17,7 +17,6 @@ defmodule ConfigHelper do ethereum: Explorer.Repo.Beacon, filecoin: Explorer.Repo.Filecoin, optimism: Explorer.Repo.Optimism, - polygon_edge: Explorer.Repo.PolygonEdge, polygon_zkevm: Explorer.Repo.PolygonZkevm, rsk: Explorer.Repo.RSK, scroll: Explorer.Repo.Scroll, @@ -355,7 +354,6 @@ defmodule ConfigHelper do "ethereum", "filecoin", "optimism", - "polygon_edge", "polygon_zkevm", "rsk", "scroll", diff --git a/config/runtime.exs b/config/runtime.exs index 731b8b2d1a43..1357c24bf5e5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -239,11 +239,7 @@ config :ethereum_jsonrpc, EthereumJSONRPC.Geth, block_traceable?: ConfigHelper.parse_bool_env_var("ETHEREUM_JSONRPC_GETH_TRACE_BY_BLOCK"), allow_empty_traces?: ConfigHelper.parse_bool_env_var("ETHEREUM_JSONRPC_GETH_ALLOW_EMPTY_TRACES"), debug_trace_timeout: System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "5s"), - tracer: - if(ConfigHelper.chain_type() == :polygon_edge, - do: "polygon_edge", - else: System.get_env("INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE", "call_tracer") - ) + tracer: System.get_env("INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE", "call_tracer") config :ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction, type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default") @@ -1293,37 +1289,6 @@ config :indexer, Indexer.Fetcher.Withdrawal.Supervisor, config :indexer, Indexer.Fetcher.Withdrawal, first_block: System.get_env("WITHDRAWALS_FIRST_BLOCK") -config :indexer, Indexer.Fetcher.PolygonEdge.Deposit.Supervisor, enabled: ConfigHelper.chain_type() == :polygon_edge - -config :indexer, Indexer.Fetcher.PolygonEdge.DepositExecute.Supervisor, - enabled: ConfigHelper.chain_type() == :polygon_edge - -config :indexer, Indexer.Fetcher.PolygonEdge.Withdrawal.Supervisor, enabled: ConfigHelper.chain_type() == :polygon_edge - -config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit.Supervisor, - enabled: ConfigHelper.chain_type() == :polygon_edge - -config :indexer, Indexer.Fetcher.PolygonEdge, - polygon_edge_l1_rpc: System.get_env("INDEXER_POLYGON_EDGE_L1_RPC"), - polygon_edge_eth_get_logs_range_size: - ConfigHelper.parse_integer_env_var("INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE", 1_000) - -config :indexer, Indexer.Fetcher.PolygonEdge.Deposit, - start_block_l1: System.get_env("INDEXER_POLYGON_EDGE_L1_DEPOSITS_START_BLOCK"), - state_sender: System.get_env("INDEXER_POLYGON_EDGE_L1_STATE_SENDER_CONTRACT") - -config :indexer, Indexer.Fetcher.PolygonEdge.DepositExecute, - start_block_l2: System.get_env("INDEXER_POLYGON_EDGE_L2_DEPOSITS_START_BLOCK"), - state_receiver: System.get_env("INDEXER_POLYGON_EDGE_L2_STATE_RECEIVER_CONTRACT") - -config :indexer, Indexer.Fetcher.PolygonEdge.Withdrawal, - start_block_l2: System.get_env("INDEXER_POLYGON_EDGE_L2_WITHDRAWALS_START_BLOCK"), - state_sender: System.get_env("INDEXER_POLYGON_EDGE_L2_STATE_SENDER_CONTRACT") - -config :indexer, Indexer.Fetcher.PolygonEdge.WithdrawalExit, - start_block_l1: System.get_env("INDEXER_POLYGON_EDGE_L1_WITHDRAWALS_START_BLOCK"), - exit_helper: System.get_env("INDEXER_POLYGON_EDGE_L1_EXIT_HELPER_CONTRACT") - config :indexer, Indexer.Fetcher.ZkSync.TransactionBatch, chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_BATCHES_CHUNK_SIZE", 50), batches_max_range: ConfigHelper.parse_integer_env_var("INDEXER_ZKSYNC_NEW_BATCHES_MAX_RANGE", 50), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 7433df1cec1f..c3f4baba4822 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -252,16 +252,6 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TX_ACTIONS_REINDEX_LAST_BLOCK= # INDEXER_TX_ACTIONS_REINDEX_PROTOCOLS= # INDEXER_TX_ACTIONS_AAVE_V3_POOL_CONTRACT= -# INDEXER_POLYGON_EDGE_L1_RPC= -# INDEXER_POLYGON_EDGE_L1_EXIT_HELPER_CONTRACT= -# INDEXER_POLYGON_EDGE_L1_WITHDRAWALS_START_BLOCK= -# INDEXER_POLYGON_EDGE_L1_STATE_SENDER_CONTRACT= -# INDEXER_POLYGON_EDGE_L1_DEPOSITS_START_BLOCK= -# INDEXER_POLYGON_EDGE_L2_STATE_SENDER_CONTRACT= -# INDEXER_POLYGON_EDGE_L2_WITHDRAWALS_START_BLOCK= -# INDEXER_POLYGON_EDGE_L2_STATE_RECEIVER_CONTRACT= -# INDEXER_POLYGON_EDGE_L2_DEPOSITS_START_BLOCK= -# INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE= # INDEXER_POLYGON_ZKEVM_BATCHES_ENABLED= # INDEXER_POLYGON_ZKEVM_BATCHES_CHUNK_SIZE= # INDEXER_POLYGON_ZKEVM_BATCHES_RECHECK_INTERVAL= From 0c57dc255b19cf11aac14266202f37c54e6277d4 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:44:41 +0400 Subject: [PATCH 0271/1006] chore: Move token transfers to a separate event handler (#13068) * chore: Move token transfers to a separate event handler * Refactor realtime event handlers structure --- .../lib/block_scout_web/application.ex | 16 +++++++++--- .../main.ex} | 3 +-- .../main_page.ex} | 2 +- .../smart_contract.ex} | 2 +- .../realtime_event_handlers/token_transfer.ex | 26 +++++++++++++++++++ .../utility/event_handlers_metrics.ex | 24 +++++++++++++---- .../lib/explorer/chain/contract_method.ex | 4 +-- .../lib/explorer/chain/transaction.ex | 1 + 8 files changed, 63 insertions(+), 15 deletions(-) rename apps/block_scout_web/lib/block_scout_web/{realtime_event_handler.ex => realtime_event_handlers/main.ex} (95%) rename apps/block_scout_web/lib/block_scout_web/{main_page_realtime_event_handler.ex => realtime_event_handlers/main_page.ex} (91%) rename apps/block_scout_web/lib/block_scout_web/{smart_contract_realtime_event_handler.ex => realtime_event_handlers/smart_contract.ex} (91%) create mode 100644 apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/token_transfer.ex diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 861da966c860..0c293d2e6c0e 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -50,7 +50,14 @@ defmodule BlockScoutWeb.Application do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.Counters.{BlocksIndexedCounter, InternalTransactionsIndexedCounter} alias BlockScoutWeb.Prometheus.{Exporter, PhoenixInstrumenter, PublicExporter} - alias BlockScoutWeb.{MainPageRealtimeEventHandler, RealtimeEventHandler, SmartContractRealtimeEventHandler} + + alias BlockScoutWeb.RealtimeEventHandlers.{ + Main, + MainPage, + SmartContract, + TokenTransfer + } + alias BlockScoutWeb.Utility.EventHandlersMetrics alias Explorer.Chain.Metrics, as: ChainMetrics @@ -70,9 +77,10 @@ defmodule BlockScoutWeb.Application do { [ {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, - {MainPageRealtimeEventHandler, name: MainPageRealtimeEventHandler}, - {RealtimeEventHandler, name: RealtimeEventHandler}, - {SmartContractRealtimeEventHandler, name: SmartContractRealtimeEventHandler}, + {MainPage, name: MainPage}, + {Main, name: Main}, + {SmartContract, name: SmartContract}, + {TokenTransfer, name: TokenTransfer}, {BlocksIndexedCounter, name: BlocksIndexedCounter}, {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, {EventHandlersMetrics, []}, diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main.ex similarity index 95% rename from apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex rename to apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main.ex index 38eadb6fd0f3..1001259edb85 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.RealtimeEventHandler do +defmodule BlockScoutWeb.RealtimeEventHandlers.Main do @moduledoc """ Subscribing process for broadcast events from realtime. """ @@ -38,7 +38,6 @@ defmodule BlockScoutWeb.RealtimeEventHandler do Subscriber.to(:block_rewards, :realtime) Subscriber.to(:internal_transactions, :realtime) Subscriber.to(:internal_transactions, :on_demand) - Subscriber.to(:token_transfers, :realtime) Subscriber.to(:addresses, :on_demand) Subscriber.to(:address_coin_balances, :on_demand) Subscriber.to(:address_current_token_balances, :on_demand) diff --git a/apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main_page.ex similarity index 91% rename from apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex rename to apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main_page.ex index e0f61898963b..5f7ac0598297 100644 --- a/apps/block_scout_web/lib/block_scout_web/main_page_realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/main_page.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.MainPageRealtimeEventHandler do +defmodule BlockScoutWeb.RealtimeEventHandlers.MainPage do @moduledoc """ Subscribing process for main page broadcast events from realtime. """ diff --git a/apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/smart_contract.ex similarity index 91% rename from apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex rename to apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/smart_contract.ex index 73c222d0699c..8e9acde0e207 100644 --- a/apps/block_scout_web/lib/block_scout_web/smart_contract_realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/smart_contract.ex @@ -1,4 +1,4 @@ -defmodule BlockScoutWeb.SmartContractRealtimeEventHandler do +defmodule BlockScoutWeb.RealtimeEventHandlers.SmartContract do @moduledoc """ Subscribing process for smart contract verification related broadcast events from realtime. """ diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/token_transfer.ex new file mode 100644 index 000000000000..f1937d9a73f0 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handlers/token_transfer.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.RealtimeEventHandlers.TokenTransfer do + @moduledoc """ + Subscribing process for token transfer broadcast events from realtime. + """ + + use GenServer + + alias BlockScoutWeb.Notifier + alias Explorer.Chain.Events.Subscriber + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + @impl true + def init([]) do + Subscriber.to(:token_transfers, :realtime) + {:ok, []} + end + + @impl true + def handle_info(event, state) do + Notifier.handle_event(event) + {:noreply, state} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex b/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex index ed2709bddce1..12742727766c 100644 --- a/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex +++ b/apps/block_scout_web/lib/block_scout_web/utility/event_handlers_metrics.ex @@ -5,7 +5,13 @@ defmodule BlockScoutWeb.Utility.EventHandlersMetrics do use GenServer - alias BlockScoutWeb.{MainPageRealtimeEventHandler, RealtimeEventHandler, SmartContractRealtimeEventHandler} + alias BlockScoutWeb.RealtimeEventHandlers.{ + Main, + MainPage, + SmartContract, + TokenTransfer + } + alias BlockScoutWeb.Prometheus.Instrumenter @interval :timer.minutes(1) @@ -29,13 +35,21 @@ defmodule BlockScoutWeb.Utility.EventHandlersMetrics do end defp set_metrics do - set_handler_metric(MainPageRealtimeEventHandler, :main_page) - set_handler_metric(RealtimeEventHandler, :common) - set_handler_metric(SmartContractRealtimeEventHandler, :smart_contract) + set_handler_metric(MainPage, :main_page) + set_handler_metric(Main, :common) + set_handler_metric(SmartContract, :smart_contract) + set_handler_metric(TokenTransfer, :token_transfer) end defp set_handler_metric(handler, label) do - {_, queue_length} = Process.info(Process.whereis(handler), :message_queue_len) + queue_length = + with pid when is_pid(pid) <- Process.whereis(handler), + {:message_queue_len, length} <- Process.info(pid, :message_queue_len) do + length + else + _ -> 0 + end + Instrumenter.event_handler_queue_length(label, queue_length) end diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 3382e422a323..15359cfb1780 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -8,7 +8,7 @@ defmodule Explorer.Chain.ContractMethod do import Ecto.Query, only: [from: 2] use Explorer.Schema - alias Explorer.Chain.{Hash, MethodIdentifier, SmartContract} + alias Explorer.Chain.{Data, Hash, MethodIdentifier, SmartContract} alias Explorer.{Chain, Repo} typed_schema "contract_methods" do @@ -67,7 +67,7 @@ defmodule Explorer.Chain.ContractMethod do @doc """ Query that finds limited number of contract methods by selector id """ - @spec find_contract_method_query(binary(), integer()) :: Ecto.Query.t() + @spec find_contract_method_query(binary() | Data.t(), integer()) :: Ecto.Query.t() def find_contract_method_query(method_id, limit) do from( contract_method in __MODULE__, diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 86774b3748c3..9e0b172551fe 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -894,6 +894,7 @@ defmodule Explorer.Chain.Transaction do methods_map, _smart_contract_full_abi_map ) do + {:ok, method_id} = MethodIdentifier.cast(method_id) methods = check_methods_cache(method_id, methods_map, options) candidates = From fbce8af68123c577248bb8e37a4695020042ef86 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 26 Aug 2025 13:50:49 +0300 Subject: [PATCH 0272/1006] chore: Bump actions major versions (#13077) --- .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/config.yml | 28 +++++++++---------- .github/workflows/generate-swagger.yml | 8 +++--- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- ...lish-docker-image-for-optimism-interop.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-suave.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- .github/workflows/release.yml | 4 +-- 54 files changed, 71 insertions(+), 71 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a2e5d38487f3..c437d1dcdf2f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 8597ac7406a9..7ef15d93c6f8 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -92,7 +92,7 @@ jobs: name: Build and Cache deps runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -159,7 +159,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -186,7 +186,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -218,7 +218,7 @@ jobs: - build-and-cache - matrix-builder steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -265,7 +265,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -295,7 +295,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -327,7 +327,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -376,7 +376,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -426,7 +426,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -467,7 +467,7 @@ jobs: runs-on: ubuntu-latest needs: build-and-cache steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -519,7 +519,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -583,7 +583,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -659,7 +659,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -732,7 +732,7 @@ jobs: # Maps tcp port 5432 on service container to the host - 5432:5432 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 29d297f5aeb4..1159bb03fd11 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -64,7 +64,7 @@ jobs: name: Build and Cache deps runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -108,7 +108,7 @@ jobs: - build-and-cache - matrix-builder steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: erlef/setup-beam@v1 with: otp-version: ${{ env.OTP_VERSION }} @@ -156,14 +156,14 @@ jobs: fi - name: Checkout specs repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: ${{ vars.API_SPECS_REPOSITORY }} token: ${{ secrets.API_SPECS_PAT }} path: api-specs - name: Download all swagger specs - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: pattern: openapi-spec-* merge-multiple: true diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index e7421f8914ec..47706fe07027 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 95a167e15fdf..9b5a28757b8e 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -19,7 +19,7 @@ jobs: RELEASE_VERSION: 9.0.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 9e26377c9faf..bf39e3471fb0 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 628c29703c0e..27aa458a28e5 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 34e1cc2e1820..e9d504b07b69 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 00d3faad172b..a96afb338020 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index d1ea40766ab5..58b4524c3986 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index ae56fa07e317..0f87f5378e61 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 5ed13cf13eba..d0a583afe0b0 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 90bafe95b43a..5d3dd4f1e746 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 19e9cf2726a1..88039594ea2e 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 7c1afb792c8d..aad6f1a5ee75 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -18,7 +18,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index a9cac327426c..403812ec106a 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -12,7 +12,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 8638adeba6d2..baba1dea31e3 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 5322683b68de..7bc8264e2b61 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: arbitrum steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 6bf210a2603f..123a29e8ecb2 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 82a2d9df95df..092cf4f65b85 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: poa steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 524c9eda1e00..868376081a9f 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: eth-sepolia steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 0cd2567056c3..c7978d1528ea 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: ethereum steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index e8c62998372f..2fef0d922ca2 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -12,7 +12,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: filecoin steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 74d7f8338469..337381a9d127 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: fuse steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 0686c91c7314..de42963a91fd 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: xdai steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index c458af09f16b..a7c70abc36de 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index ca55f12e88ab..2711e1c5a950 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: lukso steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/publish-docker-image-for-optimism-interop.yml index cbf21768e33e..29fea3a85b85 100644 --- a/.github/workflows/publish-docker-image-for-optimism-interop.yml +++ b/.github/workflows/publish-docker-image-for-optimism-interop.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index c8908ca4523c..741d1ba18a0e 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: optimism steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 48ea1141f796..829cad26de16 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: rsk steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 60477ee49ea2..1b4dce4d300c 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: scroll steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 76cdd7d8ba92..2fe18e3e5730 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -16,7 +16,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: suave steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index e0b883f52232..03de0fa2b02c 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zetachain steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index bc66fb8990a7..520bdc15b1e9 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zilliqa steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 5a427b801e80..63f19bed4257 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -13,7 +13,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zkevm steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 0194ac2e99d5..3b015a85c4a5 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -12,7 +12,7 @@ jobs: RELEASE_VERSION: 9.0.2 DOCKER_CHAIN_NAME: zksync steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 1b39414a1bb5..5b5668fcb11a 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -14,7 +14,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 7511ba4d4133..75f7eab0c33d 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -19,7 +19,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index a8955490e45e..57d3d6b65938 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -12,7 +12,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index fa893c7f31d8..cf494078ea7d 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index ce5d3decb555..6c8534f01cd4 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -17,7 +17,7 @@ jobs: RELEASE_VERSION: 9.0.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index f581ea9665b6..be8a11391ae8 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 730f5e6e8357..01b6d8c9c868 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 636b9258945b..e608b12560d7 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 61a4185d0144..cff69ef3e487 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index d1f721103f61..f326039b32d4 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 0ccefe4ea67f..e513a7170fdc 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index cd77da0bd527..64783682fe21 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 20a288a7ea92..000767f1242a 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index 04a743b8100f..ccf5e2073f40 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index cf7ea582a78c..6c985003f58a 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 47155a342883..a175768db4f0 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index d9668d3ef8d5..921aa9b489d9 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7bcd0a62e6c..3352e5c8170c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: env: RELEASE_VERSION: 9.0.2 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Setup repo uses: ./.github/actions/setup-repo id: setup @@ -121,7 +121,7 @@ jobs: # production-rsk # production-immutable # steps: - # - uses: actions/checkout@v4 + # - uses: actions/checkout@v5 # - name: Set Git config # run: | # git config --local user.email "actions@github.com" From cabe3d2a86eb6ec9777066be632668c603e6e95c Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:27:27 +0300 Subject: [PATCH 0273/1006] fix: RuntimeEnvHelper usage in Auth0.Migrated (#13075) --- .../lib/explorer/third_party_integrations/auth0/migrated.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex index a54fb61a4880..6c7bdb575e26 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/migrated.ex @@ -17,7 +17,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Migrated do """ use Utils.RuntimeEnvHelper, - auth0_application_identifier: [:ueberauth, Ueberauth.Strategy.Auth0.OAuth, :auth0_application_id] + auth0_application_identifier: [:ueberauth, [Ueberauth.Strategy.Auth0.OAuth, :auth0_application_id]] alias Explorer.ThirdPartyIntegrations.Auth0.Internal alias Ueberauth.Auth From 42e774d45c8acf8b0f774cf9c434994d1ce50b41 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:52:42 +0300 Subject: [PATCH 0274/1006] chore: Add `is_pending_update` flag to block and transaction API endpoints (#13013) * Initial plan * Add refetch_needed field to API v2 Block and Transaction responses Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Add refetch_needed field to API v2 Block and Transaction responses Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * chore: remove `asdf` dir added by mistake * Rename refetch_needed to is_pending_update and add tests for list endpoints Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * chore: add `is_pending_update` to api spec * fix: format --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> Co-authored-by: Fedor Ivanov --- .gitignore | 2 +- .../block_scout_web/schemas/api/v2/block.ex | 6 ++- .../schemas/api/v2/transaction.ex | 6 ++- .../views/api/v2/block_view.ex | 3 +- .../views/api/v2/transaction_view.ex | 3 +- .../api/v2/block_controller_test.exs | 31 +++++++++++ .../api/v2/transaction_controller_test.exs | 51 +++++++++++++++++++ 7 files changed, 95 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index 16880a0aef09..f3545add61b3 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,4 @@ tasks_in_progress /temp -openapi.yaml \ No newline at end of file +openapi.yaml diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex index 398e13e5c61b..dbca67752072 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -296,7 +296,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do burnt_fees_percentage: %Schema{type: :number, format: :float, nullable: true}, type: %Schema{type: :string, nullable: false, enum: ["block", "uncle", "reorg"]}, transaction_fees: General.IntegerString, - withdrawals_count: %Schema{type: :integer, nullable: true} + withdrawals_count: %Schema{type: :integer, nullable: true}, + is_pending_update: %Schema{type: :boolean, nullable: false} }, required: [ :height, @@ -322,7 +323,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do :burnt_fees_percentage, :type, :transaction_fees, - :withdrawals_count + :withdrawals_count, + :is_pending_update ] } |> ChainTypeCustomizations.chain_type_fields() diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex index 8e89f9148989..017e742c2194 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -421,7 +421,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction do description: "Transaction tag set in My Account" }, has_error_in_internal_transactions: %Schema{type: :boolean, nullable: true}, - authorization_list: %Schema{type: :array, items: SignedAuthorization, nullable: true} + authorization_list: %Schema{type: :array, items: SignedAuthorization, nullable: true}, + is_pending_update: %Schema{type: :boolean, nullable: true} }, required: [ :hash, @@ -459,7 +460,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction do :transaction_types, :transaction_tag, :has_error_in_internal_transactions, - :authorization_list + :authorization_list, + :is_pending_update ] } |> ChainTypeCustomizations.chain_type_fields() diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index ae3e30a01ed5..92f5c390bc47 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -70,7 +70,8 @@ defmodule BlockScoutWeb.API.V2.BlockView do "burnt_fees_percentage" => burnt_fees_percentage(block.burnt_fees, block.transactions_fees), "type" => block |> BlockView.block_type() |> String.downcase(), "transaction_fees" => block.transactions_fees, - "withdrawals_count" => count_withdrawals(block) + "withdrawals_count" => count_withdrawals(block), + "is_pending_update" => block.refetch_needed } |> chain_type_fields(block, single_block?) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 0baa1dfdb690..19ef782e459c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -520,7 +520,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "transaction_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_transaction? && conn)), "has_error_in_internal_transactions" => transaction.has_error_in_internal_transactions, - "authorization_list" => authorization_list(transaction.signed_authorizations) + "authorization_list" => authorization_list(transaction.signed_authorizations), + "is_pending_update" => transaction.block && transaction.block.refetch_needed } result diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index cdbd5461871e..a3e0f52f8dbd 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -247,6 +247,37 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert response_2 == response_1 compare_item(block, response_2) end + + test "includes is_pending_update field in response", %{conn: conn} do + block_refetch_needed = insert(:block, refetch_needed: true) + block_no_refetch = insert(:block, refetch_needed: false) + + request_1 = get(conn, "/api/v2/blocks/#{block_refetch_needed.hash}") + assert response_1 = json_response(request_1, 200) + assert response_1["is_pending_update"] == true + + request_2 = get(conn, "/api/v2/blocks/#{block_no_refetch.hash}") + assert response_2 = json_response(request_2, 200) + assert response_2["is_pending_update"] == false + end + + test "includes is_pending_update field in block lists", %{conn: conn} do + block_refetch_needed = insert(:block, refetch_needed: true) + block_no_refetch = insert(:block, refetch_needed: false) + + request = get(conn, "/api/v2/blocks") + assert response = json_response(request, 200) + + # Find the blocks in the response + refetch_block_response = + Enum.find(response["items"], fn item -> item["hash"] == to_string(block_refetch_needed.hash) end) + + no_refetch_block_response = + Enum.find(response["items"], fn item -> item["hash"] == to_string(block_no_refetch.hash) end) + + assert refetch_block_response["is_pending_update"] == true + assert no_refetch_block_response["is_pending_update"] == false + end end describe "/blocks/{block_hash_or_number}/transactions" do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index e60e6724600b..614bc51450cf 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -230,6 +230,57 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do compare_item(transaction, response) end + test "includes is_pending_update field in response", %{conn: conn} do + block_refetch_needed = insert(:block, refetch_needed: true) + block_no_refetch = insert(:block, refetch_needed: false) + + transaction_refetch_needed = + :transaction + |> insert() + |> with_block(block_refetch_needed) + + transaction_no_refetch = + :transaction + |> insert() + |> with_block(block_no_refetch) + + request_1 = get(conn, "/api/v2/transactions/" <> to_string(transaction_refetch_needed.hash)) + assert response_1 = json_response(request_1, 200) + assert response_1["is_pending_update"] == true + + request_2 = get(conn, "/api/v2/transactions/" <> to_string(transaction_no_refetch.hash)) + assert response_2 = json_response(request_2, 200) + assert response_2["is_pending_update"] == false + end + + test "includes is_pending_update field in transaction lists", %{conn: conn} do + block_refetch_needed = insert(:block, refetch_needed: true) + block_no_refetch = insert(:block, refetch_needed: false) + + transaction_refetch_needed = + :transaction + |> insert() + |> with_block(block_refetch_needed) + + transaction_no_refetch = + :transaction + |> insert() + |> with_block(block_no_refetch) + + request = get(conn, "/api/v2/transactions") + assert response = json_response(request, 200) + + # Find the transactions in the response + refetch_tx_response = + Enum.find(response["items"], fn item -> item["hash"] == to_string(transaction_refetch_needed.hash) end) + + no_refetch_tx_response = + Enum.find(response["items"], fn item -> item["hash"] == to_string(transaction_no_refetch.hash) end) + + assert refetch_tx_response["is_pending_update"] == true + assert no_refetch_tx_response["is_pending_update"] == false + end + test "batch 1155 flattened", %{conn: conn} do token = insert(:token, type: "ERC-1155") From 494052a4fe206aee9afa433d492801ea2cf3b49b Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:45:59 +0300 Subject: [PATCH 0275/1006] feat: Add x-api-key header (#13076) --- .../lib/block_scout_web/rate_limit.ex | 22 +++++++++++++------ .../block_scout_web/plug/rate_limit_test.exs | 6 +++++ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex index 9cd9615a3f79..1a51936ec24b 100644 --- a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex +++ b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex @@ -37,8 +37,8 @@ defmodule BlockScoutWeb.RateLimit do user_api_key = get_api_key(conn) - with {:api_key, false} <- {:api_key, has_api_key_param?(conn) && user_api_key == static_api_key}, - {:plan, plan} when plan in [false, nil] <- {:plan, has_api_key_param?(conn) && get_plan(conn.query_params)} do + with {:api_key, false} <- {:api_key, valid_api_key?(user_api_key) && user_api_key == static_api_key}, + {:plan, plan} when plan in [false, nil] <- {:plan, valid_api_key?(user_api_key) && get_plan(conn.query_params)} do ip_result = rate_limit("graphql_#{ip_string}", config[:time_interval_limit_by_ip], config[:limit_by_ip], multiplier) @@ -225,8 +225,9 @@ defmodule BlockScoutWeb.RateLimit do defp rate_limit_by_static_api_key(conn, route_config, default_config, global_config, bucket_key_prefix) do config = config_or_default(route_config, default_config) static_api_key = global_config[:static_api_key_value] + user_api_key = get_api_key(conn) - if has_api_key_param?(conn) && get_api_key(conn) == static_api_key do + if valid_api_key?(user_api_key) && user_api_key == static_api_key do rate_limit(static_api_key, config[:period], config[:limit], config[:cost] || 1, bucket_key_prefix) else :skip @@ -318,8 +319,7 @@ defmodule BlockScoutWeb.RateLimit do defp check_no_rate_limit_api_key(conn, no_rate_limit_api_key) do user_api_key = get_api_key(conn) - has_api_key_param?(conn) && !is_nil(user_api_key) && String.trim(user_api_key) !== "" && - user_api_key == no_rate_limit_api_key + valid_api_key?(user_api_key) && user_api_key == no_rate_limit_api_key end @doc """ @@ -359,9 +359,17 @@ defmodule BlockScoutWeb.RateLimit do end end - defp has_api_key_param?(conn), do: Map.has_key?(conn.query_params, "apikey") + defp valid_api_key?(api_key), do: !is_nil(api_key) && String.trim(api_key) !== "" + + defp get_api_key(conn) do + case Conn.get_req_header(conn, "x-api-key") do + [api_key] -> + api_key - defp get_api_key(conn), do: Map.get(conn.query_params, "apikey") + _ -> + Map.get(conn.query_params, "apikey") + end + end defp get_plan(query_params) do with true <- query_params && Map.has_key?(query_params, "apikey"), diff --git a/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs b/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs index 14f2bc8c3a7b..31ad79c7ccc6 100644 --- a/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs +++ b/apps/block_scout_web/test/block_scout_web/plug/rate_limit_test.exs @@ -343,6 +343,12 @@ defmodule BlockScoutWeb.Plug.RateLimitTest do assert get_resp_header(request, "x-ratelimit-limit") == ["-1"] assert get_resp_header(request, "x-ratelimit-remaining") == ["-1"] assert get_resp_header(request, "x-ratelimit-reset") == ["-1"] + + request = conn |> put_req_header("x-api-key", "123") |> get("/api/v2/transactions") + assert request.status == 200 + assert get_resp_header(request, "x-ratelimit-limit") == ["-1"] + assert get_resp_header(request, "x-ratelimit-remaining") == ["-1"] + assert get_resp_header(request, "x-ratelimit-reset") == ["-1"] end test "enforces rate limit with invalid API key", %{conn: conn} do From c770f72dafbda925aa23d648a0bfa1eb955fd357 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 26 Aug 2025 18:14:13 +0300 Subject: [PATCH 0276/1006] fix: `/api/v2/addresses/:hash/token-transfers` returns 500 on celo (#13050) * fix: `/api/v2/addresses/:hash/token-transfers` returns 500 on celo * fix: credo --- .../api/v2/token_transfer_controller.ex | 4 -- .../schemas/api/v2/token_transfer.ex | 33 ++++++++++-- .../api/v2/address_controller_test.exs | 51 +++++++++++++++---- .../lib/explorer/chain/transaction.ex | 44 ++++++++++++---- 4 files changed, 104 insertions(+), 28 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index a1176bf90bdb..8e57ab2d1b08 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -57,10 +57,6 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do transactions = token_transfers |> Enum.map(& &1.transaction) - # Celo's Epoch logs does not have an associated transaction and linked to - # the block instead, so we discard these token transfers for transaction - # decoding - |> Enum.reject(&is_nil/1) |> Enum.uniq() decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index 9297e9a33a6c..93c88126995f 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -1,17 +1,42 @@ +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TransactionHashCustomization do + @moduledoc false + require OpenApiSpex + alias OpenApiSpex.Schema + + alias BlockScoutWeb.Schemas.API.V2.General + + def schema do + case Application.get_env(:explorer, :chain_type) do + :celo -> + General.FullHashNullable + + _ -> + General.FullHash + end + end +end + defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do @moduledoc """ Schema for token transfer """ require OpenApiSpex - alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token, Token.Type} - alias BlockScoutWeb.Schemas.API.V2.TokenTransfer.{Total, TotalERC1155, TotalERC721} + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + + alias BlockScoutWeb.Schemas.API.V2.TokenTransfer.{ + Total, + TotalERC1155, + TotalERC721, + TransactionHashCustomization + } + alias OpenApiSpex.Schema OpenApiSpex.schema(%{ type: :object, properties: %{ - transaction_hash: General.FullHash, + transaction_hash: TransactionHashCustomization.schema(), from: Address, to: Address, total: %Schema{ @@ -29,7 +54,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do block_hash: General.FullHash, block_number: %Schema{type: :integer, nullable: false}, log_index: %Schema{type: :integer, nullable: false}, - token_type: Type + token_type: Token.Type }, required: [ :transaction_hash, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 944a1cb0a306..6dceec55b491 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do use BlockScoutWeb.ConnCase use EthereumJSONRPC.Case, async: false use BlockScoutWeb.ChannelCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias ABI.{TypeDecoder, TypeEncoder} alias Explorer.{Chain, Repo, TestHelper} @@ -2198,6 +2199,30 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end + + if @chain_type == :celo do + test "get token balance when a token transfer has no transaction", %{conn: conn} do + address = insert(:address) + block = insert(:block) + + token_transfer = + insert(:token_transfer, + from_address: address, + transaction_hash: nil, + block: block, + transaction: nil + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + + compare_item(token_transfer, Enum.at(response["items"], 0), true) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + end end describe "/addresses/{address_hash}/internal-transactions" do @@ -4577,17 +4602,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] end - defp compare_item(%TokenTransfer{} = token_transfer, json) do - assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] - assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] - assert to_string(token_transfer.transaction_hash) == json["transaction_hash"] - assert json["timestamp"] != nil - assert json["method"] != nil - assert to_string(token_transfer.block_hash) == json["block_hash"] - assert token_transfer.log_index == json["log_index"] - assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) - end - defp compare_item(%InternalTransaction{} = internal_transaction, json) do assert internal_transaction.block_number == json["block_number"] assert to_string(internal_transaction.gas) == json["gas_limit"] @@ -4706,6 +4720,21 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } = json end + defp compare_item(%TokenTransfer{} = token_transfer, json, allow_nil_method? \\ false) do + assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"] + assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"] + assert to_string(token_transfer.transaction_hash) == to_string(json["transaction_hash"]) + assert json["timestamp"] != nil + + if not allow_nil_method? do + assert json["method"] != nil + end + + assert to_string(token_transfer.block_hash) == json["block_hash"] + assert token_transfer.log_index == json["log_index"] + assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) + end + defp compare_token_instance_in_collection(%Instance{token: %Token{} = token} = instance, json) do token_type = token.type value = to_string(value(token.type, instance)) diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 9e0b172551fe..fa227232c07e 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -808,6 +808,14 @@ defmodule Explorer.Chain.Transaction do def decoded_input_data(%NotLoaded{}, _, _, _, _), do: {:error, :not_loaded} + if @chain_type == :celo do + # Celo's Epoch logs does not have an associated transaction and linked to + # the block instead, so we discard these token transfers for transaction + # decoding + def decoded_input_data(nil, _, _, _, _), + do: {:error, :celo_epoch_log} + end + # skip decoding if input is empty def decoded_input_data( %__MODULE__{input: %{bytes: bytes}}, @@ -2158,18 +2166,36 @@ defmodule Explorer.Chain.Transaction do # decode remaining transaction using methods map decoded_transactions - |> Enum.map(fn - {nil, transaction} -> - transaction - |> Map.put(:to_address, %NotLoaded{}) - |> decoded_input_data(skip_sig_provider?, opts, methods_map, smart_contract_full_abi_map) - |> format_decoded_input() + |> Enum.map( + &decode_remaining_transaction( + &1, + skip_sig_provider?, + opts, + methods_map, + smart_contract_full_abi_map + ) + ) + end - {decoded, _} -> - decoded - end) + if @chain_type == :celo do + defp decode_remaining_transaction({nil, nil}, _, _, _, _), do: nil + end + + defp decode_remaining_transaction( + {nil, transaction}, + skip_sig_provider?, + opts, + methods_map, + smart_contract_full_abi_map + ) do + transaction + |> Map.put(:to_address, %NotLoaded{}) + |> decoded_input_data(skip_sig_provider?, opts, methods_map, smart_contract_full_abi_map) + |> format_decoded_input() end + defp decode_remaining_transaction({decoded, _}, _, _, _, _), do: decoded + defp combine_smart_contract_full_abi_map(transactions) do # parse unique address hashes of smart-contracts from to_address and created_contract_address properties of the transactions list unique_to_address_hashes = From 0f480cb0c04a35c0a2166cb309aafd1ead405709 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 27 Aug 2025 13:59:24 +0300 Subject: [PATCH 0277/1006] Remove unncessary padding in ga workflow --- .github/workflows/publish-docker-image-for-eth.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index c7978d1528ea..541a11cb0179 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -29,7 +29,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 From 2426c2ce7c7ac3b0dd1c9abe68ebeda51b4b39c7 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:22:07 +0300 Subject: [PATCH 0278/1006] feat: Add possibility to forward event notification to another DB (#13064) * feat: Add possibility to forward event notification to another DB * Fix tests --- apps/block_scout_web/test/test_helper.exs | 1 + apps/explorer/config/dev.exs | 1 + apps/explorer/config/prod.exs | 1 + apps/explorer/config/test.exs | 12 ++++++++ apps/explorer/lib/explorer/application.ex | 10 ++++++- .../lib/explorer/chain/events/db_sender.ex | 6 ++-- .../lib/explorer/chain/events/listener.ex | 9 +++--- apps/explorer/lib/explorer/repo.ex | 1 + .../lib/explorer/repo/config_helper.ex | 2 ++ ...20804114005_create_event_notifications.exs | 9 ++++++ ...4_add_timestamp_to_event_notifications.exs | 11 ++++++++ apps/explorer/test/support/data_case.ex | 2 ++ apps/explorer/test/test_helper.exs | 1 + .../utils/event_notifications_cleaner.ex | 4 +-- .../event_notifications_cleaner_test.exs | 28 +++++++++---------- apps/indexer/test/test_helper.exs | 1 + config/config_helper.exs | 3 +- config/runtime.exs | 3 +- config/runtime/dev.exs | 11 ++++++++ config/runtime/prod.exs | 7 ++++- docker-compose/envs/common-blockscout.env | 1 + 21 files changed, 96 insertions(+), 28 deletions(-) create mode 100644 apps/explorer/priv/event_notifications/migrations/20220804114005_create_event_notifications.exs create mode 100644 apps/explorer/priv/event_notifications/migrations/20250704124014_add_timestamp_to_event_notifications.exs diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index 13d0c3016555..99104f43f864 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -37,6 +37,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.BridgedTokens, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Mud, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.ShrunkInternalTransactions, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, :manual) Absinthe.Test.prime(BlockScoutWeb.GraphQL.Schema) diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index b6a0f56ad222..ef5177a7c822 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -13,6 +13,7 @@ for repo <- [ Explorer.Repo.Account, Explorer.Repo.BridgedTokens, Explorer.Repo.ShrunkInternalTransactions, + Explorer.Repo.EventNotifications, # Chain-type dependent repos Explorer.Repo.Arbitrum, diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index b272b96fb969..848311e6dfc5 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -15,6 +15,7 @@ for repo <- [ Explorer.Repo.Account, Explorer.Repo.BridgedTokens, Explorer.Repo.ShrunkInternalTransactions, + Explorer.Repo.EventNotifications, # Chain-type dependent repos Explorer.Repo.Arbitrum, diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 675668e45d7a..a3b6c11f7f1b 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -20,6 +20,18 @@ config :explorer, Explorer.Repo, migration_lock: nil, log: false +config :explorer, Explorer.Repo.EventNotifications, + database: database, + hostname: hostname, + url: database_url, + pool: Ecto.Adapters.SQL.Sandbox, + # Default of `5_000` was too low for `BlockFetcher` test + ownership_timeout: :timer.minutes(1), + timeout: :timer.seconds(60), + queue_target: 1000, + migration_lock: nil, + log: false + # Configure API database config :explorer, Explorer.Repo.Replica1, database: database, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 088a01980904..41bfc2da89f9 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -310,7 +310,7 @@ defmodule Explorer.Application do ] |> List.flatten() - repos_by_chain_type() ++ account_repo() ++ mud_repo() ++ configurable_children_set + repos_by_chain_type() ++ account_repo() ++ mud_repo() ++ event_notification_repo() ++ configurable_children_set end defp repos_by_chain_type do @@ -355,6 +355,14 @@ defmodule Explorer.Application do end end + defp event_notification_repo do + if Application.get_env(:explorer, :realtime_events_sender) == Explorer.Chain.Events.DBSender || Mix.env() == :test do + [Explorer.Repo.EventNotifications] + else + [] + end + end + defp should_start?({process, _opts}) do Application.get_env(:explorer, process, [])[:enabled] == true end diff --git a/apps/explorer/lib/explorer/chain/events/db_sender.ex b/apps/explorer/lib/explorer/chain/events/db_sender.ex index 186beebec13b..3a341f8b7fea 100644 --- a/apps/explorer/lib/explorer/chain/events/db_sender.ex +++ b/apps/explorer/lib/explorer/chain/events/db_sender.ex @@ -2,7 +2,7 @@ defmodule Explorer.Chain.Events.DBSender do @moduledoc """ Sends events to Postgres. """ - alias Explorer.Repo + alias Explorer.Repo.EventNotifications, as: EventNotificationsRepo alias Explorer.Utility.EventNotification def send_data(event_type) do @@ -25,12 +25,12 @@ defmodule Explorer.Chain.Events.DBSender do end defp send_notify(payload) do - Repo.query!("select pg_notify('chain_event', $1::text);", [payload]) + EventNotificationsRepo.query!("select pg_notify('chain_event', $1::text);", [payload]) end defp save_event_notification(event_data) do event_data |> EventNotification.new_changeset() - |> Repo.insert() + |> EventNotificationsRepo.insert() end end diff --git a/apps/explorer/lib/explorer/chain/events/listener.ex b/apps/explorer/lib/explorer/chain/events/listener.ex index dad47911cd5f..a3a6244bd6d4 100644 --- a/apps/explorer/lib/explorer/chain/events/listener.ex +++ b/apps/explorer/lib/explorer/chain/events/listener.ex @@ -5,8 +5,8 @@ defmodule Explorer.Chain.Events.Listener do use GenServer - alias Explorer.Repo alias Explorer.Repo.ConfigHelper + alias Explorer.Repo.EventNotifications, as: EventNotificationsRepo alias Explorer.Utility.EventNotification alias Postgrex.Notifications @@ -17,7 +17,7 @@ defmodule Explorer.Chain.Events.Listener do def init(channel) do {:ok, pid} = :explorer - |> Application.get_env(Explorer.Repo) + |> Application.get_env(EventNotificationsRepo) |> Keyword.merge(listener_db_parameters()) |> Notifications.start_link() @@ -69,7 +69,7 @@ defmodule Explorer.Chain.Events.Listener do end defp fetch_event_notification(id) do - case Repo.get(EventNotification, id) do + case EventNotificationsRepo.get(EventNotification, id) do nil -> nil @@ -79,8 +79,7 @@ defmodule Explorer.Chain.Events.Listener do end defp listener_db_parameters do - listener_db_url = Application.get_env(:explorer, Repo)[:listener_url] || Application.get_env(:explorer, Repo)[:url] - + listener_db_url = Application.get_env(:explorer, EventNotificationsRepo)[:url] ConfigHelper.extract_parameters(listener_db_url) end end diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex index e90ac8c601dc..17c445d7bca9 100644 --- a/apps/explorer/lib/explorer/repo.ex +++ b/apps/explorer/lib/explorer/repo.ex @@ -134,6 +134,7 @@ defmodule Explorer.Repo do Explorer.Repo.Account, Explorer.Repo.BridgedTokens, Explorer.Repo.ShrunkInternalTransactions, + Explorer.Repo.EventNotifications, # Chain-type dependent repos Explorer.Repo.Arbitrum, diff --git a/apps/explorer/lib/explorer/repo/config_helper.ex b/apps/explorer/lib/explorer/repo/config_helper.ex index bfed70d369c7..a2679e38af80 100644 --- a/apps/explorer/lib/explorer/repo/config_helper.ex +++ b/apps/explorer/lib/explorer/repo/config_helper.ex @@ -34,6 +34,8 @@ defmodule Explorer.Repo.ConfigHelper do def get_mud_db_url, do: System.get_env("MUD_DATABASE_URL") || System.get_env("DATABASE_URL") + def get_event_notification_db_url, do: System.get_env("DATABASE_EVENT_URL") || System.get_env("DATABASE_URL") + def init_repo_module(module, opts) do db_url = Application.get_env(:explorer, module)[:url] repo_conf = Application.get_env(:explorer, module) diff --git a/apps/explorer/priv/event_notifications/migrations/20220804114005_create_event_notifications.exs b/apps/explorer/priv/event_notifications/migrations/20220804114005_create_event_notifications.exs new file mode 100644 index 000000000000..d2dc5abd7a69 --- /dev/null +++ b/apps/explorer/priv/event_notifications/migrations/20220804114005_create_event_notifications.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.EventNotifications.Migrations.CreateEventNotifications do + use Ecto.Migration + + def change do + create table(:event_notifications) do + add(:data, :text, null: false) + end + end +end diff --git a/apps/explorer/priv/event_notifications/migrations/20250704124014_add_timestamp_to_event_notifications.exs b/apps/explorer/priv/event_notifications/migrations/20250704124014_add_timestamp_to_event_notifications.exs new file mode 100644 index 000000000000..a04aa865ae98 --- /dev/null +++ b/apps/explorer/priv/event_notifications/migrations/20250704124014_add_timestamp_to_event_notifications.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.EventNotifications.Migrations.AddTimestampToEventNotifications do + use Ecto.Migration + + def change do + execute("TRUNCATE event_notifications;") + + alter table(:event_notifications) do + timestamps() + end + end +end diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index da18760983cc..0cbb4958a7b1 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -35,10 +35,12 @@ defmodule Explorer.DataCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.EventNotifications) unless tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, {:shared, self()}) end Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index b545e49423fa..24ceddad9e10 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -24,6 +24,7 @@ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Filecoin, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Stability, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Mud, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.ShrunkInternalTransactions, :auto) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, :auto) Mox.defmock(Explorer.Market.Source.TestSource, for: Explorer.Market.Source) Mox.defmock(Explorer.History.TestHistorian, for: Explorer.History.Historian) diff --git a/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex b/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex index f21792ac8a78..3b19e34d1843 100644 --- a/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex +++ b/apps/indexer/lib/indexer/utils/event_notifications_cleaner.ex @@ -3,7 +3,7 @@ defmodule Indexer.Utils.EventNotificationsCleaner do Module is responsible for cleaning up event notifications from the database. """ - alias Explorer.Repo + alias Explorer.Repo.EventNotifications, as: EventNotificationsRepo alias Explorer.Utility.EventNotification import Ecto.Query @@ -31,7 +31,7 @@ defmodule Indexer.Utils.EventNotificationsCleaner do {count, _} = EventNotification |> where([en], en.inserted_at < ago(^max_age(), "millisecond")) - |> Repo.delete_all() + |> EventNotificationsRepo.delete_all() Logger.info("Deleted #{count} event notifications") end diff --git a/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs b/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs index 5fc17cf69114..d1352dbbd012 100644 --- a/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs +++ b/apps/indexer/test/indexer/utils/event_notifications_cleaner_test.exs @@ -1,7 +1,7 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do use Explorer.DataCase - alias Explorer.Repo + alias Explorer.Repo.EventNotifications, as: EventNotificationsRepo alias Explorer.Utility.EventNotification alias Indexer.Utils.EventNotificationsCleaner @@ -33,11 +33,11 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond) new_time = DateTime.utc_now() |> DateTime.add(-500, :millisecond) - insert(:event_notification, data: "old_data", inserted_at: old_time) - insert(:event_notification, data: "new_data", inserted_at: new_time) + build(:event_notification, data: "old_data", inserted_at: old_time) |> EventNotificationsRepo.insert() + build(:event_notification, data: "new_data", inserted_at: new_time) |> EventNotificationsRepo.insert() # Verify both notifications exist - assert Repo.aggregate(EventNotification, :count) == 2 + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 2 # Set configuration for max_age of 1000ms config = Application.get_env(:indexer, EventNotificationsCleaner) @@ -47,8 +47,8 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do Process.sleep(500) # Verify only the old notification was deleted - assert Repo.aggregate(EventNotification, :count) == 1 - remaining = Repo.one(from(n in EventNotification, select: n.data)) + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 1 + remaining = EventNotificationsRepo.one(from(n in EventNotification, select: n.data)) assert remaining == "new_data" end @@ -56,13 +56,13 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do old_time = DateTime.utc_now() |> DateTime.add(-2000, :millisecond) # Insert multiple old notifications - insert_list(3, :event_notification, inserted_at: old_time) + build_list(3, :event_notification, inserted_at: old_time) |> Enum.map(&EventNotificationsRepo.insert(&1)) # Insert one new notification - insert(:event_notification, data: "new_data") + build(:event_notification, data: "new_data") |> EventNotificationsRepo.insert() # Verify all notifications exist - assert Repo.aggregate(EventNotification, :count) == 4 + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 4 # Set configuration config = Application.get_env(:indexer, EventNotificationsCleaner) @@ -72,17 +72,17 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do Process.sleep(500) # Verify only the new notification remains - assert Repo.aggregate(EventNotification, :count) == 1 - remaining = Repo.one(from(n in EventNotification, select: n.data)) + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 1 + remaining = EventNotificationsRepo.one(from(n in EventNotification, select: n.data)) assert remaining == "new_data" end test "does not delete notifications when none are old enough" do # Insert only new notifications - insert_list(3, :event_notification) + build_list(3, :event_notification) |> Enum.map(&EventNotificationsRepo.insert(&1)) # Verify notifications exist - assert Repo.aggregate(EventNotification, :count) == 3 + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 3 # Set configuration config = Application.get_env(:indexer, EventNotificationsCleaner) @@ -92,7 +92,7 @@ defmodule Indexer.Utils.EventNotificationsCleanerTest do Process.sleep(500) # Verify all notifications still exist - assert Repo.aggregate(EventNotification, :count) == 3 + assert EventNotificationsRepo.aggregate(EventNotification, :count) == 3 end end end diff --git a/apps/indexer/test/test_helper.exs b/apps/indexer/test/test_helper.exs index 9024fcf37bc8..a14c7297ce32 100644 --- a/apps/indexer/test/test_helper.exs +++ b/apps/indexer/test/test_helper.exs @@ -23,3 +23,4 @@ ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) ExUnit.start() Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual) +Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, :manual) diff --git a/config/config_helper.exs b/config/config_helper.exs index bf38736b1fd7..5dd1c8c2e767 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -35,7 +35,8 @@ defmodule ConfigHelper do [ {parse_bool_env_var("BRIDGED_TOKENS_ENABLED"), Explorer.Repo.BridgedTokens}, {parse_bool_env_var("MUD_INDEXER_ENABLED"), Explorer.Repo.Mud}, - {parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), Explorer.Repo.ShrunkInternalTransactions} + {parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), Explorer.Repo.ShrunkInternalTransactions}, + {mode() in [:indexer, :api], Explorer.Repo.EventNotifications} ] |> Enum.filter(&elem(&1, 0)) |> Enum.map(&elem(&1, 1)) diff --git a/config/runtime.exs b/config/runtime.exs index 1357c24bf5e5..eb70441c319c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1500,7 +1500,8 @@ config :indexer, Indexer.Fetcher.Scroll.Batch.Supervisor, disabled?: ConfigHelpe config :indexer, Indexer.Utils.EventNotificationsCleaner, interval: ConfigHelper.parse_time_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_INTERVAL", "2m"), - enabled: ConfigHelper.parse_bool_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED", "true"), + enabled: + app_mode == :indexer && ConfigHelper.parse_bool_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED", "true"), max_age: ConfigHelper.parse_time_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE", "5m") config :ex_aws, diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index 28591379ebd7..007996057cb0 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -109,6 +109,17 @@ config :explorer, Explorer.Repo.Suave, url: ExplorerConfigHelper.get_suave_db_url(), pool_size: 1 +database_event_notification = if System.get_env("DATABASE_EVENT_URL"), do: nil, else: database +hostname_event_notification = if System.get_env("DATABASE_EVENT_URL"), do: nil, else: hostname + +# Configure Event Notification database +config :explorer, Explorer.Repo.EventNotifications, + database: database_event_notification, + hostname: hostname_event_notification, + url: ExplorerConfigHelper.get_event_notification_db_url(), + pool_size: ConfigHelper.parse_integer_env_var("DATABASE_EVENT_POOL_SIZE", 10), + queue_target: queue_target + # Actually the following repos are not started, and its pool size remains # unused. Separating repos for different CHAIN_TYPE is implemented only for the # sake of keeping DB schema update relevant to the current chain type diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index a0a161af6c74..1096c999e81a 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -43,7 +43,6 @@ queue_target = ConfigHelper.parse_integer_env_var("DATABASE_QUEUE_TARGET", 50) # Configures the database config :explorer, Explorer.Repo, url: System.get_env("DATABASE_URL"), - listener_url: System.get_env("DATABASE_EVENT_URL"), pool_size: pool_size, ssl: ExplorerConfigHelper.ssl_enabled?(), queue_target: queue_target @@ -75,6 +74,12 @@ config :explorer, Explorer.Repo.Suave, pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() +config :explorer, Explorer.Repo.EventNotifications, + url: ExplorerConfigHelper.get_event_notification_db_url(), + pool_size: ConfigHelper.parse_integer_env_var("DATABASE_EVENT_POOL_SIZE", 10), + ssl: ExplorerConfigHelper.ssl_enabled?(), + queue_target: queue_target + # Actually the following repos are not started, and its pool size remains # unused. Separating repos for different chain type or feature flag is # implemented only for the sake of keeping DB schema update relevant to the diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index c3f4baba4822..627a3b267d52 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -3,6 +3,7 @@ ETHEREUM_JSONRPC_HTTP_URL=http://host.docker.internal:8545/ DISABLE_FILE_LOGGING=false DATABASE_URL=postgresql://blockscout:ceWb1MeLBEeOIfk65gU8EjF8@db:5432/blockscout +# DATABASE_EVENT_POOL_SIZE # DATABASE_EVENT_URL= # DATABASE_QUEUE_TARGET # TEST_DATABASE_URL= From ddd0d724361b7302ce31afb1569e569e575a28e0 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:09:49 +0400 Subject: [PATCH 0279/1006] Add support of Scroll codecv8 (#13090) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- apps/explorer/lib/explorer/chain/scroll/batch.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/scroll/batch.ex b/apps/explorer/lib/explorer/chain/scroll/batch.ex index af560fe4f105..3bd9f1097692 100644 --- a/apps/explorer/lib/explorer/chain/scroll/batch.ex +++ b/apps/explorer/lib/explorer/chain/scroll/batch.ex @@ -21,7 +21,7 @@ defmodule Explorer.Chain.Scroll.Batch do @required_attrs ~w(number commit_transaction_hash commit_block_number commit_timestamp container)a @zstd_magic_number <<0x28, 0xB5, 0x2F, 0xFD>> - @codec_version 7 + @codec_min_version 7 @typedoc """ Descriptor of the batch: @@ -103,7 +103,7 @@ defmodule Explorer.Chain.Scroll.Batch do <> = blob_data_raw # ensure we have correct version, blob envelope size doesn't exceed the raw data size, and compression flag is correct - with {:version_is_supported, true} <- {:version_is_supported, version == @codec_version}, + with {:version_is_supported, true} <- {:version_is_supported, version >= @codec_min_version}, {:size_is_correct, true} <- {:size_is_correct, blob_payload_size + 5 <= byte_size(blob_data_raw)}, {:compression_flag_is_correct, true} <- {:compression_flag_is_correct, is_compressed in [0, 1]}, @@ -113,7 +113,7 @@ defmodule Explorer.Chain.Scroll.Batch do decompressed else {:version_is_supported, false} -> - Logger.error("Codec version #{version} is not supported. Expected: #{@codec_version}.") + Logger.error("Codec version #{version} is not supported. Expected: >=#{@codec_min_version}") nil {:size_is_correct, false} -> From 7b7ec355fde0cbbf08b62473fb668124e252b36b Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:48:32 +0400 Subject: [PATCH 0280/1006] fix: Escape only significant characters in tokens (#13078) --- .../views/smart_contract_view.ex | 16 ++--- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 1 + apps/explorer/lib/explorer/chain/token.ex | 2 +- .../migrator/unescape_quotes_in_tokens.ex | 67 +++++++++++++++++++ .../lib/explorer/smart_contract/helper.ex | 14 ++++ .../unescape_quotes_in_tokens_test.exs | 25 +++++++ 8 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/unescape_quotes_in_tokens.ex create mode 100644 apps/explorer/test/explorer/migrator/unescape_quotes_in_tokens_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex index 2b6624d07d4e..0c099c86766d 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/smart_contract_view.ex @@ -109,22 +109,22 @@ defmodule BlockScoutWeb.SmartContractView do end def values_with_type(value, string, names, index, _components) when string in ["string", :string], - do: render_type_value("string", Helper.sanitize_input(value), fetch_name(names, index)) + do: render_type_value("string", Helper.escape_minimal(value), fetch_name(names, index)) def values_with_type(value, "bytes" <> _ = bytes_type, names, index, _components), - do: render_type_value(bytes_type, Helper.sanitize_input(value), fetch_name(names, index)) + do: render_type_value(bytes_type, Helper.escape_minimal(value), fetch_name(names, index)) def values_with_type(value, bytes, names, index, _components) when bytes in [:bytes], - do: render_type_value("bytes", Helper.sanitize_input(value), fetch_name(names, index)) + do: render_type_value("bytes", Helper.escape_minimal(value), fetch_name(names, index)) def values_with_type(value, bool, names, index, _components) when bool in ["bool", :bool], - do: render_type_value("bool", Helper.sanitize_input(to_string(value)), fetch_name(names, index)) + do: render_type_value("bool", Helper.escape_minimal(to_string(value)), fetch_name(names, index)) def values_with_type(value, type, names, index, _components), - do: render_type_value(type, Helper.sanitize_input(value), fetch_name(names, index)) + do: render_type_value(type, Helper.escape_minimal(value), fetch_name(names, index)) def values_with_type(value, :error, _components), - do: render_type_value("error", Helper.sanitize_input(value), "error") + do: render_type_value("error", Helper.escape_minimal(value), "error") def cast_address(value) do case HashAddress.cast(value) do @@ -166,11 +166,11 @@ defmodule BlockScoutWeb.SmartContractView do end defp render_type_value(type, value, type) do - "
(#{Helper.sanitize_input(type)}) : #{value}
" + "
(#{Helper.escape_minimal(type)}) : #{value}
" end defp render_type_value(type, value, name) do - "
#{Helper.sanitize_input(name)} (#{Helper.sanitize_input(type)}) : #{value}
" + "
#{Helper.escape_minimal(name)} (#{Helper.escape_minimal(type)}) : #{value}
" end defp render_array_type_value(type, values, name) do diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index afd401b39547..a9cbc4b68b74 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -134,7 +134,8 @@ for migrator <- [ Explorer.Migrator.BackfillMetadataURL, Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, Explorer.Migrator.ReindexDuplicatedInternalTransactions, - Explorer.Migrator.MergeAdjacentMissingBlockRanges + Explorer.Migrator.MergeAdjacentMissingBlockRanges, + Explorer.Migrator.UnescapeQuotesInTokens ] do config :explorer, migrator, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index f0ffa6c19f4f..362d7e534302 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -73,6 +73,7 @@ for migrator <- [ Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, Explorer.Migrator.ReindexDuplicatedInternalTransactions, Explorer.Migrator.MergeAdjacentMissingBlockRanges, + Explorer.Migrator.UnescapeQuotesInTokens, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 41bfc2da89f9..ea716298277c 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -191,6 +191,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexDuplicatedInternalTransactions, :indexer), configure_mode_dependent_process(Explorer.Migrator.MergeAdjacentMissingBlockRanges, :indexer), + configure_mode_dependent_process(Explorer.Migrator.UnescapeQuotesInTokens, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 9959c1af760d..b7d8f94e88f6 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -171,7 +171,7 @@ defmodule Explorer.Chain.Token do changeset property -> - put_change(changeset, key, Helper.sanitize_input(property)) + put_change(changeset, key, Helper.escape_minimal(property)) end end diff --git a/apps/explorer/lib/explorer/migrator/unescape_quotes_in_tokens.ex b/apps/explorer/lib/explorer/migrator/unescape_quotes_in_tokens.ex new file mode 100644 index 000000000000..1abd084c6c83 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/unescape_quotes_in_tokens.ex @@ -0,0 +1,67 @@ +defmodule Explorer.Migrator.UnescapeQuotesInTokens do + @moduledoc """ + Unescapes single and double quotes in `name` and `symbol` token fields + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Token + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "unescape_quotes_in_tokens" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> select([t], t.contract_address_hash) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + where(Token, [t], fragment("? ~ E'('|")' or ? ~ E'('|")'", t.name, t.symbol)) + end + + @impl FillingMigration + def update_batch(contract_address_hashes) do + params = + Token + |> where([t], t.contract_address_hash in ^contract_address_hashes) + |> Repo.all() + |> Enum.map(fn token -> + %{ + contract_address_hash: token.contract_address_hash, + name: do_unescape(token.name), + symbol: do_unescape(token.symbol), + type: token.type, + inserted_at: token.inserted_at, + updated_at: token.updated_at + } + end) + + Repo.insert_all(Token, params, on_conflict: {:replace, [:name, :symbol]}, conflict_target: :contract_address_hash) + end + + @impl FillingMigration + def update_cache, do: :ok + + defp do_unescape(nil), do: nil + + defp do_unescape(string) do + string + |> String.replace(""", "\"") + |> String.replace("'", "'") + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index 78470cc7de5f..9b3231443793 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -93,6 +93,20 @@ defmodule Explorer.SmartContract.Helper do |> String.trim() end + @doc """ + Escapes only <, > and & symbols + """ + @spec escape_minimal(any()) :: any() + def escape_minimal(input) when is_binary(input) do + input + # should always be the first to replace + |> String.replace("&", "&") + |> String.replace("<", "<") + |> String.replace(">", ">") + end + + def escape_minimal(other), do: other + def sol_file?(filename) do case List.last(String.split(String.downcase(filename), ".")) do "sol" -> diff --git a/apps/explorer/test/explorer/migrator/unescape_quotes_in_tokens_test.exs b/apps/explorer/test/explorer/migrator/unescape_quotes_in_tokens_test.exs new file mode 100644 index 000000000000..ec550478036f --- /dev/null +++ b/apps/explorer/test/explorer/migrator/unescape_quotes_in_tokens_test.exs @@ -0,0 +1,25 @@ +defmodule Explorer.Migrator.UnescapeQuotesInTokensTest do + use Explorer.DataCase, async: false + + alias Explorer.Migrator.{UnescapeQuotesInTokens, MigrationStatus} + alias Explorer.Repo + + test "Unescapes quotes in tokens" do + escaped_name_token = insert(:token, name: "Smth's") + escaped_symbol_token = insert(:token, symbol: ""Double quoted"") + escaped_both_token = insert(:token, name: "Smth's", symbol: ""Double quoted"") + common_token = :token |> insert() |> Repo.reload() + + assert MigrationStatus.get_status("unescape_quotes_in_tokens") == nil + UnescapeQuotesInTokens.start_link([]) + + Process.sleep(100) + + assert Repo.reload(escaped_name_token).name == "Smth's" + assert Repo.reload(escaped_symbol_token).symbol == "\"Double quoted\"" + assert %{name: "Smth's", symbol: "\"Double quoted\""} = Repo.reload(escaped_both_token) + assert ^common_token = Repo.reload(common_token) + + assert MigrationStatus.get_status("unescape_quotes_in_tokens") == "completed" + end +end From d8f3ea49867779aed33f921f8ad882e5b341881f Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:00:37 +0300 Subject: [PATCH 0281/1006] chore: Bump default rps to 5 (#13089) --- config/runtime.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index eb70441c319c..3159470dbaa2 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -132,11 +132,11 @@ config :block_scout_web, :api_rate_limit, period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_WHITELISTED_IP_TIME_INTERVAL", "1s") }, ip: %{ - limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_IP", 500), - period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_IP_TIME_INTERVAL", "15m") + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_BY_IP", 300), + period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_BY_IP_TIME_INTERVAL", "1m") }, temporary_token: %{ - limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 4), + limit: ConfigHelper.parse_integer_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN", 5), period: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_UI_V2_WITH_TOKEN_TIME_INTERVAL", "1s") }, account_api_key: %{ From cc02251bad47838e5103246b6a9b1fde838d6985 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 27 Aug 2025 17:16:44 +0300 Subject: [PATCH 0282/1006] feat: Add envs to configure RemoteIp lib usage (#13082) --- .../lib/block_scout_web/views/access_helper.ex | 4 ++-- config/runtime.exs | 6 +++++- docker-compose/envs/common-blockscout.env | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex index ba9d7bffb2a1..9e79ef68a706 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex @@ -108,9 +108,9 @@ defmodule BlockScoutWeb.AccessHelper do """ @spec conn_to_ip_string(Plug.Conn.t()) :: String.t() def conn_to_ip_string(conn) do - is_blockscout_behind_proxy = Application.get_env(:block_scout_web, :api_rate_limit)[:is_blockscout_behind_proxy] + config = Application.get_env(:block_scout_web, :remote_ip) - remote_ip_from_headers = is_blockscout_behind_proxy && RemoteIp.from(conn.req_headers) + remote_ip_from_headers = config[:is_blockscout_behind_proxy] && RemoteIp.from(conn.req_headers, config) ip = remote_ip_from_headers || conn.remote_ip to_string(:inet_parse.ntoa(ip)) diff --git a/config/runtime.exs b/config/runtime.exs index 3159470dbaa2..cb20139d0bb1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -144,7 +144,6 @@ config :block_scout_web, :api_rate_limit, }, no_rate_limit_api_key_value: System.get_env("API_NO_RATE_LIMIT_API_KEY"), whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS"), - is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"), api_v2_token_ttl: ConfigHelper.parse_time_env_var("API_RATE_LIMIT_UI_V2_TOKEN_TTL", "30m"), eth_json_rpc_max_batch_size: ConfigHelper.parse_integer_env_var("ETH_JSON_RPC_MAX_BATCH_SIZE", 5), redis_url: if(api_rate_limit_redis_url == "", do: nil, else: api_rate_limit_redis_url), @@ -155,6 +154,11 @@ config :block_scout_web, :api_rate_limit, ), config_url: System.get_env("API_RATE_LIMIT_CONFIG_URL") +config :block_scout_web, :remote_ip, + is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"), + headers: ConfigHelper.parse_list_env_var("API_RATE_LIMIT_REMOTE_IP_HEADERS", "x-forwarded-for"), + proxies: ConfigHelper.parse_list_env_var("API_RATE_LIMIT_REMOTE_IP_KNOWN_PROXIES", "") + default_graphql_rate_limit = 10 config :block_scout_web, Api.GraphQL, diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 627a3b267d52..6aee91659a7f 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -159,6 +159,8 @@ API_V1_WRITE_METHODS_DISABLED=false # API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 # API_RATE_LIMIT_BY_IP=3000 # API_RATE_LIMIT_CONFIG_URL= +# API_RATE_LIMIT_REMOTE_IP_HEADERS= +# API_RATE_LIMIT_REMOTE_IP_KNOWN_PROXIES= # API_NO_RATE_LIMIT_API_KEY= # API_GRAPHQL_ENABLED= # API_GRAPHQL_MAX_COMPLEXITY= From 2e2428756af7768b42cd86a9cf7920d6078581de Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:30:58 +0400 Subject: [PATCH 0283/1006] chore: Replace ReindexDuplicatedInternalTransactions grouping field (#13084) --- .../reindex_duplicated_internal_transactions.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex index d0b656219711..5f6d4c91d238 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex @@ -1,6 +1,6 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do @moduledoc """ - Searches for all blocks that contains internal transactions with duplicated block_hash, transaction_index, index, + Searches for all blocks that contains internal transactions with duplicated block_number, transaction_index, index, deletes all internal transactions for such blocks and adds them to pending operations. """ @@ -43,25 +43,25 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do def unprocessed_data_query do from( it in InternalTransaction, - group_by: [it.block_hash, it.transaction_index, it.index], + group_by: [it.block_number, it.transaction_index, it.index], having: count("*") > 1, - select: it.block_hash + select: it.block_number ) end @impl FillingMigration - def update_batch(block_hashes) do + def update_batch(block_numbers) do now = DateTime.utc_now() result = Repo.transaction(fn -> InternalTransaction - |> where([it], it.block_hash in ^block_hashes) + |> where([it], it.block_number in ^block_numbers) |> Repo.delete_all() pbo_params = Block - |> where([b], b.hash in ^block_hashes) + |> where([b], b.number in ^block_numbers) |> where([b], b.consensus == true) |> select([b], %{block_hash: b.hash, block_number: b.number}) |> Repo.all() From 51cee4c768aced3672a693d59876a775a38f8249 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 28 Aug 2025 12:58:31 +0300 Subject: [PATCH 0284/1006] chore: Remove obsolete circleci config (#13097) --- .circleci/config.yml | 626 ------------------------------------------- 1 file changed, 626 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 03275c1f98df..000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,626 +0,0 @@ -version: 2 -jobs: - build: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3-node-browsers - environment: - MIX_ENV: test - # match POSTGRES_PASSWORD for postgres image below - PGPASSWORD: postgres - # match POSTGRES_USER for postgres image below - PGUSER: postgres - - working_directory: ~/app - - steps: - - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool - - - checkout - - run: - command: ./bin/install_chrome_headless.sh - no_output_timeout: 2400 - - - run: mix local.hex --force - - run: mix local.rebar --force - - - run: - name: "ELIXIR_VERSION.lock" - command: echo "${ELIXIR_VERSION}" > ELIXIR_VERSION.lock - - run: - name: "OTP_VERSION.lock" - command: echo "${OTP_VERSION}" > OTP_VERSION.lock - - - restore_cache: - keys: - - v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }} - - v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }} - - v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }} - - - run: mix deps.get - - - restore_cache: - keys: - - v8-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} - - v8-npm-install-{{ .Branch }} - - v8-npm-install - - - run: - command: npm install - working_directory: "apps/explorer" - - - save_cache: - key: v3-npm-install-{{ .Branch }}-{{ checksum "apps/explorer/package-lock.json" }} - paths: "apps/explorer/node_modules" - - save_cache: - key: v3-npm-install-{{ .Branch }} - paths: "apps/explorer/node_modules" - - save_cache: - key: v3-npm-install - paths: "apps/explorer/node_modules" - - - run: - command: npm install - working_directory: "apps/block_scout_web/assets" - - - save_cache: - key: v8-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }} - paths: "apps/block_scout_web/assets/node_modules" - - save_cache: - key: v8-npm-install-{{ .Branch }} - paths: "apps/block_scout_web/assets/node_modules" - - save_cache: - key: v8-npm-install - paths: "apps/block_scout_web/assets/node_modules" - - - run: mix compile - - - run: - command: make - working_directory: "deps/ex_secp256k1" - - # `deps` needs to be cached with `_build` because `_build` will symlink into `deps` - - - save_cache: - key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }} - paths: - - deps - - _build - - save_cache: - key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }} - paths: - - deps - - _build - - save_cache: - key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }} - paths: - - deps - - _build - - - run: - name: Build assets - command: node node_modules/webpack/bin/webpack.js --mode development - working_directory: "apps/block_scout_web/assets" - - - persist_to_workspace: - root: . - paths: - - .circleci - - .credo.exs - - .dialyzer_ignore.exs - - .formatter.exs - - .git - - .gitignore - - ELIXIR_VERSION.lock - - Gemfile - - Gemfile.lock - - OTP_VERSION.lock - - _build - - apps - - bin - - config - - deps - - doc - - mix.exs - - mix.lock - - appspec.yml - - rel - check_formatted: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix format --check-formatted - credo: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix local.hex --force - - - run: mix credo - deploy_aws: - docker: - # Ensure .tool-versions matches - - image: circleci/python:2.7-stretch - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - add_ssh_keys: - fingerprints: - - "c4:fd:a8:f8:48:a8:09:e5:3e:be:30:62:4d:6f:6f:36" - - - run: - name: Deploy to AWS - command: bin/deploy - dialyzer: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix local.hex --force - - - restore_cache: - keys: - - v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }} - - v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }} - - v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }} - - - run: - name: Unpack PLT cache - command: | - mkdir -p _build/test - cp plts/dialyxir*.plt _build/test/ || true - mkdir -p ~/.mix - cp plts/dialyxir*.plt ~/.mix/ || true - - - run: mix dialyzer --plt - - - run: - name: Pack PLT cache - command: | - mkdir -p plts - cp _build/test/dialyxir*.plt plts/ - cp ~/.mix/dialyxir*.plt plts/ - - - save_cache: - key: v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }} - paths: - - plts - - save_cache: - key: v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }} - paths: - - plts - - save_cache: - key: v8-mix-dialyzer-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }} - paths: - - plts - - - run: mix dialyzer --halt-exit-status - eslint: - docker: - # Ensure .tool-versions matches - - image: circleci/node:12.18.2-browsers-legacy - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: - name: ESLint - command: ./node_modules/.bin/eslint --format=junit --output-file="test/eslint/junit.xml" js/** - working_directory: apps/block_scout_web/assets - - - store_test_results: - path: apps/block_scout_web/assets/test - gettext: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix local.hex --force - - - run: - name: Check for missed translations - command: | - mix gettext.extract --merge | tee stdout.txt - ! grep "Wrote " stdout.txt - working_directory: "apps/block_scout_web" - - - store_artifacts: - path: apps/block_scout_web/priv/gettext - jest: - docker: - # Ensure .tool-versions matches - - image: circleci/node:12.18.2-browsers-legacy - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: - name: Jest - command: ./node_modules/.bin/jest - working_directory: apps/block_scout_web/assets - release: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: prod - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix local.hex --force - - run: mix local.rebar --force - - run: MIX_ENV=prod mix release - - run: - name: Collecting artifacts - command: | - find -name 'blockscout.tar.gz' -exec sh -c 'mkdir -p ci_artifact && cp "$@" ci_artifact/ci_artifact_blockscout.tar.gz' _ {} + - when: always - - - store_artifacts: - name: Uploading CI artifacts - path: ci_artifact/ci_artifact_blockscout.tar.gz - destination: ci_artifact_blockscout.tar.gz - sobelow: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: mix local.hex --force - - - run: - name: Scan explorer for vulnerabilities - command: mix sobelow --config - working_directory: "apps/explorer" - - - run: - name: Scan block_scout_web for vulnerabilities - command: mix sobelow --config - working_directory: "apps/block_scout_web" - # test_geth_http_websocket: - # docker: - # # Ensure .tool-versions matches - # - image: circleci/elixir:1.10.3-node-browsers - # environment: - # MIX_ENV: test - # # match POSTGRES_PASSWORD for postgres image below - # PGPASSWORD: postgres - # # match POSTGRES_USER for postgres image below - # PGUSER: postgres - # ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Geth.HTTPWebSocket" - # ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Geth" - # - image: circleci/postgres:10.10-alpine - # environment: - # # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database - # POSTGRES_DB: explorer_test - # # match PGPASSWORD for elixir image above - # POSTGRES_PASSWORD: postgres - # # match PGUSER for elixir image above - # POSTGRES_USER: postgres - - # working_directory: ~/app - - # steps: - # - attach_workspace: - # at: . - - # - run: - # command: ./bin/install_chrome_headless.sh - # no_output_timeout: 2400 - - # - run: mix local.hex --force - # - run: mix local.rebar --force - - # - run: - # name: Wait for DB - # command: dockerize -wait tcp://localhost:5432 -timeout 1m - - # - run: - # name: mix test --exclude no_geth - # command: | - # # Don't submit coverage report for forks, but let the build succeed - # if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then - # mix coveralls.html --exclude no_geth --parallel --umbrella - # else - # mix coveralls.circle --exclude no_geth --parallel --umbrella || - # # if mix failed, then coveralls_merge won't run, so signal done here and return original exit status - # (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && return $retval) - # fi - - # - store_artifacts: - # path: cover/excoveralls.html - # - store_test_results: - # path: _build/test/junit - # test_geth_mox: - # docker: - # # Ensure .tool-versions matches - # - image: circleci/elixir:1.10.3-node-browsers - # environment: - # MIX_ENV: test - # # match POSTGRES_PASSWORD for postgres image below - # PGPASSWORD: postgres - # # match POSTGRES_USER for postgres image below - # PGUSER: postgres - # ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Geth.Mox" - # ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" - # - image: circleci/postgres:10.10-alpine - # environment: - # # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database - # POSTGRES_DB: explorer_test - # # match PGPASSWORD for elixir image above - # POSTGRES_PASSWORD: postgres - # # match PGUSER for elixir image above - # POSTGRES_USER: postgres - - # working_directory: ~/app - - # steps: - # - attach_workspace: - # at: . - - # - run: - # command: ./bin/install_chrome_headless.sh - # no_output_timeout: 2400 - - # - run: mix local.hex --force - # - run: mix local.rebar --force - - # - run: - # name: Wait for DB - # command: dockerize -wait tcp://localhost:5432 -timeout 1m - - # - run: - # name: mix test --exclude no_geth - # command: | - # # Don't submit coverage report for forks, but let the build succeed - # if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then - # mix coveralls.html --exclude no_geth --parallel --umbrella - # else - # mix coveralls.circle --exclude no_geth --parallel --umbrella || - # # if mix failed, then coveralls_merge won't run, so signal done here and return original exit status - # (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && return $retval) - # fi - - # - store_artifacts: - # path: cover/excoveralls.html - # - store_test_results: - # path: _build/test/junit - # test_nethermind_http_websocket: - # docker: - # # Ensure .tool-versions matches - # - image: circleci/elixir:1.10.3-node-browsers - # environment: - # MIX_ENV: test - # # match POSTGRES_PASSWORD for postgres image below - # PGPASSWORD: postgres - # # match POSTGRES_USER for postgres image below - # PGUSER: postgres - # ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.HTTPWebSocket" - # ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Nethermind" - # - image: circleci/postgres:10.10-alpine - # environment: - # # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database - # POSTGRES_DB: explorer_test - # # match PGPASSWORD for elixir image above - # POSTGRES_PASSWORD: postgres - # # match PGUSER for elixir image above - # POSTGRES_USER: postgres - - # working_directory: ~/app - - # steps: - # - attach_workspace: - # at: . - - # - run: - # command: ./bin/install_chrome_headless.sh - # no_output_timeout: 2400 - - # - run: mix local.hex --force - # - run: mix local.rebar --force - - # - run: - # name: Wait for DB - # command: dockerize -wait tcp://localhost:5432 -timeout 1m - - # - run: - # name: mix test --exclude no_nethermind - # command: | - # # Don't submit coverage report for forks, but let the build succeed - # if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then - # mix coveralls.html --exclude no_nethermind --parallel --umbrella - # else - # mix coveralls.circle --exclude no_nethermind --parallel --umbrella || - # # if mix failed, then coveralls_merge won't run, so signal done here and return original exit status - # (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && return $retval) - # fi - - # - store_artifacts: - # path: cover/excoveralls.html - # - store_test_results: - # path: _build/test/junit - test_nethermind_mox: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3-node-browsers - environment: - MIX_ENV: test - # match POSTGRES_PASSWORD for postgres image below - PGPASSWORD: postgres - # match POSTGRES_USER for postgres image below - PGUSER: postgres - ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox" - ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox" - - image: circleci/postgres:10.10-alpine - environment: - # Match apps/explorer/config/test.exs config :explorer, Explorer.Repo, database - POSTGRES_DB: explorer_test - # match PGPASSWORD for elixir image above - POSTGRES_PASSWORD: postgres - # match PGUSER for elixir image above - POSTGRES_USER: postgres - - working_directory: ~/app - - steps: - - attach_workspace: - at: . - - - run: - command: ./bin/install_chrome_headless.sh - no_output_timeout: 2400 - - - run: mix local.hex --force - - run: mix local.rebar --force - - - run: - name: Wait for DB - command: dockerize -wait tcp://localhost:5432 -timeout 1m - - - run: - name: mix test --exclude no_nethermind - command: | - # Don't submit coverage report for forks, but let the build succeed - if [[ -z "$COVERALLS_REPO_TOKEN" ]]; then - mix coveralls.html --exclude no_nethermind --parallel --umbrella - else - mix coveralls.circle --exclude no_nethermind --parallel --umbrella || - # if mix failed, then coveralls_merge won't run, so signal done here and return original exit status - (retval=$? && curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" && return $retval) - fi - - - store_artifacts: - path: cover/excoveralls.html - - store_test_results: - path: _build/test/junit - coveralls_merge: - docker: - # Ensure .tool-versions matches - - image: circleci/elixir:1.10.3 - environment: - MIX_ENV: test - - steps: - - run: - name: Tell coveralls.io build is done - command: curl -k https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN -d "payload[build_num]=$CIRCLE_WORKFLOW_WORKSPACE_ID&payload[status]=done" -workflows: - version: 2 - primary: - jobs: - - build - - check_formatted: - requires: - - build - # This unfortunately will only fire if all the tests pass because of how `requires` works - - coveralls_merge: - requires: - # - test_nethermind_http_websocket - - test_nethermind_mox - # - test_geth_http_websocket - # - test_geth_mox - - credo: - requires: - - build - - deploy_aws: - filters: - branches: - only: - - production - - staging - - /deploy-[A-Za-z0-9]+$/ - requires: - - check_formatted - - credo - - eslint - - jest - - sobelow - # - test_nethermind_http_websocket - - test_nethermind_mox - # - test_geth_http_websocket - # - test_geth_mox - - dialyzer: - requires: - - build - - eslint: - requires: - - build - - gettext: - requires: - - build - - jest: - requires: - - build - - release: - requires: - - build - - sobelow: - requires: - - build - # - test_nethermind_http_websocket: - # requires: - # - build - - test_nethermind_mox: - requires: - - build - # - test_geth_http_websocket: - # requires: - # - build - # - test_geth_mox: - # requires: - # - build From 5d806c9545be63492aa0c112cca25d45b619133f Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 28 Aug 2025 15:05:22 +0400 Subject: [PATCH 0285/1006] chore: Add final check for ReindexDuplicatedInternalTransactions (#13091) * chore: Add final check for ReindexDuplicatedInternalTransactions * Refactor ReindexDuplicatedInternalTransactions.unprocessed_data_query/1 --- .../explorer/migrator/filling_migration.ex | 3 + ...eindex_duplicated_internal_transactions.ex | 39 +++++++++--- ..._duplicated_internal_transactions_test.exs | 62 +++++++++++++++---- 3 files changed, 84 insertions(+), 20 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/filling_migration.ex b/apps/explorer/lib/explorer/migrator/filling_migration.ex index 506fb1129f5f..c2cf7ecc0584 100644 --- a/apps/explorer/lib/explorer/migrator/filling_migration.ex +++ b/apps/explorer/lib/explorer/migrator/filling_migration.ex @@ -62,6 +62,7 @@ defmodule Explorer.Migrator.FillingMigration do limiting is handled within `last_unprocessed_identifiers/1`. """ @callback unprocessed_data_query :: Ecto.Query.t() | nil + @callback unprocessed_data_query(map()) :: Ecto.Query.t() | nil @doc """ This callback retrieves the next batch of data for migration processing. It returns @@ -153,6 +154,8 @@ defmodule Explorer.Migrator.FillingMigration do """ @callback before_start :: any() + @optional_callbacks unprocessed_data_query: 0, unprocessed_data_query: 1 + defmacro __using__(opts) do quote do @behaviour Explorer.Migrator.FillingMigration diff --git a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex index 5f6d4c91d238..ee6ffdc24970 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex @@ -14,6 +14,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do alias Explorer.Chain.{ Block, + Hash, InternalTransaction, PendingBlockOperation } @@ -31,37 +32,59 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do limit = batch_size() * concurrency() ids = - unprocessed_data_query() + state + |> unprocessed_data_query() |> distinct(true) |> limit(^limit) |> Repo.all(timeout: :infinity) - {ids, state} + case {ids, state["step"]} do + {[], step} when step != "finalize" -> + new_state = %{"step" => "finalize"} + MigrationStatus.update_meta(migration_name(), new_state) + last_unprocessed_identifiers(new_state) + + {ids, _field} -> + {ids, state} + end end @impl FillingMigration - def unprocessed_data_query do + def unprocessed_data_query(state) do + field = + case state["step"] do + "finalize" -> :block_hash + _ -> :block_number + end + from( it in InternalTransaction, - group_by: [it.block_number, it.transaction_index, it.index], + where: not is_nil(field(it, ^field)), + group_by: [field(it, ^field), it.transaction_index, it.index], having: count("*") > 1, - select: it.block_number + select: field(it, ^field) ) end @impl FillingMigration - def update_batch(block_numbers) do + def update_batch(block_numbers_or_hashes) do now = DateTime.utc_now() + {it_field, block_field} = + case block_numbers_or_hashes do + [number | _] when is_integer(number) -> {:block_number, :number} + [%Hash{} | _] -> {:block_hash, :hash} + end + result = Repo.transaction(fn -> InternalTransaction - |> where([it], it.block_number in ^block_numbers) + |> where([it], field(it, ^it_field) in ^block_numbers_or_hashes) |> Repo.delete_all() pbo_params = Block - |> where([b], b.number in ^block_numbers) + |> where([b], field(b, ^block_field) in ^block_numbers_or_hashes) |> where([b], b.consensus == true) |> select([b], %{block_hash: b.hash, block_number: b.number}) |> Repo.all() diff --git a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs index 438f0d410e1f..86b591e9ecc4 100644 --- a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs +++ b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs @@ -6,7 +6,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do alias Explorer.Repo test "Reindex duplicated internal transactions" do - transaction_from_invalid_block = + transaction_from_invalid_block_1 = :transaction |> insert() |> with_block() @@ -14,11 +14,11 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do Enum.each(1..10, fn index -> insert( :internal_transaction, - transaction: transaction_from_invalid_block, + transaction: transaction_from_invalid_block_1, index: index, - block_number: transaction_from_invalid_block.block_number, - transaction_index: transaction_from_invalid_block.index, - block_hash: transaction_from_invalid_block.block_hash, + block_number: transaction_from_invalid_block_1.block_number, + transaction_index: transaction_from_invalid_block_1.index, + block_hash: transaction_from_invalid_block_1.block_hash, block_index: index ) end) @@ -26,11 +26,40 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do Enum.each(11..13, fn index -> insert( :internal_transaction, - transaction: transaction_from_invalid_block, + transaction: transaction_from_invalid_block_1, index: index - 10, - block_number: transaction_from_invalid_block.block_number, - transaction_index: transaction_from_invalid_block.index, - block_hash: transaction_from_invalid_block.block_hash, + block_number: transaction_from_invalid_block_1.block_number, + transaction_index: transaction_from_invalid_block_1.index, + block_hash: transaction_from_invalid_block_1.block_hash, + block_index: index + ) + end) + + transaction_from_invalid_block_2 = + :transaction + |> insert() + |> with_block() + + Enum.each(1..10, fn index -> + insert( + :internal_transaction, + transaction: transaction_from_invalid_block_2, + index: index, + block_number: transaction_from_invalid_block_2.block_number, + transaction_index: transaction_from_invalid_block_2.index, + block_hash: transaction_from_invalid_block_2.block_hash, + block_index: index + ) + end) + + Enum.each(11..13, fn index -> + insert( + :internal_transaction, + transaction: transaction_from_invalid_block_2, + index: index - 10, + block_number: nil, + transaction_index: transaction_from_invalid_block_2.index, + block_hash: transaction_from_invalid_block_2.block_hash, block_index: index ) end) @@ -70,9 +99,18 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do assert it.block_hash == transaction_from_valid_block.block_hash end) - pbo = Repo.one(PendingBlockOperation) + pending_operations = Repo.all(PendingBlockOperation) + + assert Enum.count(pending_operations) == 2 + + assert Enum.any?(pending_operations, fn pbo -> + pbo.block_hash == transaction_from_invalid_block_1.block_hash and + pbo.block_number == transaction_from_invalid_block_1.block_number + end) - assert pbo.block_hash == transaction_from_invalid_block.block_hash - assert pbo.block_number == transaction_from_invalid_block.block_number + assert Enum.any?(pending_operations, fn pbo -> + pbo.block_hash == transaction_from_invalid_block_2.block_hash and + pbo.block_number == transaction_from_invalid_block_2.block_number + end) end end From d8649e9d9faa6ecad2b18261b7af709a57cb9323 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 28 Aug 2025 14:18:13 +0300 Subject: [PATCH 0286/1006] Bump versions of prometheus and prometheus_ex libs (#13096) --- apps/block_scout_web/mix.exs | 5 ++--- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/mix.exs | 5 ++--- apps/indexer/mix.exs | 2 +- mix.exs | 2 +- mix.lock | 5 +++-- 6 files changed, 10 insertions(+), 11 deletions(-) diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 2591b865998a..a2ab8276f7b1 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -127,15 +127,14 @@ defmodule BlockScoutWeb.Mixfile do {:phoenix_live_reload, "~> 1.2", only: [:dev]}, {:phoenix_live_view, "~> 0.17"}, {:phoenix_pubsub, "~> 2.0"}, - {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + {:prometheus_ex, "~> 5.0.0", override: true}, # use `:cowboy` for WebServer with `:plug` {:plug_cowboy, "~> 2.2"}, # Waiting for the Pretty Print to be implemented at the Jason lib # https://github.com/michalmuskala/jason/issues/15 {:poison, "~> 4.0.1"}, {:postgrex, ">= 0.0.0"}, - # For compatibility with `prometheus_process_collector`, which hasn't been updated yet - {:prometheus, "~> 4.0", override: true}, + {:prometheus, "~> 6.0", override: true}, # Gather methods for Phoenix requests {:prometheus_phoenix, "~> 1.2"}, # Expose metrics from URL Prometheus server can scrape diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index fcbe2b1dced4..48c9a92fd8ce 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -69,7 +69,7 @@ defmodule EthereumJSONRPC.MixProject do {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport` and `EthereumJSONRPC.HTTP` so we avoid hitting real chains for local testing {:mox, "~> 1.1.0", only: [:test]}, - {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + {:prometheus_ex, "~> 5.0.0", override: true}, # Tracing {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 806b81b794f4..9824e00ba40d 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -95,11 +95,10 @@ defmodule Explorer.Mixfile do {:poison, "~> 4.0.1"}, {:nimble_csv, "~> 1.1"}, {:postgrex, ">= 0.0.0"}, - # For compatibility with `prometheus_process_collector`, which hasn't been updated yet - {:prometheus, "~> 4.0", override: true}, + {:prometheus, "~> 6.0", override: true}, # Prometheus metrics for query duration {:prometheus_ecto, "~> 1.4.3"}, - {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + {:prometheus_ex, "~> 5.0.0", override: true}, # bypass optional dependency {:plug_cowboy, "~> 2.2", only: [:dev, :test]}, {:que, "~> 0.10.1"}, diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index e2ee18ec3de8..7687d3ac6163 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -64,7 +64,7 @@ defmodule Indexer.MixProject do {:logger_file_backend, "~> 0.0.10"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing {:mox, "~> 1.1.0"}, - {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + {:prometheus_ex, "~> 5.0.0", override: true}, # Tracing {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog diff --git a/mix.exs b/mix.exs index 5262998f30fe..645ee82b35bf 100644 --- a/mix.exs +++ b/mix.exs @@ -94,7 +94,7 @@ defmodule BlockScout.Mixfile do # and cannot be accessed from applications inside the apps folder defp deps do [ - {:prometheus_ex, git: "https://github.com/lanodan/prometheus.ex", branch: "fix/elixir-1.14", override: true}, + {:prometheus_ex, "~> 5.0.0", override: true}, {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, {:tesla, "~> 1.15.3"}, {:mint, "~> 1.7.1"}, diff --git a/mix.lock b/mix.lock index 63ade0804537..69acafb14e0a 100644 --- a/mix.lock +++ b/mix.lock @@ -36,6 +36,7 @@ "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, + "ddskerl": {:hex, :ddskerl, "0.4.2", "a51a90be9ac9b36a94017670bed479c623b10ca9d4bda1edf3a0e48caeeada2a", [:rebar3], [], "hexpm", "63f907373d7e548151d584d4da8a38928fd26ec9477b94c0ffaad87d7cb69fe7"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, @@ -138,9 +139,9 @@ "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, - "prometheus": {:hex, :prometheus, "4.11.0", "b95f8de8530f541bd95951e18e355a840003672e5eda4788c5fa6183406ba29a", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "719862351aabf4df7079b05dc085d2bbcbe3ac0ac3009e956671b1d5ab88247d"}, + "prometheus": {:hex, :prometheus, "6.0.3", "95302236124c0f919163a7762bf7d2b171b919b6ff6148d26eb38a5d2def7b81", [:rebar3], [{:ddskerl, "0.4.2", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "53554ecadac0354066801d514d1a244dd026175e4ee3a9a30192b71d530c8268"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, - "prometheus_ex": {:git, "https://github.com/lanodan/prometheus.ex", "31f7fbe4b71b79ba27efc2a5085746c4011ceb8f", [branch: "fix/elixir-1.14"]}, + "prometheus_ex": {:hex, :prometheus_ex, "5.0.0", "8e5514e385d5408b559792698dee2cfa60faed3b354d2a1046622923ecad4a68", [:mix], [{:prometheus, "~> 6.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "148bf80b110799362152e4bb154b508cba233456a171b5de6fe97e652bc90019"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "prometheus_process_collector": {:git, "https://github.com/Phybbit/prometheus_process_collector.git", "3dc94dcff422d7b9cbd7ddf6bf2a896446705f3f", [ref: "3dc94dcff422d7b9cbd7ddf6bf2a896446705f3f"]}, From 966be7b168d8a5ab20398b6d9eea42500aee9ba4 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 28 Aug 2025 15:23:45 +0300 Subject: [PATCH 0287/1006] Add mix deps.clean --all in CI --- .github/workflows/config.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 7ef15d93c6f8..7f81adfcd609 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -123,6 +123,7 @@ jobs: run: | mix local.hex --force mix local.rebar --force + mix deps.clean --all mix deps.get mix deps.compile --skip-umbrella-children From 4b2535109f0e7a33acc955346a08468391641248 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 29 Aug 2025 12:16:55 +0300 Subject: [PATCH 0288/1006] fix: Fix failing ETH RPC tests (#13099) * fix: Fix failing ETH RPC tests * remove previous implementation --- .../controllers/api/rpc/rpc_translator.ex | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index 365404354e27..931d751e400a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -20,9 +20,52 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.API.RPC.RPCView + + alias BlockScoutWeb.API.RPC.{ + AddressController, + BlockController, + CeloController, + ContractController, + LogsController, + RPCView, + StatsController, + TokenController, + TransactionController + } + alias Phoenix.Controller alias Plug.Conn + @on_load :load_atoms + + @doc """ + Ensures that the specified controller modules are loaded into memory. + + This function iterates over a predefined list of controller modules and calls `Code.ensure_loaded?/1` + on each, which loads the module if it hasn't been loaded yet. This is useful for ensuring that + all necessary controllers are available before performing operations that depend on them. + + Returns `:ok` after attempting to load all modules. + """ + @spec load_atoms() :: :ok + def load_atoms do + Enum.each( + [ + AddressController, + BlockController, + CeloController, + ContractController, + LogsController, + StatsController, + TokenController, + TransactionController + ], + &Code.ensure_loaded?/1 + ) + + :ok + end + def init(opts) do opts end From e58712ecddc66876b9a309f38b3e29e1ce8f43d0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 29 Aug 2025 14:01:03 +0300 Subject: [PATCH 0289/1006] fix: Fix recv timeout option in Universal proxy config (#13046) --- .../third_party_integrations/universal_proxy.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex index 439a60643d9c..d4c7b5cc6eb4 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex @@ -2,7 +2,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @moduledoc """ Module for universal proxying 3rd party API endpoints """ - + require Logger alias Explorer.{Helper, HttpClient} @recv_timeout 60_000 @@ -29,6 +29,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @allowed_methods [:get, :post, :put, :patch, :delete] @reserved_param_types ~w(address chain_id chain_id_dependent) + @unexpected_error "Unexpected error when calling proxied endpoint" @cache_name :universal_proxy_config @@ -105,14 +106,15 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do with {:invalid_config, false} <- {:invalid_config, is_nil(url)}, {:invalid_config, false} <- {:invalid_config, is_nil(method)}, {:ok, %{status_code: status, body: body}} <- - HttpClient.request(method, url, headers, body, timeout: @recv_timeout) do + HttpClient.request(method, url, headers, body, recv_timeout: @recv_timeout) do {Helper.decode_json(body), status} else {:invalid_config, true} -> {"Invalid config: #{error_message}", 422} - _ -> - {"Unexpected error when calling proxied endpoint", 500} + err -> + Logger.error(fn -> ["#{@unexpected_error}: ", inspect(err)] end) + {@unexpected_error, 500} end end From fec84032cd19f0203bcd07922e89c3e85aa110bb Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 29 Aug 2025 14:03:25 +0300 Subject: [PATCH 0290/1006] chore: Update and format pull request template (#13028) [no ci] --- PULL_REQUEST_TEMPLATE.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 442777d478c7..91ea9121b82f 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,32 +1,36 @@ -*[GitHub keywords to close any associated issues](https://blog.github.com/2013-05-14-closing-issues-via-pull-requests/)* +_[GitHub keywords to close any associated issues](https://docs.github.com/en/issues/tracking-your-work-with-issues/closing-issues-using-keywords)_ ## Motivation -*Why we should merge these changes. If using GitHub keywords to close [issues](https://github.com/poanetwork/blockscout/issues), this is optional as the motivation can be read on the issue page.* +_Why we should merge these changes. If using GitHub keywords to close [issues](https://github.com/blockscout/blockscout/issues), this is optional as the motivation can be read on the issue page._ ## Changelog ### Enhancements -*Things you added that don't break anything. Regression tests for Bug Fixes count as Enhancements.* +_Things you added that don't break anything. Regression tests for Bug Fixes count as Enhancements._ ### Bug Fixes -*Things you changed that fix bugs. If it fixes a bug, but in so doing adds a new requirement, removes code, or requires a database reset and reindex, the breaking part of the change should be added to Incompatible Changes below also.* +_Things you changed that fix bugs. If it fixes a bug but, in so doing, adds a new requirement, removes code, or requires a database reset and reindex, the breaking part of the change should also be added to "Incompatible Changes" below._ ### Incompatible Changes -*Things you broke while doing Enhancements and Bug Fixes. Breaking changes include (1) adding new requirements and (2) removing code. Renaming counts as (2) because a rename is a removal followed by an add.* +_Things you broke while doing Enhancements and Bug Fixes. Breaking changes include (1) adding new requirements and (2) removing code. Renaming counts as (2) because a rename is a removal followed by an add._ ## Upgrading -*If you have any Incompatible Changes in the above Changelog, outline how users of prior versions can upgrade once this PR lands or when reviewers are testing locally. A common upgrading step is "Database reset and re-index required".* +_If you have any Incompatible Changes in the above Changelog, outline how users of prior versions can upgrade once this PR lands or when reviewers are testing locally. A common upgrading step is "Database reset and re-index required"._ ## Checklist for your Pull Request (PR) +- [ ] I verified this PR does not break any public APIs, contracts, or interfaces that external consumers depend on. - [ ] If I added new functionality, I added tests covering it. - [ ] If I fixed a bug, I added a regression test to prevent the bug from silently reappearing again. -- [ ] I checked whether I should update the docs and did so by submitting a PR to [docs repository](https://github.com/blockscout/docs). -- [ ] If I added/changed/removed ENV var, I submitted a PR to [docs repository](https://github.com/blockscout/docs) to update the list of [env vars](https://github.com/blockscout/docs/blob/master/setup/env-variables/README.md) and I updated the version to `master` in the Version column. If I removed variable, I added it to [Deprecated ENV Variables](https://github.com/blockscout/docs/blob/master/setup/env-variables/deprecated-env-variables/README.md) page. After merging docs PR, changes will be reflected in these [pages](https://docs.blockscout.com/setup/env-variables). +- [ ] I updated documentation if needed: + - [ ] General docs: submitted PR to [docs repository](https://github.com/blockscout/docs). + - [ ] ENV vars: updated [env vars list](https://github.com/blockscout/docs/tree/main/setup/env-variables) and set version parameter to `master`. + - [ ] Deprecated vars: added to [deprecated env vars list](https://github.com/blockscout/docs/tree/main/setup/env-variables/deprecated-env-variables). +- [ ] If I modified API endpoints, I updated the Swagger/OpenAPI schemas accordingly and checked that schemas are asserted in tests. - [ ] If I added new DB indices, I checked, that they are not redundant, with PGHero or other tools. - [ ] If I added/removed chain type, I modified the Github CI matrix and PR labels accordingly. From 2ab549a61cf4d61f1bb124edad77b04a8bccf03d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 11:40:17 +0300 Subject: [PATCH 0291/1006] fix: Return internal transactions for consensus blocks only in /api/v2/internal-transactions (#13041) * fix: Return internal transactions for consensus blocks only in /api/v2/internal-transactions * Process review comment * Add test * Add missing condition --------- Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- .../explorer/chain/internal_transaction.ex | 14 ++ .../chain/internal_transaction_test.exs | 132 +++++++++++------- 2 files changed, 98 insertions(+), 48 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 8385d3b920b2..def767fe8bae 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -970,6 +970,7 @@ defmodule Explorer.Chain.InternalTransaction do |> where_nonpending_block() |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) |> where_internal_transactions_by_transaction_hash(Keyword.get(options, :transaction_hash)) + |> where_consensus_transactions() |> order_by([internal_transaction], desc: internal_transaction.block_number, desc: internal_transaction.transaction_index, @@ -992,6 +993,19 @@ defmodule Explorer.Chain.InternalTransaction do %{"block_index" => block_index} end + defp where_consensus_transactions(query) do + if DenormalizationHelper.transactions_denormalization_finished?() do + query + |> join(:inner, [internal_transaction], transaction in assoc(internal_transaction, :transaction)) + |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) + |> where([_internal_transaction, transaction], transaction.block_consensus == true) + else + query + |> join(:inner, [internal_transaction], block in assoc(internal_transaction, :block)) + |> where([_internal_transaction, block], block.consensus == true) + end + end + defp where_internal_transactions_by_transaction_hash(query, nil), do: query defp where_internal_transactions_by_transaction_hash(query, transaction_hash) do diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index f82532e9f1de..b35407ad8de5 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,12 +1,10 @@ defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase - alias Explorer.Chain.{Address, Block, Data, InternalTransaction, Transaction, Wei} - alias Explorer.Factory + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction} + alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.PagingOptions - import EthereumJSONRPC, only: [integer_to_quantity: 1] - doctest InternalTransaction describe "changeset/2" do @@ -1090,51 +1088,89 @@ defmodule Explorer.Chain.InternalTransactionTest do end end - defp call_type(opts) do - defaults = [ - type: :call, - call_type: :call, - to_address_hash: Factory.address_hash(), - from_address_hash: Factory.address_hash(), - input: Factory.transaction_input(), - output: Factory.transaction_input(), - gas: Decimal.new(50_000), - gas_used: Decimal.new(25_000), - value: %Wei{value: 100}, - index: 0, - trace_address: [] - ] - - struct!(InternalTransaction, Keyword.merge(defaults, opts)) - end + describe "fetch/1" do + test "with consensus transactions only" do + block_non_consensus = insert(:block, number: 2000, consensus: false) + block_consensus = insert(:block, number: 3000) - defp create_type(opts) do - defaults = [ - type: :create, - from_address_hash: Factory.address_hash(), - gas: Decimal.new(50_000), - gas_used: Decimal.new(25_000), - value: %Wei{value: 100}, - index: 0, - init: Factory.transaction_input(), - trace_address: [] - ] - - struct!(InternalTransaction, Keyword.merge(defaults, opts)) - end + transaction_1 = + :transaction + |> insert( + block_hash: block_non_consensus.hash, + block_number: block_non_consensus.number, + block_consensus: false, + cumulative_gas_used: 100_500, + gas_used: 100_500, + index: 1 + ) + + transaction_2 = + :transaction + |> insert() + |> with_block(block_consensus) + + %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = + insert(:internal_transaction, + index: 1, + transaction: transaction_1, + block_number: transaction_1.block_number, + block_hash: transaction_1.block_hash, + block_index: 1, + transaction_index: transaction_1.index + ) + + %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = + insert(:internal_transaction, + index: 2, + transaction: transaction_2, + block_number: transaction_2.block_number, + block_hash: transaction_2.block_hash, + block_index: 2, + transaction_index: transaction_2.index + ) + + result = + [] + |> InternalTransaction.fetch() + |> Enum.map(&{&1.transaction_hash, &1.index}) + + refute Enum.member?(result, {first_transaction_hash, first_index}) + assert Enum.member?(result, {second_transaction_hash, second_index}) + end + + test "with consensus transactions and blocks only" do + BackgroundMigrations.set_transactions_denormalization_finished(true) + block_non_consensus = insert(:block, number: 2000, consensus: false) + block_consensus = insert(:block, number: 3000) + + transaction = + :transaction + |> insert() + |> with_block(block_consensus) + + insert(:internal_transaction, + index: 1, + transaction: transaction, + block_number: transaction.block_number, + block_hash: block_non_consensus.hash, + block_index: 1, + transaction_index: transaction.index + ) + + consensus_it = + insert(:internal_transaction, + index: 2, + transaction: transaction, + block_number: transaction.block_number, + block_hash: block_consensus.hash, + block_index: 2, + transaction_index: transaction.index + ) - defp selfdestruct_type(opts) do - defaults = [ - type: :selfdestruct, - from_address_hash: Factory.address_hash(), - to_address_hash: Factory.address_hash(), - gas: Decimal.new(50_000), - gas_used: Decimal.new(25_000), - value: %Wei{value: 100}, - index: 0, - trace_address: [] - ] - - struct!(InternalTransaction, Keyword.merge(defaults, opts)) + assert [{consensus_it.transaction_hash, consensus_it.index, consensus_it.block_hash}] == + [] + |> InternalTransaction.fetch() + |> Enum.map(&{&1.transaction_hash, &1.index, &1.block_hash}) + end end end From 11526a62be4e467ceaf49e21e47bbb8a5ee05dc9 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 12:08:28 +0300 Subject: [PATCH 0292/1006] chore: Change release workflow (#13087) * Change release workflow * Process AI agent review --- .github/workflows/pre-release-arbitrum.yml | 9 +- .github/workflows/pre-release-celo.yml | 9 +- .github/workflows/pre-release-eth.yml | 9 +- .github/workflows/pre-release-filecoin.yml | 9 +- .github/workflows/pre-release-fuse.yml | 9 +- .github/workflows/pre-release-optimism.yml | 9 +- .../workflows/pre-release-polygon-zkevm.yml | 9 +- .github/workflows/pre-release-rootstock.yml | 9 +- .github/workflows/pre-release-scroll.yml | 9 +- .github/workflows/pre-release-zilliqa.yml | 9 +- .github/workflows/pre-release-zksync.yml | 9 +- .github/workflows/pre-release.yml | 17 +- .../publish-docker-image-custom-build.yml | 8 +- .../publish-docker-image-every-push.yml | 16 +- .../publish-docker-image-for-arbitrum.yml | 8 +- .../publish-docker-image-for-celo.yml | 8 +- .../publish-docker-image-for-core.yml | 6 +- .../publish-docker-image-for-eth-sepolia.yml | 8 +- .../publish-docker-image-for-eth.yml | 8 +- .../publish-docker-image-for-filecoin.yml | 8 +- .../publish-docker-image-for-fuse.yml | 6 +- .../publish-docker-image-for-gnosis-chain.yml | 8 +- .../publish-docker-image-for-l2-staging.yml | 6 +- .../publish-docker-image-for-lukso.yml | 6 +- .../publish-docker-image-for-optimism.yml | 8 +- .../publish-docker-image-for-rootstock.yml | 6 +- .../publish-docker-image-for-scroll.yml | 8 +- .../publish-docker-image-for-suave.yml | 6 +- .../publish-docker-image-for-zetachain.yml | 6 +- .../publish-docker-image-for-zilliqa.yml | 8 +- .../publish-docker-image-for-zkevm.yml | 8 +- .../publish-docker-image-for-zksync.yml | 8 +- .../workflows/publish-docker-image-old-ui.yml | 4 + ...publish-docker-image-staging-on-demand.yml | 4 + ...publish-regular-docker-image-on-demand.yml | 4 + .github/workflows/release-arbitrum-public.yml | 62 +++++++ .github/workflows/release-arbitrum.yml | 8 +- .github/workflows/release-celo-public.yml | 66 ++++++++ .github/workflows/release-celo.yml | 8 +- ...release.yml => release-default-public.yml} | 6 +- .github/workflows/release-default.yml | 153 ++++++++++++++++++ .github/workflows/release-eth-public.yml | 63 ++++++++ .github/workflows/release-eth.yml | 8 +- .github/workflows/release-filecoin-public.yml | 62 +++++++ .github/workflows/release-filecoin.yml | 8 +- .github/workflows/release-fuse-public.yml | 62 +++++++ .github/workflows/release-fuse.yml | 8 +- .github/workflows/release-gnosis-public.yml | 64 ++++++++ .github/workflows/release-gnosis.yml | 8 +- .github/workflows/release-optimism-public.yml | 62 +++++++ .github/workflows/release-optimism.yml | 8 +- .../release-polygon-zkevm-public.yml | 63 ++++++++ .github/workflows/release-polygon-zkevm.yml | 8 +- .../workflows/release-rootstock-public.yml | 63 ++++++++ .github/workflows/release-rootstock.yml | 8 +- .github/workflows/release-scroll-public.yml | 63 ++++++++ .github/workflows/release-scroll.yml | 8 +- .github/workflows/release-suave-public.yml | 62 +++++++ .github/workflows/release-suave.yml | 8 +- .../workflows/release-zetachain-public.yml | 62 +++++++ .github/workflows/release-zetachain.yml | 8 +- ...interop.yml => release-zilliqa-public.yml} | 30 ++-- .github/workflows/release-zilliqa.yml | 8 +- .github/workflows/release-zksync-public.yml | 62 +++++++ .github/workflows/release-zksync.yml | 8 +- 65 files changed, 1292 insertions(+), 107 deletions(-) create mode 100644 .github/workflows/release-arbitrum-public.yml create mode 100644 .github/workflows/release-celo-public.yml rename .github/workflows/{release.yml => release-default-public.yml} (99%) create mode 100644 .github/workflows/release-default.yml create mode 100644 .github/workflows/release-eth-public.yml create mode 100644 .github/workflows/release-filecoin-public.yml create mode 100644 .github/workflows/release-fuse-public.yml create mode 100644 .github/workflows/release-gnosis-public.yml create mode 100644 .github/workflows/release-optimism-public.yml create mode 100644 .github/workflows/release-polygon-zkevm-public.yml create mode 100644 .github/workflows/release-rootstock-public.yml create mode 100644 .github/workflows/release-scroll-public.yml create mode 100644 .github/workflows/release-suave-public.yml create mode 100644 .github/workflows/release-zetachain-public.yml rename .github/workflows/{publish-docker-image-for-optimism-interop.yml => release-zilliqa-public.yml} (66%) create mode 100644 .github/workflows/release-zksync-public.yml diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 47706fe07027..08850fa2e7d8 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-arbitrum-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-arbitrum-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 9b5a28757b8e..618b895a2c7c 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -35,7 +40,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-celo-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -51,7 +56,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-celo-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index bf39e3471fb0..c252b9ac4271 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-ethereum-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-ethereum-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 27aa458a28e5..df43f328d3c6 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-filecoin-private:latest, ghcr.io/blockscout/blockscout-filecoin-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-filecoin-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index e9d504b07b69..339c21a1a742 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-fuse-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-fuse-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index a96afb338020..a9b539a72565 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-optimism-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-optimism-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 58b4524c3986..75623a5272b6 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-zkevm-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-zkevm-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 0f87f5378e61..20646a6616d9 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-rsk-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-rsk-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index d0a583afe0b0..245b23904566 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-scroll-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-scroll-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 5d3dd4f1e746..76b94ad3fb91 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-zilliqa-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-zilliqa-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 88039594ea2e..688b87f8690a 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,7 +39,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + tags: ghcr.io/blockscout/blockscout-zksync-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +55,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + tags: ghcr.io/blockscout/blockscout-zksync-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index aad6f1a5ee75..0f1c9b198884 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -5,8 +5,13 @@ on: inputs: number: type: number + description: Number of pre-release alpha iteration required: true +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -34,9 +39,9 @@ jobs: context: . file: ./docker/Dockerfile push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:master, ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:master, ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -56,9 +61,9 @@ jobs: context: . file: ./docker/Dockerfile push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 403812ec106a..5c3c1118ccc6 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -5,6 +5,10 @@ on: push: branches: - custom-build +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -28,7 +32,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-postrelease-custom-build-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-postrelease-custom-build-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -43,7 +47,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-postrelease-custom-build-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-postrelease-custom-build-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index baba1dea31e3..500f706f1e4b 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -13,6 +13,10 @@ env: ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} RELEASE_VERSION: 9.0.2 +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -34,9 +38,9 @@ jobs: context: . file: ./docker/Dockerfile push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:master, ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}.commit.${{ env.SHORT_SHA }} + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:master, ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}.commit.${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -56,7 +60,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}.commit.${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}.commit.${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -77,8 +81,8 @@ jobs: context: . file: ./docker/Dockerfile push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - tags: ghcr.io/blockscout/blockscout:frontend-main + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + tags: ghcr.io/blockscout/blockscout-private:frontend-main labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 7bc8264e2b61..a069365f5b64 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -5,6 +5,10 @@ on: push: branches: - production-arbitrum +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 123a29e8ecb2..70b5cc2d1e12 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -5,6 +5,10 @@ on: push: branches: - production-celo +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -30,7 +34,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -47,7 +51,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 092cf4f65b85..fa914c89d97b 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -5,6 +5,10 @@ on: push: branches: - production-core +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 868376081a9f..cc67e63900ba 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -5,6 +5,10 @@ on: push: branches: - production-eth-sepolia +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 541a11cb0179..91951d593695 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -5,6 +5,10 @@ on: push: branches: - production-eth +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 2fef0d922ca2..a0687cb96cac 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -4,6 +4,10 @@ on: push: branches: - production-filecoin +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -28,7 +32,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -44,7 +48,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 337381a9d127..6539f4013090 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -5,6 +5,10 @@ on: push: branches: - production-fuse +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index de42963a91fd..604cbf953b22 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -5,6 +5,10 @@ on: push: branches: - production-xdai +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -46,7 +50,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index a7c70abc36de..2db53086e0d8 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -5,6 +5,10 @@ on: push: branches: - staging-l2 +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 2711e1c5a950..446fb0fdfc90 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -5,6 +5,10 @@ on: push: branches: - production-lukso +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:latest, ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 741d1ba18a0e..f66b89f1a08f 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -5,6 +5,10 @@ on: push: branches: - production-optimism +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 829cad26de16..4ef0a0fde5eb 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -5,6 +5,10 @@ on: push: branches: - production-rsk +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 1b4dce4d300c..c68529f03d22 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -5,6 +5,10 @@ on: push: branches: - production-scroll +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 2fe18e3e5730..fd4db0326b89 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -8,6 +8,10 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index 03de0fa2b02c..c6a560335417 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -5,6 +5,10 @@ on: push: branches: - production-zetachain +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 520bdc15b1e9..69c98f27ed8c 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -5,6 +5,10 @@ on: push: branches: - production-zilliqa +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 63f19bed4257..0766cffb35c1 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -5,6 +5,10 @@ on: push: branches: - production-zkevm +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -29,7 +33,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -45,7 +49,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 3b015a85c4a5..48e564fcce22 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -4,6 +4,10 @@ on: push: branches: - production-zksync +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry @@ -28,7 +32,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -44,7 +48,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 5b5668fcb11a..492e67f9d46c 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -7,6 +7,10 @@ env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 75f7eab0c33d..490b811cb694 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -14,6 +14,10 @@ env: ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} RELEASE_VERSION: 9.0.2 +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 57d3d6b65938..5343b6132bec 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -7,6 +7,10 @@ env: ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} RELEASE_VERSION: 9.0.2 +permissions: + contents: read + packages: write + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry diff --git a/.github/workflows/release-arbitrum-public.yml b/.github/workflows/release-arbitrum-public.yml new file mode 100644 index 000000000000..8574b10209c5 --- /dev/null +++ b/.github/workflows/release-arbitrum-public.yml @@ -0,0 +1,62 @@ +name: Release for Arbitrum + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Arbitrum (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-arbitrum:latest, ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=arbitrum + + - name: Build and push Docker image for Arbitrum (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=arbitrum diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index cf494078ea7d..fdb31507fae1 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:latest, ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-arbitrum-private:latest, ghcr.io/blockscout/blockscout-arbitrum-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-arbitrum-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-celo-public.yml b/.github/workflows/release-celo-public.yml new file mode 100644 index 000000000000..8c8f6a2bb0fe --- /dev/null +++ b/.github/workflows/release-celo-public.yml @@ -0,0 +1,66 @@ +name: Release for Celo + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + API_GRAPHQL_MAX_COMPLEXITY: 10400 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for CELO (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-celo:latest, ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=celo + + - name: Build and push Docker image for CELO (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=celo + diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 6c8534f01cd4..6ca90db50953 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -33,7 +37,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-celo:latest, ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-celo-private:latest, ghcr.io/blockscout/blockscout-celo-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -50,7 +54,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-celo-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release.yml b/.github/workflows/release-default-public.yml similarity index 99% rename from .github/workflows/release.yml rename to .github/workflows/release-default-public.yml index 3352e5c8170c..aa32b4856f01 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release-default-public.yml @@ -2,8 +2,10 @@ name: Release on: workflow_dispatch: - release: - types: [published] + +permissions: + contents: read + packages: write env: OTP_VERSION: ${{ vars.OTP_VERSION }} diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml new file mode 100644 index 000000000000..345353298739 --- /dev/null +++ b/.github/workflows/release-default.yml @@ -0,0 +1,153 @@ +name: Release + +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build & Push Core Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:latest, ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + - name: Build & Push Core Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + + - name: Build & Push Docker image with an old UI (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/oldUI.Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout-private:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout-private:${{ env.RELEASE_VERSION }}-with-old-ui + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + # - name: Send release announcement to Slack workflow + # id: slack + # uses: slackapi/slack-github-action@v1.24.0 + # with: + # payload: | + # { + # "release-version": "${{ env.RELEASE_VERSION }}", + # "release-link": "https://github.com/blockscout/blockscout/releases/tag/v${{ env.RELEASE_VERSION }}" + # } + # env: + # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + # merge-master-after-release: + # name: Merge 'master' to specific branch after release + # runs-on: ubuntu-latest + # env: + # BRANCHES: | + # production-core + # production-sokol + # production-eth-experimental + # production-eth-goerli + # production-lukso + # production-xdai + # production-polygon-supernets + # production-rsk + # production-immutable + # steps: + # - uses: actions/checkout@v5 + # - name: Set Git config + # run: | + # git config --local user.email "actions@github.com" + # git config --local user.name "Github Actions" + # - name: Merge master back after release + # run: | + # git fetch --unshallow + # touch errors.txt + # for branch in $BRANCHES; + # do + # git reset --merge + # git checkout master + # git fetch origin + # echo $branch + # git ls-remote --exit-code --heads origin $branch || { echo $branch >> errors.txt; continue; } + # echo "Merge 'master' to $branch" + # git checkout $branch + # git pull || { echo $branch >> errors.txt; continue; } + # git merge --no-ff master -m "Auto-merge master back to $branch" || { echo $branch >> errors.txt; continue; } + # git push || { echo $branch >> errors.txt; continue; } + # git checkout master; + # done + # [ -s errors.txt ] && echo "There are problems with merging 'master' to branches:" || echo "Errors file is empty" + # cat errors.txt + # [ ! -s errors.txt ] diff --git a/.github/workflows/release-eth-public.yml b/.github/workflows/release-eth-public.yml new file mode 100644 index 000000000000..75f1fc13cad5 --- /dev/null +++ b/.github/workflows/release-eth-public.yml @@ -0,0 +1,63 @@ +name: Release for Ethereum + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Ethereum (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-ethereum:latest, ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum + + - name: Build and push Docker image for Ethereum (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum + diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index be8a11391ae8..aba9de8e8197 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-ethereum:latest, ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-ethereum-private:latest, ghcr.io/blockscout/blockscout-ethereum-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-ethereum-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-filecoin-public.yml b/.github/workflows/release-filecoin-public.yml new file mode 100644 index 000000000000..0c4e3bf34d45 --- /dev/null +++ b/.github/workflows/release-filecoin-public.yml @@ -0,0 +1,62 @@ +name: Release for Filecoin + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Filecoin (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=filecoin + + - name: Build and push Docker image for Filecoin (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=filecoin diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 01b6d8c9c868..c20aa5158819 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-filecoin-private:latest, ghcr.io/blockscout/blockscout-filecoin-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-filecoin-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-fuse-public.yml b/.github/workflows/release-fuse-public.yml new file mode 100644 index 000000000000..f5eb82528bb3 --- /dev/null +++ b/.github/workflows/release-fuse-public.yml @@ -0,0 +1,62 @@ +name: Release for Fuse + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Fuse (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-fuse:latest, ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + + - name: Build and push Docker image for Fuse (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index e608b12560d7..396adef0ab08 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-fuse:latest, ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-fuse-private:latest, ghcr.io/blockscout/blockscout-fuse-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-fuse-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-gnosis-public.yml b/.github/workflows/release-gnosis-public.yml new file mode 100644 index 000000000000..c4ebe9cd8f79 --- /dev/null +++ b/.github/workflows/release-gnosis-public.yml @@ -0,0 +1,64 @@ +name: Release for Gnosis Chain + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Gnosis chain (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai:latest, ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum + + - name: Build and push Docker image for Gnosis chain (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index cff69ef3e487..138f99384be9 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-xdai:latest, ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-xdai-private:latest, ghcr.io/blockscout/blockscout-xdai-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -49,7 +53,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-xdai-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-optimism-public.yml b/.github/workflows/release-optimism-public.yml new file mode 100644 index 000000000000..f40842442aad --- /dev/null +++ b/.github/workflows/release-optimism-public.yml @@ -0,0 +1,62 @@ +name: Release for Optimism + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Optimism (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-optimism:latest, ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + + - name: Build and push Docker image for Optimism (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index f326039b32d4..3721f4940c5f 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-optimism:latest, ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-optimism-private:latest, ghcr.io/blockscout/blockscout-optimism-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-optimism-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-polygon-zkevm-public.yml b/.github/workflows/release-polygon-zkevm-public.yml new file mode 100644 index 000000000000..fd758c76c62e --- /dev/null +++ b/.github/workflows/release-polygon-zkevm-public.yml @@ -0,0 +1,63 @@ +name: Release for Polygon zkEVM + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Polygon zkEVM (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zkevm:latest, ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=polygon_zkevm + + - name: Build and push Docker image for Polygon zkEVM (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=polygon_zkevm + diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index e513a7170fdc..ff3dd0691d48 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zkevm:latest, ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-zkevm-private:latest, ghcr.io/blockscout/blockscout-zkevm-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-zkevm-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-rootstock-public.yml b/.github/workflows/release-rootstock-public.yml new file mode 100644 index 000000000000..ecf0f78cf7e2 --- /dev/null +++ b/.github/workflows/release-rootstock-public.yml @@ -0,0 +1,63 @@ +name: Release for Rootstock + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Rootstock (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-rsk:latest, ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=rsk + + - name: Build and push Docker image for Rootstock (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=rsk + diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 64783682fe21..86d1d39f298e 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-rsk:latest, ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-rsk-private:latest, ghcr.io/blockscout/blockscout-rsk-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-rsk-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-scroll-public.yml b/.github/workflows/release-scroll-public.yml new file mode 100644 index 000000000000..4623624357d9 --- /dev/null +++ b/.github/workflows/release-scroll-public.yml @@ -0,0 +1,63 @@ +name: Release for Scroll + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Scroll (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-scroll:latest, ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=scroll + + - name: Build and push Docker image for Scroll (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=scroll + diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 000767f1242a..cee2698313e1 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-scroll:latest, ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-scroll-private:latest, ghcr.io/blockscout/blockscout-scroll-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-scroll-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-suave-public.yml b/.github/workflows/release-suave-public.yml new file mode 100644 index 000000000000..2ef54a9b7ed5 --- /dev/null +++ b/.github/workflows/release-suave-public.yml @@ -0,0 +1,62 @@ +name: Release for SUAVE + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for SUAVE (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-suave:latest, ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=suave + + - name: Build and push Docker image for SUAVE (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=suave diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index ccf5e2073f40..fffa6e2ac7ab 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-suave:latest, ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-suave-private:latest, ghcr.io/blockscout/blockscout-suave-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-suave-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-zetachain-public.yml b/.github/workflows/release-zetachain-public.yml new file mode 100644 index 000000000000..c71136b321c1 --- /dev/null +++ b/.github/workflows/release-zetachain-public.yml @@ -0,0 +1,62 @@ +name: Release for Zetachain + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Zetachain (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zetachain:latest, ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain + + - name: Build and push Docker image for Zetachain (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 6c985003f58a..e47bae1ff003 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zetachain:latest, ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-zetachain-private:latest, ghcr.io/blockscout/blockscout-zetachain-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-zetachain-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/publish-docker-image-for-optimism-interop.yml b/.github/workflows/release-zilliqa-public.yml similarity index 66% rename from .github/workflows/publish-docker-image-for-optimism-interop.yml rename to .github/workflows/release-zilliqa-public.yml index 29fea3a85b85..7fdfd1a605b9 100644 --- a/.github/workflows/publish-docker-image-for-optimism-interop.yml +++ b/.github/workflows/release-zilliqa-public.yml @@ -1,17 +1,22 @@ -name: Optimism Interop Publish Docker image (temporary) +name: Release for Zilliqa on: workflow_dispatch: - push: - branches: - - production-optimism-interop + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + jobs: push_to_registry: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: RELEASE_VERSION: 9.0.2 - DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 - name: Setup repo @@ -22,22 +27,22 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-interop:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} + tags: ghcr.io/blockscout/blockscout-zilliqa:latest, ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 linux/arm64/v8 build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism + CHAIN_TYPE=zilliqa - name: Build and push Docker image (indexer) uses: docker/build-push-action@v6 @@ -45,14 +50,15 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-interop:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-indexer + tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 linux/arm64/v8 build-args: | DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism + CHAIN_TYPE=zilliqa + diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index a175768db4f0..712de0b19d16 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:latest, ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-zilliqa-private:latest, ghcr.io/blockscout/blockscout-zilliqa-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-zilliqa-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 diff --git a/.github/workflows/release-zksync-public.yml b/.github/workflows/release-zksync-public.yml new file mode 100644 index 000000000000..db9ea101b767 --- /dev/null +++ b/.github/workflows/release-zksync-public.yml @@ -0,0 +1,62 @@ +name: Release for ZkSync + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.0.2 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for ZkSync (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zksync:latest, ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zksync + + - name: Build and push Docker image for ZkSync (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zksync diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 921aa9b489d9..fc8520416eb9 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -5,6 +5,10 @@ on: release: types: [published] +permissions: + contents: read + packages: write + env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} @@ -32,7 +36,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zksync:latest, ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }} + tags: ghcr.io/blockscout/blockscout-zksync-private:latest, ghcr.io/blockscout/blockscout-zksync-private:${{ env.RELEASE_VERSION }} labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 @@ -48,7 +52,7 @@ jobs: context: . file: ./docker/Dockerfile push: true - tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-indexer + tags: ghcr.io/blockscout/blockscout-zksync-private:${{ env.RELEASE_VERSION }}-indexer labels: ${{ steps.setup.outputs.docker-labels }} platforms: | linux/amd64 From 30e6914647623fe5be74d288c2a93dc6337bcdd5 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:05:28 +0300 Subject: [PATCH 0293/1006] fix: Set for read ops in NFT backfillers (#13116) --- .../lib/indexer/fetcher/token_instance/sanitize_erc1155.ex | 2 +- .../lib/indexer/fetcher/token_instance/sanitize_erc721.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex index 4496d1091009..2361f98753d4 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex @@ -43,7 +43,7 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155 do instances_to_fetch = (concurrency * batch_size) |> Instance.not_inserted_erc_1155_token_instances() - |> Repo.all() + |> Repo.all(timeout: :infinity) if Enum.empty?(instances_to_fetch) do MigrationStatus.set_status(@migration_name, "completed") diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex index 2f415507426f..f203f753b6b5 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc721.ex @@ -54,7 +54,7 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721 do address_hashes = state[:tokens_queue_size] |> Token.ordered_erc_721_token_address_hashes_list_query(state[:last_token_address_hash]) - |> Repo.all() + |> Repo.all(timeout: :infinity) if Enum.empty?(address_hashes) do MigrationStatus.set_status(@migration_name, "completed") @@ -81,7 +81,7 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721 do instances_to_fetch = (concurrency * batch_size) |> Instance.not_inserted_token_instances_query_by_token(current_address_hash) - |> Repo.all() + |> Repo.all(timeout: :infinity) if Enum.empty?(instances_to_fetch) do Constants.insert_last_processed_token_address_hash(current_address_hash) From fbdbca4336c44bf185d86ec8561eec344dcd4f0a Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 1 Sep 2025 15:59:01 +0300 Subject: [PATCH 0294/1006] feat: Add `reputation` property where applicable (#13070) * feat: Add is_scam property where applicable * Fix tests * bool -> enum * Add tests and update specs * Fix after rebase * Fix warn * Process review comments --- .dialyzer_ignore.exs | 4 +- .../block_scout_web/schemas/api/v2/address.ex | 8 + .../schemas/api/v2/address/token_balance.ex | 11 +- .../block_scout_web/schemas/api/v2/token.ex | 12 +- .../schemas/api/v2/token_transfer.ex | 12 +- .../views/api/v2/address_view.ex | 8 +- .../views/api/v2/advanced_filter_view.ex | 3 +- .../block_scout_web/views/api/v2/helper.ex | 2 + .../views/api/v2/search_view.ex | 12 +- .../views/api/v2/smart_contract_view.ex | 6 +- .../views/api/v2/token_transfer_view.ex | 3 +- .../views/api/v2/token_view.ex | 6 +- .../api/v2/address_controller_test.exs | 1026 ++++++++++++++++- .../v2/advanced_filter_controller_test.exs | 79 ++ .../api/v2/search_controller_test.exs | 143 ++- .../api/v2/smart_contract_controller_test.exs | 61 + .../api/v2/token_controller_test.exs | 61 + .../api/v2/token_transfer_controller_test.exs | 81 ++ .../api/v2/transaction_controller_test.exs | 254 ++++ apps/explorer/lib/explorer/chain/address.ex | 2 + .../chain/address/current_token_balance.ex | 3 +- .../lib/explorer/chain/address/reputation.ex | 8 + .../lib/explorer/chain/advanced_filter.ex | 17 +- apps/explorer/lib/explorer/chain/search.ex | 89 +- .../lib/explorer/chain/smart_contract.ex | 2 + apps/explorer/lib/explorer/chain/token.ex | 3 +- .../lib/explorer/chain/token/instance.ex | 7 +- .../lib/explorer/chain/token_transfer.ex | 2 + apps/explorer/lib/explorer/helper.ex | 90 +- apps/explorer/test/support/factory.ex | 7 + 30 files changed, 1901 insertions(+), 121 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/address/reputation.ex diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 233b0967a97f..05fec4076945 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -11,6 +11,6 @@ {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 14}, ~r/lib\/phoenix\/router.ex/, {"lib/explorer/chain/search.ex", :pattern_match, 100}, - {"lib/explorer/chain/search.ex", :pattern_match, 283}, - {"lib/explorer/chain/search.ex", :pattern_match, 383} + {"lib/explorer/chain/search.ex", :pattern_match, 282}, + {"lib/explorer/chain/search.ex", :pattern_match, 379} ] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index 7356f79fd070..e17a77798a43 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -53,6 +53,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do alias BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations alias BlockScoutWeb.Schemas.API.V2.{General, Proxy} + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema( @@ -64,6 +65,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do is_contract: %Schema{type: :boolean, description: "Has address contract code?", nullable: true}, name: %Schema{type: :string, description: "Name associated with the address", nullable: true}, is_scam: %Schema{type: :boolean, description: "Has address scam badge?", nullable: false}, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the address", + nullable: false + }, proxy_type: General.ProxyType, implementations: %Schema{ description: "Implementations linked with the contract", @@ -98,6 +105,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do :is_contract, :name, :is_scam, + :reputation, :proxy_type, :implementations, :is_verified, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex index 573d92abe9b7..683487d9bc6a 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do require OpenApiSpex alias BlockScoutWeb.Schemas.API.V2.{General, Token, TokenInstance} + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -13,8 +14,14 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do value: General.IntegerString, token: %Schema{allOf: [Token], nullable: true}, token_id: General.IntegerStringNullable, - token_instance: %Schema{allOf: [TokenInstance], nullable: true} + token_instance: %Schema{allOf: [TokenInstance], nullable: true}, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the token balance", + nullable: true + } }, - required: [:value, :token, :token_id, :token_instance] + required: [:value, :token, :token_id, :token_instance, :reputation] }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index 6827c6f6307d..7120c9c923d3 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -42,6 +42,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do alias BlockScoutWeb.Schemas.API.V2.General alias BlockScoutWeb.Schemas.API.V2.Token.{ChainTypeCustomizations, Type} + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema( @@ -62,7 +63,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do volume_24h: General.FloatStringNullable, total_supply: General.IntegerStringNullable, icon_url: General.URLNullable, - circulating_market_cap: General.FloatStringNullable + circulating_market_cap: General.FloatStringNullable, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the token", + nullable: true + } }, required: [ :address_hash, @@ -75,7 +82,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do :volume_24h, :total_supply, :icon_url, - :circulating_market_cap + :circulating_market_cap, + :reputation ] } |> ChainTypeCustomizations.chain_type_fields() diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index 93c88126995f..aa2b80a3fb08 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -31,6 +31,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do TransactionHashCustomization } + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -54,7 +55,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do block_hash: General.FullHash, block_number: %Schema{type: :integer, nullable: false}, log_index: %Schema{type: :integer, nullable: false}, - token_type: Token.Type + token_type: Token.Type, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the token transfer", + nullable: true + } }, required: [ :transaction_hash, @@ -68,7 +75,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do :block_hash, :block_number, :log_index, - :token_type + :token_type, + :reputation ] }) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index a2da2fe63718..2d51ee5fa34e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -146,7 +146,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do token_balance.address_hash, token_balance ) - ) + ), + "reputation" => token_balance.reputation } end @@ -183,7 +184,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do defp prepare_nft(nft, token) do Map.merge( - %{"token_type" => token.type, "value" => value(token.type, nft)}, + %{"token_type" => token.type, "value" => value(token.type, nft), "reputation" => nft.reputation}, TokenView.prepare_token_instance(nft, token) ) end @@ -195,7 +196,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do "token_instances" => Enum.map(collection.preloaded_token_instances, fn instance -> prepare_nft_for_collection(collection.token.type, instance) - end) + end), + "reputation" => collection.reputation } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index f8985f4ebd53..0e659b5f1de3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -173,7 +173,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do internal_transaction_index: advanced_filter.internal_transaction_index, token_transfer_index: advanced_filter.token_transfer_index, token_transfer_batch_index: advanced_filter.token_transfer_batch_index, - fee: advanced_filter.fee + fee: advanced_filter.fee, + reputation: advanced_filter.reputation } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index c12b5f664617..2c8e71c1eecf 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -82,6 +82,7 @@ defmodule BlockScoutWeb.API.V2.Helper do "is_contract" => smart_contract?, "name" => address_name(address), "is_scam" => address_marked_as_scam?(address), + "reputation" => (address_marked_as_scam?(address) && "scam") || "ok", "proxy_type" => proxy_implementations && proxy_implementations.proxy_type, "implementations" => Proxy.proxy_object_info(proxy_implementations), "is_verified" => smart_contract_verified?(address) || verified_as_proxy?(proxy_implementations), @@ -112,6 +113,7 @@ defmodule BlockScoutWeb.API.V2.Helper do "is_contract" => false, "name" => nil, "is_scam" => false, + "reputation" => "ok", "proxy_type" => nil, "implementations" => [], "is_verified" => nil, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index 27399c7ad9d0..c24b3ff18711 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -43,7 +43,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do search_result.circulating_market_cap && to_string(search_result.circulating_market_cap), "is_verified_via_admin_panel" => search_result.is_verified_via_admin_panel, "certified" => search_result.certified || false, - "priority" => search_result.priority + "priority" => search_result.priority, + "reputation" => search_result.reputation } end @@ -56,7 +57,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), - "priority" => search_result.priority + "priority" => search_result.priority, + "reputation" => search_result.reputation } end @@ -70,7 +72,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), - "priority" => search_result.priority + "priority" => search_result.priority, + "reputation" => search_result.reputation } end @@ -84,7 +87,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), "priority" => search_result.priority, - "metadata" => search_result.metadata + "metadata" => search_result.metadata, + "reputation" => search_result.reputation } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index c5469b1118ad..be39b4852b9f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -304,7 +304,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do defp prepare_smart_contract_address_for_list( %Address{ smart_contract: %SmartContract{} = smart_contract, - token: token + token: token, + reputation: reputation } = address ) do smart_contract_info = @@ -319,7 +320,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "has_constructor_args" => !is_nil(smart_contract.constructor_arguments), "coin_balance" => if(address.fetched_coin_balance, do: address.fetched_coin_balance.value), "license_type" => smart_contract.license_type, - "certified" => if(smart_contract.certified, do: smart_contract.certified, else: false) + "certified" => if(smart_contract.certified, do: smart_contract.certified, else: false), + "reputation" => reputation } smart_contract_info diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex index 7420c4192948..e4c691773b36 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex @@ -60,7 +60,8 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do "block_hash" => to_string(token_transfer.block_hash), "block_number" => token_transfer.block_number, "log_index" => token_transfer.log_index, - "token_type" => token_transfer.token_type + "token_type" => token_transfer.token_type, + "reputation" => token_transfer.reputation } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index d8b10f82716d..e52a04a91613 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -20,7 +20,8 @@ defmodule BlockScoutWeb.API.V2.TokenView do "volume_24h" => nil, "total_supply" => nil, "icon_url" => nil, - "circulating_market_cap" => nil + "circulating_market_cap" => nil, + "reputation" => nil } |> maybe_append_bridged_info(token) end @@ -41,7 +42,8 @@ defmodule BlockScoutWeb.API.V2.TokenView do "volume_24h" => token.volume_24h, "total_supply" => token.total_supply, "icon_url" => token.icon_url, - "circulating_market_cap" => token.circulating_market_cap + "circulating_market_cap" => token.circulating_market_cap, + "reputation" => token.reputation } |> maybe_append_bridged_info(token) |> chain_type_fields(%{address: token.contract_address, field_prefix: nil}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 6dceec55b491..9b0254ea4cf4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -1198,8 +1198,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do from_address = insert(:address) to_address = build(:address) - transaction = - insert(:transaction, from_address: from_address, to_address_hash: to_address.hash, to_address: to_address) + insert(:transaction, from_address: from_address, to_address_hash: to_address.hash, to_address: to_address) Explorer.Repo.delete(to_address) @@ -1291,6 +1290,120 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end describe "/addresses/{address_hash}/token-transfers" do + test "get token transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-transfers") + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") |> json_response(200) + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token transfers with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + insert(:scam_badge_to_address, address_hash: token_transfer.token_contract_address_hash) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-transfers") + + response = json_response(request, 200) + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") + response = json_response(request, 200) + + assert response["items"] == [] + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + token_transfer = + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address + ) + + insert(:scam_badge_to_address, address_hash: token_transfer.token_contract_address_hash) + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + test "get 200 on non existing address", %{conn: conn} do address = build(:address) @@ -2455,6 +2568,90 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end describe "/addresses/{address_hash}/token-balances" do + test "get token balances with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + insert(:address_current_token_balance_with_token_id, address: address) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-balances") + + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + + assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-balances") |> json_response(200) + + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token balances with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + ctb = insert(:address_current_token_balance_with_token_id, address: address) + + insert(:scam_badge_to_address, address_hash: ctb.token_contract_address_hash) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-balances") + + response = json_response(request, 200) + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response)["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") + response = json_response(request, 200) + + assert response == [] + + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) + end + + test "get smart-contract with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + insert(:address_current_token_balance_with_token_id, address: address) + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) + end + + test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + ctb = insert(:address_current_token_balance_with_token_id, address: address) + + insert(:scam_badge_to_address, address_hash: ctb.token_contract_address_hash) + + request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + + assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) + end + test "get empty list on non existing address", %{conn: conn} do address = build(:address) @@ -3119,6 +3316,104 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end describe "/addresses/{address_hash}/tokens" do + test "get token balances with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/addresses/#{address.hash}/tokens") |> json_response(200) + + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token balances with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + ctbs_erc_1155 = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + insert(:scam_badge_to_address, address_hash: ctbs_erc_1155.token_contract_address_hash) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/tokens") + response = json_response(request, 200) + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") + response = json_response(request, 200) + + assert response["items"] == [] + + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token balances with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get token balances with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + ctbs_erc_1155 = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: Enum.random(1..100_000) + ) + + insert(:scam_badge_to_address, address_hash: ctbs_erc_1155.token_contract_address_hash) + + request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + test "get empty list on non existing address", %{conn: conn} do address = build(:address) @@ -4020,69 +4315,378 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end - test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do + test "get token with ok reputation", %{conn: conn, endpoint: endpoint} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + address = insert(:address) - insert_list(51, :token_instance) + # --- ERC-721 --- + erc_721_token = insert(:token, type: "ERC-721") - token_instances = - for _ <- 0..50 do - erc_721_token = insert(:token, type: "ERC-721") + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) - insert(:token_instance, - owner_address_hash: address.hash, - token_contract_address_hash: erc_721_token.contract_address_hash - ) - |> Repo.preload([:token]) - end - # works because one token_id per token, despite ordering in DB: [asc: ti.token_contract_address_hash, desc: ti.token_id] - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + # --- ERC-1155 --- - request = get(conn, endpoint.(address.hash)) - assert response = json_response(request, 200) + token = insert(:token, type: "ERC-1155") - request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) - assert response_2nd_page = json_response(request_2nd_page, 200) + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) - check_paginated_response(response, response_2nd_page, token_instances) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - end + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) - test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do - address = insert(:address) + # --- ERC-404 --- + token = insert(:token, type: "ERC-404") - insert_list(51, :address_current_token_balance_with_token_id) + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) - token_instances = - for _ <- 0..50 do - token = insert(:token, type: "ERC-1155") + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) - ti = - insert(:token_instance, - token_contract_address_hash: token.contract_address_hash - ) - |> Repo.preload([:token]) + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) - current_token_balance = - insert(:address_current_token_balance_with_token_id_and_fixed_token_type, - address: address, - token_type: "ERC-1155", - token_id: ti.token_id, - token_contract_address_hash: token.contract_address_hash - ) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - %Instance{ti | current_token_balance: current_token_balance} - end - |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + assert List.first(response["items"])["reputation"] == "ok" - request = get(conn, endpoint.(address.hash)) - assert response = json_response(request, 200) + assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) |> json_response(200) - request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) - assert response_2nd_page = json_response(request_2nd_page, 200) + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) - check_paginated_response(response, response_2nd_page, token_instances) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) |> json_response(200) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) |> json_response(200) + end + + test "get token with scam reputation", %{conn: conn, endpoint: endpoint} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + address = insert(:address) + # --- ERC-721 --- + erc_721_token = insert(:token, type: "ERC-721") + + insert(:scam_badge_to_address, address_hash: erc_721_token.contract_address_hash) + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + + # --- ERC-1155 --- + token = insert(:token, type: "ERC-1155") + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + # --- ERC-404 --- + token = insert(:token, type: "ERC-404") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert response["items"] == [] + + # --- ERC-1155 --- + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert response["items"] == [] + + # --- ERC-404 --- + request = + conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert response["items"] == [] + end + + test "get token with ok reputation with hide_scam_addresses=false", %{conn: conn, endpoint: endpoint} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + # --- ERC-721 --- + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + + # --- ERC-1155 --- + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + # --- ERC-404 --- + token = insert(:token, type: "ERC-404") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token with scam reputation with hide_scam_addresses=false", %{conn: conn, endpoint: endpoint} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + # --- ERC-721 --- + erc_721_token = insert(:token, type: "ERC-721") + + insert(:scam_badge_to_address, address_hash: erc_721_token.contract_address_hash) + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + + # --- ERC-1155 --- + token = insert(:token, type: "ERC-1155") + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + # --- ERC-404 --- + token = insert(:token, type: "ERC-404") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + + request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) + response = json_response(request, 200) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :token_instance) + + token_instances = + for _ <- 0..50 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + |> Repo.preload([:token]) + end + # works because one token_id per token, despite ordering in DB: [asc: ti.token_contract_address_hash, desc: ti.token_id] + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + insert_list(51, :address_current_token_balance_with_token_id) + + token_instances = + for _ <- 0..50 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: current_token_balance} + end + |> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id}, :desc) + + request = get(conn, endpoint.(address.hash)) + assert response = json_response(request, 200) + + request_2nd_page = get(conn, endpoint.(address.hash), response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, token_instances) assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end @@ -4268,6 +4872,330 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do {:ok, endpoint: &"/api/v2/addresses/#{&1}/nft/collections"} end + test "get nft collections with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + token = insert(:token, type: "ERC-721") + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == + conn + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + |> json_response(200) + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == + conn + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + |> json_response(200) + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get nft collections with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + token = insert(:token, type: "ERC-721") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + token = insert(:token, type: "ERC-1155") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + response = json_response(request, 200) + + assert response["items"] == [] + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + response = json_response(request, 200) + + assert response["items"] == [] + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get smart-contract with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + address = insert(:address) + + token = insert(:token, type: "ERC-721") + + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + token = insert(:token, type: "ERC-1155") + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + address = insert(:address) + + token = insert(:token, type: "ERC-721") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + amount = Enum.random(16..50) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-721", + token_id: nil, + token_contract_address_hash: token.contract_address_hash, + value: amount + ) + |> Repo.preload([:token]) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + token = insert(:token, type: "ERC-1155") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do address = build(:address) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index c3c817c50450..d5224e264e34 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -6,6 +6,85 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do alias Explorer.{Factory, TestHelper} describe "/advanced_filters" do + test "get token-transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: transaction) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == + conn + |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = insert(:transaction) |> with_block() + + tt = insert(:token_transfer, transaction: transaction) + insert(:scam_badge_to_address, address_hash: tt.token_contract_address_hash) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get token-transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = insert(:transaction) |> with_block() + + insert(:token_transfer, transaction: transaction) + + request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = insert(:transaction) |> with_block() + tt = insert(:token_transfer, transaction: transaction) + insert(:scam_badge_to_address, address_hash: tt.token_contract_address_hash) + + request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "empty list", %{conn: conn} do request = get(conn, "/api/v2/advanced-filters") assert response = json_response(request, 200) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 0b00186f24ba..40db80adb2da 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -6,6 +6,76 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do alias Plug.Conn.Query describe "/search" do + test "get token-transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/search?q=#{token.name}") + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/search?q=#{token.name}") |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/search?q=#{token.name}") + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/search?q=#{token.name}") + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get token-transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + request = conn |> get("/api/v2/search?q=#{token.name}") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = conn |> get("/api/v2/search?q=#{token.name}") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "search block", %{conn: conn} do block = insert(:block) @@ -1847,6 +1917,76 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end describe "/search/quick" do + test "get token with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/search/quick?q=#{token.name}") + + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + + assert response == conn |> get("/api/v2/search/quick?q=#{token.name}") |> json_response(200) + end + + test "get token with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/search/quick?q=#{token.name}") + + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "scam" + + request = conn |> get("/api/v2/search/quick?q=#{token.name}") + response = json_response(request, 200) + + assert response == [] + end + + test "get token with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + request = conn |> get("/api/v2/search/quick?q=#{token.name}") + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + end + + test "get token with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:unique_token) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = conn |> get("/api/v2/search/quick?q=#{token.name}") + response = json_response(request, 200) + + assert List.first(response)["reputation"] == "ok" + end + test "check that all categories are in response list", %{conn: conn} do name = "156000" @@ -2304,7 +2444,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "name" => nil, "priority" => 0, "type" => "address", - "url" => "/address/#{address_hash}" + "url" => "/address/#{address_hash}", + "reputation" => "ok" } | for( i <- 0..48, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index cb989999e3e7..012f8c8d1fc1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -1542,6 +1542,67 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do :ok end + test "get smart-contract with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + insert(:smart_contract) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/smart-contracts") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/smart-contracts") |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + target_contract = insert(:smart_contract) + insert(:scam_badge_to_address, address_hash: target_contract.address_hash) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/smart-contracts") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/smart-contracts") + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get smart-contract with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + insert(:smart_contract) + + request = conn |> get("/api/v2/smart-contracts") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + target_contract = insert(:smart_contract) + insert(:scam_badge_to_address, address_hash: target_contract.address_hash) + + request = conn |> get("/api/v2/smart-contracts") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "get [] on empty db", %{conn: conn} do request = get(conn, "/api/v2/smart-contracts") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index da95abc6122b..a7f234ec31bc 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -630,6 +630,67 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do check_paginated_response(response, response_2nd_page, tokens) end + test "get token with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + insert(:token) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/tokens") |> json_response(200) + end + + test "get token with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + target_token = insert(:token) + insert(:scam_badge_to_address, address_hash: target_token.contract_address_hash) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/tokens") + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get token with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + insert(:token) + + request = conn |> get("/api/v2/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + target_token = insert(:token) + insert(:scam_badge_to_address, address_hash: target_token.contract_address_hash) + + request = conn |> get("/api/v2/tokens") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "tokens are filtered by single type", %{conn: conn} do erc_20_tokens = for i <- 0..50 do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs index fe293c2c73b3..61b206db1b6e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs @@ -4,6 +4,87 @@ defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do alias Explorer.Chain.{Address, TokenTransfer} describe "/token-transfers" do + test "get token-transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, transaction: transaction) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/token-transfers") |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = + :transaction + |> insert() + |> with_block() + + tt = insert(:token_transfer, transaction: transaction) + insert(:scam_badge_to_address, address_hash: tt.token_contract_address_hash) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/token-transfers") + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get token-transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, transaction: transaction) + + request = conn |> get("/api/v2/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + transaction = + :transaction + |> insert() + |> with_block() + + tt = insert(:token_transfer, transaction: transaction) + insert(:scam_badge_to_address, address_hash: tt.token_contract_address_hash) + + request = conn |> get("/api/v2/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "empty list", %{conn: conn} do request = get(conn, "/api/v2/token-transfers") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 614bc51450cf..881cc3affa3c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -205,6 +205,129 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end describe "/transactions/{transaction_hash}" do + test "get token-transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/transactions/#{transaction.hash}") + response = json_response(request, 200) + + assert List.first(response["token_transfers"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/transactions/#{transaction.hash}") |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/transactions/#{transaction.hash}") + response = json_response(request, 200) + + assert List.first(response["token_transfers"])["reputation"] == "scam" + + request = conn |> get("/api/v2/transactions/#{transaction.hash}") + response = json_response(request, 200) + + assert response["token_transfers"] == [] + end + + test "get token-transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + request = conn |> get("/api/v2/transactions/#{transaction.hash}") + response = json_response(request, 200) + + assert List.first(response["token_transfers"])["reputation"] == "ok" + end + + test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = conn |> get("/api/v2/transactions/#{transaction.hash}") + response = json_response(request, 200) + + assert List.first(response["token_transfers"])["reputation"] == "ok" + end + test "return 404 on non existing transaction", %{conn: conn} do transaction = build(:transaction) request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}") @@ -643,6 +766,137 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end describe "/transactions/{transaction_hash}/token-transfers" do + test "get token-transfers with ok reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") |> json_response(200) + end + + test "get smart-contract with scam reputation", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") + response = json_response(request, 200) + + assert response["items"] == [] + end + + test "get token-transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + + test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, false) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + token = insert(:token, type: "ERC-1155") + + transaction = + :transaction + |> insert() + |> with_block() + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_ids: Enum.map(0..50, fn x -> x end), + token_type: "ERC-1155", + amounts: Enum.map(0..50, fn x -> x end) + ) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + end + test "return 404 on non existing transaction", %{conn: conn} do transaction = build(:transaction) request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/token-transfers") diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 1d64aea9bf27..67321522aed3 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -9,6 +9,7 @@ defmodule Explorer.Chain.Address.Schema do alias Explorer.Chain.{ Address, + Address.Reputation, Block, Data, Hash, @@ -88,6 +89,7 @@ defmodule Explorer.Chain.Address.Schema do field(:gas_used, :integer) field(:ens_domain_name, :string, virtual: true) field(:metadata, :any, virtual: true) + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) has_one(:smart_contract, SmartContract, references: :hash) has_one(:token, Token, foreign_key: :contract_address_hash, references: :hash) diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index a692f5d7af62..8a06708a069e 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -15,7 +15,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, CurrencyHelper, Hash, Token} - alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Address.{Reputation, TokenBalance} @default_paging_options %PagingOptions{page_size: 50} @@ -40,6 +40,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do field(:distinct_token_instances_count, :integer, virtual: true) field(:token_ids, {:array, :decimal}, virtual: true) field(:preloaded_token_instances, {:array, :any}, virtual: true) + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) # A transient field for deriving token holder count deltas during address_current_token_balances upserts field(:old_value, :decimal) diff --git a/apps/explorer/lib/explorer/chain/address/reputation.ex b/apps/explorer/lib/explorer/chain/address/reputation.ex new file mode 100644 index 000000000000..8eb5d61e089a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/address/reputation.ex @@ -0,0 +1,8 @@ +defmodule Explorer.Chain.Address.Reputation do + @moduledoc """ + This module defines the reputation enum values. + """ + + @enum_values [:ok, :scam] + def enum_values, do: @enum_values +end diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index a5cec7026381..e6090c9c7461 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -10,7 +10,18 @@ defmodule Explorer.Chain.AdvancedFilter do alias Explorer.Helper, as: ExplorerHelper alias Explorer.{Chain, Helper, PagingOptions} - alias Explorer.Chain.{Address, Data, DenormalizationHelper, Hash, InternalTransaction, TokenTransfer, Transaction} + + alias Explorer.Chain.{ + Address, + Address.Reputation, + Data, + DenormalizationHelper, + Hash, + InternalTransaction, + TokenTransfer, + Transaction + } + alias Explorer.Chain.Block.Reader.General, as: BlockGeneralReader @primary_key false @@ -19,6 +30,7 @@ defmodule Explorer.Chain.AdvancedFilter do field(:type, :string) field(:input, Data) field(:timestamp, :utc_datetime_usec) + field(:reputation, Ecto.Enum, values: Reputation.enum_values()) belongs_to( :from_address, @@ -225,7 +237,8 @@ defmodule Explorer.Chain.AdvancedFilter do block_number: token_transfer.block_number, transaction_index: token_transfer.transaction.index, token_transfer_index: token_transfer.log_index, - token_transfer_batch_index: token_transfer.reverse_index_in_batch + token_transfer_batch_index: token_transfer.reverse_index_in_batch, + reputation: token_transfer.reputation } end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index f3fa09592896..bbf815eb486f 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -174,8 +174,7 @@ defmodule Explorer.Chain.Search do defp address_hash_search_if_first_page(%PagingOptions{key: nil}, address_hash, options) do address_hash - |> search_token_by_address_hash_query() - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> search_token_by_address_hash_query(options) |> union_all( ^(address_hash |> search_address_by_address_hash_query()) @@ -311,18 +310,15 @@ defmodule Explorer.Chain.Search do defp search_by_string(term, paging_options, metadata_tags, options) do tokens_query_certified = term - |> search_token_query_certified(paging_options) - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> search_token_query_certified(paging_options, options) tokens_query_not_certified = term - |> search_token_query_not_certified(paging_options) - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> search_token_query_not_certified(paging_options, options) metadata_tags_addresses_query = join_metadata_tags_with_addresses(metadata_tags, options) - contracts_query = - term |> search_contract_query(paging_options) |> ExplorerHelper.maybe_hide_scam_addresses(:address_hash, options) + contracts_query = term |> search_contract_query(paging_options, options) labels_query = search_label_query(term, paging_options) @@ -440,18 +436,15 @@ defmodule Explorer.Chain.Search do tokens_results = (term - |> search_token_query_certified(paging_options) - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> search_token_query_certified(paging_options, options) |> select_repo(options).all()) ++ (term - |> search_token_query_not_certified(paging_options) - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> search_token_query_not_certified(paging_options, options) |> select_repo(options).all()) contracts_results = term - |> search_contract_query(paging_options) - |> ExplorerHelper.maybe_hide_scam_addresses(:address_hash, options) + |> search_contract_query(paging_options, options) |> select_repo(options).all() labels_results = term |> search_label_query(paging_options) |> select_repo(options).all() @@ -534,44 +527,51 @@ defmodule Explorer.Chain.Search do |> page_search_results(paging_options, "label") end - defp search_token_query_not_certified(term, paging_options) do + defp search_token_query_not_certified(term, paging_options, options) do term - |> search_token_by_symbol_or_name_query(paging_options) + |> search_token_by_symbol_or_name_query(paging_options, options) |> where([smart_contract: smart_contract], is_nil(smart_contract.certified) or not smart_contract.certified) end - defp search_token_query_certified(term, paging_options) do + defp search_token_query_certified(term, paging_options, options) do term - |> search_token_by_symbol_or_name_query(paging_options) + |> search_token_by_symbol_or_name_query(paging_options, options) |> where([smart_contract: smart_contract], smart_contract.certified) end - defp search_token_by_symbol_or_name_query(term, paging_options) do + defp search_token_by_symbol_or_name_query(term, paging_options, options) do base_query = from(token in Token, as: :token, left_join: smart_contract in SmartContract, as: :smart_contract, on: token.contract_address_hash == smart_contract.address_hash, - where: fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^term), - select: ^token_search_fields() + where: fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^term) ) - base_query |> apply_sorting([], @token_sorting) |> page_search_results(paging_options, "token") + base_query + |> apply_sorting([], @token_sorting) + |> page_search_results(paging_options, "token") + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:contract_address_hash, options) + |> select(^(token_search_fields() |> add_reputation_to_search_fields(options))) end - defp search_token_by_address_hash_query(address_hash) do - from(token in Token, - as: :token, - left_join: smart_contract in SmartContract, - as: :smart_contract, - on: token.contract_address_hash == smart_contract.address_hash, - where: token.contract_address_hash == ^address_hash, - select: ^token_search_fields() - ) + defp search_token_by_address_hash_query(address_hash, options) do + query = + from(token in Token, + as: :token, + left_join: smart_contract in SmartContract, + as: :smart_contract, + on: token.contract_address_hash == smart_contract.address_hash, + where: token.contract_address_hash == ^address_hash + ) + + query + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:contract_address_hash, options) + |> select(^(token_search_fields() |> add_reputation_to_search_fields(options))) end - defp search_contract_query(term, paging_options) do + defp search_contract_query(term, paging_options, options) do contract_search_fields = search_fields() |> Map.put(:address_hash, dynamic([smart_contract: smart_contract], smart_contract.address_hash)) @@ -581,15 +581,17 @@ defmodule Explorer.Chain.Search do |> Map.put(:certified, dynamic([smart_contract: smart_contract], smart_contract.certified)) |> Map.put(:verified, true) |> Map.put(:priority, 0) + |> add_reputation_to_search_fields(options) base_query = from(smart_contract in SmartContract, as: :smart_contract, - where: fragment("to_tsvector('english', ?) @@ to_tsquery(?)", smart_contract.name, ^term), - select: ^contract_search_fields + where: fragment("to_tsvector('english', ?) @@ to_tsquery(?)", smart_contract.name, ^term) ) base_query + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:address_hash, options) + |> select(^contract_search_fields) |> apply_sorting([], @contract_sorting) |> page_search_results(paging_options, "contract") end @@ -753,8 +755,8 @@ defmodule Explorer.Chain.Search do as: :metadata_tag, on: address.hash == tag.address_hash ) - |> select(^metadata_tags_search_fields()) - |> ExplorerHelper.maybe_hide_scam_addresses(:hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:hash, options) + |> select(^(metadata_tags_search_fields() |> add_reputation_to_search_fields(options))) end defp page_search_results( @@ -1036,7 +1038,8 @@ defmodule Explorer.Chain.Search do is_verified_via_admin_panel: nil, order: 0, metadata: dynamic(type(^nil, :map)), - addresses_index: 0 + addresses_index: 0, + reputation: "ok" } end @@ -1071,6 +1074,18 @@ defmodule Explorer.Chain.Search do |> Map.put(:priority, 1) end + defp add_reputation_to_search_fields(search_fields, options) do + if ExplorerHelper.force_show_scam_addresses?(options) do + search_fields + |> Map.put( + :reputation, + dynamic([sabm: sabm], fragment("CASE WHEN ? THEN ? ELSE ? END", is_nil(sabm.address_hash), "ok", "scam")) + ) + else + search_fields + end + end + @paginated_types ["label", "contract", "token", "metadata_tag", "tac_operation"] defp trim_list_and_prepare_next_page_params( diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 0b4db2173569..25756f320a2c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.SmartContract.Schema do alias Explorer.Chain.{ Address, + Address.Reputation, Hash, SmartContractAdditionalSource } @@ -63,6 +64,7 @@ defmodule Explorer.Chain.SmartContract.Schema do field(:certified, :boolean) field(:is_blueprint, :boolean) field(:language, Ecto.Enum, values: @languages_enum, default: :solidity) + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to( :address, diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index b7d8f94e88f6..ac396c9568ee 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -2,7 +2,7 @@ defmodule Explorer.Chain.Token.Schema do @moduledoc false use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] - alias Explorer.Chain.{Address, Hash} + alias Explorer.Chain.{Address, Address.Reputation, Hash} if @bridged_tokens_enabled do @bridged_field [ @@ -34,6 +34,7 @@ defmodule Explorer.Chain.Token.Schema do field(:is_verified_via_admin_panel, :boolean) field(:volume_24h, :decimal) field(:transfer_count, :integer) + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to( :contract_address, diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index c1587bfc20f0..9d24edf23c30 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Token.Instance do alias Explorer.{Chain, Helper, QueryHelper, Repo} alias Explorer.Chain.{Address, Hash, Token, TokenTransfer, Transaction} - alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Address.{CurrentTokenBalance, Reputation} alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Chain.Token.Instance.Thumbnails alias Explorer.Helper, as: ExplorerHelper @@ -53,6 +53,7 @@ defmodule Explorer.Chain.Token.Instance do field(:cdn_upload_error, :string) field(:metadata_url, :string) field(:skip_metadata_url, :boolean) + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to(:owner, Address, foreign_key: :owner_address_hash, references: :hash, type: Hash.Address) @@ -383,7 +384,6 @@ defmodule Explorer.Chain.Token.Instance do CurrentTokenBalance |> where([ctb], ctb.address_hash == ^address_hash and ctb.value > 0 and ctb.token_type == "ERC-1155") - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> group_by([ctb], ctb.token_contract_address_hash) |> order_by([ctb], asc: ctb.token_contract_address_hash) |> select([ctb], %{ @@ -391,6 +391,7 @@ defmodule Explorer.Chain.Token.Instance do distinct_token_instances_count: fragment("COUNT(*)"), token_ids: fragment("array_agg(?)", ctb.token_id) }) + |> ExplorerHelper.maybe_hide_scam_addresses_with_aggregate(:token_contract_address_hash, options) |> page_erc_1155_nft_collections(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() @@ -416,7 +417,6 @@ defmodule Explorer.Chain.Token.Instance do CurrentTokenBalance |> where([ctb], ctb.address_hash == ^address_hash and not is_nil(ctb.token_id) and ctb.token_type == "ERC-404") - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> group_by([ctb], ctb.token_contract_address_hash) |> order_by([ctb], asc: ctb.token_contract_address_hash) |> select([ctb], %{ @@ -424,6 +424,7 @@ defmodule Explorer.Chain.Token.Instance do distinct_token_instances_count: fragment("COUNT(*)"), token_ids: fragment("array_agg(?)", ctb.token_id) }) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> page_erc_404_nft_collections(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index e06d8c8bf2eb..4d6886f4ae86 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -9,6 +9,7 @@ defmodule Explorer.Chain.TokenTransfer.Schema do alias Explorer.Chain.{ Address, + Address.Reputation, Block, Hash, Transaction @@ -60,6 +61,7 @@ defmodule Explorer.Chain.TokenTransfer.Schema do field(:token_type, :string) field(:block_consensus, :boolean) field(:token_instance, :any, virtual: true) :: Instance.t() | nil + field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to(:from_address, Address, foreign_key: :from_address_hash, diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index d25037b78252..4f23abc8ca2b 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -251,15 +251,93 @@ defmodule Explorer.Helper do def maybe_hide_scam_addresses(nil, _address_hash_key, _options), do: nil def maybe_hide_scam_addresses(query, address_hash_key, options) do - if Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] do - query - |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) - |> where([sabm: sabm], is_nil(sabm.address_hash)) - else - query + cond do + Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> where([sabm: sabm], is_nil(sabm.address_hash)) + |> select_merge([q], %{reputation: "ok"}) + + Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> select_merge([q, sabm: sabm], %{ + reputation: fragment("CASE WHEN ? THEN ? ELSE ? END", is_nil(sabm.address_hash), "ok", "scam") + }) + + true -> + query + |> select_merge([q], %{reputation: "ok"}) + end + end + + @doc """ + Conditionally hides scam addresses in the given query, does not select the reputation field. + """ + @spec maybe_hide_scam_addresses_without_select(nil | Ecto.Query.t(), atom(), [ + Chain.paging_options() | Chain.api?() | Chain.show_scam_tokens?() + ]) :: Ecto.Query.t() + def maybe_hide_scam_addresses_without_select(nil, _address_hash_key, _options), do: nil + + def maybe_hide_scam_addresses_without_select(query, address_hash_key, options) do + cond do + Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> where([sabm: sabm], is_nil(sabm.address_hash)) + + Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + + true -> + query end end + @doc """ + Conditionally hides scam addresses in the given query with aggregate functions. + """ + @spec maybe_hide_scam_addresses_with_aggregate(nil | Ecto.Query.t(), atom(), [ + Chain.paging_options() | Chain.api?() | Chain.show_scam_tokens?() + ]) :: Ecto.Query.t() + def maybe_hide_scam_addresses_with_aggregate(nil, _address_hash_key, _options), do: nil + + def maybe_hide_scam_addresses_with_aggregate(query, address_hash_key, options) do + cond do + Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> where([sabm: sabm], is_nil(sabm.address_hash)) + |> select_merge([q], %{reputation: "ok"}) + + Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> + query + |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) + |> select_merge([q, sabm: sabm], %{ + reputation: + fragment( + "CASE WHEN MAX(CASE WHEN ? THEN 0 ELSE 1 END) = 0 THEN ? ELSE ? END", + is_nil(sabm.address_hash), + "ok", + "scam" + ) + }) + + true -> + query + |> select_merge([q], %{reputation: "ok"}) + end + end + + @doc """ + Function used for identify cases when user explicitly requests to show scam addresses and there are enabled scam addresses in the application. + """ + @spec force_show_scam_addresses?(keyword()) :: boolean() + def force_show_scam_addresses?(options) do + Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] + end + @doc """ Checks if a specified time interval has passed since a given datetime. diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 73aad9512c26..e5ecc5a03601 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -32,6 +32,7 @@ defmodule Explorer.Factory do Address.TokenBalance, Address.CoinBalance, Address.CoinBalanceDaily, + Address.ScamBadgeToAddress, Block, ContractMethod, Data, @@ -1444,4 +1445,10 @@ defmodule Explorer.Factory do inserted_at: DateTime.utc_now() } end + + def scam_badge_to_address_factory do + %ScamBadgeToAddress{ + address_hash: insert(:address).hash + } + end end From e0389190615a9b23288ecaa371d6490f91922778 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 16:18:51 +0300 Subject: [PATCH 0295/1006] 9.1.0 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .github/workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .github/workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-suave.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .github/workflows/publish-docker-image-staging-on-demand.yml | 2 +- .github/workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum-public.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo-public.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default-public.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth-public.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin-public.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse-public.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis-public.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism-public.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm-public.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock-public.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll-public.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave-public.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain-public.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa-public.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync-public.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker-compose/docker-compose.yml | 4 ++-- docker-compose/no-services.yml | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 78 files changed, 79 insertions(+), 79 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 1159bb03fd11..a3b8cb7ac4d6 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 08850fa2e7d8..2fa9b5893b5c 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 618b895a2c7c..2a198ad618e9 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index c252b9ac4271..d185fecf6815 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index df43f328d3c6..8db8e199ab0f 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 339c21a1a742..846b9b2e8299 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index a9b539a72565..5c4d73b49fcc 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 75623a5272b6..d29cfe78d580 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 20646a6616d9..b0445fbca73e 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 245b23904566..fddb88a6c987 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 76b94ad3fb91..70a209655844 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 688b87f8690a..6c307ed3b8d8 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 0f1c9b198884..84157a461218 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 5c3c1118ccc6..2bf407689e3b 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 500f706f1e4b..f45baf845a8a 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index a069365f5b64..4beeb74c2ab1 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 70b5cc2d1e12..f4778db7b069 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index fa914c89d97b..d7343c52ab45 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index cc67e63900ba..ff8fa48e8a2b 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 91951d593695..372060c8dc60 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index a0687cb96cac..1f93285c2273 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 6539f4013090..63276b277b0c 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 604cbf953b22..d160636dffcd 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 2db53086e0d8..2720a06bd5df 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 446fb0fdfc90..353912dec0e5 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index f66b89f1a08f..c6b4d07e5cdb 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 4ef0a0fde5eb..ad12dd7a5c53 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index c68529f03d22..4431a37725fe 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index fd4db0326b89..2f8a7e0509ec 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -17,7 +17,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index c6a560335417..2035625de910 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 69c98f27ed8c..5cc1a8bf62d3 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 0766cffb35c1..8ee44fbc2b51 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 48e564fcce22..010f13a7c02e 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 492e67f9d46c..911e8a30e823 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 490b811cb694..bd89045e48eb 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 5343b6132bec..4f4996f0e9c7 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 permissions: contents: read diff --git a/.github/workflows/release-arbitrum-public.yml b/.github/workflows/release-arbitrum-public.yml index 8574b10209c5..7691e46f40a4 100644 --- a/.github/workflows/release-arbitrum-public.yml +++ b/.github/workflows/release-arbitrum-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index fdb31507fae1..94d22807654a 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo-public.yml b/.github/workflows/release-celo-public.yml index 8c8f6a2bb0fe..2ac51205cf61 100644 --- a/.github/workflows/release-celo-public.yml +++ b/.github/workflows/release-celo-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 6ca90db50953..dcd3b1cec778 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default-public.yml b/.github/workflows/release-default-public.yml index aa32b4856f01..4672abe82000 100644 --- a/.github/workflows/release-default-public.yml +++ b/.github/workflows/release-default-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 345353298739..6f65f1295e90 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth-public.yml b/.github/workflows/release-eth-public.yml index 75f1fc13cad5..6182552f732e 100644 --- a/.github/workflows/release-eth-public.yml +++ b/.github/workflows/release-eth-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index aba9de8e8197..7e0b07664c95 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin-public.yml b/.github/workflows/release-filecoin-public.yml index 0c4e3bf34d45..aa50be28abfb 100644 --- a/.github/workflows/release-filecoin-public.yml +++ b/.github/workflows/release-filecoin-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index c20aa5158819..18b31b794483 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse-public.yml b/.github/workflows/release-fuse-public.yml index f5eb82528bb3..1753b065c15c 100644 --- a/.github/workflows/release-fuse-public.yml +++ b/.github/workflows/release-fuse-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 396adef0ab08..de19190fa275 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis-public.yml b/.github/workflows/release-gnosis-public.yml index c4ebe9cd8f79..f7ecf2e11994 100644 --- a/.github/workflows/release-gnosis-public.yml +++ b/.github/workflows/release-gnosis-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 138f99384be9..9ca4c5edc812 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism-public.yml b/.github/workflows/release-optimism-public.yml index f40842442aad..e1a233f5a453 100644 --- a/.github/workflows/release-optimism-public.yml +++ b/.github/workflows/release-optimism-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 3721f4940c5f..ac0adfc45259 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm-public.yml b/.github/workflows/release-polygon-zkevm-public.yml index fd758c76c62e..daf4d7cfaa77 100644 --- a/.github/workflows/release-polygon-zkevm-public.yml +++ b/.github/workflows/release-polygon-zkevm-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index ff3dd0691d48..e8d338678d98 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock-public.yml b/.github/workflows/release-rootstock-public.yml index ecf0f78cf7e2..a26824665ffe 100644 --- a/.github/workflows/release-rootstock-public.yml +++ b/.github/workflows/release-rootstock-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 86d1d39f298e..409dff0afaa4 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll-public.yml b/.github/workflows/release-scroll-public.yml index 4623624357d9..716b58580bd6 100644 --- a/.github/workflows/release-scroll-public.yml +++ b/.github/workflows/release-scroll-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index cee2698313e1..f054013c8c78 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-suave-public.yml b/.github/workflows/release-suave-public.yml index 2ef54a9b7ed5..d2db1392bd6c 100644 --- a/.github/workflows/release-suave-public.yml +++ b/.github/workflows/release-suave-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index fffa6e2ac7ab..3c7c125feb1d 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain-public.yml b/.github/workflows/release-zetachain-public.yml index c71136b321c1..e8fe4bd49697 100644 --- a/.github/workflows/release-zetachain-public.yml +++ b/.github/workflows/release-zetachain-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index e47bae1ff003..bd401f9d5544 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa-public.yml b/.github/workflows/release-zilliqa-public.yml index 7fdfd1a605b9..f9aa9112c332 100644 --- a/.github/workflows/release-zilliqa-public.yml +++ b/.github/workflows/release-zilliqa-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 712de0b19d16..9dc71c98f1f6 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync-public.yml b/.github/workflows/release-zksync-public.yml index db9ea101b767..cf0aada5e694 100644 --- a/.github/workflows/release-zksync-public.yml +++ b/.github/workflows/release-zksync-public.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index fc8520416eb9..2ed8acb4f8a7 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index a2ab8276f7b1..dfbb0cc270c4 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.2", + version: "9.1.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 48c9a92fd8ce..39a8d22e539e 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.2" + version: "9.1.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 7d0b0b7ab557..ff15b049b5da 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.0.2"}] + @default_headers [{"User-Agent", "blockscout-9.1.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 9824e00ba40d..697ec2bb4519 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.0.2", + version: "9.1.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 7687d3ac6163..7734655ada05 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.0.2", + version: "9.1.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 36eec9350335..0386675c2aaf 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.0.2", + version: "9.1.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 9818e0db811b..30dab7424acd 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.0.2", + version: "9.1.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 0babc774018b..cf363b963cba 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index 99b1ce18a614..11d5703479c0 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 9.0.2 + RELEASE_VERSION: 9.1.0 links: - db:database environment: diff --git a/docker/Makefile b/docker/Makefile index d0838a6db327..98f767decf38 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.0.2' +RELEASE_VERSION ?= '9.1.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 645ee82b35bf..7e46f36c36a9 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.0.2", + version: "9.1.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 7944eb621a5d..530794a9bf20 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.0.2" + set version: "9.1.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From 1cdbe4866253b8a8cd48106fa4844d8d1e2ea17e Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Mon, 1 Sep 2025 18:07:19 +0400 Subject: [PATCH 0296/1006] feat: improved proxy detection (#12846) * feat: improved proxy detection * chore: refactor * fix: tests * feat: add simple tests for conflicting implementations * fix: apply suggestion * fix: halt return value * fix: apply suggestions * fix: filecoin address preloads --- .../views/api/v2/smart_contract_view.ex | 6 +- .../account/custom_abi_controller_test.exs | 3 +- .../address_contract_controller_test.exs | 3 +- .../address_read_contract_controller_test.exs | 3 +- .../address_read_proxy_controller_test.exs | 3 +- ...address_write_contract_controller_test.exs | 3 +- .../address_write_proxy_controller_test.exs | 3 +- .../api/rpc/contract_controller_test.exs | 35 +- .../api/v2/address_controller_test.exs | 36 +- .../v2/advanced_filter_controller_test.exs | 3 +- .../api/v2/smart_contract_controller_test.exs | 208 ++++-- .../api/v2/transaction_controller_test.exs | 3 +- .../api/v2/utils_controller_test.exs | 6 +- .../api/v2/verification_controller_test.exs | 3 +- .../smart_contract_controller_test.exs | 57 +- .../tokens/read_contract_controller_test.exs | 3 +- ...saction_token_transfer_controller_test.exs | 3 +- .../lib/ethereum_jsonrpc/contract.ex | 22 +- apps/explorer/lib/explorer/chain/address.ex | 4 +- .../lib/explorer/chain/smart_contract.ex | 2 +- .../explorer/chain/smart_contract/proxy.ex | 686 ++++++++---------- .../proxy/basic_implementation_getter.ex | 47 ++ .../proxy/clone_with_immutable_arguments.ex | 50 +- .../chain/smart_contract/proxy/eip_1167.ex | 58 +- .../chain/smart_contract/proxy/eip_1822.ex | 42 +- .../chain/smart_contract/proxy/eip_1967.ex | 126 +--- .../chain/smart_contract/proxy/eip_2535.ex | 69 +- .../chain/smart_contract/proxy/eip_7702.ex | 83 +-- .../chain/smart_contract/proxy/erc_7760.ex | 152 ++-- .../chain/smart_contract/proxy/master_copy.ex | 97 +-- .../proxy/models/implementation.ex | 318 ++++---- .../proxy/resolved_delegate_proxy.ex | 183 ++--- .../proxy/resolver_behaviour.ex | 55 ++ .../lib/explorer/etherscan/contracts.ex | 4 +- apps/explorer/lib/test_helper.ex | 364 +++++----- ...8092418_refactor_proxy_implementations.exs | 29 + .../smart_contract/proxy/eip_7702_test.exs | 5 - .../smart_contract/proxy/erc_7760_test.exs | 164 ++--- .../proxy/models/implementation_test.exs | 157 +++- .../chain/smart_contract/proxy_test.exs | 231 ++---- .../test/explorer/chain/transaction_test.exs | 9 +- .../fetcher/on_demand/contract_code_test.exs | 4 +- 42 files changed, 1538 insertions(+), 1804 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex create mode 100644 apps/explorer/lib/explorer/chain/smart_contract/proxy/resolver_behaviour.ex create mode 100644 apps/explorer/priv/repo/migrations/20250718092418_refactor_proxy_implementations.exs delete mode 100644 apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index be39b4852b9f..1ee8ae9eefba 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -172,7 +172,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do proxy_type = implementations && implementations.proxy_type - minimal_proxy? = proxy_type in ["eip1167", "clone_with_immutable_arguments", "erc7760"] + minimal_proxy? = proxy_type in [:eip1167, :eip7702, :clone_with_immutable_arguments, :erc7760] target_contract = if smart_contract_verified, do: smart_contract, else: bytecode_twin_contract @@ -195,6 +195,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do "is_verified_via_verifier_alliance" => smart_contract.verified_via_verifier_alliance, "proxy_type" => proxy_type, "implementations" => Proxy.proxy_object_info(implementations), + "conflicting_implementations" => Proxy.conflicting_implementations_info(implementations), "sourcify_repo_url" => if(smart_contract_verified_via_sourcify, do: AddressContractView.sourcify_repo_url(address.hash, smart_contract.partially_verified) @@ -237,7 +238,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do defp prepare_smart_contract(%Address{proxy_implementations: implementations} = address, _conn) do %{ "proxy_type" => implementations && implementations.proxy_type, - "implementations" => Proxy.proxy_object_info(implementations) + "implementations" => Proxy.proxy_object_info(implementations), + "conflicting_implementations" => Proxy.conflicting_implementations_info(implementations) } |> Map.merge(bytecode_info(address)) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs index 5f1d55ea2f81..367448b98660 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs @@ -171,7 +171,8 @@ defmodule BlockScoutWeb.Account.CustomABIControllerTest do "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true},{\"type\":\"function\",\"outputs\":[{\"type\":\"bool\",\"name\":\"success\"}],\"name\":\"approve\",\"inputs\":[{\"type\":\"address\",\"name\":\"_spender\"},{\"type\":\"uint256\",\"name\":\"_value\"}],\"constant\":false}]" } - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() result_conn = conn diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs index 6d936655b588..768507b3e06b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs @@ -55,7 +55,8 @@ defmodule BlockScoutWeb.AddressContractControllerTest do block_index: 0 ) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs index b982eeace979..09d7835f6352 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs @@ -43,7 +43,8 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, address_read_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs index e9ce06774223..b1f72acf031e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs @@ -43,7 +43,8 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs index 2b13b6a136d4..654a57e58f25 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs @@ -45,7 +45,8 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, address_write_contract_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs index 1b0f7755ecc3..f004a761072c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs @@ -43,7 +43,8 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, address_write_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index e6b6c4e30b1a..298737f772ac 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -471,7 +471,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } ] - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert response = conn @@ -736,7 +737,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } ] - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert response = conn @@ -845,7 +847,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } ] - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert response = conn @@ -916,7 +919,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "addressHash" => "0xf26594F585De4EB0Ae9De865d9053FEe02ac6eF1" } - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn |> get("/api", params) @@ -1316,7 +1320,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } ] test "verify", %{conn: conn, params: params} do - proxy_contract_address = insert(:contract_address) + proxy_contract_address = insert(:contract_address, contract_code: "0xDEADBEEF5c60da1bDEADBEEF") insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") @@ -1328,25 +1332,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do contract_code_md5: "123" ) - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - TestHelper.get_all_proxies_implementation_zero_addresses() - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x000000000000000000000000" <> implementation_contract_address_hash_string - } - ]} - end - ) + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(basic_implementation: implementation_contract_address.hash) %{ "message" => "OK", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 9b0254ea4cf4..4acde86ee732 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -34,6 +34,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" @instances_amount_in_collection 9 + @resolved_delegate_proxy "0x608060408181523060009081526001602090815282822054908290529181207FBF40FAC1000000000000000000000000000000000000000000000000000000009093529173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9091169063BF40FAC19061006D9060846101E2565B602060405180830381865AFA15801561008A573D6000803E3D6000FD5B505050506040513D601F19601F820116820180604052508101906100AE91906102C5565B905073FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8116610157576040517F08C379A000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527F5265736F6C76656444656C656761746550726F78793A2074617267657420616460448201527F6472657373206D75737420626520696E697469616C697A656400000000000000606482015260840160405180910390FD5B6000808273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16600036604051610182929190610302565B600060405180830381855AF49150503D80600081146101BD576040519150601F19603F3D011682016040523D82523D6000602084013E6101C2565B606091505B5090925090508115156001036101DA57805160208201F35B805160208201FD5B600060208083526000845481600182811C91508083168061020457607F831692505B858310810361023A577F4E487B710000000000000000000000000000000000000000000000000000000085526022600452602485FD5B878601838152602001818015610257576001811461028B576102B6565B7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF008616825284151560051B820196506102B6565B60008B81526020902060005B868110156102B057815484820152908501908901610297565B83019750505B50949998505050505050505050565B6000602082840312156102D757600080FD5B815173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF811681146102FB57600080FD5B9392505050565B818382376000910190815291905056FEA164736F6C634300080F000A" setup :set_mox_global @@ -362,7 +363,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do implementation_address = insert(:address) implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) request = get(conn, "/api/v2/addresses/#{Address.checksum(smart_contract.address_hash)}") @@ -392,22 +395,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end test "get Resolved Delegate Proxy contract info", %{conn: conn} do - address_manager_address = insert(:address) - - "0x" <> address_manager_address_hash_string_without_0x = to_string(address_manager_address.hash) - - owner_address = insert(:address) - - "0x" <> owner_address_hash_string_without_0x = to_string(owner_address.hash) - - proxy_smart_contract = - insert(:smart_contract, - abi: ResolvedDelegateProxy.resolved_delegate_proxy_abi(), - constructor_arguments: - "0x000000000000000000000000" <> - address_manager_address_hash_string_without_0x <> - "0000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001a4f564d5f4c3143726f7373446f6d61696e4d657373656e676572000000000000" - ) + proxy_address = insert(:address, contract_code: @resolved_delegate_proxy) + proxy_smart_contract = insert(:smart_contract, address_hash: proxy_address.hash) transaction = insert(:transaction, @@ -428,17 +417,14 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do from = Address.checksum(transaction.from_address_hash) transaction_hash = to_string(transaction.hash) checksummed_proxy_address_hash = Address.checksum(proxy_smart_contract.address_hash) - "0x" <> proxy_address_hash_string_without_0x = to_string(proxy_smart_contract.address_hash) implementation_address = insert(:address) + implementation_address_hash_string = implementation_address.hash |> Address.checksum() |> to_string() - "0x" <> implementation_address_hash_string_without_0x = to_string(implementation_address.hash) - implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) - - TestHelper.get_resolved_delegate_proxy_implementation_non_zero_address( - owner_address_hash_string_without_0x, - implementation_address_hash_string_without_0x, - proxy_address_hash_string_without_0x + EthereumJSONRPC.Mox + |> TestHelper.mock_resolved_delegate_proxy_requests( + proxy_smart_contract.address_hash, + implementation_address.hash ) request = get(conn, "/api/v2/addresses/#{checksummed_proxy_address_hash}") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index d5224e264e34..168114b870bc 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -262,7 +262,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do end test "filter by methods", %{conn: conn} do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() transaction = :transaction |> insert() |> with_block() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index 012f8c8d1fc1..e21d97f2c70e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -59,7 +59,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do test "get unverified smart-contract info", %{conn: conn} do address = insert(:contract_address) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") response = json_response(request, 200) @@ -70,7 +71,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [], "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => nil, - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } insert(:transaction, @@ -80,8 +82,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do ) |> with_block(status: :ok) - TestHelper.get_eip1967_implementation_error_response() - request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") response = json_response(request, 200) @@ -92,7 +92,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } end @@ -108,7 +109,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do ) |> with_block(status: :error) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") response = json_response(request, 200) @@ -119,7 +121,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "implementations" => [], "deployed_bytecode" => "0x", "creation_bytecode" => creation_bytecode, - "creation_status" => "failed" + "creation_status" => "failed", + "conflicting_implementations" => nil } end @@ -166,8 +169,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do |> with_block(status: :ok) implementation_address = insert(:address) - implementation_address_hash_string = to_string(implementation_address.hash) - formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) correct_response = %{ "verified_twin_address_hash" => nil, @@ -202,10 +203,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "abi" => target_contract.abi, "proxy_type" => "eip1967", "implementations" => [ - %{ - "address_hash" => formatted_implementation_address_hash_string, + prepare_implementation(%{ + "address_hash" => implementation_address.hash |> Address.checksum() |> to_string(), "name" => nil - } + }) ], "is_verified_via_eth_bytecode_db" => target_contract.verified_via_eth_bytecode_db, "is_verified_via_verifier_alliance" => target_contract.verified_via_verifier_alliance, @@ -213,10 +214,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none", "certified" => false, "is_blueprint" => false, - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") response = json_response(request, 200) @@ -224,7 +227,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do result_props = correct_response |> Map.keys() for prop <- result_props do - assert prepare_implementation(correct_response[prop]) == response[prop] + assert correct_response[prop] == response[prop] end end @@ -318,10 +321,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "language" => target_contract |> SmartContract.language() |> to_string(), "license_type" => "gnu_agpl_v3", "certified" => false, - "is_blueprint" => false + "is_blueprint" => false, + "conflicting_implementations" => nil } - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") response = json_response(request, 200) @@ -422,10 +427,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "language" => target_contract |> SmartContract.language() |> to_string(), "license_type" => "none", "certified" => false, - "is_blueprint" => false + "is_blueprint" => false, + "conflicting_implementations" => nil } - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") response = json_response(request, 200) @@ -511,11 +518,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "creation_bytecode" => proxy_transaction_input, "proxy_type" => "eip1167", "implementations" => [ - %{ + prepare_implementation(%{ "address_hash" => Address.checksum(implementation_contract.address_hash), "name" => implementation_contract.name - } - ] + }) + ], + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") @@ -524,7 +532,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do result_props = correct_response |> Map.keys() for prop <- result_props do - assert prepare_implementation(correct_response[prop]) == response[prop] + assert correct_response[prop] == response[prop] end end @@ -580,10 +588,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "license_type" => "none", "certified" => false, "is_blueprint" => true, - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(target_contract.address_hash)}") response = json_response(request, 200) @@ -661,18 +671,17 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do ) |> with_block(status: :ok) - formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_contract.address_hash)) - correct_response = %{ "proxy_type" => "clone_with_immutable_arguments", "implementations" => [ - %{ - "address_hash" => formatted_implementation_address_hash_string, + prepare_implementation(%{ + "address_hash" => implementation_contract.address_hash |> Address.checksum() |> to_string(), "name" => implementation_contract.name - } + }) ], "deployed_bytecode" => proxy_deployed_bytecode, - "creation_bytecode" => proxy_transaction_input + "creation_bytecode" => proxy_transaction_input, + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_address.hash)}") @@ -683,7 +692,71 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do result_props = correct_response |> Map.keys() for prop <- result_props do - assert prepare_implementation(correct_response[prop]) == response[prop] + assert correct_response[prop] == response[prop] + end + end + + test "returns smart-contract with conflicting implementations", %{conn: conn} do + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + proxy_smart_contract = insert(:smart_contract) + implementation_smart_contract1 = insert(:smart_contract, name: "implementation1") + implementation_smart_contract2 = insert(:smart_contract, name: "implementation2") + implementation_smart_contract3 = insert(:smart_contract, name: "implementation3") + + correct_response = %{ + "proxy_type" => "eip1967", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => implementation_smart_contract1.address_hash |> Address.checksum() |> to_string(), + "name" => implementation_smart_contract1.name + }) + ], + "conflicting_implementations" => [ + %{ + "proxy_type" => "eip1967", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => implementation_smart_contract1.address_hash |> Address.checksum() |> to_string(), + "name" => implementation_smart_contract1.name + }) + ] + }, + %{ + "proxy_type" => "eip1822", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => implementation_smart_contract3.address_hash |> Address.checksum() |> to_string() + }) + ] + }, + %{ + "proxy_type" => "eip1967_oz", + "implementations" => [ + prepare_implementation(%{ + "address_hash" => implementation_smart_contract2.address_hash |> Address.checksum() |> to_string() + }) + ] + } + ] + } + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests( + eip1967: implementation_smart_contract1.address_hash, + eip1967_oz: implementation_smart_contract2.address_hash, + eip1822: implementation_smart_contract3.address_hash + ) + + request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(proxy_smart_contract.address_hash)}") + response = json_response(request, 200) + + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + + result_props = correct_response |> Map.keys() + + for prop <- result_props do + assert correct_response[prop] == response[prop] end end @@ -750,7 +823,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -777,11 +851,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } - TestHelper.get_all_proxies_implementation_zero_addresses() - request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert response = json_response(request, 200) assert %{"is_verified" => true} = response @@ -817,7 +890,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -844,11 +918,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } - TestHelper.get_all_proxies_implementation_zero_addresses() - request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert response = json_response(request, 200) assert %{"is_verified" => true} = response @@ -928,7 +1001,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -950,12 +1024,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do assert response == %{ - "proxy_type" => "unknown", + "proxy_type" => nil, "implementations" => [], "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -997,7 +1072,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, eth_bytecode_response) end) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1024,11 +1100,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } - TestHelper.get_all_proxies_implementation_zero_addresses() - request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") assert response = json_response(request, 200) @@ -1088,9 +1163,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end) implementation_address = insert(:address) - implementation_address_hash_string = to_string(implementation_address.hash) - formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + formatted_implementation_address_hash_string = implementation_address.hash |> Address.checksum() |> to_string() + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1122,7 +1198,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1205,9 +1282,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end) implementation_address = insert(:address) - implementation_address_hash_string = to_string(implementation_address.hash) - formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + formatted_implementation_address_hash_string = implementation_address.hash |> Address.checksum() |> to_string() + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1239,7 +1317,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1322,9 +1401,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do end) implementation_address = insert(:address) - implementation_address_hash_string = to_string(implementation_address.hash) - formatted_implementation_address_hash_string = to_string(Address.checksum(implementation_address.hash)) - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + formatted_implementation_address_hash_string = implementation_address.hash |> Address.checksum() |> to_string() + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1356,7 +1436,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do "deployed_bytecode" => to_string(address.contract_code), "creation_bytecode" => "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582061b7676067d537e410bb704932a9984739a959416170ea17bda192ac1218d2790029", - "creation_status" => "success" + "creation_status" => "success", + "conflicting_implementations" => nil } request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1433,7 +1514,8 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do Conn.resp(conn, 200, "{\"sources\": []}") end) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() _request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(address.hash)}") @@ -1835,11 +1917,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) end - defp prepare_implementation(items) when is_list(items) do - Enum.map(items, &prepare_implementation/1) - end - - defp prepare_implementation(%{"address_hash" => _, "name" => _} = implementation) do + defp prepare_implementation(%{"address_hash" => _} = implementation) do case Application.get_env(:explorer, :chain_type) do :filecoin -> Map.put(implementation, "filecoin_robust_address", nil) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 881cc3affa3c..c53f3e74b828 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -2192,7 +2192,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do |> with_block() |> Repo.preload(to_address: :smart_contract) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary?just_request_body=true") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs index d9c23fdc813b..3c28c50d7000 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/utils_controller_test.exs @@ -9,7 +9,8 @@ defmodule BlockScoutWeb.API.V2.UtilsControllerTest do :transaction_to_verified_contract |> insert() - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert conn |> get("/api/v2/utils/decode-calldata", %{ @@ -25,7 +26,8 @@ defmodule BlockScoutWeb.API.V2.UtilsControllerTest do } } - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert conn |> post("/api/v2/utils/decode-calldata", %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs index de46a0b10b0b..17eaf4021e4b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -340,7 +340,8 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do :timer.seconds(300) # Assert that the `is_blueprint=true` is stored in the database after verification - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() request = get(conn, "/api/v2/smart-contracts/#{Address.checksum(contract_address.hash)}") response = json_response(request, 200) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs index 12bc66f2d54f..c666292931ef 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs @@ -86,7 +86,8 @@ defmodule BlockScoutWeb.SmartContractControllerTest do contract_code_md5: "123" ) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() path = smart_contract_path(BlockScoutWeb.Endpoint, :index, @@ -106,6 +107,7 @@ defmodule BlockScoutWeb.SmartContractControllerTest do test "lists [] proxy read only functions if no verified eip-1967 implementation" do token_contract_address = insert(:contract_address) + implementation_address = insert(:address) insert(:smart_contract, address_hash: token_contract_address.hash, @@ -123,44 +125,8 @@ defmodule BlockScoutWeb.SmartContractControllerTest do contract_code_md5: "123" ) - blockchain_get_implementation_mock() - - path = - smart_contract_path(BlockScoutWeb.Endpoint, :index, - hash: token_contract_address.hash, - type: :proxy, - action: :read - ) - - conn = - build_conn() - |> put_req_header("x-requested-with", "xmlhttprequest") - |> get(path) - - assert conn.status == 200 - assert conn.assigns.read_only_functions == [] - end - - test "lists [] proxy read only functions if no verified eip-1967 implementation and eth_getStorageAt returns not normalized address hash" do - token_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: token_contract_address.hash, - abi: [ - %{ - "type" => "function", - "stateMutability" => "nonpayable", - "payable" => false, - "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], - "name" => "implementation", - "inputs" => [], - "constant" => false - } - ], - contract_code_md5: "123" - ) - - blockchain_get_implementation_mock_2() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) path = smart_contract_path(BlockScoutWeb.Endpoint, :index, @@ -276,17 +242,4 @@ defmodule BlockScoutWeb.SmartContractControllerTest do end ) end - - defp blockchain_get_implementation_mock do - EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, "0xcebb2CCCFe291F0c442841cBE9C1D06EED61Ca02") - end - - defp blockchain_get_implementation_mock_2 do - EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request( - false, - "0x000000000000000000000000cebb2CCCFe291F0c442841cBE9C1D06EED61Ca02" - ) - end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs index fbebeaee0478..b7af01531e21 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs @@ -57,7 +57,8 @@ defmodule BlockScoutWeb.Tokens.ContractControllerTest do token: token ) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() conn = get(conn, token_read_contract_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs index fcff1a3c260a..4d41f24a26ad 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs @@ -159,7 +159,8 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do end test "preloads to_address smart contract verified", %{conn: conn} do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() transaction = insert(:transaction_to_verified_contract) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex index 5bef1c21d628..75b5e69ef49f 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex @@ -206,22 +206,14 @@ defmodule EthereumJSONRPC.Contract do request(full_params) end - def eth_get_storage_at_request(contract_address, storage_pointer, block_number, json_rpc_named_arguments) do - block = - case block_number do - nil -> "latest" - block_number -> integer_to_quantity(block_number) - end - - result = - %{id: 0, method: "eth_getStorageAt", params: [contract_address, storage_pointer, block]} - |> request() - |> json_rpc(json_rpc_named_arguments) + def eth_get_storage_at_request(contract_address, storage_pointer, id) do + full_params = %{ + id: id, + method: "eth_getStorageAt", + params: [contract_address, storage_pointer, "latest"] + } - case result do - {:ok, storage_value} -> {:ok, storage_value} - other -> other - end + request(full_params) end defp format_error(nil), do: {:error, ""} diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 67321522aed3..fa07c7fce2c3 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -615,8 +615,8 @@ defmodule Explorer.Chain.Address do - `nil` if the contract code hasn't been loaded """ @spec eoa_with_code?(any()) :: boolean() | nil - def eoa_with_code?(%__MODULE__{contract_code: %Data{bytes: code}}) do - EIP7702.get_delegate_address(code) != nil + def eoa_with_code?(%__MODULE__{} = address) do + !is_nil(EIP7702.quick_resolve_implementations(address)) end def eoa_with_code?(%NotLoaded{}), do: nil diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 25756f320a2c..dac0bcf2cf22 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -1147,7 +1147,7 @@ defmodule Explorer.Chain.SmartContract do implementation_address_fetched?: false, refetch_necessity_checked?: false }, - Keyword.put(options, :proxy_without_abi?, true) + options ) {implementation_smart_contract, true} diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex index 6de7e9bbd1d3..2795f569659b 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy.ex @@ -4,12 +4,18 @@ defmodule Explorer.Chain.SmartContract.Proxy do """ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + require Logger + + import EthereumJSONRPC, only: [id_to_params: 1, json_rpc: 2] + alias EthereumJSONRPC.Contract - alias Explorer.Chain.{Address, Hash, SmartContract} + alias Explorer.Chain.{Address, Data, Hash, SmartContract} alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour alias Explorer.Chain.SmartContract.Proxy.{ + BasicImplementationGetter, CloneWithImmutableArguments, EIP1167, EIP1822, @@ -21,58 +27,63 @@ defmodule Explorer.Chain.SmartContract.Proxy do ResolvedDelegateProxy } - alias Explorer.SmartContract.Helper, as: SmartContractHelper - import Explorer.Chain, only: [ join_associations: 2, - select_repo: 1, - string_to_address_hash: 1 + select_repo: 1 ] - import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [ - is_burn_signature: 1, get_implementation: 2, get_proxy_implementations: 1, - save_implementation_data: 4 + save_implementation_data: 2 ] - # supported signatures: - # 5c60da1b = keccak256(implementation()) - @implementation_signature "5c60da1b" - # aaf10f42 = keccak256(getImplementation()) - @get_implementation_signature "aaf10f42" - # bb82aa5e = keccak256(comptrollerImplementation()) Compound protocol proxy pattern - @comptroller_implementation_signature "bb82aa5e" - - @typep options :: [{:api?, true | false}, {:proxy_without_abi?, true | false}] + @proxy_resolvers [ + # bytecode-matching proxy types + {EIP1167, :eip1167}, + {EIP7702, :eip7702}, + {MasterCopy, :master_copy}, + {ERC7760, :erc7760}, + {CloneWithImmutableArguments, :clone_with_immutable_arguments}, + {ResolvedDelegateProxy, :resolved_delegate_proxy}, + + # generic proxy types + {EIP1967, :eip1967}, + {EIP1822, :eip1822}, + {EIP1967, :eip1967_beacon}, + {EIP2535, :eip2535}, + {EIP1967, :eip1967_oz}, + {BasicImplementationGetter, :basic_implementation}, + {BasicImplementationGetter, :basic_get_implementation}, + {BasicImplementationGetter, :comptroller} + ] + + @zero_address_hash_string "0x0000000000000000000000000000000000000000" + @zero_bytes32_string "0x0000000000000000000000000000000000000000000000000000000000000000" + + @type options :: [{:api?, true | false}] + + @spec zero_hex_string?(any()) :: boolean() + defp zero_hex_string?(term), do: term in ["0x", "0x0", @zero_address_hash_string, @zero_bytes32_string] @doc """ Fetches into DB proxy contract implementation's address and name from different proxy patterns """ - @spec fetch_implementation_address_hash(Hash.Address.t(), list(), options) :: - Implementation.t() | :empty | :error - def fetch_implementation_address_hash(proxy_address_hash, proxy_abi, options) + @spec fetch_implementation_address_hash(Hash.Address.t() | nil, options()) :: Implementation.t() | :empty | :error + def fetch_implementation_address_hash(proxy_address_hash, options) when not is_nil(proxy_address_hash) do - %{implementation_address_hash_strings: implementation_address_hash_strings, proxy_type: proxy_type} = - try_to_get_implementation_from_known_proxy_patterns( - proxy_address_hash, - proxy_abi, - options[:proxy_without_abi?] - ) + proxy_address = Address.get(proxy_address_hash, options) - save_implementation_data( - implementation_address_hash_strings, - proxy_address_hash, - proxy_type, - options - ) + case try_to_get_implementation_from_known_proxy_patterns(proxy_address) do + :empty -> :empty + :error -> :error + proxy_implementations -> save_implementation_data(proxy_implementations, options) + end end - def fetch_implementation_address_hash(_, _, _) do + def fetch_implementation_address_hash(_, _) do :empty end @@ -81,69 +92,17 @@ defmodule Explorer.Chain.SmartContract.Proxy do """ @spec proxy_contract?(SmartContract.t(), Keyword.t()) :: boolean() def proxy_contract?(smart_contract, options \\ []) do - {:ok, burn_address_hash} = string_to_address_hash(SmartContract.burn_address_hash_string()) proxy_implementations = get_proxy_implementations(smart_contract.address_hash) - with false <- is_nil(proxy_implementations), - false <- Enum.empty?(proxy_implementations.address_hashes), - implementation_address_hash = Enum.at(proxy_implementations.address_hashes, 0), - false <- implementation_address_hash.bytes == burn_address_hash.bytes do + if !is_nil(proxy_implementations) and !Enum.empty?(proxy_implementations.address_hashes) do true else - _ -> - implementation = get_implementation(smart_contract, options) - - with false <- is_nil(implementation), - false <- Enum.empty?(implementation.address_hashes) do - has_not_burn_address_hash?(implementation.address_hashes, burn_address_hash) - else - _ -> - false - end - end - end - - @spec has_not_burn_address_hash?([Hash.Address.t()], Hash.Address.t()) :: boolean() - defp has_not_burn_address_hash?(address_hashes, burn_address_hash) do - address_hashes - |> Enum.reduce_while(false, fn implementation_address_hash, acc -> - if implementation_address_hash.bytes == burn_address_hash.bytes, do: {:cont, acc}, else: {:halt, true} - end) - end - - @doc """ - Decodes and formats an address output from a smart contract ABI. + implementation = get_implementation(smart_contract, options) - This function handles various input formats and edge cases when decoding - address outputs from smart contract function calls or events. - - ## Parameters - - `address`: The address output to decode. Can be `nil`, `"0x"`, a binary string, or `:error`. - - ## Returns - - `nil` if the input is `nil`. - - The burn address hash string if the input is `"0x"`. - - A formatted address string if the input is a valid binary string. - - `:error` if the input is `:error`. - - `nil` for any other input type. - """ - @spec abi_decode_address_output(any()) :: nil | :error | binary() - def abi_decode_address_output(nil), do: nil - - def abi_decode_address_output("0x"), do: SmartContract.burn_address_hash_string() - - def abi_decode_address_output(address) when is_binary(address) do - if String.length(address) > 42 do - "0x" <> String.slice(address, -40, 40) - else - address + !is_nil(implementation) and !Enum.empty?(implementation.address_hashes) end end - def abi_decode_address_output(:error), do: :error - - def abi_decode_address_output(_), do: nil - @doc """ Gets implementation ABI for given proxy smart-contract """ @@ -164,272 +123,236 @@ defmodule Explorer.Chain.SmartContract.Proxy do def get_implementation_abi_from_proxy(_, _), do: [] - @doc """ - Gets implementation from proxy contract's specific storage - """ - @spec get_implementation_from_storage(Hash.Address.t(), String.t(), any()) :: String.t() | :error | nil - def get_implementation_from_storage(proxy_address_hash, storage_slot, json_rpc_named_arguments) do - case Contract.eth_get_storage_at_request( - proxy_address_hash, - storage_slot, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address_hash_string} - when is_burn_signature(empty_address_hash_string) -> - nil - - {:ok, "0x" <> storage_value} -> - extract_address_hex_from_storage_pointer(storage_value) - - {:error, _error} -> - :error - - _ -> - nil - end - end - @doc """ Tries to get implementation address from known proxy patterns - """ - @spec try_to_get_implementation_from_known_proxy_patterns(Hash.Address.t(), list() | nil, bool()) :: - %{implementation_address_hash_strings: [String.t()] | :error, proxy_type: atom()} - - def try_to_get_implementation_from_known_proxy_patterns(proxy_address_hash, proxy_abi, proxy_without_abi?) - when not is_nil(proxy_abi) or proxy_without_abi? == true do - functions = - [ - :get_implementation_address_hash_string_eip1167, - :get_implementation_address_hash_string_eip7702, - :get_implementation_address_hash_string_clones_with_immutable_arguments, - :get_implementation_address_hash_string_eip1967, - :get_implementation_address_hash_string_eip1822, - :get_implementation_address_hash_string_eip2535, - :get_implementation_address_hash_string_erc7760, - :get_implementation_address_hash_string_resolved_delegate_proxy - ] - - %{implementation_address_hash_strings: implementation_address_hash_strings, proxy_type: proxy_type} = - functions - |> Enum.reduce_while(nil, fn fun, _acc -> - %{ - implementation_address_hash_strings: implementation_address_hash_strings, - proxy_type: _proxy_type - } = result = apply(__MODULE__, fun, [proxy_address_hash]) - - case implementation_address_hash_strings do - [] -> {:cont, result} - :error -> {:halt, result} - _ -> {:halt, result} - end - end) - cond do - implementation_address_hash_strings == :error -> - fallback_proxy_detection(proxy_address_hash, proxy_abi, implementation_address_hash_strings_fallback(:error)) + ## Parameters + - `proxy_address`: The address to try to detect implementations for. - implementation_address_hash_strings == [] || - implementation_address_hash_strings == [burn_address_hash_string()] -> - fallback_proxy_detection(proxy_address_hash, proxy_abi, implementation_address_hash_strings_fallback(nil)) + ## Returns + - Pre-filled `proxy_implementations` if the result should be saved to the database. + - `:error` if the implementation detection failed, nothing should be saved to the database. + - `:empty` if the address is empty or not a smart contract, nothing should be saved to the database. + """ + @spec try_to_get_implementation_from_known_proxy_patterns(Address.t()) :: + %{ + proxy_address_hash: Hash.Address.t(), + address_hashes: [Hash.Address.t()], + proxy_type: atom() | nil, + conflicting_proxy_types: [atom()] | nil, + conflicting_address_hashes: [[Hash.Address.t()]] | nil + } + | :error + | :empty + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + def try_to_get_implementation_from_known_proxy_patterns(proxy_address) do + with true <- Address.smart_contract?(proxy_address), + # first, we try to immediately resolve proxy types by matching bytecodes, + # while collecting fetch requirements for all other proxy types + resolvers_and_requirements when is_list(resolvers_and_requirements) <- + Enum.reduce_while(@proxy_resolvers, [], fn {module, proxy_type}, acc -> + case module.quick_resolve_implementations(proxy_address, proxy_type) do + {:ok, address_hashes} -> + filtered_address_hashes = address_hashes |> Enum.reject(&(&1 == proxy_address.hash)) + + if Enum.empty?(filtered_address_hashes) do + {:halt, + %{ + proxy_address_hash: proxy_address.hash, + address_hashes: [] + }} + else + {:halt, + %{ + proxy_address_hash: proxy_address.hash, + address_hashes: filtered_address_hashes, + proxy_type: proxy_type + }} + end + + {:cont, requirements} -> + {:cont, [{{module, proxy_type}, requirements} | acc]} + + :error -> + Logger.error( + "Failed to quick resolve implementations for proxy address #{proxy_address.hash} and proxy type #{proxy_type}" + ) + + {:halt, :error} + + _ -> + {:cont, acc} + end + end), + # didn't match any known bytecode pattern, proceed with fetching required values + {:ok, resolvers_and_fetched_values} <- + resolvers_and_requirements + |> Enum.reverse() + |> prefetch_values(proxy_address.hash), + generic_results when is_list(generic_results) <- + Enum.reduce_while(resolvers_and_fetched_values, [], fn {{module, proxy_type}, values}, acc -> + case module.resolve_implementations(proxy_address, proxy_type, values) do + {:ok, implementation_address_hashes} -> + filtered_address_hashes = implementation_address_hashes |> Enum.reject(&(&1 == proxy_address.hash)) + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if Enum.empty?(filtered_address_hashes) do + {:cont, acc} + else + {:cont, [{proxy_type, filtered_address_hashes} | acc]} + end + + :error -> + Logger.error( + "Failed to resolve implementations for proxy address #{proxy_address.hash} and proxy type #{proxy_type}" + ) + + {:halt, :error} + + _ -> + {:cont, acc} + end + end) do + {address_hashes, proxy_type, conflicting_proxy_types, conflicting_address_hashes} = + case Enum.reverse(generic_results) do + [] -> + {[], nil, nil, nil} + + [{proxy_type, address_hashes}] -> + {address_hashes, proxy_type, nil, nil} + + [{proxy_type, address_hashes} | rest] -> + address_hashes_sorted = address_hashes |> Enum.sort() + + if Enum.all?(rest, &(&1 |> elem(1) |> Enum.sort() == address_hashes_sorted)) do + {address_hashes, proxy_type, nil, nil} + else + {conflicting_proxy_types, conflicting_address_hashes} = Enum.unzip(rest) + {address_hashes, proxy_type, conflicting_proxy_types, conflicting_address_hashes} + end + end - true -> - %{implementation_address_hash_strings: implementation_address_hash_strings, proxy_type: proxy_type} + %{ + proxy_address_hash: proxy_address.hash, + address_hashes: address_hashes, + proxy_type: proxy_type, + conflicting_proxy_types: conflicting_proxy_types, + conflicting_address_hashes: conflicting_address_hashes + } + else + %{proxy_type: _} = result -> result + :error -> :error + _ -> :empty end end - def try_to_get_implementation_from_known_proxy_patterns(proxy_address_hash, proxy_abi, _proxy_without_abi?) do - fallback_proxy_detection(proxy_address_hash, proxy_abi, implementation_address_hash_strings_fallback(nil)) - end - - @spec get_implementation_address_hash_string_eip1167(Hash.Address.t()) :: - %{implementation_address_hash_strings: [String.t() | :error | nil], proxy_type: atom()} - def get_implementation_address_hash_string_eip1167(proxy_address_hash) do - get_implementation_address_hash_string_by_module(EIP1167, :eip1167, proxy_address_hash) - end - - @spec get_implementation_address_hash_string_clones_with_immutable_arguments(Hash.Address.t()) :: - %{implementation_address_hash_strings: [String.t()] | :error, proxy_type: atom()} - def get_implementation_address_hash_string_clones_with_immutable_arguments(proxy_address_hash) do - get_implementation_address_hash_string_by_module( - CloneWithImmutableArguments, - :clone_with_immutable_arguments, - proxy_address_hash - ) - end - - @spec get_implementation_address_hash_string_eip7702(Hash.Address.t()) :: - %{implementation_address_hash_strings: [String.t()] | :error, proxy_type: atom()} - def get_implementation_address_hash_string_eip7702(proxy_address_hash) do - get_implementation_address_hash_string_by_module(EIP7702, :eip7702, proxy_address_hash) - end - - @spec get_implementation_address_hash_string_eip1967(Hash.Address.t()) :: %{ - implementation_address_hash_strings: [String.t() | :error | nil], - proxy_type: atom() - } - def get_implementation_address_hash_string_eip1967(proxy_address_hash) do - get_implementation_address_hash_string_by_module(EIP1967, :eip1967, proxy_address_hash) - end + @doc """ + Fetches all required eth_getStorageAt and eth_call results for given proxy resolvers and requirements. - @spec get_implementation_address_hash_string_eip1822(Hash.Address.t()) :: %{ - implementation_address_hash_strings: [String.t() | :error | nil], - proxy_type: atom() - } - def get_implementation_address_hash_string_eip1822(proxy_address_hash) do - get_implementation_address_hash_string_by_module(EIP1822, :eip1822, proxy_address_hash) - end + ## Parameters + - `resolvers_and_requirements`: The list of proxy resolvers and their requirements. + - `address_hash`: The address hash to fetch the values for. - @spec get_implementation_address_hash_string_eip2535(Hash.Address.t()) :: %{ - implementation_address_hash_strings: [String.t() | :error | nil], - proxy_type: atom() - } - def get_implementation_address_hash_string_eip2535(proxy_address_hash) do - get_implementation_address_hash_string_by_module(EIP2535, :eip2535, proxy_address_hash) + ## Returns + - `{:ok, [{any(), ResolverBehaviour.fetched_values()}]}` if all of the values are fetched successfully, + map can contain nil values for failed/reverted eth_call requests. + - `:error` if the prefetching failed. + """ + @spec prefetch_values([{any(), ResolverBehaviour.fetch_requirements()}], Hash.Address.t()) :: + {:ok, [{any(), ResolverBehaviour.fetched_values()}]} | :error + def prefetch_values(resolvers_and_requirements, address_hash) do + with {:ok, fetched_values} <- + resolvers_and_requirements + |> Enum.flat_map(fn {_, reqs} -> Map.values(reqs) end) + |> fetch_values(address_hash), + resolvers_and_fetched_values when is_list(resolvers_and_fetched_values) <- + Enum.reduce_while(resolvers_and_requirements, [], fn {resolver, reqs}, acc -> + values = Enum.into(reqs, %{}, fn {name, req} -> {name, Map.get(fetched_values, req, :error)} end) + + if Enum.any?(values, &(elem(&1, 1) == :error)) do + {:halt, :error} + else + {:cont, [{resolver, values} | acc]} + end + end) do + {:ok, Enum.reverse(resolvers_and_fetched_values)} + end end - @spec get_implementation_address_hash_string_erc7760(Hash.Address.t()) :: %{ - implementation_address_hash_strings: [String.t() | :error | nil], - proxy_type: atom() - } - def get_implementation_address_hash_string_erc7760(proxy_address_hash) do - get_implementation_address_hash_string_by_module(ERC7760, :erc7760, proxy_address_hash) - end + @doc """ + Fetches values for given eth_getStorageAt and eth_call requirements for a given address hash. - @spec get_implementation_address_hash_string_resolved_delegate_proxy(Hash.Address.t()) :: - %{implementation_address_hash_strings: [String.t() | :error | nil], proxy_type: atom()} - def get_implementation_address_hash_string_resolved_delegate_proxy(proxy_address_hash) do - get_implementation_address_hash_string_by_module( - ResolvedDelegateProxy, - :resolved_delegate_proxy, - proxy_address_hash - ) - end + ## Parameters + - `reqs`: The list of eth_getStorageAt and eth_call requirements to fetch the values for. + - `address_hash`: The address hash to fetch the values for. - defp get_implementation_address_hash_string_by_module( - module, - proxy_type, - proxy_address_hash - ) do - implementation_address_hash_strings = module.get_implementation_address_hash_strings(proxy_address_hash, api?: true) - - if implementation_address_hash_strings == [] || - implementation_address_hash_strings == [burn_address_hash_string()] || - implementation_address_hash_strings == :error do - implementation_address_hash_strings_fallback(implementation_address_hash_strings) + ## Returns + - `{:ok, prefetched_values()}` if all of the values are fetched successfully, + map can contain nil values for failed/reverted eth_call requests. + - `:error` if the prefetching failed. + """ + @spec fetch_values([ResolverBehaviour.fetch_requirement()], Hash.Address.t()) :: + {:ok, %{ResolverBehaviour.fetch_requirement() => String.t() | nil}} | :error + def fetch_values(reqs, address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + id_to_params = id_to_params(reqs) + + with {:ok, responses} <- + id_to_params + |> Enum.map(fn {index, req} -> encode_request(req, address_hash, index) end) + |> json_rpc(json_rpc_named_arguments), + fetched_values when is_map(fetched_values) <- + Enum.reduce_while(responses, %{}, fn result, acc -> + with %{id: id} <- result, + {:ok, req} = Map.fetch(id_to_params, id), + {:ok, value} <- handle_response(req, result) do + {:cont, Map.put(acc, req, value)} + else + _ -> + {:halt, :error} + end + end) do + {:ok, fetched_values} else - %{ - implementation_address_hash_strings: implementation_address_hash_strings, - proxy_type: proxy_type - } + _ -> :error end end - defp implementation_address_hash_strings_fallback(implementation_value) do - value = if implementation_value == :error, do: :error, else: [] - - %{implementation_address_hash_strings: value, proxy_type: :unknown} - end - - @spec fallback_proxy_detection(Hash.Address.t(), list() | nil, %{ - implementation_address_hash_strings: [String.t()] | :error, - proxy_type: atom() - }) :: %{ - implementation_address_hash_strings: [String.t()] | :error, - proxy_type: atom() - } - defp fallback_proxy_detection(proxy_address_hash, proxy_abi, fallback_value) do - proxy_type = define_fallback_proxy_type(proxy_abi) - - case proxy_type do - :implementation -> - implementation_address_hash_string = - SmartContractHelper.get_binary_string_from_contract_getter( - @implementation_signature, - to_string(proxy_address_hash), - proxy_abi - ) - - %{ - implementation_address_hash_strings: - implementation_address_hash_string_to_list(implementation_address_hash_string), - proxy_type: :basic_implementation - } - - :get_implementation -> - implementation_address_hash_string = - SmartContractHelper.get_binary_string_from_contract_getter( - @get_implementation_signature, - to_string(proxy_address_hash), - proxy_abi - ) - - %{ - implementation_address_hash_strings: - implementation_address_hash_string_to_list(implementation_address_hash_string), - proxy_type: :basic_get_implementation - } - - :master_copy -> - implementation_address_hash_string = MasterCopy.get_implementation_address_hash_string(proxy_address_hash) - - %{ - implementation_address_hash_strings: - implementation_address_hash_string_to_list(implementation_address_hash_string), - proxy_type: :master_copy - } + @doc """ + Fetches value for the given eth_getStorageAt or eth_call request for a given address hash. - :comptroller -> - implementation_address_hash_string = - SmartContractHelper.get_binary_string_from_contract_getter( - @comptroller_implementation_signature, - proxy_address_hash, - proxy_abi - ) + The eth_call request is allowed to fail/revert, nil will be returned in such case. - %{ - implementation_address_hash_strings: - implementation_address_hash_string_to_list(implementation_address_hash_string), - proxy_type: :comptroller - } + ## Parameters + - `req`: The eth_getStorageAt or eth_call request to fetch the value for. + - `address_hash`: The address hash to fetch the value for. - _ -> - fallback_value - end - end + ## Returns + - `{:ok, String.t() | nil}` if the value is fetched successfully. + - `:error` if the fetch request failed. + """ + @spec fetch_value(ResolverBehaviour.fetch_requirement(), Hash.Address.t()) :: {:ok, String.t() | nil} | :error + def fetch_value(req, address_hash) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - defp implementation_address_hash_string_to_list(implementation_address_hash_string) do - case implementation_address_hash_string do - :error -> :error - nil -> [] - hash -> [hash] + case req |> encode_request(address_hash, 0) |> json_rpc(json_rpc_named_arguments) do + {:ok, response} -> handle_response(req, %{result: response}) + {:error, error} -> handle_response(req, %{error: error}) end end - defp define_fallback_proxy_type(nil), do: nil - - defp define_fallback_proxy_type(proxy_abi) do - methods_to_proxy_types = %{ - "implementation" => :implementation, - "getImplementation" => :get_implementation, - "comptrollerImplementation" => :comptroller, - "facetAddresses" => :diamond - } + defp encode_request({:storage, value}, address_hash, index), + do: Contract.eth_get_storage_at_request(to_string(address_hash), value, index) - proxy_abi - |> Enum.reduce_while(nil, fn method, acc -> - cond do - Map.get(method, "name") in Map.keys(methods_to_proxy_types) && Map.get(method, "stateMutability") == "view" -> - {:halt, methods_to_proxy_types[Map.get(method, "name")]} + defp encode_request({:call, value}, address_hash, index), + do: Contract.eth_call_request(value, to_string(address_hash), index, nil, nil) - MasterCopy.pattern?(method) -> - {:halt, :master_copy} - - true -> - {:cont, acc} - end - end) - end + defp handle_response({:storage, _}, %{result: result}) when is_binary(result), do: {:ok, result} + defp handle_response({:call, _}, %{result: result}) when is_binary(result), do: {:ok, result} + # TODO: it'll be better to return nil only for the revert-related errors + defp handle_response({:call, _}, %{error: _}), do: {:ok, nil} + defp handle_response(_, _), do: :error @doc """ Returns combined ABI from proxy and implementation smart-contracts @@ -446,13 +369,19 @@ defmodule Explorer.Chain.SmartContract.Proxy do end @doc """ - Decodes 20 bytes address hex from smart-contract storage pointer value + Decodes non-zero address hash from raw smart-contract hex response """ - @spec extract_address_hex_from_storage_pointer(binary()) :: binary() - def extract_address_hex_from_storage_pointer(storage_value) when is_binary(storage_value) do - address_hex = storage_value |> String.slice(-40, 40) |> String.pad_leading(40, ["0"]) - - "0x" <> address_hex + @spec extract_address_hash(String.t() | nil) :: {:ok, Hash.Address.t()} | :error | nil + def extract_address_hash(value) do + with false <- is_nil(value), + false <- zero_hex_string?(value), + {:ok, %Data{bytes: bytes}} <- Data.cast(value), + false <- byte_size(bytes) > 32 do + Hash.Address.cast((<<0::160>> <> bytes) |> binary_slice(-20, 20)) + else + :error -> :error + _ -> nil + end end @doc """ @@ -473,52 +402,33 @@ defmodule Explorer.Chain.SmartContract.Proxy do end @doc """ - Retrieves formatted proxy object based on its implementation addresses and names. + Retrieves formatted proxy implementation objects with addresses and names. ## Parameters - * `implementation_addresses` - A list of implementation addresses for the proxy object. - * `implementation_names` - A list of implementation names for the proxy object. + * `proxy_implementation` - An `Implementation.t()` struct. ## Returns - A list of maps containing information about the proxy object. + A list of maps containing information about the proxy implementations. """ @spec proxy_object_info(Implementation.t() | nil) :: [map()] def proxy_object_info(nil), do: [] def proxy_object_info(proxy_implementation) do - implementations_info = prepare_implementations(proxy_implementation) + implementations_info = prepare_implementations(proxy_implementation.addresses) implementation_addresses = proxy_implementation.address_hashes implementation_names = proxy_implementation.names implementation_addresses |> Enum.zip(implementation_names) - |> Enum.reduce([], fn {address, name}, acc -> - case address do - %Hash{} = address_hash -> - [ - %{ - "address_hash" => Address.checksum(address_hash), - "name" => name - } - |> chain_type_fields(implementations_info) - | acc - ] - - _ -> - with {:ok, address_hash} <- string_to_address_hash(address), - checksummed_address <- Address.checksum(address_hash) do - [ - %{"address_hash" => checksummed_address, "name" => name} - |> chain_type_fields(implementations_info) - | acc - ] - else - _ -> acc - end - end + |> Enum.map(fn {address_hash, name} -> + %{ + "address_hash" => Address.checksum(address_hash), + "name" => name + } + |> chain_type_fields(implementations_info) end) end @@ -527,7 +437,7 @@ defmodule Explorer.Chain.SmartContract.Proxy do Map.put(address, "filecoin_robust_address", implementations_info[address_hash]) end - def prepare_implementations(%Implementation{addresses: [_ | _] = addresses}) do + def prepare_implementations(addresses) when is_list(addresses) do Enum.into(addresses, %{}, fn address -> {Address.checksum(address.hash), address.filecoin_robust} end) end @@ -543,4 +453,52 @@ defmodule Explorer.Chain.SmartContract.Proxy do :ignore end end + + @doc """ + Returns conflicting implementations info for a given proxy implementation. + + ## Parameters + + * `proxy_implementation` - An `Implementation.t()` struct. + + ## Returns + + A list of maps containing information about the conflicting proxy implementations, if more than 1 proxy type is present. + + """ + @spec conflicting_implementations_info(Implementation.t() | nil) :: [map()] | nil + def conflicting_implementations_info( + %{ + proxy_type: proxy_type, + conflicting_proxy_types: conflicting_proxy_types, + conflicting_address_hashes: conflicting_address_hashes + } = proxy_implementation + ) + when not is_nil(proxy_type) and is_list(conflicting_proxy_types) and is_list(conflicting_address_hashes) do + implementations_info = prepare_implementations(proxy_implementation.conflicting_addresses) + + conflicting_implementations = + conflicting_proxy_types + |> Enum.zip(conflicting_address_hashes) + |> Enum.map(fn {proxy_type, address_hashes} -> + %{ + "proxy_type" => proxy_type, + "implementations" => + Enum.map( + address_hashes, + &(%{"address_hash" => Address.checksum(&1)} |> chain_type_fields(implementations_info)) + ) + } + end) + + [ + %{ + "proxy_type" => proxy_type, + "implementations" => proxy_object_info(proxy_implementation) + } + | conflicting_implementations + ] + end + + def conflicting_implementations_info(_proxy_implementation), do: nil end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex new file mode 100644 index 000000000000..0e8ab7c0d8d6 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex @@ -0,0 +1,47 @@ +defmodule Explorer.Chain.SmartContract.Proxy.BasicImplementationGetter do + @moduledoc """ + Module for fetching proxy implementation from public smart-contract method + """ + + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour + + @behaviour ResolverBehaviour + + # 0x5c60da1b = keccak256(implementation()) + @implementation_signature <<0x5C60DA1B::4-unit(8)>> + # 0xaaf10f42 = keccak256(getImplementation()) + @get_implementation_signature <<0xAAF10F42::4-unit(8)>> + # 0xbb82aa5e = keccak256(comptrollerImplementation()) + @comptroller_implementation_signature <<0xBB82AA5E::4-unit(8)>> + + def quick_resolve_implementations(address, proxy_type) do + signature = + case proxy_type do + :basic_implementation -> @implementation_signature + :basic_get_implementation -> @get_implementation_signature + :comptroller -> @comptroller_implementation_signature + _ -> nil + end + + # don't resolve implementations if the bytecode doesn't contain function selector in it + if signature && address.contract_code && :binary.match(address.contract_code.bytes, signature) != :nomatch do + {:cont, + %{ + implementation_getter: {:call, "0x" <> Base.encode16(signature, case: :lower)} + }} + else + nil + end + end + + def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do + with {:ok, value} <- Map.fetch(prefetched_values, :implementation_getter), + {:ok, address_hash} <- Proxy.extract_address_hash(value) do + {:ok, [address_hash]} + else + :error -> :error + _ -> nil + end + end +end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex index 6ccc06c433cc..bb202bd383d9 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex @@ -3,49 +3,17 @@ defmodule Explorer.Chain.SmartContract.Proxy.CloneWithImmutableArguments do Module for fetching proxy implementation from https://github.com/wighawag/clones-with-immutable-args """ - alias Explorer.Chain - alias Explorer.Chain.{Address, Hash} - alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour - @doc """ - Get implementation address hash string following "Clone with immutable arguments" proxy pattern. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] - def get_implementation_address_hash_strings(proxy_address_hash, options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash, options) do - nil -> [] - implementation_address_hash_string -> [implementation_address_hash_string] - end - end - - # Get implementation address hash string following "Clone with immutable arguments" pattern - @spec get_implementation_address_hash_string(Hash.Address.t(), Keyword.t()) :: binary() | nil - defp get_implementation_address_hash_string(proxy_address_hash, options) do - case Chain.select_repo(options).get(Address, proxy_address_hash) do - nil -> - nil - - target_address -> - contract_code = target_address.contract_code - - case contract_code do - %Chain.Data{bytes: contract_code_bytes} -> - contract_bytecode = Base.encode16(contract_code_bytes, case: :lower) - - contract_bytecode |> get_proxy_clone_with_immutable_arguments() |> Proxy.abi_decode_address_output() - - _ -> - nil - end - end - end + @behaviour ResolverBehaviour - defp get_proxy_clone_with_immutable_arguments(contract_bytecode) do - case contract_bytecode do - "3d3d3d3d363d3d3761" <> - <<_::binary-size(4)>> <> - "603736393661" <> <<_::binary-size(4)>> <> "013d73" <> <> <> _ -> - "0x" <> template_address + def quick_resolve_implementations(proxy_address, _proxy_type) do + case proxy_address.contract_code && proxy_address.contract_code.bytes do + <<0x3D3D3D3D363D3D3761::9-unit(8), _::2-bytes, 0x603736393661::6-unit(8), _::2-bytes, 0x013D73::3-unit(8), + template_address::20-bytes, _::binary>> -> + {:ok, template_address_hash} = Hash.Address.cast(template_address) + {:ok, [template_address_hash]} _ -> nil diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex index acaf1716cf55..af71b0b783f1 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex @@ -6,48 +6,19 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1167 do alias Explorer.Chain alias Explorer.Chain.{Address, Hash, SmartContract} alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour - @doc """ - Get implementation address hash string following EIP-1167. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] - def get_implementation_address_hash_strings(proxy_address_hash, options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash, options) do - nil -> [] - implementation_address_hash_string -> [implementation_address_hash_string] - end - end - - # Get implementation address hash string following EIP-1167 - @spec get_implementation_address_hash_string(Hash.Address.t(), Keyword.t()) :: binary() | nil - defp get_implementation_address_hash_string(proxy_address_hash, options) do - case Chain.select_repo(options).get(Address, proxy_address_hash) do - nil -> - nil - - target_address -> - contract_code = target_address.contract_code + @behaviour ResolverBehaviour - case contract_code do - %Chain.Data{bytes: contract_code_bytes} -> - contract_bytecode = Base.encode16(contract_code_bytes, case: :lower) + def quick_resolve_implementations(proxy_address, _proxy_type) do + case proxy_address.contract_code && proxy_address.contract_code.bytes do + <<0x363D3D373D3D3D363D73::10-unit(8), template_address::20-bytes, 0x5AF43D82803E903D91602B57FD5BF3::15-unit(8)>> -> + {:ok, template_address_hash} = Hash.Address.cast(template_address) + {:ok, [template_address_hash]} - contract_bytecode |> get_proxy_eip_1167() |> Proxy.abi_decode_address_output() - - _ -> - nil - end - end - end - - defp get_proxy_eip_1167(contract_bytecode) do - case contract_bytecode do - "363d3d373d3d3d363d73" <> <> <> "5af43d82803e903d91602b57fd5bf3" -> - "0x" <> template_address - - # https://medium.com/coinmonks/the-more-minimal-proxy-5756ae08ee48 - "3d3d3d3d363d3d37363d73" <> <> <> "5af43d3d93803e602a57fd5bf3" -> - "0x" <> template_address + <<0x3D3D3D3D363D3D37363D73::11-unit(8), template_address::20-bytes, 0x5AF43D3D93803E602A57FD5BF3::13-unit(8)>> -> + {:ok, template_address_hash} = Hash.Address.cast(template_address) + {:ok, [template_address_hash]} _ -> nil @@ -59,8 +30,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1167 do """ @spec get_implementation_smart_contract(Hash.Address.t(), Keyword.t()) :: SmartContract.t() | nil def get_implementation_smart_contract(address_hash, options \\ []) do - address_hash - |> get_implementation_address_hash_string(options) - |> Proxy.implementation_to_smart_contract(options) + address = Chain.select_repo(options).get(Address, address_hash) + + case address && quick_resolve_implementations(address, :eip1167) do + {:ok, [address_hash]} -> Proxy.implementation_to_smart_contract(address_hash, options) + _ -> nil + end end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex index 0fc51b5644c4..0649eb723505 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex @@ -2,37 +2,29 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1822 do @moduledoc """ Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-1822 Universal Upgradeable Proxy Standard (UUPS) """ - alias Explorer.Chain - alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour + + @behaviour ResolverBehaviour # keccak256("PROXIABLE") @storage_slot_proxiable "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" - @doc """ - Get implementation address hash string following EIP-1822. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] | :error - def get_implementation_address_hash_strings(proxy_address_hash, _options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash) do - nil -> [] + def quick_resolve_implementations(_proxy_address, _proxy_type), + do: + {:cont, + %{ + implementation_slot: {:storage, @storage_slot_proxiable} + }} + + def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do + with {:ok, value} <- Map.fetch(prefetched_values, :implementation_slot), + {:ok, address_hash} <- Proxy.extract_address_hash(value) do + {:ok, [address_hash]} + else :error -> :error - implementation_address_hash_string -> [implementation_address_hash_string] + _ -> nil end end - - # Get implementation address hash string following EIP-1822 - @spec get_implementation_address_hash_string(Hash.Address.t()) :: binary() | nil | :error - defp get_implementation_address_hash_string(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - proxiable_contract_address_hash_string = - Proxy.get_implementation_from_storage( - proxy_address_hash, - @storage_slot_proxiable, - json_rpc_named_arguments - ) - - Proxy.abi_decode_address_output(proxiable_contract_address_hash_string) - end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex index 83950860a438..001b078e1898 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex @@ -2,14 +2,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1967 do @moduledoc """ Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-1967 (Proxy Storage Slots) """ - alias Explorer.Chain - alias Explorer.Chain.Hash alias Explorer.Chain.SmartContract.Proxy - alias Explorer.SmartContract.Helper, as: SmartContractHelper + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour - # supported signatures: - # 5c60da1b = keccak256(implementation()) - @implementation_signature "5c60da1b" + @behaviour ResolverBehaviour + + # 0x5c60da1b = keccak256(implementation()) + @implementation_signature "0x5c60da1b" # obtained as bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1) @storage_slot_logic_contract_address "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc" @@ -21,100 +20,39 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1967 do # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation" @storage_slot_openzeppelin_contract_address "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3" - @implementation_method_abi [ - %{ - "type" => "function", - "stateMutability" => "view", - "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}], - "name" => "implementation", - "inputs" => [] - } - ] + def quick_resolve_implementations(_proxy_address, proxy_type) do + storage_slot = + case proxy_type do + :eip1967 -> @storage_slot_logic_contract_address + :eip1967_oz -> @storage_slot_openzeppelin_contract_address + :eip1967_beacon -> @storage_slot_beacon_contract_address + _ -> nil + end - @doc """ - Get implementation address hash string following EIP-1967. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] | :error - def get_implementation_address_hash_strings(proxy_address_hash, _options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash) do - nil -> [] - :error -> :error - implementation_address_hash_string -> [implementation_address_hash_string] + if is_nil(storage_slot) do + nil + else + {:cont, + %{ + implementation_slot: {:storage, storage_slot} + }} end end - # Get implementation address hash string following EIP-1967 - @spec get_implementation_address_hash_string(Hash.Address.t()) :: binary() | nil | :error - defp get_implementation_address_hash_string(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - implementation_address_hash_string_from_logic_storage_slot = - Proxy.get_implementation_from_storage( - proxy_address_hash, - @storage_slot_logic_contract_address, - json_rpc_named_arguments - ) - - implementation_address_hash_string = - if implementation_address_hash_string_from_logic_storage_slot && - implementation_address_hash_string_from_logic_storage_slot !== :error do - implementation_address_hash_string_from_logic_storage_slot - else - implementation_address_hash_string_from_beacon_storage_slot = - fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) - - if implementation_address_hash_string_from_beacon_storage_slot && - implementation_address_hash_string_from_beacon_storage_slot !== :error do - implementation_address_hash_string_from_beacon_storage_slot - else - Proxy.get_implementation_from_storage( - proxy_address_hash, - @storage_slot_openzeppelin_contract_address, - json_rpc_named_arguments - ) + def resolve_implementations(_proxy_address, proxy_type, prefetched_values) do + with {:ok, value} <- Map.fetch(prefetched_values, :implementation_slot), + {:ok, stored_address_hash} <- Proxy.extract_address_hash(value) do + if proxy_type == :eip1967_beacon do + with {:ok, value} <- Proxy.fetch_value({:call, @implementation_signature}, stored_address_hash), + {:ok, implementation_address_hash} <- Proxy.extract_address_hash(value) do + {:ok, [implementation_address_hash]} end + else + {:ok, [stored_address_hash]} end - - Proxy.abi_decode_address_output(implementation_address_hash_string) - end - - # changes requested by https://github.com/blockscout/blockscout/issues/4770 - # for support BeaconProxy pattern - defp fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do - eip1967_beacon_address_hash_string = - Proxy.get_implementation_from_storage( - proxy_address_hash, - @storage_slot_beacon_contract_address, - json_rpc_named_arguments - ) - - case eip1967_beacon_address_hash_string do - :error -> - :error - - nil -> - nil - - _ -> - case @implementation_signature - |> SmartContractHelper.get_binary_string_from_contract_getter( - eip1967_beacon_address_hash_string, - @implementation_method_abi - ) do - <> -> - implementation_address_hash_string - - _ -> - nil - end + else + :error -> :error + _ -> nil end end - - @doc """ - Shares logic storage slot to other modules - """ - @spec storage_slot_logic_contract_address() :: String.t() - def storage_slot_logic_contract_address do - @storage_slot_logic_contract_address - end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex index c7c754f2fa98..80ae60c17f90 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex @@ -3,35 +3,54 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP2535 do Module for fetching proxy implementation from https://eips.ethereum.org/EIPS/eip-2535 (Diamond Proxy) """ - # 52ef6b2c = keccak256(facetAddresses()) - @facet_addresses_signature "52ef6b2c" + alias ABI.TypeDecoder + alias Explorer.Chain.{Data, Hash} + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour - alias Explorer.Chain - alias Explorer.Chain.Hash - alias Explorer.SmartContract.Helper, as: SmartContractHelper + @behaviour ResolverBehaviour - @facet_addresses_method_abi [ - %{ - "inputs" => [], - "name" => "facetAddresses", - "outputs" => [%{"internalType" => "address[]", "name" => "facetAddresses_", "type" => "address[]"}], - "stateMutability" => "view", - "type" => "function" - } - ] + # 0x52ef6b2c = keccak256(facetAddresses()) + @facet_addresses_signature "0x52ef6b2c" - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] - def get_implementation_address_hash_strings(proxy_address_hash, _options \\ []) do - case @facet_addresses_signature - |> SmartContractHelper.get_binary_string_from_contract_getter( - to_string(proxy_address_hash), - @facet_addresses_method_abi - ) do - implementation_addresses when is_list(implementation_addresses) -> - implementation_addresses + @max_implementations_number_per_proxy 100 - _ -> - [] + def quick_resolve_implementations(_proxy_address, _proxy_type), + do: + {:cont, + %{ + implementation_getter: {:call, @facet_addresses_signature} + }} + + def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do + with {:ok, value} <- Map.fetch(prefetched_values, :implementation_getter), + {:ok, address_hashes} <- extract_address_hashes(value) do + {:ok, address_hashes} + else + :error -> :error + _ -> nil + end + end + + # Decodes unique non-zero address hashes from raw smart-contract hex response + @spec extract_address_hashes(String.t() | nil) :: {:ok, [Hash.Address.t()]} | :error | nil + defp extract_address_hashes(value) do + with false <- is_nil(value), + {:ok, %Data{bytes: bytes}} <- Data.cast(value), + [all_address_hashes] when is_list(all_address_hashes) <- + (try do + TypeDecoder.decode_raw(bytes, [{:array, :address}]) + rescue + _ -> nil + end) do + {:ok, + all_address_hashes + |> Enum.reject(&(&1 == <<0::160>>)) + |> Enum.map(&(&1 |> Hash.Address.cast() |> elem(1))) + |> Enum.take(@max_implementations_number_per_proxy) + |> Enum.uniq()} + else + :error -> :error + _ -> nil end end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex index 11d19996894c..b7eb984ed49a 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex @@ -3,83 +3,16 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP7702 do Module for fetching EOA delegate from https://eips.ethereum.org/EIPS/eip-7702 """ - alias Explorer.Chain - alias Explorer.Chain.{Address, Hash} - alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour - @doc """ - Get implementation address hash string following EIP-7702. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] - def get_implementation_address_hash_strings(address_hash, options \\ []) do - case get_implementation_address_hash_string(address_hash, options) do - nil -> [] - implementation_address_hash_string -> [implementation_address_hash_string] - end - end - - # Retrieves the delegate address hash string for an EIP-7702 compatible EOA. - - # This function fetches the contract code for the given address and extracts - # the delegate address according to the EIP-7702 specification. - - # ## Parameters - # - `address_hash`: The address of the contract to check. - # - `options`: Optional keyword list of options (default: `[]`). - - # ## Returns - # - The delegate address in the hex string format if found and successfully decoded. - # - `nil` if the address doesn't exist, has no contract code, or the delegate address - # couldn't be extracted or decoded. - @spec get_implementation_address_hash_string(Hash.Address.t(), [Chain.api?()]) :: binary() | nil - defp get_implementation_address_hash_string(address_hash, options) do - case Chain.select_repo(options).get(Address, address_hash) do - nil -> - nil - - target_address -> - contract_code = target_address.contract_code - - case contract_code do - %Chain.Data{bytes: contract_code_bytes} -> - contract_code_bytes |> get_delegate_address() |> Proxy.abi_decode_address_output() + @behaviour ResolverBehaviour - _ -> - nil - end - end - end - - @doc """ - Extracts the EIP-7702 delegate address from the bytecode. - - This function analyzes the given bytecode to identify and extract the delegate - address according to the EIP-7702 specification. - - ## Parameters - - `contract_code_bytes`: The binary representation of the contract bytecode. - - ## Returns - - A string representation of the delegate address prefixed with "0x" if found. - - `nil` if the delegate address is not present in the bytecode. - - ## Examples - iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<239, 1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>>) - "0x0102030405060708090a0b0c0d0e0f1011121314" - - iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<239, 1, 0, 0x30, 0x78, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20>>) - "0x3078030405060708090a0b0c0d0e0f1011121314" - - iex> Explorer.Chain.SmartContract.Proxy.EIP7702.get_delegate_address(<<1, 2, 3>>) - nil - """ - @spec get_delegate_address(binary()) :: String.t() | nil - def get_delegate_address(contract_code_bytes) do - case contract_code_bytes do - # 0xef0100 <> address - <<239, 1, 0>> <> <> -> - {:ok, address_hash} = Hash.Address.cast(address) - to_string(address_hash) + def quick_resolve_implementations(proxy_address, _proxy_type \\ :eip7702) do + case proxy_address.contract_code && proxy_address.contract_code.bytes do + <<0xEF0100::3-unit(8), template_address::20-bytes>> -> + {:ok, template_address_hash} = Hash.Address.cast(template_address) + {:ok, [template_address_hash]} _ -> nil diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex index ef1ba2abbd0c..5db8750ab491 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex @@ -3,98 +3,68 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760 do Module for fetching proxy implementation from https://github.com/ethereum/ERCs/blob/master/ERCS/erc-7760.md """ - alias Explorer.Chain - alias Explorer.Chain.{Address, Hash} alias Explorer.Chain.SmartContract.Proxy - alias Explorer.Chain.SmartContract.Proxy.EIP1967 - - @uups_basic_variant "363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3" - @uups_l_variant "365814604357363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3" - @beacon_basic_variant "363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3" - @beacon_l_variant "363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3" - @transparent_basic_variant_20_left "3d3d3373" - @transparent_basic_variant_20_right "14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d356020355560408036111560525736038060403d373d3d355af43d6000803e6052573d6000fd" - @transparent_basic_variant_14_left "3d3d336d" - @transparent_basic_variant_14_right "14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e604c573d6000fd" - @transparent_l_variant_20_left "3658146083573d3d3373" - @transparent_l_variant_20_right "14605d57363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6058573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d373d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3" - @transparent_l_variant_14_left "365814607d573d3d336d" - @transparent_l_variant_14_right "14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d373d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3" - - @doc """ - Get implementation address hash string following ERC-7760. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t()) :: [binary()] - def get_implementation_address_hash_strings(proxy_address_hash, options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash, options) do - nil -> [] - implementation_address_hash_string -> [implementation_address_hash_string] - end - end - - # Get implementation address hash string following ERC-7760 - @spec get_implementation_address_hash_string(Hash.Address.t(), Keyword.t()) :: binary() | nil - defp get_implementation_address_hash_string(proxy_address_hash, options) do - case Chain.select_repo(options).get(Address, proxy_address_hash) do - nil -> - nil - - target_address -> - contract_code = target_address.contract_code - - case contract_code do - %Chain.Data{bytes: contract_code_bytes} -> - contract_bytecode = Base.encode16(contract_code_bytes, case: :lower) - - contract_bytecode |> get_proxy_erc_7760(proxy_address_hash) - - _ -> - nil - end - end - end - - # credo:disable-for-next-line /Complexity/ - defp get_proxy_erc_7760(contract_bytecode, proxy_address_hash) do - case String.downcase(contract_bytecode) do - @transparent_basic_variant_20_left <> - <<_factory_address::binary-size(40)>> <> @transparent_basic_variant_20_right <> _ -> - fetch_implementation(proxy_address_hash) - - @transparent_basic_variant_14_left <> - <<_factory_address::binary-size(28)>> <> @transparent_basic_variant_14_right <> _ -> - fetch_implementation(proxy_address_hash) - - @transparent_l_variant_20_left <> <<_factory_address::binary-size(40)>> <> @transparent_l_variant_20_right <> _ -> - fetch_implementation(proxy_address_hash) - - @transparent_l_variant_14_left <> <<_factory_address::binary-size(28)>> <> @transparent_l_variant_14_right <> _ -> - fetch_implementation(proxy_address_hash) - - @uups_basic_variant <> _ -> - fetch_implementation(proxy_address_hash) - - @uups_l_variant <> _ -> - fetch_implementation(proxy_address_hash) - - @beacon_basic_variant <> _ -> - fetch_implementation(proxy_address_hash) - - @beacon_l_variant <> _ -> - fetch_implementation(proxy_address_hash) - - _ -> - nil + alias Explorer.Chain.SmartContract.Proxy.{EIP1967, ResolverBehaviour} + + @behaviour ResolverBehaviour + + @transparent_basic_variant_20_left <<0x3D3D3373::4-unit(8)>> + @transparent_basic_variant_20_right <<0x14605757363D3D37363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E6052573D6000FD5B3D6000F35B3D356020355560408036111560525736038060403D373D3D355AF43D6000803E6052573D6000FD::824>> + @transparent_basic_variant_14_left <<0x3D3D336D::4-unit(8)>> + @transparent_basic_variant_14_right <<0x14605157363D3D37363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E604C573D6000FD5B3D6000F35B3D3560203555604080361115604C5736038060403D373D3D355AF43D6000803E604C573D6000FD::824>> + @transparent_i_variant_20_left <<0x3658146083573D3D3373::10-unit(8)>> + @transparent_i_variant_20_right <<0x14605D57363D3D37363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E6058573D6000FD5B3D6000F35B3D35602035556040360380156058578060403D373D3D355AF43D6000803E6058573D6000FD5B602060293D393D51543D52593DF3::928>> + @transparent_i_variant_14_left <<0x365814607D573D3D336D::10-unit(8)>> + @transparent_i_variant_14_right <<0x14605757363D3D37363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E6052573D6000FD5B3D6000F35B3D35602035556040360380156052578060403D373D3D355AF43D6000803E6052573D6000FD5B602060233D393D51543D52593DF3::928>> + @uups_basic_variant <<0x363D3D373D3D363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E6038573D6000FD5B3D6000F3::488>> + @uups_i_variant <<0x365814604357363D3D373D3D363D7F360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC545AF43D6000803E603E573D6000FD5B3D6000F35B6020600F3D393D51543D52593DF3::656>> + @beacon_basic_variant <<0x363D3D373D3D363D602036600436635C60DA1B60E01B36527FA3F0AD74E5423AEBFD80D3EF4346578335A9A72AEAEE59FF6CB3582B35133D50545AFA5036515AF43D6000803E604D573D6000FD5B3D6000F3::656>> + @beacon_i_variant <<0x363D3D373D3D363D602036600436635C60DA1B60E01B36527FA3F0AD74E5423AEBFD80D3EF4346578335A9A72AEAEE59FF6CB3582B35133D50545AFA361460525736515AF43D600060013E6052573D6001FD5B3D6001F3::696>> + + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + def quick_resolve_implementations(proxy_address, _proxy_type \\ :erc7760) do + eip1967_proxy_type = + case proxy_address.contract_code && proxy_address.contract_code.bytes do + <<@transparent_basic_variant_20_left, _::20-bytes, @transparent_basic_variant_20_right, _::binary>> -> + :eip1967 + + <<@transparent_basic_variant_14_left, _::14-bytes, @transparent_basic_variant_14_right, _::binary>> -> + :eip1967 + + <<@transparent_i_variant_20_left, _::20-bytes, @transparent_i_variant_20_right, _::binary>> -> + :eip1967 + + <<@transparent_i_variant_14_left, _::14-bytes, @transparent_i_variant_14_right, _::binary>> -> + :eip1967 + + @uups_basic_variant <> _ -> + :eip1967 + + @uups_i_variant <> _ -> + :eip1967 + + @beacon_basic_variant <> _ -> + :eip1967_beacon + + @beacon_i_variant <> _ -> + :eip1967_beacon + + _ -> + nil + end + + with false <- is_nil(eip1967_proxy_type), + {:cont, reqs} <- EIP1967.quick_resolve_implementations(proxy_address, eip1967_proxy_type), + resolvers_and_requirements = [{{EIP1967, eip1967_proxy_type}, reqs}], + {:ok, [{_, prefetched_values}]} <- Proxy.prefetch_values(resolvers_and_requirements, proxy_address.hash), + {:ok, address_hashes} <- EIP1967.resolve_implementations(proxy_address, eip1967_proxy_type, prefetched_values) do + {:ok, address_hashes} + else + :error -> :error + # proceed to other proxy types only if bytecode doesn't match + true -> nil + # if bytecode matches but resolution fails, we should halt + _ -> {:ok, []} end end - - defp fetch_implementation(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - Proxy.get_implementation_from_storage( - proxy_address_hash, - EIP1967.storage_slot_logic_contract_address(), - json_rpc_named_arguments - ) - end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex index f7ef76c0eda4..a6c91dfaf909 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex @@ -3,66 +3,43 @@ defmodule Explorer.Chain.SmartContract.Proxy.MasterCopy do Module for fetching master-copy proxy implementation """ - alias EthereumJSONRPC.Contract - alias Explorer.Chain.Hash alias Explorer.Chain.SmartContract.Proxy - - import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [is_burn_signature: 1] - - @doc """ - Gets implementation address hash string for proxy contract from master-copy pattern - """ - @spec get_implementation_address_hash_string(Hash.Address.t()) :: binary() | nil - def get_implementation_address_hash_string(proxy_address_hash) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - master_copy_storage_pointer = "0x0" - - implementation_address = - case Contract.eth_get_storage_at_request( - proxy_address_hash, - master_copy_storage_pointer, - nil, - json_rpc_named_arguments - ) do - {:ok, empty_address} - when is_burn_signature(empty_address) -> - "0x" - - {:ok, "0x" <> storage_value} -> - logic_contract_address = Proxy.extract_address_hex_from_storage_pointer(storage_value) - logic_contract_address - - _ -> - nil - end - - Proxy.abi_decode_address_output(implementation_address) - end - - @doc """ - Checks if the input of the smart-contract follows master-copy (or Safe) proxy pattern before - fetching its implementation from 0x0 storage pointer - """ - @spec pattern?(map()) :: any() - def pattern?(method) do - Map.get(method, "type") == "constructor" && - method - |> Enum.find(fn item -> - case item do - {"inputs", inputs} -> - find_input_by_name(inputs, "_masterCopy") || find_input_by_name(inputs, "_singleton") - - _ -> - false - end - end) - end - - defp find_input_by_name(inputs, name) do - inputs - |> Enum.find(fn input -> - Map.get(input, "name") == name - end) + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour + + @behaviour ResolverBehaviour + + # All known Safe Proxy bytecodes + # Proxy factory addresses are fetched from https://github.com/safe-global/safe-deployments/tree/main/src/assets + # Bytecodes are fetched from contract instances created by those factories + @safe_proxy_v1_0_0 <<0x608060405273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600054163660008037600080366000845AF43D6000803E6000811415603D573D6000FD5B3D6000F3FEA165627A7A723058201E7D648B83CFAC072CBCCEFC2FFC62A6999D4A050EE87A721942DE1DA9670DB80029::880>> + @safe_proxy_v1_1_1 <<0x608060405273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600054167FA619486E0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000F35B3660008037600080366000845AF43D6000803E60008114156070573D6000FD5B3D6000F3FEA265627A7A72315820D8A00DC4FE6BF675A9D7416FC2D00BB3433362AA8186B750F76C4027269667FF64736F6C634300050E0032::1360>> + @safe_proxy_v1_3_0 <<0x608060405273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600054167FA619486E0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000F35B3660008037600080366000845AF43D6000803E60008114156070573D6000FD5B3D6000F3FEA2646970667358221220D1429297349653A4918076D650332DE1A1068C5F3E07C5C82360C277770B955264736F6C63430007060033::1368>> + @safe_proxy_v1_3_0_zksync <<0x000400000000000200000000030100190000006003300270000000360430019700030000004103550002000000010355000000360030019D000100000000001F0000008001000039000000400010043F0000000101200190000000150000C13D000000000100041A00000037021001970000000201000367000000000301043B0000003D0330009C000000590000C13D00000000002004350000003E01000041000000D20001042E0000000001000416000000000110004C000000570000C13D0000000203000367000000400100043D00000000020000310000001F0420018F0000000505200272000000270000613D000000000600001900000005076002100000000008710019000000000773034F000000000707043B00000000007804350000000106600039000000000756004B0000001F0000413D000000000640004C000000360000613D0000000505500210000000000353034F00000000055100190000000304400210000000000605043300000000064601CF000000000646022F000000000303043B0000010004400089000000000343022F00000000034301CF000000000363019F00000000003504350000000003120019000000400030043F000000200220008C000000570000413D00000000010104330000003701100198000000BD0000C13D00000064013000390000003A02000041000000000021043500000044013000390000003B0200004100000000002104350000002401300039000000220200003900000000002104350000003C010000410000000000130435000000040130003900000020020000390000000000210435000000400100043D000000000213004900000084022000390000003603000041000000360420009C0000000002038019000000360410009C000000000103801900000040011002100000006002200210000000000112019F000000D3000104300000000001000019000000D30001043000000000030000310000001F0430018F0000000503300272000000650000613D00000000050000190000000506500210000000000761034F000000000707043B00000000007604350000000105500039000000000635004B0000005E0000413D000000000540004C000000730000613D00000003044002100000000503300210000000000503043300000000054501CF000000000545022F000000000131034F000000000101043B0000010004400089000000000141022F00000000014101CF000000000151019F000000000013043500000000010000310000000003000414000000040420008C000000930000C13D000000030100036700000001020000310000001F0320018F0000000502200272000000840000613D00000000040000190000000505400210000000000651034F000000000606043B00000000006504350000000104400039000000000524004B0000007D0000413D000000000430004C000000BA0000613D00000003033002100000000502200210000000000402043300000000043401CF000000000434022F000000000121034F000000000101043B0000010003300089000000000131022F00000000013101CF000000000141019F0000000000120435000000BA0000013D0000003604000041000000360530009C0000000003048019000000C0033002100000006001100210000000000113001900D100CC0000040F0003000000010355000000000301001900000060043002700000001F0340018F000100360040019D00000036044001970000000504400272000000AA0000613D00000000050000190000000506500210000000000761034F000000000707043B00000000007604350000000105500039000000000645004B000000A30000413D000000000530004C000000B80000613D00000003033002100000000504400210000000000504043300000000053501CF000000000535022F000000000141034F000000000101043B0000010003300089000000000131022F00000000013101CF000000000151019F00000000001404350000000101200190000000C60000613D000000600100003900000001011001FF000000D20001042E000000000200041A0000003802200197000000000112019F000000000010041B0000002001000039000001000010044300000120000004430000003901000041000000D20001042E00000036010000410000000102000031000000360320009C00000000010240190000006001100210000000D300010430000000CF002104250000000102000039000000000001042D0000000002000019000000000001042D000000D100000432000000D20001042E000000D300010430000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFF000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000002000000000000000000000000000000400000010000000000000000006564000000000000000000000000000000000000000000000000000000000000496E76616C69642073696E676C65746F6E20616464726573732070726F76696408C379A000000000000000000000000000000000000000000000000000000000A619486E0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ACBE897875BB4F3E88089713FAB44968F091FDEB912D0AFADD2FE5700E4E0CC6::16640>> + @safe_proxy_v1_4_1 <<0x608060405273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF600054167FA619486E0000000000000000000000000000000000000000000000000000000060003514156050578060005260206000F35B3660008037600080366000845AF43D6000803E60008114156070573D6000FD5B3D6000F3FEA264697066735822122003D1488EE65E08FA41E58E888A9865554C535F2C77126A82CB4C0F917F31441364736F6C63430007060033::1368>> + @safe_proxy_v1_4_1_zksync <<0x00010000000000020000008004000039000000400040043F000000000301001900000060033002700000002E033001970000000100200190000000100000C13D000000000200041A0000003002200197000000000401043B000000360040009C0000004B0000C13D000000000020043F0000003701000041000000B40001042E0000000002000416000000000002004B000000490000C13D0000001F0530018F0000002F0630019800000080026000390000001C0000613D000000000701034F000000007807043C0000000004840436000000000024004B000000180000C13D000000000005004B000000290000613D000000000161034F0000000304500210000000000502043300000000054501CF000000000545022F000000000101043B0000010004400089000000000141022F00000000014101CF000000000151019F00000000001204350000008001300039000000400010043F000000200030008C000000490000413D000000800200043D0000003002200198000000A30000C13D00000033020000410000000000210435000000E40130003900000034020000410000000000210435000000C40130003900000035020000410000000000210435000000A40130003900000022020000390000000000210435000000840130003900000020020000390000000000210435000000400100043D000000000213004900000104022000390000002E0020009C0000002E0200804100000060022002100000002E0010009C0000002E010080410000004001100210000000000112019F000000B5000104300000000001000019000000B5000104300000001F0530018F0000002F04300198000000540000613D000000000601034F0000000007000019000000006806043C0000000007870436000000000047004B000000500000C13D000000000005004B000000610000613D000000000641034F0000000305500210000000000704043300000000075701CF000000000757022F000000000606043B0000010005500089000000000656022F00000000055601CF000000000575019F00000000005404350000000004000414000000040020008C0000007D0000C13D000000000331034F000000000100003100000038021001980000001F0410018F0000006F0000613D000000000503034F0000000006000019000000005705043C0000000006760436000000000026004B0000006B0000C13D000000000004004B0000009F0000613D000000000323034F0000000304400210000000000502043300000000054501CF000000000545022F000000000303043B0000010004400089000000000343022F00000000034301CF000000000353019F00000000003204350000009F0000013D00000060013002100000002E0040009C0000002E04008041000000C003400210000000000113019F00B300AE0000040F000000000301001900000060033002700000001F0530018F0000002E0030019D0000002F043001980000008F0000613D000000000601034F0000000007000019000000006806043C0000000007870436000000000047004B0000008B0000C13D000000000005004B0000009C0000613D000000000141034F0000000305500210000000000604043300000000065601CF000000000656022F000000000101043B0000010005500089000000000151022F00000000015101CF000000000161019F00000000001404350000002E013001970000000100200190000000AC0000613D0000002E0010009C0000002E010080410000006001100210000000B40001042E000000000100041A0000003101100197000000000121019F000000000010041B0000002001000039000001000010044300000120000004430000003201000041000000B40001042E0000006001100210000000B500010430000000B1002104250000000102000039000000000001042D0000000002000019000000000001042D000000B300000432000000B40001042E000000B5000104300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFF00000000000000000000000000000000000000000000000000000000FFFFFFE0000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000020000000000000000000000000000004000000100000000000000000008C379A0000000000000000000000000000000000000000000000000000000006564000000000000000000000000000000000000000000000000000000000000496E76616C69642073696E676C65746F6E20616464726573732070726F766964A619486E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000000000000000000000000000000000000000000000000004D3D6AC1CA9BD3D9AB93E603DE7967DA33803409D7B4500E28F36CE7E4DD5A21::15104>> + @safe_proxy_v1_5_0 <<0x608060405260005463A619486E60003560E01C14156024578060601B606C5260206060F35B3660008037600080366000845AF43D6000803E806040573D6000FD5B3D6000F3FEA2646970667358221220E61834EBD2D8CD909D362BF67C47EF58FD665DF38E6DD036CE65611101D072E964736F6C63430007060033::984>> + + def quick_resolve_implementations(proxy_address, _proxy_type) do + with true <- + proxy_address.contract_code && + proxy_address.contract_code.bytes in [ + @safe_proxy_v1_0_0, + @safe_proxy_v1_1_1, + @safe_proxy_v1_3_0, + @safe_proxy_v1_3_0_zksync, + @safe_proxy_v1_4_1, + @safe_proxy_v1_4_1_zksync, + @safe_proxy_v1_5_0 + ], + {:ok, value} <- Proxy.fetch_value({:storage, "0x0"}, proxy_address.hash), + {:ok, address_hash} <- Proxy.extract_address_hash(value) do + {:ok, [address_hash]} + else + :error -> :error + # proceed to other proxy types only if bytecode doesn't match + false -> nil + # if bytecode matches but resolution fails, we should halt + _ -> {:ok, []} + end end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index 5c5cf97eabe8..f94085581b7e 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -22,11 +22,22 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do alias Explorer.Chain.SmartContract.Proxy.EIP7702 alias Timex.Duration - @burn_address_hash_string "0x0000000000000000000000000000000000000000" - @burn_address_hash_string_32 "0x0000000000000000000000000000000000000000000000000000000000000000" - @max_implementations_number_per_proxy 100 - - defguard is_burn_signature(term) when term in ["0x", "0x0", @burn_address_hash_string, @burn_address_hash_string_32] + @proxy_types [ + :eip1167, + :eip1967, + :eip1822, + :eip1967_oz, + :eip1967_beacon, + :master_copy, + :basic_implementation, + :basic_get_implementation, + :comptroller, + :eip2535, + :clone_with_immutable_arguments, + :eip7702, + :resolved_delegate_proxy, + :erc7760 + ] @typedoc """ * `proxy_address_hash` - proxy `smart_contract` address hash. @@ -39,32 +50,16 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do field(:proxy_address_hash, Hash.Address, primary_key: true, null: false) # the order matches order of enum values in the DB - field(:proxy_type, Ecto.Enum, - values: [ - :eip1167, - :eip1967, - :eip1822, - # todo: it is obsolete. Consider re-define the custom type in the future to remove this value. - :eip930, - :master_copy, - :basic_implementation, - :basic_get_implementation, - :comptroller, - :eip2535, - :clone_with_immutable_arguments, - :eip7702, - :resolved_delegate_proxy, - :erc7760, - :unknown - ], - null: true - ) + field(:proxy_type, Ecto.Enum, values: @proxy_types, null: true) field(:address_hashes, {:array, Hash.Address}, null: false) field(:names, {:array, :string}, null: false) + field(:conflicting_proxy_types, {:array, Ecto.Enum}, values: @proxy_types, null: true) + field(:conflicting_address_hashes, {:array, {:array, Hash.Address}}, null: true) has_many(:addresses, Address, foreign_key: :hash, references: :address_hashes) has_many(:smart_contracts, SmartContract, foreign_key: :address_hash, references: :address_hashes) + has_many(:conflicting_addresses, Address, foreign_key: :hash, references: :conflicting_address_hashes) belongs_to( :address, @@ -83,9 +78,15 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do :proxy_address_hash, :proxy_type, :address_hashes, + :names, + :conflicting_proxy_types, + :conflicting_address_hashes + ]) + |> validate_required([ + :proxy_address_hash, + :address_hashes, :names ]) - |> validate_required([:proxy_address_hash, :proxy_type, :address_hashes, :names]) |> unique_constraint([:proxy_address_hash]) end @@ -200,8 +201,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do def get_implementation( %{ updated: %SmartContract{ - address_hash: address_hash, - abi: abi + address_hash: address_hash }, implementation_updated_at: implementation_updated_at, implementation_address_fetched?: implementation_address_fetched?, @@ -218,7 +218,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do Task.async(fn -> # Here and only here we fetch implementations for the given address # using requests to the JSON RPC node for known proxy patterns - result = Proxy.fetch_implementation_address_hash(address_hash, abi, options) + result = Proxy.fetch_implementation_address_hash(address_hash, options) callback = Keyword.get(options, :callback, nil) uid = Keyword.get(options, :uid) @@ -311,123 +311,82 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do @doc """ Saves proxy's implementation into the DB """ - @spec save_implementation_data([String.t()], Hash.Address.t(), atom() | nil, Keyword.t()) :: - __MODULE__.t() | :empty | :error - def save_implementation_data(:error, _proxy_address_hash, _proxy_type, _options) do - :error - end - - def save_implementation_data(implementation_address_hash_strings, proxy_address_hash, proxy_type, options) - when implementation_address_hash_strings == [] do - upsert_implementations(proxy_address_hash, proxy_type, [], [], options) - - :empty - end - - def save_implementation_data( - [empty_implementation_address_hash_string], - proxy_address_hash, - proxy_type, - options + @spec save_implementation_data( + %{ + proxy_address_hash: Hash.Address.t(), + address_hashes: [Hash.Address.t()], + proxy_type: atom() | nil, + conflicting_proxy_types: [atom()] | nil, + conflicting_address_hashes: [[Hash.Address.t()]] | nil + }, + Keyword.t() + ) :: __MODULE__.t() | :error + def save_implementation_data(proxy_implementations, options) do + query = + from( + address in Address, + where: address.hash in ^proxy_implementations.address_hashes, + left_join: s1 in assoc(address, :smart_contract), + left_join: s2 in SmartContract, + on: fragment("md5(?)", address.contract_code) == s2.contract_code_md5 and is_nil(s1), + select: {address.hash, fragment("COALESCE(?, ?)", s1.name, s2.name)} ) - when is_burn_signature(empty_implementation_address_hash_string) do - upsert_implementations(proxy_address_hash, proxy_type, [], [], options) - :empty - end - - def save_implementation_data( - implementation_address_hash_strings, - proxy_address_hash, - proxy_type, - options - ) do - {implementation_addresses, implementation_names} = - implementation_address_hash_strings - |> Enum.map(fn implementation_address_hash_string -> - with {:ok, implementation_address_hash} <- Chain.string_to_address_hash(implementation_address_hash_string), - {:implementation, {%SmartContract{name: name}, _}} <- { - :implementation, - SmartContract.address_hash_to_smart_contract_with_bytecode_twin( - implementation_address_hash, - options, - false - ) - } do - {implementation_address_hash_string, name} - else - :error -> - :error - - {:implementation, _} -> - {implementation_address_hash_string, nil} - end - end) - |> Enum.filter(&(&1 !== :error)) - |> Enum.unzip() - - if Enum.empty?(implementation_addresses) do - :empty - else - case upsert_implementations( - proxy_address_hash, - proxy_type, - implementation_addresses, - implementation_names, - options - ) do - {:ok, result} -> - result + implementation_names_map = + query + |> Chain.select_repo(options).all() + |> Enum.into(%{}) - {:error, error} -> - Logger.error("Error while upserting proxy implementations data into the DB: #{inspect(error)}") - :error - end - end - end + implementation_names = proxy_implementations.address_hashes |> Enum.map(&Map.get(implementation_names_map, &1)) - defp upsert_implementations(proxy_address_hash, proxy_type, implementation_address_hash_strings, names, options) do - proxy = get_proxy_implementations(proxy_address_hash, options) + case upsert_implementations(proxy_implementations |> Map.put(:names, implementation_names)) do + {:ok, result} -> + result - if proxy do - update_implementations(proxy, proxy_type, implementation_address_hash_strings, names) - else - insert_implementations(proxy_address_hash, proxy_type, implementation_address_hash_strings, names) + {:error, error} -> + Logger.error("Error while upserting proxy implementations data into the DB: #{inspect(error)}") + :error end end - @spec insert_implementations(Hash.Address.t(), atom() | nil, [EthereumJSONRPC.hash()], [String.t()]) :: - {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - defp insert_implementations(proxy_address_hash, proxy_type, implementation_address_hash_strings, names) - when not is_nil(proxy_address_hash) do - sanitized_implementation_address_hash_strings = - sanitize_implementation_address_hash_strings(implementation_address_hash_strings) - - changeset = %{ - proxy_address_hash: proxy_address_hash, - proxy_type: proxy_type, - address_hashes: sanitized_implementation_address_hash_strings, - names: names - } - + @spec upsert_implementations(%{ + proxy_address_hash: Hash.Address.t(), + address_hashes: [Hash.Address.t()], + proxy_type: atom() | nil, + conflicting_proxy_types: [atom()] | nil, + conflicting_address_hashes: [[Hash.Address.t()]] | nil, + names: [String.t()] + }) :: {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} + defp upsert_implementations(proxy_implementations) do %__MODULE__{} - |> changeset(changeset) - |> Repo.insert() + |> changeset(proxy_implementations) + |> Repo.insert(on_conflict: on_conflict(), conflict_target: [:proxy_address_hash], allow_stale: true) end - @spec update_implementations(__MODULE__.t(), atom() | nil, [EthereumJSONRPC.hash()], [String.t()]) :: - {:ok, Ecto.Schema.t()} | {:error, Ecto.Changeset.t()} - defp update_implementations(proxy, proxy_type, implementation_address_hash_strings, names) do - sanitized_implementation_address_hash_strings = - sanitize_implementation_address_hash_strings(implementation_address_hash_strings) - - proxy - |> changeset(%{ - proxy_type: proxy_type, - address_hashes: sanitized_implementation_address_hash_strings, - names: names - }) - |> Repo.update() + defp on_conflict do + from( + proxy_implementations in __MODULE__, + update: [ + set: [ + proxy_type: fragment("EXCLUDED.proxy_type"), + address_hashes: fragment("EXCLUDED.address_hashes"), + names: fragment("EXCLUDED.names"), + conflicting_proxy_types: fragment("EXCLUDED.conflicting_proxy_types"), + conflicting_address_hashes: fragment("EXCLUDED.conflicting_address_hashes"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", proxy_implementations.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", proxy_implementations.updated_at) + ] + ], + where: + fragment("EXCLUDED.proxy_type <> ?", proxy_implementations.proxy_type) or + fragment("EXCLUDED.address_hashes <> ?", proxy_implementations.address_hashes) or + fragment("EXCLUDED.names <> ?", proxy_implementations.names) or + fragment("EXCLUDED.conflicting_proxy_types IS DISTINCT FROM ?", proxy_implementations.conflicting_proxy_types) or + fragment( + "EXCLUDED.conflicting_address_hashes IS DISTINCT FROM ?", + proxy_implementations.conflicting_address_hashes + ) + ) end @doc """ @@ -471,28 +430,28 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do addresses |> Repo.preload(:smart_contract) |> Enum.flat_map(fn address -> - with true <- Address.smart_contract?(address), - delegate_string when not is_nil(delegate_string) <- - EIP7702.get_delegate_address(address.contract_code.bytes), - {:ok, delegate_address_hash} <- Hash.Address.cast(delegate_string) do - name = - case address do - %{smart_contract: %{name: name}} -> name - _ -> nil - end - - [ - %{ - proxy_address_hash: address.hash, - proxy_type: :eip7702, - address_hashes: [delegate_address_hash], - names: [name], - inserted_at: now, - updated_at: now - } - ] - else - _ -> [] + case EIP7702.quick_resolve_implementations(address) do + {:ok, [delegate_address_hash]} -> + # credo:disable-for-lines:2 Credo.Check.Refactor.Nesting + name = + case address do + %{smart_contract: %{name: name}} -> name + _ -> nil + end + + [ + %{ + proxy_address_hash: address.hash, + proxy_type: :eip7702, + address_hashes: [delegate_address_hash], + names: [name], + inserted_at: now, + updated_at: now + } + ] + + _ -> + [] end end) @@ -509,6 +468,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do set: [ address_hashes: fragment("EXCLUDED.address_hashes"), names: fragment("EXCLUDED.names"), + conflicting_proxy_types: fragment("EXCLUDED.conflicting_proxy_types"), + conflicting_address_hashes: fragment("EXCLUDED.conflicting_address_hashes"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", proxy_implementations.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", proxy_implementations.updated_at) ] @@ -516,17 +477,18 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do where: fragment("EXCLUDED.proxy_type = ?", proxy_implementations.proxy_type) and (fragment("EXCLUDED.address_hashes <> ?", proxy_implementations.address_hashes) or - fragment("EXCLUDED.names <> ?", proxy_implementations.names)) + fragment("EXCLUDED.names <> ?", proxy_implementations.names) or + fragment( + "EXCLUDED.conflicting_proxy_types IS DISTINCT FROM ?", + proxy_implementations.conflicting_proxy_types + ) or + fragment( + "EXCLUDED.conflicting_address_hashes IS DISTINCT FROM ?", + proxy_implementations.conflicting_address_hashes + )) ) end - # Cut off implementations per proxy up to @max_implementations_number_per_proxy number - # before insert into the DB to prevent DoS via the verification endpoint of Diamond smart contracts. - @spec sanitize_implementation_address_hash_strings([EthereumJSONRPC.hash()]) :: [EthereumJSONRPC.hash()] - defp sanitize_implementation_address_hash_strings(implementation_address_hash_strings) do - Enum.take(implementation_address_hash_strings, @max_implementations_number_per_proxy) - end - @doc """ Returns proxy's implementation names """ @@ -536,7 +498,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do def names(proxy_address, options) when not is_nil(proxy_address) do proxy_implementations = get_proxy_implementations(proxy_address.hash, options) - if proxy_implementations && not Enum.empty?(proxy_implementations.names) do + if proxy_implementations do proxy_implementations.names else [] @@ -605,7 +567,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do |> Map.new(&{&1.hash, &1}) for ids <- nested_ids, - address when not is_nil(address) <- ids |> Enum.map(&addresses_map[&1]) do + address when not is_nil(address) <- ids |> List.flatten() |> Enum.map(&addresses_map[&1]) do {ids, address} end end @@ -618,10 +580,15 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do ## Examples iex> Explorer.Chain.SmartContract.Proxy.Models.Implementation.proxy_implementations_association() - [proxy_implementations: [addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1]] + [ + proxy_implementations: [ + addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1, + conflicting_addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1 + ] + ] """ @spec proxy_implementations_association() :: [ - proxy_implementations: [addresses: fun()] + proxy_implementations: [addresses: fun(), conflicting_addresses: fun()] ] def proxy_implementations_association do [proxy_implementations: proxy_implementations_addresses_association()] @@ -635,18 +602,25 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do ## Examples iex> Explorer.Chain.SmartContract.Proxy.Models.Implementation.proxy_implementations_addresses_association() - [addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1] + [ + addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1, + conflicting_addresses: &Explorer.Chain.SmartContract.Proxy.Models.Implementation.addresses_association_for_filecoin/1 + ] """ - @spec proxy_implementations_association() :: [addresses: fun()] + @spec proxy_implementations_addresses_association() :: [addresses: fun(), conflicting_addresses: fun()] def proxy_implementations_addresses_association do - [addresses: &__MODULE__.addresses_association_for_filecoin/1] + [ + addresses: &__MODULE__.addresses_association_for_filecoin/1, + conflicting_addresses: &__MODULE__.addresses_association_for_filecoin/1 + ] end def proxy_implementations_smart_contracts_association do [ proxy_implementations: [ addresses: &__MODULE__.addresses_association_for_filecoin/1, + conflicting_addresses: &__MODULE__.addresses_association_for_filecoin/1, smart_contracts: &__MODULE__.smart_contract_association_for_implementations/1 ] ] diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex index 60e44d5430c0..46741fe73979 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex @@ -2,147 +2,66 @@ defmodule Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy do @moduledoc """ Module for fetching proxy implementation from ResolvedDelegateProxy https://github.com/ethereum-optimism/optimism/blob/9580179013a04b15e6213ae8aa8d43c3f559ed9a/packages/contracts-bedrock/src/legacy/ResolvedDelegateProxy.sol """ - alias Explorer.Chain - alias Explorer.Chain.{Hash, SmartContract} - alias Explorer.SmartContract.Helper, as: SmartContractHelper - # 8da5cb5b = keccak256(owner()) - @owner_signature "8da5cb5b" - - # 204e1c7a = keccak256(getProxyImplementation(address)) - @get_proxy_implementation_signature "204e1c7a" - - @resolved_delegate_proxy_abi [ - %{ - "inputs" => [ - %{ - "internalType" => "contract AddressManager", - "name" => "_addressManager", - "type" => "address" - }, - %{ - "internalType" => "string", - "name" => "_implementationName", - "type" => "string" - } - ], - "stateMutability" => "nonpayable", - "type" => "constructor" - }, - %{"stateMutability" => "payable", "type" => "fallback"} - ] - - @owner_method_abi [ - %{ - "inputs" => [], - "name" => "owner", - "outputs" => [ - %{ - "internalType" => "address", - "name" => "", - "type" => "address" - } - ], - "stateMutability" => "view", - "type" => "function" - } - ] - - @get_proxy_implementation_method_abi [ - %{ - "inputs" => [ - %{ - "internalType" => "address", - "name" => "_proxy", - "type" => "address" - } - ], - "name" => "getProxyImplementation", - "outputs" => [ - %{ - "internalType" => "address", - "name" => "", - "type" => "address" - } - ], - "stateMutability" => "view", - "type" => "function" - } - ] - - @doc """ - Get implementation address hash string following ResolvedDelegateProxy proxy pattern. It returns the value as array of the strings. - """ - @spec get_implementation_address_hash_strings(Hash.Address.t(), [Chain.api?()]) :: [binary()] | :error - def get_implementation_address_hash_strings(proxy_address_hash, options \\ []) do - case get_implementation_address_hash_string(proxy_address_hash, options) do - nil -> [] + alias Explorer.Chain.{Data, Hash} + alias Explorer.Chain.SmartContract.Proxy + alias Explorer.Chain.SmartContract.Proxy.ResolverBehaviour + + @behaviour ResolverBehaviour + + @resolved_delegate_proxy <<0x608060408181523060009081526001602090815282822054908290529181207FBF40FAC1000000000000000000000000000000000000000000000000000000009093529173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9091169063BF40FAC19061006D9060846101E2565B602060405180830381865AFA15801561008A573D6000803E3D6000FD5B505050506040513D601F19601F820116820180604052508101906100AE91906102C5565B905073FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8116610157576040517F08C379A000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527F5265736F6C76656444656C656761746550726F78793A2074617267657420616460448201527F6472657373206D75737420626520696E697469616C697A656400000000000000606482015260840160405180910390FD5B6000808273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16600036604051610182929190610302565B600060405180830381855AF49150503D80600081146101BD576040519150601F19603F3D011682016040523D82523D6000602084013E6101C2565B606091505B5090925090508115156001036101DA57805160208201F35B805160208201FD5B600060208083526000845481600182811C91508083168061020457607F831692505B858310810361023A577F4E487B710000000000000000000000000000000000000000000000000000000085526022600452602485FD5B878601838152602001818015610257576001811461028B576102B6565B7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF008616825284151560051B820196506102B6565B60008B81526020902060005B868110156102B057815484820152908501908901610297565B83019750505B50949998505050505050505050565B6000602082840312156102D757600080FD5B815173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF811681146102FB57600080FD5B9392505050565B818382376000910190815291905056FEA164736F6C634300080F000A::6392>> + + def quick_resolve_implementations(proxy_address, _proxy_type) do + with {:match, @resolved_delegate_proxy} <- + {:match, proxy_address.contract_code && proxy_address.contract_code.bytes}, + reqs = get_fetch_requirements(proxy_address.hash), + {:ok, values} <- Proxy.fetch_values(reqs, proxy_address.hash), + {:ok, implementation_name} <- extract_short_string(values[reqs |> Enum.at(0)]), + {:ok, address_manager_address_hash} <- Proxy.extract_address_hash(values[reqs |> Enum.at(1)]), + {:ok, implementation_value} <- + Proxy.fetch_value( + {:call, "0x" <> Base.encode16(ABI.encode("getAddress(string)", [implementation_name]), case: :lower)}, + address_manager_address_hash + ), + {:ok, address_hash} <- Proxy.extract_address_hash(implementation_value) do + {:ok, [address_hash]} + else :error -> :error - implementation_address_hash_string -> [implementation_address_hash_string] + # proceed to other proxy types only if bytecode doesn't match + {:match, _} -> nil + # if bytecode matches but resolution fails, we should halt + _ -> {:ok, []} end end - @doc """ - Returns the ABI of the ResolvedDelegateProxy smart contract. - """ - @spec resolved_delegate_proxy_abi() :: [map()] - def resolved_delegate_proxy_abi do - @resolved_delegate_proxy_abi - end - - # Get implementation address hash string following ResolvedDelegateProxy proxy pattern - @spec get_implementation_address_hash_string(Hash.Address.t(), Keyword.t()) :: binary() | nil | :error - defp get_implementation_address_hash_string(proxy_address_hash, options) do - proxy_smart_contract = - proxy_address_hash - |> SmartContract.address_hash_to_smart_contract(options) - - if proxy_smart_contract && proxy_smart_contract.abi == @resolved_delegate_proxy_abi do - case SmartContract.format_constructor_arguments( - proxy_smart_contract.abi, - proxy_smart_contract.constructor_arguments - ) do - [[address_manager_hash_string, _address_manager_type_abi], _] -> - owner_address_hash_string = get_owner_from_address_manager(address_manager_hash_string) + @spec get_fetch_requirements(Hash.Address.t()) :: [ResolverBehaviour.fetch_requirement()] + def get_fetch_requirements(proxy_address_hash) do + # slot 0 + # mapping(address => string) private implementationName; + implementation_name_slot = ExKeccak.hash_256(<<0::96, proxy_address_hash.bytes::binary, 0::256>>) - get_implementation_from_owner(owner_address_hash_string, proxy_address_hash) + # slot 1 + # mapping(address => AddressManager) private addressManager; + address_manager_slot = ExKeccak.hash_256(<<0::96, proxy_address_hash.bytes::binary, 1::256>>) - _ -> - :error - end - else - nil - end - end - - defp get_owner_from_address_manager(address_manager_hash_string) do - case @owner_signature - |> SmartContractHelper.get_binary_string_from_contract_getter( - address_manager_hash_string, - @owner_method_abi - ) do - <> -> - owner_address_hash_string - - _other_result -> - nil - end + [ + storage: "0x" <> Base.encode16(implementation_name_slot, case: :lower), + storage: "0x" <> Base.encode16(address_manager_slot, case: :lower) + ] end - defp get_implementation_from_owner(nil, _proxy_address_hash), do: nil - - defp get_implementation_from_owner(owner_address_hash_string, proxy_address_hash) do - case @get_proxy_implementation_signature - |> SmartContractHelper.get_binary_string_from_contract_getter( - owner_address_hash_string, - @get_proxy_implementation_method_abi, - [to_string(proxy_address_hash)] - ) do - <> -> - implementation_address_hash_string - - _other_result -> - :error + # Decodes string value from smart-contract storage value, works only for short strings (<= 31 bytes) + @spec extract_short_string(String.t() | nil) :: {:ok, String.t()} | :error | nil + defp extract_short_string(value) do + with false <- is_nil(value), + {:ok, %Data{bytes: bytes}} <- Data.cast(value), + 32 <- byte_size(bytes), + double_length when double_length > 0 and double_length < 64 <- :binary.last(bytes), + 0 <- rem(double_length, 2) do + {:ok, binary_part(bytes, 0, div(double_length, 2))} + else + :error -> :error + _ -> nil end end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolver_behaviour.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolver_behaviour.ex new file mode 100644 index 000000000000..8eb46aa24b1b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolver_behaviour.ex @@ -0,0 +1,55 @@ +defmodule Explorer.Chain.SmartContract.Proxy.ResolverBehaviour do + @moduledoc """ + Behaviour for smart contract proxy resolvers + """ + + alias Explorer.Chain.{Address, Hash} + + @type fetch_requirement :: {:storage | :call, String.t()} + @type fetch_requirements :: %{atom() => fetch_requirement()} + @type fetched_values :: %{atom() => String.t() | nil} + + @optional_callbacks resolve_implementations: 3 + + @doc """ + Tries to immediately resolve implementations for the given proxy address and type, + e.g., when proxy matches the well-known bytecode pattern, so there is no ambiguity. + + For other proxy types, returns a list of fetch requirements that need to be fetched + before calling resolve_implementations, or nil if proxy type should not be resolved further. + + ## Parameters + - `proxy_address`: The address of the proxy contract. + - `proxy_type`: The type of the proxy contract. + + ## Returns + - `{:ok, [Hash.Address.t()]}` if proxy implementations are resolved immediately without ambiguity. + - `{:cont, fetch_requirements()}` if implementations cannot be resolved immediately, + caller should fetch given requirements and call resolve_implementations. + - `:error` if proxy resolution failed. + - `nil` if proxy pattern does not match and no further resolution for this type is necessary. + """ + @callback quick_resolve_implementations(proxy_address :: Address.t(), proxy_type :: atom()) :: + {:ok, [Hash.Address.t()]} | {:cont, fetch_requirements()} | :error | nil + + @doc """ + Resolves implementations for the given proxy address and type. + + ## Parameters + - `proxy_address`: The address of the proxy contract. + - `proxy_type`: The type of the proxy contract. + - `prefetched_values`: The values that were fetched in advance, according + to the fetch requirements returned by quick_resolve_implementations. + + ## Returns + - `{:ok, [Address.t()]}` if implementations are resolved. + - `:error` if resolution failed. + - `nil` if proxy pattern does not match. + """ + @callback resolve_implementations( + proxy_address :: Address.t(), + proxy_type :: atom(), + prefetched_values :: fetched_values() + ) :: + {:ok, [Hash.Address.t()]} | :error | nil +end diff --git a/apps/explorer/lib/explorer/etherscan/contracts.ex b/apps/explorer/lib/explorer/etherscan/contracts.ex index dd470fff87c2..ce33dc54fa5c 100644 --- a/apps/explorer/lib/explorer/etherscan/contracts.ex +++ b/apps/explorer/lib/explorer/etherscan/contracts.ex @@ -51,9 +51,7 @@ defmodule Explorer.Etherscan.Contracts do implementation_address_fetched?: false, refetch_necessity_checked?: false }, - [ - {:proxy_without_abi?, true} - ] + [] ) address_verified_bytecode_twin_contract = diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex index 8f7406485190..54977d5326bf 100644 --- a/apps/explorer/lib/test_helper.ex +++ b/apps/explorer/lib/test_helper.ex @@ -4,113 +4,193 @@ defmodule Explorer.TestHelper do import Mox alias ABI.TypeEncoder + alias Explorer.Chain.Hash + alias Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy - def mock_logic_storage_pointer_request( + @zero_address_hash %Hash{byte_count: 20, bytes: <<0::160>>} + @random_beacon_address_hash %Hash{byte_count: 20, bytes: <<0x3C7EC3E3B80D78FBDD348D796466AB828B45234F::160>>} + @random_address_manager_address_hash %Hash{byte_count: 20, bytes: <<0xBFCEF74A0522F50A48C759D05BCE97FAB2CA84C6::160>>} + + @implementation_name_storage_value "0x494d504c454d454e544154494f4e00000000000000000000000000000000001c" + # cast cd 'getAddress(string)' IMPLEMENTATION + @address_manager_calldata "0xbf40fac10000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e494d504c454d454e544154494f4e000000000000000000000000000000000000" + + def mock_erc7760_basic_requests( mox, error?, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" + %Hash{} = address_hash \\ @zero_address_hash ) do - response = if error?, do: {:error, "error"}, else: {:ok, resp} - - expect(mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", - params: [ - _, - "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", - "latest" - ] - }, + expect(mox, :json_rpc, fn [ + %{ + id: id, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + } + ], _options -> - response + if error?, + do: {:error, "error"}, + else: + {:ok, + [ + %{id: id, result: address_hash_to_full_hash_string(address_hash)} + ]} end) end - def mock_beacon_storage_pointer_request( + def mock_erc7760_beacon_requests( mox, error?, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" + %Hash{} = address_hash \\ @zero_address_hash ) do - response = if error?, do: {:error, "error"}, else: {:ok, resp} + if error? do + expect(mox, :json_rpc, fn [ + %{ + id: _, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + } + ], + _options -> + {:error, "error"} + end) + else + beacon_address_hash_string = to_string(@random_beacon_address_hash) - expect(mox, :json_rpc, fn %{ - id: 0, - method: "eth_getStorageAt", + mox + |> expect(:json_rpc, fn [ + %{ + id: id, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{id: id, result: address_hash_to_full_hash_string(@random_beacon_address_hash)} + ]} + end) + |> expect(:json_rpc, fn %{ + id: _, + method: "eth_call", params: [ - _, - "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + %{ + data: "0x5c60da1b", + to: ^beacon_address_hash_string + }, "latest" ] }, _options -> - response - end) + {:ok, address_hash_to_full_hash_string(address_hash)} + end) + end end - def mock_oz_storage_pointer_request( + def mock_resolved_delegate_proxy_requests( mox, - error?, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" + %Hash{} = proxy_address_hash, + %Hash{} = implementation_address_hash \\ @zero_address_hash ) do - response = if error?, do: {:error, "error"}, else: {:ok, resp} + proxy_address_hash_string = to_string(proxy_address_hash) + address_manager_address_hash_string = to_string(@random_address_manager_address_hash) - expect(mox, :json_rpc, fn %{ - id: 0, + [ + storage: implementation_name_slot, + storage: address_manager_slot + ] = ResolvedDelegateProxy.get_fetch_requirements(proxy_address_hash) + + mox + |> expect(:json_rpc, fn [ + %{ + id: id1, method: "eth_getStorageAt", params: [ - _, - "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", + ^proxy_address_hash_string, + ^implementation_name_slot, "latest" ] }, - _options -> - response - end) - end - - def mock_eip_1822_storage_pointer_request( - mox, - error?, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" - ) do - response = if error?, do: {:error, "error"}, else: {:ok, resp} - - expect(mox, :json_rpc, fn %{ - id: 0, + %{ + id: id2, method: "eth_getStorageAt", params: [ - _, - "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + ^proxy_address_hash_string, + ^address_manager_slot, "latest" ] - }, - _options -> - response + } + ], + _options -> + {:ok, + [ + %{id: id1, result: @implementation_name_storage_value}, + %{id: id2, result: address_hash_to_full_hash_string(@random_address_manager_address_hash)} + ]} end) - end - - def mock_eip_2535_storage_pointer_request( - mox, - error?, - resp \\ "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" - ) do - response = - if error?, - do: {:error, "error"}, - else: - {:ok, - [ + |> expect( + :json_rpc, + fn %{ + id: _, + method: "eth_call", + params: [ %{ - id: 0, - jsonrpc: "2.0", - result: resp - } - ]} + data: @address_manager_calldata, + to: ^address_manager_address_hash_string + }, + "latest" + ] + }, + _options -> + {:ok, address_hash_to_full_hash_string(implementation_address_hash)} + end + ) + end + def mock_generic_proxy_requests(mox, mocks \\ []) do expect(mox, :json_rpc, fn [ %{ - id: 0, - jsonrpc: "2.0", + id: id1, + method: "eth_getStorageAt", + params: [ + _, + "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc", + "latest" + ] + }, + %{ + id: id2, + method: "eth_getStorageAt", + params: [ + _, + "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7", + "latest" + ] + }, + %{ + id: id3, + method: "eth_getStorageAt", + params: [ + _, + "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50", + "latest" + ] + }, + %{ + id: id4, method: "eth_call", params: [ %{ @@ -119,133 +199,61 @@ defmodule Explorer.TestHelper do }, "latest" ] - } - ], - _options -> - response - end) - end - - def mock_resolved_delegate_proxy_get_owner_request( - mox, - error?, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" - ) do - response = - if error?, - do: {:error, "error"}, - else: - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: resp - } - ]} - - expect(mox, :json_rpc, fn [ + }, %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", + id: id5, + method: "eth_getStorageAt", params: [ - %{ - data: "0x8da5cb5b", - to: _ - }, + _, + "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3", "latest" ] } + | rest ], _options -> - response + {:ok, + [ + mocks |> Keyword.get(:eip1967, @zero_address_hash) |> encode_in_batch_response(id1), + mocks |> Keyword.get(:eip1822, @zero_address_hash) |> encode_in_batch_response(id2), + mocks |> Keyword.get(:eip1967_beacon, @zero_address_hash) |> encode_in_batch_response(id3), + %{id: id4, error: "error"}, + mocks |> Keyword.get(:eip1967_oz, @zero_address_hash) |> encode_in_batch_response(id5) + ] ++ + Enum.map(rest, fn + %{id: id6, method: "eth_call", params: [%{data: "0x5c60da1b", to: _}, "latest"]} -> + mocks |> Keyword.get(:basic_implementation, @zero_address_hash) |> encode_in_batch_response(id6) + end)} end) - end - - def mock_resolved_delegate_proxy_get_implementation_from_owner_request( - mox, - error?, - proxy_address_hash_string_without_0x, - resp \\ "0x0000000000000000000000000000000000000000000000000000000000000000" - ) do - data = "0x204e1c7a" <> "000000000000000000000000" <> proxy_address_hash_string_without_0x - response = - if error?, - do: {:error, "error"}, - else: - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: resp - } - ]} + if Keyword.get(mocks, :eip1967_beacon) && Keyword.get(mocks, :eip1967_beacon_implementation) do + beacon_address_hash_string = to_string(Keyword.get(mocks, :eip1967_beacon)) - expect(mox, :json_rpc, fn [ - %{ - id: _, - jsonrpc: "2.0", + expect(mox, :json_rpc, fn %{ + id: 0, method: "eth_call", params: [ %{ - data: ^data, - to: _ + data: "0x5c60da1b", + to: ^beacon_address_hash_string }, "latest" ] - } - ], - _options -> - response - end) + }, + _options -> + {:ok, address_hash_to_full_hash_string(Keyword.get(mocks, :eip1967_beacon_implementation))} + end) + end end - def get_eip1967_implementation_non_zero_address(implementation_address_hash_string) do - EthereumJSONRPC.Mox - |> mock_logic_storage_pointer_request(false) - |> mock_beacon_storage_pointer_request(false) - |> mock_oz_storage_pointer_request(false, "0x000000000000000000000000" <> implementation_address_hash_string) - end + defp encode_in_batch_response(%Hash{byte_count: 20, bytes: _} = address_hash, id), + do: %{id: id, result: address_hash_to_full_hash_string(address_hash)} - def get_resolved_delegate_proxy_implementation_non_zero_address( - owner_address_hash_string_without_0x, - implementation_address_hash_string_without_0x, - proxy_address_hash_string_without_0x - ) do - EthereumJSONRPC.Mox - |> mock_logic_storage_pointer_request(false) - |> mock_beacon_storage_pointer_request(false) - |> mock_oz_storage_pointer_request(false) - |> mock_eip_1822_storage_pointer_request(false) - |> mock_eip_2535_storage_pointer_request(false) - |> mock_resolved_delegate_proxy_get_owner_request( - false, - "0x000000000000000000000000" <> owner_address_hash_string_without_0x - ) - |> mock_resolved_delegate_proxy_get_implementation_from_owner_request( - false, - proxy_address_hash_string_without_0x, - "0x000000000000000000000000" <> implementation_address_hash_string_without_0x - ) - end + defp encode_in_batch_response(:error, id), + do: %{id: id, error: "error"} - def get_all_proxies_implementation_zero_addresses do - EthereumJSONRPC.Mox - |> mock_logic_storage_pointer_request(false) - |> mock_beacon_storage_pointer_request(false) - |> mock_oz_storage_pointer_request(false) - |> mock_eip_1822_storage_pointer_request(false) - |> mock_eip_2535_storage_pointer_request(false) - end - - def get_eip1967_implementation_error_response do - EthereumJSONRPC.Mox - |> mock_logic_storage_pointer_request(true) - |> mock_beacon_storage_pointer_request(true) - |> mock_oz_storage_pointer_request(true) + defp address_hash_to_full_hash_string(%Hash{byte_count: 20, bytes: bytes}) do + to_string(%Hash{byte_count: 32, bytes: <<0::96, bytes::binary>>}) end def fetch_token_uri_mock(url, token_contract_address_hash_string) do diff --git a/apps/explorer/priv/repo/migrations/20250718092418_refactor_proxy_implementations.exs b/apps/explorer/priv/repo/migrations/20250718092418_refactor_proxy_implementations.exs new file mode 100644 index 000000000000..b3fcd055cd7d --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250718092418_refactor_proxy_implementations.exs @@ -0,0 +1,29 @@ +defmodule Explorer.Repo.Migrations.RefactorProxyImplementations do + use Ecto.Migration + + def change do + execute(""" + UPDATE proxy_implementations + SET proxy_type = NULL, + address_hashes = ARRAY []::bytea[], + names = ARRAY []::bytea[] + WHERE proxy_type IN ('eip930', 'unknown') + OR address_hashes = ARRAY ['\\x0000000000000000000000000000000000000000'::bytea] + """) + + alter table(:proxy_implementations) do + add(:conflicting_proxy_types, {:array, :proxy_type}) + add(:conflicting_address_hashes, {:array, {:array, :bytea}}) + end + + execute( + "ALTER TYPE proxy_type RENAME VALUE 'eip930' TO 'eip1967_oz'", + "ALTER TYPE proxy_type RENAME VALUE 'eip1967_oz' TO 'eip930'" + ) + + execute( + "ALTER TYPE proxy_type RENAME VALUE 'unknown' TO 'eip1967_beacon'", + "ALTER TYPE proxy_type RENAME VALUE 'eip1967_beacon' TO 'unknown'" + ) + end +end diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs deleted file mode 100644 index d8d45d3f93d4..000000000000 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy/eip_7702_test.exs +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Explorer.Chain.SmartContract.Proxy.EIP7702Test do - use Explorer.DataCase - - doctest Explorer.Chain.SmartContract.Proxy.EIP7702 -end diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/erc_7760_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/erc_7760_test.exs index be09813a3f2d..14f4daaae324 100644 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy/erc_7760_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/erc_7760_test.exs @@ -5,44 +5,40 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760Test do alias Explorer.TestHelper @uups_basic_variant "363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6038573d6000fd5b3d6000f3" - @uups_l_variant "365814604357363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3" + @uups_i_variant "365814604357363d3d373d3d363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e603e573d6000fd5b3d6000f35b6020600f3d393d51543d52593df3" @beacon_basic_variant "363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa5036515af43d6000803e604d573d6000fd5b3d6000f3" - @beacon_l_variant "363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3" + @beacon_i_variant "363d3d373d3d363d602036600436635c60da1b60e01b36527fa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50545afa361460525736515af43d600060013e6052573d6001fd5b3d6001f3" @transparent_basic_variant_20_left "3d3d3373" @transparent_basic_variant_20_right "14605757363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d356020355560408036111560525736038060403d373d3d355af43d6000803e6052573d6000fd" @transparent_basic_variant_14_left "3d3d336d" @transparent_basic_variant_14_right "14605157363d3d37363d7f360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e604c573d6000fd5b3d6000f35b3d3560203555604080361115604c5736038060403d373d3d355af43d6000803e604c573d6000fd" - @transparent_l_variant_20_left "3658146083573d3d3373" - @transparent_l_variant_20_right "14605D57363d3d37363D7f360894a13ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6058573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d373d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3" - @transparent_l_variant_14_left "365814607d573d3d336d" - @transparent_l_variant_14_right "14605757363d3D37363d7F360894A13Ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d373d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3" + @transparent_i_variant_20_left "3658146083573d3d3373" + @transparent_i_variant_20_right "14605D57363d3d37363D7f360894a13ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6058573d6000fd5b3d6000f35b3d35602035556040360380156058578060403d373d3d355af43d6000803e6058573d6000fd5b602060293d393d51543d52593df3" + @transparent_i_variant_14_left "365814607d573d3d336d" + @transparent_i_variant_14_right "14605757363d3D37363d7F360894A13Ba1A3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc545af43d6000803e6052573d6000fd5b3d6000f35b3d35602035556040360380156052578060403d373d3d355af43d6000803e6052573d6000fd5b602060233d393d51543d52593df3" - describe "get_implementation_address_hash_strings/2" do - test "returns implementation address hash string for valid proxy address with uups_basic_variant" do + describe "quick_resolve_implementations/2" do + test "returns implementation address hash for valid proxy address with uups_basic_variant" do proxy_address = insert(:address, contract_code: "0x" <> @uups_basic_variant) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with uups_basic_variant and offset" do + test "returns implementation address hash for valid proxy address with uups_basic_variant and offset" do proxy_address = insert(:address, contract_code: "0x" <> @uups_basic_variant <> "1234") implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_basic_variant_20" do + test "returns implementation address hash for valid proxy address with transparent_basic_variant_20" do proxy_address = insert(:address, contract_code: @@ -54,14 +50,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760Test do implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_basic_variant_20 and offset" do + test "returns implementation address hash for valid proxy address with transparent_basic_variant_20 and offset" do proxy_address = insert(:address, contract_code: @@ -73,14 +67,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760Test do implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_basic_variant_14" do + test "returns implementation address hash for valid proxy address with transparent_basic_variant_14" do proxy_address = insert(:address, contract_code: @@ -92,14 +84,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760Test do implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_basic_variant_14 and offset" do + test "returns implementation address hash for valid proxy address with transparent_basic_variant_14 and offset" do proxy_address = insert(:address, contract_code: @@ -111,157 +101,135 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760Test do implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_l_variant_20" do + test "returns implementation address hash for valid proxy address with transparent_i_variant_20" do proxy_address = insert(:address, contract_code: "0x" <> - @transparent_l_variant_20_left <> - "1234567890123456789012345678901234567890" <> @transparent_l_variant_20_right + @transparent_i_variant_20_left <> + "1234567890123456789012345678901234567890" <> @transparent_i_variant_20_right ) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_l_variant_20 and offset" do + test "returns implementation address hash for valid proxy address with transparent_i_variant_20 and offset" do proxy_address = insert(:address, contract_code: "0x" <> - @transparent_l_variant_20_left <> - "1234567890123456789012345678901234567890" <> @transparent_l_variant_20_right <> "1234" + @transparent_i_variant_20_left <> + "1234567890123456789012345678901234567890" <> @transparent_i_variant_20_right <> "1234" ) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_l_variant_14" do + test "returns implementation address hash for valid proxy address with transparent_i_variant_14" do proxy_address = insert(:address, contract_code: - "0x" <> @transparent_l_variant_14_left <> "1234567890123456789012341234" <> @transparent_l_variant_14_right + "0x" <> @transparent_i_variant_14_left <> "1234567890123456789012341234" <> @transparent_i_variant_14_right ) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with transparent_l_variant_14 and offset" do + test "returns implementation address hash for valid proxy address with transparent_i_variant_14 and offset" do proxy_address = insert(:address, contract_code: "0x" <> - @transparent_l_variant_14_left <> - "1234567890123456789012341234" <> @transparent_l_variant_14_right <> "1234" + @transparent_i_variant_14_left <> + "1234567890123456789012341234" <> @transparent_i_variant_14_right <> "1234" ) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with uups_l_variant" do - proxy_address = insert(:address, contract_code: "0x" <> @uups_l_variant) + test "returns implementation address hash for valid proxy address with uups_i_variant" do + proxy_address = insert(:address, contract_code: "0x" <> @uups_i_variant) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with uups_l_variant and offset" do - proxy_address = insert(:address, contract_code: "0x" <> @uups_l_variant <> "1234") + test "returns implementation address hash for valid proxy address with uups_i_variant and offset" do + proxy_address = insert(:address, contract_code: "0x" <> @uups_i_variant <> "1234") implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_basic_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with beacon_basic_variant" do + test "returns implementation address hash for valid proxy address with beacon_basic_variant" do proxy_address = insert(:address, contract_code: "0x" <> @beacon_basic_variant) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_beacon_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with beacon_basic_variant and offset" do + test "returns implementation address hash for valid proxy address with beacon_basic_variant and offset" do proxy_address = insert(:address, contract_code: "0x" <> @beacon_basic_variant <> "1234") implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_beacon_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with beacon_l_variant" do - proxy_address = insert(:address, contract_code: "0x" <> @beacon_l_variant) + test "returns implementation address hash for valid proxy address with beacon_i_variant" do + proxy_address = insert(:address, contract_code: "0x" <> @beacon_i_variant) implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_beacon_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end - test "returns implementation address hash string for valid proxy address with beacon_l_variant and offset" do - proxy_address = insert(:address, contract_code: "0x" <> @beacon_l_variant <> "1234") + test "returns implementation address hash for valid proxy address with beacon_i_variant and offset" do + proxy_address = insert(:address, contract_code: "0x" <> @beacon_i_variant <> "1234") implementation_address = insert(:address) EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, to_string(implementation_address.hash)) + |> TestHelper.mock_erc7760_beacon_requests(false, implementation_address.hash) - assert ERC7760.get_implementation_address_hash_strings(proxy_address.hash) == [ - to_string(implementation_address.hash) - ] + assert ERC7760.quick_resolve_implementations(proxy_address) == {:ok, [implementation_address.hash]} end end end diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs index ac8709d1b7e1..818a6b5d7f1f 100644 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs @@ -26,9 +26,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do refute_implementations(smart_contract.address_hash) - # fetch nil implementation and don't save it to db - TestHelper.get_all_proxies_implementation_zero_addresses() - assert is_nil(Implementation.get_implementation(smart_contract)) + # fetch nil implementation + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() + + assert %Implementation{address_hashes: [], names: [], proxy_type: nil} = + Implementation.get_implementation(smart_contract) + verify!(EthereumJSONRPC.Mox) assert_empty_implementation(smart_contract.address_hash) @@ -40,15 +44,15 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do Application.put_env(:explorer, :proxy, proxy) - implementation_address_hash_string = to_string(implementation_smart_contract.address_hash) - - expect_address_in_oz_slot_response(implementation_address_hash_string) implementation_address_hash = implementation_smart_contract.address_hash + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967_oz: implementation_address_hash) + assert %Implementation{ address_hashes: [^implementation_address_hash], names: ["implementation"], - proxy_type: :eip1967 + proxy_type: :eip1967_oz } = Implementation.get_implementation(smart_contract) verify!(EthereumJSONRPC.Mox) @@ -59,12 +63,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do implementation_smart_contract.name ) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: :error) assert %Implementation{ address_hashes: [^implementation_address_hash], names: ["implementation"], - proxy_type: :eip1967 + proxy_type: :eip1967_oz } = Implementation.get_implementation(smart_contract) verify!(EthereumJSONRPC.Mox) @@ -88,7 +93,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do assert %Implementation{ address_hashes: [^implementation_address_hash], names: ["implementation"], - proxy_type: :eip1967 + proxy_type: :eip1967_oz } = Implementation.get_implementation(smart_contract) {contract_2, _} = SmartContract.address_hash_to_smart_contract_with_bytecode_twin(smart_contract.address_hash) @@ -104,14 +109,14 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do Application.put_env(:explorer, :proxy, proxy) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() - assert is_nil(Implementation.get_implementation(smart_contract)) + assert %Implementation{address_hashes: [], names: [], proxy_type: nil} = + Implementation.get_implementation(smart_contract) verify!(EthereumJSONRPC.Mox) - - assert implementation_1.updated_at == implementation_2.updated_at && - contract_1.updated_at == contract_2.updated_at + assert_empty_implementation(smart_contract.address_hash) proxy = :explorer @@ -127,7 +132,9 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do smart_contract = insert(:smart_contract) twin_address = insert(:contract_address) - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() + {bytecode_twin, _} = SmartContract.address_hash_to_smart_contract_with_bytecode_twin(twin_address.hash) implementation_smart_contract = insert(:smart_contract, name: "implementation") @@ -140,16 +147,18 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do Application.put_env(:explorer, :proxy, proxy) # fetch nil implementation - assert %Implementation{address_hashes: [], names: [], proxy_type: :unknown} = + assert %Implementation{address_hashes: [], names: [], proxy_type: nil} = Implementation.get_implementation(bytecode_twin) verify!(EthereumJSONRPC.Mox) + assert_empty_implementation(bytecode_twin.address_hash) refute_implementations(smart_contract.address_hash) - assert %Implementation{address_hashes: [], names: [], proxy_type: :unknown} = + assert %Implementation{address_hashes: [], names: [], proxy_type: nil} = Implementation.get_implementation(bytecode_twin) verify!(EthereumJSONRPC.Mox) + assert_empty_implementation(bytecode_twin.address_hash) refute_implementations(smart_contract.address_hash) proxy = @@ -160,11 +169,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do Application.put_env(:explorer, :proxy, proxy) - string_implementation_address_hash = to_string(implementation_smart_contract.address_hash) - - expect_address_in_oz_slot_response(string_implementation_address_hash) implementation_address_hash = implementation_smart_contract.address_hash + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address_hash) + assert %Implementation{ address_hashes: [^implementation_address_hash], names: ["implementation"], @@ -236,7 +245,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do # refute_implementations(smart_contract.address_hash) - # TestHelper.get_all_proxies_implementation_zero_addresses() + # EthereumJSONRPC.Mox + # |> TestHelper.mock_generic_proxy_requests() # {:ok, addr} = # Chain.find_contract_address( @@ -261,12 +271,14 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do # Application.put_env(:explorer, :proxy, proxy) # # fetch nil implementation - # TestHelper.get_all_proxies_implementation_zero_addresses() + # EthereumJSONRPC.Mox + # |> TestHelper.mock_generic_proxy_requests() # assert {[], [], nil} = Implementation.get_implementation(bytecode_twin) # verify!(EthereumJSONRPC.Mox) # refute_implementations(smart_contract.address_hash) - # TestHelper.get_all_proxies_implementation_zero_addresses() + # EthereumJSONRPC.Mox + # |> TestHelper.mock_generic_proxy_requests() # assert {[], [], nil} = Implementation.get_implementation(bytecode_twin) # verify!(EthereumJSONRPC.Mox) # refute_implementations(smart_contract.address_hash) @@ -281,7 +293,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do # refute_implementations(smart_contract.address_hash) - # TestHelper.get_all_proxies_implementation_zero_addresses() + # EthereumJSONRPC.Mox + # |> TestHelper.mock_generic_proxy_requests() # assert {[], [], nil} = Implementation.get_implementation(bytecode_twin) @@ -291,6 +304,89 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do end end + test "get_implementation/1 with conflicting implementations" do + smart_contract = insert(:smart_contract) + implementation_smart_contract1 = insert(:smart_contract, name: "implementation1") + implementation_smart_contract2 = insert(:smart_contract, name: "implementation2") + implementation_smart_contract3 = insert(:smart_contract, name: "implementation3") + + implementation_address_hash1 = implementation_smart_contract1.address_hash + implementation_address_hash2 = implementation_smart_contract2.address_hash + implementation_address_hash3 = implementation_smart_contract3.address_hash + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests( + eip1967: implementation_address_hash1, + eip1967_oz: implementation_address_hash1, + eip1822: implementation_address_hash1 + ) + + assert %Implementation{ + address_hashes: [^implementation_address_hash1], + names: ["implementation1"], + proxy_type: :eip1967, + conflicting_proxy_types: nil, + conflicting_address_hashes: nil + } = Implementation.get_implementation(smart_contract) + + verify!(EthereumJSONRPC.Mox) + + assert_exact_name_and_address( + smart_contract.address_hash, + implementation_address_hash1, + implementation_smart_contract1.name + ) + + assert %Implementation{ + conflicting_proxy_types: nil, + conflicting_address_hashes: nil + } = Implementation.get_proxy_implementations(smart_contract.address_hash) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(0)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests( + eip1967: implementation_address_hash1, + eip1967_oz: implementation_address_hash2, + eip1822: implementation_address_hash3 + ) + + assert %Implementation{ + address_hashes: [^implementation_address_hash1], + names: ["implementation1"], + proxy_type: :eip1967, + conflicting_proxy_types: [:eip1822, :eip1967_oz], + conflicting_address_hashes: [[^implementation_address_hash3], [^implementation_address_hash2]] + } = Implementation.get_implementation(smart_contract) + + verify!(EthereumJSONRPC.Mox) + + proxy = + :explorer + |> Application.get_env(:proxy) + |> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20)) + |> Keyword.replace(:implementation_data_fetching_timeout, :timer.seconds(20)) + + Application.put_env(:explorer, :proxy, proxy) + + assert_exact_name_and_address( + smart_contract.address_hash, + implementation_address_hash1, + implementation_smart_contract1.name + ) + + assert %Implementation{ + conflicting_proxy_types: [:eip1822, :eip1967_oz], + conflicting_address_hashes: [[^implementation_address_hash3], [^implementation_address_hash2]] + } = Implementation.get_proxy_implementations(smart_contract.address_hash) + end + def assert_exact_name_and_address(address_hash, implementation_address_hash, implementation_name) do implementation = Implementation.get_proxy_implementations(address_hash) assert implementation.proxy_type @@ -308,13 +404,6 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do assert implementation.names end - defp expect_address_in_oz_slot_response(string_implementation_address_hash) do - EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false) - |> TestHelper.mock_beacon_storage_pointer_request(false) - |> TestHelper.mock_oz_storage_pointer_request(false, string_implementation_address_hash) - end - def refute_implementations(address_hash) do implementations = Implementation.get_proxy_implementations(address_hash) refute implementations @@ -322,9 +411,11 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do def assert_empty_implementation(address_hash) do implementation = Implementation.get_proxy_implementations(address_hash) - assert implementation.proxy_type == :unknown + assert is_nil(implementation.proxy_type) assert implementation.updated_at assert implementation.names == [] assert implementation.address_hashes == [] + assert is_nil(implementation.conflicting_proxy_types) + assert is_nil(implementation.conflicting_address_hashes) end end diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs index 5a3f74a04c56..faa57ae70c04 100644 --- a/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs @@ -1,6 +1,7 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do use Explorer.DataCase, async: false import Mox + alias Explorer.Chain.Hash alias Explorer.Chain.SmartContract alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.SmartContract.Proxy.Models.Implementation @@ -139,7 +140,8 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do end test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() proxy_contract_address = insert(:contract_address) @@ -150,7 +152,8 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do end test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() proxy_contract_address = insert(:contract_address) @@ -207,13 +210,14 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do smart_contract = insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() assert Proxy.get_implementation_abi_from_proxy(smart_contract, []) == [] end - test "get_implementation_abi_from_proxy/2 returns implementation abi if implementation is verified" do - proxy_contract_address = insert(:contract_address) + test "get_implementation_abi_from_proxy/2 returns implementation abi if implementation is verified (basic_implementation proxy)" do + proxy_contract_address = insert(:contract_address, contract_code: "0xDEADBEEF5c60da1bDEADBEEF") smart_contract = insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123") @@ -226,25 +230,8 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do contract_code_md5: "123" ) - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - TestHelper.get_all_proxies_implementation_zero_addresses() - - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: _, params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x000000000000000000000000" <> implementation_contract_address_hash_string - } - ]} - end - ) + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(basic_implementation: implementation_contract_address.hash) implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) @@ -265,13 +252,8 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do contract_code_md5: "123" ) - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - response = "0x000000000000000000000000" <> implementation_contract_address_hash_string - EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false, response) + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_contract_address.hash) implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) @@ -302,8 +284,6 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do contract_code_md5: "123" ) - beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) - implementation_contract_address = insert(:contract_address) insert(:smart_contract, @@ -312,13 +292,10 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do contract_code_md5: "123" ) - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - eip_1967_beacon_proxy_mock_requests( - beacon_contract_address_hash_string, - implementation_contract_address_hash_string, - :full_32 + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests( + eip1967_beacon: beacon_contract_address.hash, + eip1967_beacon_implementation: implementation_contract_address.hash ) implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) @@ -327,82 +304,48 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do assert implementation_abi == @implementation_abi end - test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (beacon contract) when eth_getStorageAt returns 20 bytes address" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - - beacon_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: beacon_contract_address.hash, - abi: @beacon_abi, - contract_code_md5: "123" - ) - - beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - eip_1967_beacon_proxy_mock_requests( - beacon_contract_address_hash_string, - implementation_contract_address_hash_string, - :exact_20 - ) - - implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) - verify!(EthereumJSONRPC.Mox) - - assert implementation_abi == @implementation_abi - end - - test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern (beacon contract) when eth_getStorageAt returns less 20 bytes address" do - proxy_contract_address = insert(:contract_address) - - smart_contract = - insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123") - - beacon_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: beacon_contract_address.hash, - abi: @beacon_abi, - contract_code_md5: "123" - ) - - beacon_contract_address_hash_string = Base.encode16(beacon_contract_address.hash.bytes, case: :lower) - - implementation_contract_address = insert(:contract_address) - - insert(:smart_contract, - address_hash: implementation_contract_address.hash, - abi: @implementation_abi, - contract_code_md5: "123" - ) - - implementation_contract_address_hash_string = - Base.encode16(implementation_contract_address.hash.bytes, case: :lower) - - eip_1967_beacon_proxy_mock_requests( - beacon_contract_address_hash_string, - implementation_contract_address_hash_string, - :short - ) - - implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, []) - verify!(EthereumJSONRPC.Mox) - - assert implementation_abi == @implementation_abi + test "extract_address_hash/1" do + assert Proxy.extract_address_hash(nil) == nil + assert Proxy.extract_address_hash("0x") == nil + assert Proxy.extract_address_hash("0x0") == nil + assert Proxy.extract_address_hash("0x0000000000000000000000000000000000000000") == nil + assert Proxy.extract_address_hash("0x0000000000000000000000000000000000000000000000000000000000000000") == nil + assert Proxy.extract_address_hash("INVALID") == :error + + # 20-bytes value + assert Proxy.extract_address_hash("0x1111111111111111111111111111111111111111") == + {:ok, + %Hash{ + byte_count: 20, + bytes: <<0x1111111111111111111111111111111111111111::160>> + }} + + # 19-bytes value + assert Proxy.extract_address_hash("0x11111111111111111111111111111111111111") == + {:ok, + %Hash{ + byte_count: 20, + bytes: <<0x0011111111111111111111111111111111111111::160>> + }} + + # 21-bytes value + assert Proxy.extract_address_hash("0x001111111111111111111111111111111111111111") == + {:ok, + %Hash{ + byte_count: 20, + bytes: <<0x1111111111111111111111111111111111111111::160>> + }} + + # canonical 32-bytes value + assert Proxy.extract_address_hash("0x0000000000000000000000001111111111111111111111111111111111111111") == + {:ok, + %Hash{ + byte_count: 20, + bytes: <<0x1111111111111111111111111111111111111111::160>> + }} + + # 33-bytes value + assert Proxy.extract_address_hash("0x000000000000000000000000001111111111111111111111111111111111111111") == nil end test "check proxy_contract?/1 function" do @@ -419,7 +362,9 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do refute_implementations(smart_contract.address_hash) # fetch nil implementation and don't save it to db - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() + refute Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) assert_empty_implementation(smart_contract.address_hash) @@ -431,13 +376,17 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do Application.put_env(:explorer, :proxy, proxy) - TestHelper.get_eip1967_implementation_error_response() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: :error) + refute Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) implementation_address = insert(:address) - implementation_address_hash_string = to_string(implementation_address.hash) - TestHelper.get_eip1967_implementation_non_zero_address(implementation_address_hash_string) + + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests(eip1967: implementation_address.hash) + assert Proxy.proxy_contract?(smart_contract) verify!(EthereumJSONRPC.Mox) assert_implementation_address(smart_contract.address_hash) @@ -476,48 +425,6 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do Application.put_env(:explorer, :proxy, proxy) end - defp eip_1967_beacon_proxy_mock_requests( - beacon_contract_address_hash_string, - implementation_contract_address_hash_string, - mode - ) do - response = - case mode do - :full_32 -> "0x000000000000000000000000" <> beacon_contract_address_hash_string - :exact_20 -> "0x" <> beacon_contract_address_hash_string - :short -> "0x" <> String.slice(beacon_contract_address_hash_string, 10..-1//1) - end - - EthereumJSONRPC.Mox - |> TestHelper.mock_logic_storage_pointer_request(false) - |> TestHelper.mock_beacon_storage_pointer_request(false, response) - |> expect( - :json_rpc, - fn [ - %{ - id: id, - method: "eth_call", - params: [ - %{data: "0x5c60da1b", to: "0x" <> ^beacon_contract_address_hash_string}, - "latest" - ] - } - ], - _options -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x000000000000000000000000" <> implementation_contract_address_hash_string - } - ] - } - end - ) - end - def assert_implementation_address(address_hash) do implementation = Implementation.get_proxy_implementations(address_hash) assert implementation.proxy_type @@ -532,7 +439,7 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do def assert_empty_implementation(address_hash) do implementation = Implementation.get_proxy_implementations(address_hash) - assert implementation.proxy_type == :unknown + assert is_nil(implementation.proxy_type) assert implementation.updated_at assert implementation.names == [] assert implementation.address_hashes == [] diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index f08e2ca061b4..c97680326219 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -265,7 +265,8 @@ defmodule Explorer.Chain.TransactionTest do end test "that a contract call transaction that has a verified contract returns the decoded input data" do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() transaction = :transaction_to_verified_contract @@ -277,7 +278,8 @@ defmodule Explorer.Chain.TransactionTest do end test "that a contract call will look up a match in contract_methods table" do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() :transaction_to_verified_contract |> insert() @@ -300,7 +302,8 @@ defmodule Explorer.Chain.TransactionTest do end test "arguments name in function call replaced with argN if it's empty string" do - TestHelper.get_all_proxies_implementation_zero_addresses() + EthereumJSONRPC.Mox + |> TestHelper.mock_generic_proxy_requests() contract = insert(:smart_contract, diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs index 0741e5d3b8a3..a93b76daea64 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs @@ -260,11 +260,11 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do if Data.empty?(code) do assert is_nil(proxy_implementations) else - implementations = [EIP7702.get_delegate_address(code.bytes) |> Hash.Address.cast() |> elem(1)] + {:ok, address_hashes} = EIP7702.quick_resolve_implementations(address) assert proxy_implementations.proxy_address_hash == address_hash assert proxy_implementations.proxy_type == :eip7702 - assert proxy_implementations.address_hashes == implementations + assert proxy_implementations.address_hashes == address_hashes assert proxy_implementations.names == [nil] end end) From b75f045e889e04e0b9c6a42e5a8cbd63ff9f5b35 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 1 Sep 2025 17:08:22 +0300 Subject: [PATCH 0297/1006] fix: duplicates of smart contracts additional sources (#13018) * fix: create unique index for smart contract additional sources table * refactor: run smart contract updates within a transaction * fix: move migration to main scope * fix: mismatched Ecto.Multi step name * chore: process review by @coderabbitai * refactor: run migrations in background * chore: update supported table names --- apps/explorer/config/config.exs | 6 +- apps/explorer/config/runtime/test.exs | 4 +- apps/explorer/lib/explorer/application.ex | 5 ++ .../lib/explorer/chain/smart_contract.ex | 84 ++++++++++--------- .../chain/smart_contract_additional_source.ex | 11 +-- .../migrator/heavy_db_index_operation.ex | 1 + ...ontract_additional_sources_unique_index.ex | 67 +++++++++++++++ ...icate_smart_contract_additional_sources.ex | 60 +++++++++++++ ...smart_contract_additional_sources_test.exs | 76 +++++++++++++++++ 9 files changed, 267 insertions(+), 47 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_smart_contract_additional_sources_unique_index.ex create mode 100644 apps/explorer/lib/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources.ex create mode 100644 apps/explorer/test/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources_test.exs diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index a9cbc4b68b74..b7a2acc14ffa 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -135,7 +135,8 @@ for migrator <- [ Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, Explorer.Migrator.ReindexDuplicatedInternalTransactions, Explorer.Migrator.MergeAdjacentMissingBlockRanges, - Explorer.Migrator.UnescapeQuotesInTokens + Explorer.Migrator.UnescapeQuotesInTokens, + Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources ] do config :explorer, migrator, enabled: true end @@ -168,7 +169,8 @@ for index_operation <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex ] do config :explorer, index_operation, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 362d7e534302..3bfdfb43792d 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -74,6 +74,7 @@ for migrator <- [ Explorer.Migrator.ReindexDuplicatedInternalTransactions, Explorer.Migrator.MergeAdjacentMissingBlockRanges, Explorer.Migrator.UnescapeQuotesInTokens, + Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, @@ -102,7 +103,8 @@ for migrator <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex ] do config :explorer, migrator, enabled: false end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index ea716298277c..547c2b8bb440 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -193,6 +193,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.MergeAdjacentMissingBlockRanges, :indexer), configure_mode_dependent_process(Explorer.Migrator.UnescapeQuotesInTokens, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), + configure_mode_dependent_process(Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer @@ -303,6 +304,10 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index dac0bcf2cf22..76b9fbb2ea50 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -963,14 +963,11 @@ defmodule Explorer.Chain.SmartContract do |> __MODULE__.changeset(attrs) |> Changeset.put_change(:external_libraries, external_libraries) - # Prepares changesets for additional sources associated with the contract - new_contract_additional_source = %SmartContractAdditionalSource{} - smart_contract_additional_sources_changesets = if secondary_sources do secondary_sources |> Enum.map(fn changeset -> - new_contract_additional_source + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(changeset) end) else @@ -1042,51 +1039,63 @@ defmodule Explorer.Chain.SmartContract do def update_smart_contract(attrs \\ %{}, external_libraries \\ [], secondary_sources \\ []) do address_hash = Map.get(attrs, :address_hash) - # Removes all additional sources associated with the contract - query_sources = - from( - source in SmartContractAdditionalSource, - where: source.address_hash == ^address_hash - ) - - _delete_sources = Repo.delete_all(query_sources) - - # Retrieve the existing smart contract - smart_contract = address_hash_to_smart_contract(address_hash) + # Prepares the queries to clear the primary flag for the contract address in + # Explorer.Chain.Address.Name if any (enforce ShareLocks tables order (see + # docs: sharelocks.md)) and updated the contract details. + insert_contract_query = + Multi.new() + |> Multi.run(:acquire_smart_contract, fn repo, _ -> + # Get smart contract with lock INSIDE the transaction + smart_contract = + address_hash + |> get_by_address_hash_query() + |> lock("FOR UPDATE") + |> repo.one() - # Updates existing changeset and extends it with external libraries. - # As part of changeset preparation and verification, contract methods are - # updated as so if new ABI does not contain some of previous methods, they - # are still kept in the database - smart_contract_changeset = - smart_contract - |> __MODULE__.changeset(attrs) - |> Changeset.put_change(:external_libraries, external_libraries) + {:ok, smart_contract} + end) + |> Multi.run(:delete_sources, fn repo, _ -> + {count, _} = + repo.delete_all( + from( + source in SmartContractAdditionalSource, + where: source.address_hash == ^address_hash + ) + ) - # Prepares changesets for additional sources associated with the contract - new_contract_additional_source = %SmartContractAdditionalSource{} + {:ok, count} + end) + |> Multi.run(:clear_primary_address_names, fn repo, _ -> + AddressName.clear_primary_address_names(repo, address_hash) + end) + |> Multi.run( + :smart_contract, + fn repo, %{acquire_smart_contract: smart_contract} -> + smart_contract + |> __MODULE__.changeset(attrs) + |> Changeset.put_change(:external_libraries, external_libraries) + |> repo.update() + end + ) + |> Multi.run( + :set_primary_address_name, + fn repo, %{smart_contract: %SmartContract{name: name}} -> + result = AddressName.create_primary_address_name(repo, name, address_hash) + {:ok, result} + end + ) smart_contract_additional_sources_changesets = if secondary_sources do secondary_sources |> Enum.map(fn changeset -> - new_contract_additional_source + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(changeset) end) else [] end - # Prepares the queries to clear the primary flag for the contract address in - # Explorer.Chain.Address.Name if any (enforce ShareLocks tables order (see - # docs: sharelocks.md)) and updated the contract details. - insert_contract_query = - Multi.new() - |> Multi.run(:clear_primary_address_names, fn repo, _ -> - AddressName.clear_primary_address_names(repo, address_hash) - end) - |> Multi.update(:smart_contract, smart_contract_changeset) - # Updates the queries from the previous step with inserting additional sources # of the contract insert_contract_query_with_additional_sources = @@ -1101,9 +1110,6 @@ defmodule Explorer.Chain.SmartContract do insert_contract_query_with_additional_sources |> Repo.transaction() - # Set the primary mark for the contract name - AddressName.create_primary_address_name(Repo, Changeset.get_field(smart_contract_changeset, :name), address_hash) - case insert_result do {:ok, %{smart_contract: smart_contract}} -> {:ok, smart_contract} diff --git a/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex b/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex index 108f1e611f61..38c716442946 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.SmartContractAdditionalSource do field(:file_name, :string, null: false) field(:contract_source_code, :string, null: false) + # TODO: Why not use smart_contract_id? belongs_to( :smart_contract, SmartContract, @@ -38,19 +39,19 @@ defmodule Explorer.Chain.SmartContractAdditionalSource do :contract_source_code, :address_hash ]) - |> validate_required([:file_name, :contract_source_code, :address_hash]) - |> unique_constraint(:address_hash) + |> validate_required([:address_hash, :file_name, :contract_source_code]) + |> unique_constraint([:address_hash, :file_name]) end def invalid_contract_changeset(%__MODULE__{} = smart_contract_additional_source, attrs, error) do validated = smart_contract_additional_source |> cast(attrs, [ + :address_hash, :file_name, - :contract_source_code, - :address_hash + :contract_source_code ]) - |> validate_required([:file_name, :address_hash]) + |> validate_required([:address_hash, :file_name]) add_error(validated, :contract_source_code, error_message(error)) end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex index 0b25143f6b68..2b154dfa3792 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex @@ -30,6 +30,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation do | :addresses | :smart_contracts | :arbitrum_batch_l2_blocks + | :smart_contracts_additional_sources @doc """ Specifies the type of operation to be performed on the database index. diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_smart_contract_additional_sources_unique_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_smart_contract_additional_sources_unique_index.ex new file mode 100644 index 000000000000..563679f68cbf --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_smart_contract_additional_sources_unique_index.ex @@ -0,0 +1,67 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex do + @moduledoc """ + Creates a unique index on the smart_contract_additional_sources table. + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + require Logger + + alias Explorer.Migrator.{ + HeavyDbIndexOperation, + MigrationStatus, + SanitizeDuplicateSmartContractAdditionalSources + } + + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :smart_contracts_additional_sources + @index_name "smart_contracts_additional_sources_file_name_address_hash_index" + @operation_type :create + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations, + do: [SanitizeDuplicateSmartContractAdditionalSources.migration_name()] + + @query_string """ + CREATE UNIQUE INDEX #{HeavyDbIndexOperationHelper.add_concurrently_flag?()} IF NOT EXISTS "#{@index_name}" + ON #{@table_name} (address_hash, file_name); + """ + + @impl HeavyDbIndexOperation + def db_index_operation do + HeavyDbIndexOperationHelper.create_db_index(@query_string) + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, @query_string) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_creation_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache, do: :ok +end diff --git a/apps/explorer/lib/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources.ex b/apps/explorer/lib/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources.ex new file mode 100644 index 000000000000..d3ac807cd6af --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources.ex @@ -0,0 +1,60 @@ +defmodule Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources do + @moduledoc """ + Sanitizes the smart_contract_additional_sources table by removing duplicates. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.SmartContractAdditionalSource + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "sanitize_duplicate_smart_contract_additional_sources" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> select([t], t.id) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + query = + from(sc in SmartContractAdditionalSource, + select: %{ + id: sc.id, + rn: + fragment( + "ROW_NUMBER() OVER (PARTITION BY ?, ? ORDER BY ?)", + sc.address_hash, + sc.file_name, + sc.id + ) + } + ) + + from(t in subquery(query), where: t.rn > 1) + end + + @impl FillingMigration + def update_batch(ids) do + SmartContractAdditionalSource + |> where([sc], sc.id in ^ids) + |> Repo.delete_all(timeout: :infinity) + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/test/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources_test.exs b/apps/explorer/test/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources_test.exs new file mode 100644 index 000000000000..ee9fcbbcda71 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/sanitize_duplicate_smart_contract_additional_sources_test.exs @@ -0,0 +1,76 @@ +defmodule Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSourcesTest do + use Explorer.DataCase, async: false + + import Ecto.Query + + alias Explorer.Chain.SmartContractAdditionalSource + alias Explorer.Migrator.{MigrationStatus, SanitizeDuplicateSmartContractAdditionalSources} + alias Explorer.Repo + + describe "sanitize duplicates in smart_contracts_additional_sources" do + test "removes duplicate rows keeping a single record per (address_hash, file_name)" do + sc1 = insert(:smart_contract) + sc2 = insert(:smart_contract) + + # for sc1: create duplicates for FileA.sol and a unique FileB.sol + attrs_a = %{address_hash: sc1.address_hash, file_name: "FileA.sol", contract_source_code: "// A1"} + attrs_a_dup = %{address_hash: sc1.address_hash, file_name: "FileA.sol", contract_source_code: "// A2"} + attrs_b = %{address_hash: sc1.address_hash, file_name: "FileB.sol", contract_source_code: "// B"} + + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(attrs_a) |> Repo.insert!() + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(attrs_a_dup) |> Repo.insert!() + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(attrs_b) |> Repo.insert!() + + # for sc2: duplicates for the same file name, independent from sc1 + attrs_c = %{address_hash: sc2.address_hash, file_name: "Common.sol", contract_source_code: "// C1"} + attrs_c_dup = %{address_hash: sc2.address_hash, file_name: "Common.sol", contract_source_code: "// C2"} + + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(attrs_c) |> Repo.insert!() + %SmartContractAdditionalSource{} |> SmartContractAdditionalSource.changeset(attrs_c_dup) |> Repo.insert!() + + assert MigrationStatus.get_status("sanitize_duplicate_smart_contract_additional_sources") == nil + + SanitizeDuplicateSmartContractAdditionalSources.start_link([]) + Process.sleep(150) + + # Migration completes and duplicates are deleted + assert MigrationStatus.get_status("sanitize_duplicate_smart_contract_additional_sources") == "completed" + + remaining = + SmartContractAdditionalSource + |> order_by([s], asc: s.address_hash, asc: s.file_name, asc: s.id) + |> Repo.all() + + # Expect one per (address_hash, file_name) + grouped = Enum.group_by(remaining, &{&1.address_hash, &1.file_name}) + assert grouped |> Map.values() |> Enum.all?(fn list -> length(list) == 1 end) + + # Ensure the specific keys exist with a single row + assert Map.has_key?(grouped, {sc1.address_hash, "FileA.sol"}) + assert Map.has_key?(grouped, {sc1.address_hash, "FileB.sol"}) + assert Map.has_key?(grouped, {sc2.address_hash, "Common.sol"}) + end + + test "completes gracefully when there are no duplicates" do + sc = insert(:smart_contract) + + %SmartContractAdditionalSource{} + |> SmartContractAdditionalSource.changeset(%{ + address_hash: sc.address_hash, + file_name: "Unique.sol", + contract_source_code: "// only" + }) + |> Repo.insert!() + + assert MigrationStatus.get_status("sanitize_duplicate_smart_contract_additional_sources") == nil + + SanitizeDuplicateSmartContractAdditionalSources.start_link([]) + Process.sleep(100) + + assert MigrationStatus.get_status("sanitize_duplicate_smart_contract_additional_sources") == "completed" + + count = Repo.aggregate(SmartContractAdditionalSource, :count) + assert count == 1 + end + end +end From 2613ce05c23603d1e4a6cdcf43a0029b9f49dfbd Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 17:17:46 +0300 Subject: [PATCH 0298/1006] Rename public release workflows --- .github/workflows/release-arbitrum-public.yml | 2 +- .github/workflows/release-celo-public.yml | 2 +- .github/workflows/release-default-public.yml | 2 +- .github/workflows/release-eth-public.yml | 2 +- .github/workflows/release-filecoin-public.yml | 2 +- .github/workflows/release-fuse-public.yml | 2 +- .github/workflows/release-gnosis-public.yml | 2 +- .github/workflows/release-optimism-public.yml | 2 +- .github/workflows/release-polygon-zkevm-public.yml | 2 +- .github/workflows/release-rootstock-public.yml | 2 +- .github/workflows/release-scroll-public.yml | 2 +- .github/workflows/release-suave-public.yml | 2 +- .github/workflows/release-zetachain-public.yml | 2 +- .github/workflows/release-zilliqa-public.yml | 2 +- .github/workflows/release-zksync-public.yml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release-arbitrum-public.yml b/.github/workflows/release-arbitrum-public.yml index 7691e46f40a4..7530a2f384c6 100644 --- a/.github/workflows/release-arbitrum-public.yml +++ b/.github/workflows/release-arbitrum-public.yml @@ -1,4 +1,4 @@ -name: Release for Arbitrum +name: Public Release for Arbitrum on: workflow_dispatch: diff --git a/.github/workflows/release-celo-public.yml b/.github/workflows/release-celo-public.yml index 2ac51205cf61..170e96a1de1b 100644 --- a/.github/workflows/release-celo-public.yml +++ b/.github/workflows/release-celo-public.yml @@ -1,4 +1,4 @@ -name: Release for Celo +name: Public Release for Celo on: workflow_dispatch: diff --git a/.github/workflows/release-default-public.yml b/.github/workflows/release-default-public.yml index 4672abe82000..de6132a33c1d 100644 --- a/.github/workflows/release-default-public.yml +++ b/.github/workflows/release-default-public.yml @@ -1,4 +1,4 @@ -name: Release +name: Public Release on: workflow_dispatch: diff --git a/.github/workflows/release-eth-public.yml b/.github/workflows/release-eth-public.yml index 6182552f732e..465327829df3 100644 --- a/.github/workflows/release-eth-public.yml +++ b/.github/workflows/release-eth-public.yml @@ -1,4 +1,4 @@ -name: Release for Ethereum +name: Public Release for Ethereum on: workflow_dispatch: diff --git a/.github/workflows/release-filecoin-public.yml b/.github/workflows/release-filecoin-public.yml index aa50be28abfb..2f877f6f06f2 100644 --- a/.github/workflows/release-filecoin-public.yml +++ b/.github/workflows/release-filecoin-public.yml @@ -1,4 +1,4 @@ -name: Release for Filecoin +name: Public Release for Filecoin on: workflow_dispatch: diff --git a/.github/workflows/release-fuse-public.yml b/.github/workflows/release-fuse-public.yml index 1753b065c15c..e9883e60ac1a 100644 --- a/.github/workflows/release-fuse-public.yml +++ b/.github/workflows/release-fuse-public.yml @@ -1,4 +1,4 @@ -name: Release for Fuse +name: Public Release for Fuse on: workflow_dispatch: diff --git a/.github/workflows/release-gnosis-public.yml b/.github/workflows/release-gnosis-public.yml index f7ecf2e11994..d6882a17f922 100644 --- a/.github/workflows/release-gnosis-public.yml +++ b/.github/workflows/release-gnosis-public.yml @@ -1,4 +1,4 @@ -name: Release for Gnosis Chain +name: Public Release for Gnosis Chain on: workflow_dispatch: diff --git a/.github/workflows/release-optimism-public.yml b/.github/workflows/release-optimism-public.yml index e1a233f5a453..06f1ef9d2557 100644 --- a/.github/workflows/release-optimism-public.yml +++ b/.github/workflows/release-optimism-public.yml @@ -1,4 +1,4 @@ -name: Release for Optimism +name: Public Release for Optimism on: workflow_dispatch: diff --git a/.github/workflows/release-polygon-zkevm-public.yml b/.github/workflows/release-polygon-zkevm-public.yml index daf4d7cfaa77..c5a93bb8350d 100644 --- a/.github/workflows/release-polygon-zkevm-public.yml +++ b/.github/workflows/release-polygon-zkevm-public.yml @@ -1,4 +1,4 @@ -name: Release for Polygon zkEVM +name: Public Release for Polygon zkEVM on: workflow_dispatch: diff --git a/.github/workflows/release-rootstock-public.yml b/.github/workflows/release-rootstock-public.yml index a26824665ffe..2d624f9b5789 100644 --- a/.github/workflows/release-rootstock-public.yml +++ b/.github/workflows/release-rootstock-public.yml @@ -1,4 +1,4 @@ -name: Release for Rootstock +name: Public Release for Rootstock on: workflow_dispatch: diff --git a/.github/workflows/release-scroll-public.yml b/.github/workflows/release-scroll-public.yml index 716b58580bd6..6e61bf33f62f 100644 --- a/.github/workflows/release-scroll-public.yml +++ b/.github/workflows/release-scroll-public.yml @@ -1,4 +1,4 @@ -name: Release for Scroll +name: Public Release for Scroll on: workflow_dispatch: diff --git a/.github/workflows/release-suave-public.yml b/.github/workflows/release-suave-public.yml index d2db1392bd6c..7038416783f8 100644 --- a/.github/workflows/release-suave-public.yml +++ b/.github/workflows/release-suave-public.yml @@ -1,4 +1,4 @@ -name: Release for SUAVE +name: Public Release for SUAVE on: workflow_dispatch: diff --git a/.github/workflows/release-zetachain-public.yml b/.github/workflows/release-zetachain-public.yml index e8fe4bd49697..0219b31ec966 100644 --- a/.github/workflows/release-zetachain-public.yml +++ b/.github/workflows/release-zetachain-public.yml @@ -1,4 +1,4 @@ -name: Release for Zetachain +name: Public Release for Zetachain on: workflow_dispatch: diff --git a/.github/workflows/release-zilliqa-public.yml b/.github/workflows/release-zilliqa-public.yml index f9aa9112c332..6736e46dcfe2 100644 --- a/.github/workflows/release-zilliqa-public.yml +++ b/.github/workflows/release-zilliqa-public.yml @@ -1,4 +1,4 @@ -name: Release for Zilliqa +name: Public Release for Zilliqa on: workflow_dispatch: diff --git a/.github/workflows/release-zksync-public.yml b/.github/workflows/release-zksync-public.yml index cf0aada5e694..c596894987e2 100644 --- a/.github/workflows/release-zksync-public.yml +++ b/.github/workflows/release-zksync-public.yml @@ -1,4 +1,4 @@ -name: Release for ZkSync +name: Public Release for ZkSync on: workflow_dispatch: From 345dbbb18055b787865932ded241efdced473c43 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:26:18 +0300 Subject: [PATCH 0299/1006] fix: Replace get_constant_by_key with get_constant_value in get_last_processed_token_address_hash (#13118) --- .../lib/explorer/application/constants.ex | 2 +- .../token_instance/sanitize_erc721_test.exs | 158 ++++++++++++++++++ 2 files changed, 159 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/application/constants.ex b/apps/explorer/lib/explorer/application/constants.ex index d5e2413d90ea..e240763f26c8 100644 --- a/apps/explorer/lib/explorer/application/constants.ex +++ b/apps/explorer/lib/explorer/application/constants.ex @@ -100,6 +100,6 @@ defmodule Explorer.Application.Constants do """ @spec get_last_processed_token_address_hash(keyword()) :: nil | Explorer.Chain.Hash.t() def get_last_processed_token_address_hash(options \\ []) do - @last_processed_erc_721_token |> get_constant_by_key(options) |> Chain.string_to_address_hash_or_nil() + @last_processed_erc_721_token |> get_constant_value(options) |> Chain.string_to_address_hash_or_nil() end end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs index 51db3b70d478..ee2ad30c8e6d 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs @@ -3,8 +3,20 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do alias Explorer.Repo alias Explorer.Chain.Token.Instance + alias Explorer.Application.Constants + alias Explorer.Migrator.MigrationStatus describe "sanitizer test" do + setup do + initial_env = Application.get_env(:indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721) + + on_exit(fn -> + Application.put_env(:indexer, Indexer.Fetcher.TokenInstance.SanitizeERC721, initial_env) + end) + + {:ok, initial_env: initial_env} + end + test "imports token instances" do for x <- 0..3 do erc_721_token = insert(:token, type: "ERC-721") @@ -34,6 +46,152 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do assert Enum.count(instances) == 4 assert Enum.all?(instances, fn instance -> !is_nil(instance.error) and is_nil(instance.metadata) end) + assert MigrationStatus.get_status("backfill_erc721") == "completed" + end + + test "imports token instances with low tokens queue size", %{initial_env: initial_env} do + tokens = + for x <- 0..5 do + erc_721_token = insert(:token, type: "ERC-721") + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + address = insert(:address) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + + erc_721_token + end + + assert [] = Repo.all(Instance) + + Application.put_env( + :indexer, + Indexer.Fetcher.TokenInstance.SanitizeERC721, + Keyword.put(initial_env, :tokens_queue_size, 1) + ) + + start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC721, []}) + start_supervised!({Indexer.Fetcher.TokenInstance.Sanitize.Supervisor, [[flush_interval: 1]]}) + + :timer.sleep(500) + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 6 + assert Enum.all?(instances, fn instance -> !is_nil(instance.error) and is_nil(instance.metadata) end) + + assert MigrationStatus.get_status("backfill_erc721") == "completed" + assert List.last(tokens).contract_address_hash == Constants.get_last_processed_token_address_hash() + end + + test "don't start if completed" do + MigrationStatus.set_status("backfill_erc721", "completed") + + for x <- 0..5 do + erc_721_token = insert(:token, type: "ERC-721") + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + address = insert(:address) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + end + + assert [] = Repo.all(Instance) + + start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC721, []}) + start_supervised!({Indexer.Fetcher.TokenInstance.Sanitize.Supervisor, [[flush_interval: 1]]}) + + :timer.sleep(500) + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 0 + + assert MigrationStatus.get_status("backfill_erc721") == "completed" + end + + test "takes into account the last processed token address hash" do + tokens = + for x <- 0..5 do + erc_721_token = insert(:token, type: "ERC-721") + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + address = insert(:address) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: erc_721_token.contract_address, + token_ids: [x] + ) + + erc_721_token + end + + assert [] = Repo.all(Instance) + + pid_sanitize_erc721 = start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC721, []}) + start_supervised!({Indexer.Fetcher.TokenInstance.Sanitize.Supervisor, [[flush_interval: 1]]}) + + :timer.sleep(500) + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 6 + last_token = List.last(tokens) + assert MigrationStatus.get_status("backfill_erc721") == "completed" + assert last_token.contract_address_hash == Constants.get_last_processed_token_address_hash() + refute Process.alive?(pid_sanitize_erc721) + + first_token = List.first(tokens) + + transaction = insert(:transaction, input: "0xabcd010203040506") |> with_block() + + address = insert(:address) + + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + from_address: address, + token_contract_address: first_token.contract_address, + token_ids: [6] + ) + + MigrationStatus.set_status("backfill_erc721", "started") + + {:ok, supervisor} = ExUnit.fetch_test_supervisor() + + {:ok, _pid_sanitize_erc721} = + Supervisor.restart_child(supervisor, Indexer.Fetcher.TokenInstance.SanitizeERC721) + + :timer.sleep(500) + + assert MigrationStatus.get_status("backfill_erc721") == "completed" + assert last_token.contract_address_hash == Constants.get_last_processed_token_address_hash() + + instances = Repo.all(Instance) + + assert Enum.count(instances) == 6 end end end From 6e5701dd8570f3182e117a48a78cb5335f8e2068 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 17:27:31 +0300 Subject: [PATCH 0300/1006] Rename public release workflows files --- .github/workflows/release-arbitrum-public.yml | 62 ------- .github/workflows/release-celo-public.yml | 66 -------- .github/workflows/release-default-public.yml | 151 ------------------ .github/workflows/release-eth-public.yml | 63 -------- .github/workflows/release-filecoin-public.yml | 62 ------- .github/workflows/release-fuse-public.yml | 62 ------- .github/workflows/release-gnosis-public.yml | 64 -------- .github/workflows/release-optimism-public.yml | 62 ------- .../release-polygon-zkevm-public.yml | 63 -------- .../workflows/release-rootstock-public.yml | 63 -------- .github/workflows/release-scroll-public.yml | 63 -------- .github/workflows/release-suave-public.yml | 62 ------- .../workflows/release-zetachain-public.yml | 62 ------- .github/workflows/release-zilliqa-public.yml | 64 -------- .github/workflows/release-zksync-public.yml | 62 ------- 15 files changed, 1031 deletions(-) delete mode 100644 .github/workflows/release-arbitrum-public.yml delete mode 100644 .github/workflows/release-celo-public.yml delete mode 100644 .github/workflows/release-default-public.yml delete mode 100644 .github/workflows/release-eth-public.yml delete mode 100644 .github/workflows/release-filecoin-public.yml delete mode 100644 .github/workflows/release-fuse-public.yml delete mode 100644 .github/workflows/release-gnosis-public.yml delete mode 100644 .github/workflows/release-optimism-public.yml delete mode 100644 .github/workflows/release-polygon-zkevm-public.yml delete mode 100644 .github/workflows/release-rootstock-public.yml delete mode 100644 .github/workflows/release-scroll-public.yml delete mode 100644 .github/workflows/release-suave-public.yml delete mode 100644 .github/workflows/release-zetachain-public.yml delete mode 100644 .github/workflows/release-zilliqa-public.yml delete mode 100644 .github/workflows/release-zksync-public.yml diff --git a/.github/workflows/release-arbitrum-public.yml b/.github/workflows/release-arbitrum-public.yml deleted file mode 100644 index 7530a2f384c6..000000000000 --- a/.github/workflows/release-arbitrum-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Arbitrum - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Arbitrum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:latest, ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=arbitrum - - - name: Build and push Docker image for Arbitrum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=arbitrum diff --git a/.github/workflows/release-celo-public.yml b/.github/workflows/release-celo-public.yml deleted file mode 100644 index 170e96a1de1b..000000000000 --- a/.github/workflows/release-celo-public.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Public Release for Celo - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - API_GRAPHQL_MAX_COMPLEXITY: 10400 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for CELO (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-celo:latest, ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo - - - name: Build and push Docker image for CELO (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo - diff --git a/.github/workflows/release-default-public.yml b/.github/workflows/release-default-public.yml deleted file mode 100644 index de6132a33c1d..000000000000 --- a/.github/workflows/release-default-public.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Public Release - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build & Push Core Docker image (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:latest, ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - - name: Build & Push Core Docker image (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - - - name: Build & Push Docker image with an old UI (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/oldUI.Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-with-old-ui - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - # - name: Send release announcement to Slack workflow - # id: slack - # uses: slackapi/slack-github-action@v1.24.0 - # with: - # payload: | - # { - # "release-version": "${{ env.RELEASE_VERSION }}", - # "release-link": "https://github.com/blockscout/blockscout/releases/tag/v${{ env.RELEASE_VERSION }}" - # } - # env: - # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - - # merge-master-after-release: - # name: Merge 'master' to specific branch after release - # runs-on: ubuntu-latest - # env: - # BRANCHES: | - # production-core - # production-sokol - # production-eth-experimental - # production-eth-goerli - # production-lukso - # production-xdai - # production-polygon-supernets - # production-rsk - # production-immutable - # steps: - # - uses: actions/checkout@v5 - # - name: Set Git config - # run: | - # git config --local user.email "actions@github.com" - # git config --local user.name "Github Actions" - # - name: Merge master back after release - # run: | - # git fetch --unshallow - # touch errors.txt - # for branch in $BRANCHES; - # do - # git reset --merge - # git checkout master - # git fetch origin - # echo $branch - # git ls-remote --exit-code --heads origin $branch || { echo $branch >> errors.txt; continue; } - # echo "Merge 'master' to $branch" - # git checkout $branch - # git pull || { echo $branch >> errors.txt; continue; } - # git merge --no-ff master -m "Auto-merge master back to $branch" || { echo $branch >> errors.txt; continue; } - # git push || { echo $branch >> errors.txt; continue; } - # git checkout master; - # done - # [ -s errors.txt ] && echo "There are problems with merging 'master' to branches:" || echo "Errors file is empty" - # cat errors.txt - # [ ! -s errors.txt ] diff --git a/.github/workflows/release-eth-public.yml b/.github/workflows/release-eth-public.yml deleted file mode 100644 index 465327829df3..000000000000 --- a/.github/workflows/release-eth-public.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Ethereum - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Ethereum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-ethereum:latest, ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=ethereum - - - name: Build and push Docker image for Ethereum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=ethereum - diff --git a/.github/workflows/release-filecoin-public.yml b/.github/workflows/release-filecoin-public.yml deleted file mode 100644 index 2f877f6f06f2..000000000000 --- a/.github/workflows/release-filecoin-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Filecoin - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Filecoin (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=filecoin - - - name: Build and push Docker image for Filecoin (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=filecoin diff --git a/.github/workflows/release-fuse-public.yml b/.github/workflows/release-fuse-public.yml deleted file mode 100644 index e9883e60ac1a..000000000000 --- a/.github/workflows/release-fuse-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Fuse - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Fuse (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-fuse:latest, ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - - - name: Build and push Docker image for Fuse (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true diff --git a/.github/workflows/release-gnosis-public.yml b/.github/workflows/release-gnosis-public.yml deleted file mode 100644 index d6882a17f922..000000000000 --- a/.github/workflows/release-gnosis-public.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Public Release for Gnosis Chain - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Gnosis chain (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-xdai:latest, ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - CHAIN_TYPE=ethereum - - - name: Build and push Docker image for Gnosis chain (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - CHAIN_TYPE=ethereum diff --git a/.github/workflows/release-optimism-public.yml b/.github/workflows/release-optimism-public.yml deleted file mode 100644 index 06f1ef9d2557..000000000000 --- a/.github/workflows/release-optimism-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Optimism - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Optimism (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-optimism:latest, ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism - - - name: Build and push Docker image for Optimism (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism diff --git a/.github/workflows/release-polygon-zkevm-public.yml b/.github/workflows/release-polygon-zkevm-public.yml deleted file mode 100644 index c5a93bb8350d..000000000000 --- a/.github/workflows/release-polygon-zkevm-public.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Polygon zkEVM - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Polygon zkEVM (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zkevm:latest, ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=polygon_zkevm - - - name: Build and push Docker image for Polygon zkEVM (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=polygon_zkevm - diff --git a/.github/workflows/release-rootstock-public.yml b/.github/workflows/release-rootstock-public.yml deleted file mode 100644 index 2d624f9b5789..000000000000 --- a/.github/workflows/release-rootstock-public.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Rootstock - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Rootstock (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-rsk:latest, ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=rsk - - - name: Build and push Docker image for Rootstock (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=rsk - diff --git a/.github/workflows/release-scroll-public.yml b/.github/workflows/release-scroll-public.yml deleted file mode 100644 index 6e61bf33f62f..000000000000 --- a/.github/workflows/release-scroll-public.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Scroll - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Scroll (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-scroll:latest, ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=scroll - - - name: Build and push Docker image for Scroll (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=scroll - diff --git a/.github/workflows/release-suave-public.yml b/.github/workflows/release-suave-public.yml deleted file mode 100644 index 7038416783f8..000000000000 --- a/.github/workflows/release-suave-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for SUAVE - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for SUAVE (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave:latest, ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave - - - name: Build and push Docker image for SUAVE (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave diff --git a/.github/workflows/release-zetachain-public.yml b/.github/workflows/release-zetachain-public.yml deleted file mode 100644 index 0219b31ec966..000000000000 --- a/.github/workflows/release-zetachain-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Zetachain - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Zetachain (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zetachain:latest, ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zetachain - - - name: Build and push Docker image for Zetachain (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zetachain diff --git a/.github/workflows/release-zilliqa-public.yml b/.github/workflows/release-zilliqa-public.yml deleted file mode 100644 index 6736e46dcfe2..000000000000 --- a/.github/workflows/release-zilliqa-public.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Public Release for Zilliqa - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:latest, ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zilliqa - - - name: Build and push Docker image (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zilliqa - - diff --git a/.github/workflows/release-zksync-public.yml b/.github/workflows/release-zksync-public.yml deleted file mode 100644 index c596894987e2..000000000000 --- a/.github/workflows/release-zksync-public.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for ZkSync - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for ZkSync (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zksync:latest, ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zksync - - - name: Build and push Docker image for ZkSync (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zksync From 429557619eae34117471cf3868c99392135e88ff Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 17:27:42 +0300 Subject: [PATCH 0301/1006] Rename public release workflows files --- .github/workflows/public-release-arbitrum.yml | 62 +++++++ .github/workflows/public-release-celo.yml | 66 ++++++++ .github/workflows/public-release-default.yml | 151 ++++++++++++++++++ .github/workflows/public-release-eth.yml | 63 ++++++++ .github/workflows/public-release-filecoin.yml | 62 +++++++ .github/workflows/public-release-fuse.yml | 62 +++++++ .github/workflows/public-release-gnosis.yml | 64 ++++++++ .github/workflows/public-release-optimism.yml | 62 +++++++ .../public-release-polygon-zkevm.yml | 63 ++++++++ .../workflows/public-release-rootstock.yml | 63 ++++++++ .github/workflows/public-release-scroll.yml | 63 ++++++++ .github/workflows/public-release-suave.yml | 62 +++++++ .../workflows/public-release-zetachain.yml | 62 +++++++ .github/workflows/public-release-zilliqa.yml | 64 ++++++++ .github/workflows/public-release-zksync.yml | 62 +++++++ 15 files changed, 1031 insertions(+) create mode 100644 .github/workflows/public-release-arbitrum.yml create mode 100644 .github/workflows/public-release-celo.yml create mode 100644 .github/workflows/public-release-default.yml create mode 100644 .github/workflows/public-release-eth.yml create mode 100644 .github/workflows/public-release-filecoin.yml create mode 100644 .github/workflows/public-release-fuse.yml create mode 100644 .github/workflows/public-release-gnosis.yml create mode 100644 .github/workflows/public-release-optimism.yml create mode 100644 .github/workflows/public-release-polygon-zkevm.yml create mode 100644 .github/workflows/public-release-rootstock.yml create mode 100644 .github/workflows/public-release-scroll.yml create mode 100644 .github/workflows/public-release-suave.yml create mode 100644 .github/workflows/public-release-zetachain.yml create mode 100644 .github/workflows/public-release-zilliqa.yml create mode 100644 .github/workflows/public-release-zksync.yml diff --git a/.github/workflows/public-release-arbitrum.yml b/.github/workflows/public-release-arbitrum.yml new file mode 100644 index 000000000000..7530a2f384c6 --- /dev/null +++ b/.github/workflows/public-release-arbitrum.yml @@ -0,0 +1,62 @@ +name: Public Release for Arbitrum + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Arbitrum (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-arbitrum:latest, ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=arbitrum + + - name: Build and push Docker image for Arbitrum (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=arbitrum diff --git a/.github/workflows/public-release-celo.yml b/.github/workflows/public-release-celo.yml new file mode 100644 index 000000000000..170e96a1de1b --- /dev/null +++ b/.github/workflows/public-release-celo.yml @@ -0,0 +1,66 @@ +name: Public Release for Celo + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + API_GRAPHQL_MAX_COMPLEXITY: 10400 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for CELO (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-celo:latest, ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=celo + + - name: Build and push Docker image for CELO (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=celo + diff --git a/.github/workflows/public-release-default.yml b/.github/workflows/public-release-default.yml new file mode 100644 index 000000000000..de6132a33c1d --- /dev/null +++ b/.github/workflows/public-release-default.yml @@ -0,0 +1,151 @@ +name: Public Release + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build & Push Core Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout:latest, ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + - name: Build & Push Core Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + + - name: Build & Push Docker image with an old UI (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/oldUI.Dockerfile + push: true + cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache + cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max + tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-with-old-ui + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DECODE_NOT_A_CONTRACT_CALLS=false + MIXPANEL_URL= + MIXPANEL_TOKEN= + AMPLITUDE_URL= + AMPLITUDE_API_KEY= + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + + # - name: Send release announcement to Slack workflow + # id: slack + # uses: slackapi/slack-github-action@v1.24.0 + # with: + # payload: | + # { + # "release-version": "${{ env.RELEASE_VERSION }}", + # "release-link": "https://github.com/blockscout/blockscout/releases/tag/v${{ env.RELEASE_VERSION }}" + # } + # env: + # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + + # merge-master-after-release: + # name: Merge 'master' to specific branch after release + # runs-on: ubuntu-latest + # env: + # BRANCHES: | + # production-core + # production-sokol + # production-eth-experimental + # production-eth-goerli + # production-lukso + # production-xdai + # production-polygon-supernets + # production-rsk + # production-immutable + # steps: + # - uses: actions/checkout@v5 + # - name: Set Git config + # run: | + # git config --local user.email "actions@github.com" + # git config --local user.name "Github Actions" + # - name: Merge master back after release + # run: | + # git fetch --unshallow + # touch errors.txt + # for branch in $BRANCHES; + # do + # git reset --merge + # git checkout master + # git fetch origin + # echo $branch + # git ls-remote --exit-code --heads origin $branch || { echo $branch >> errors.txt; continue; } + # echo "Merge 'master' to $branch" + # git checkout $branch + # git pull || { echo $branch >> errors.txt; continue; } + # git merge --no-ff master -m "Auto-merge master back to $branch" || { echo $branch >> errors.txt; continue; } + # git push || { echo $branch >> errors.txt; continue; } + # git checkout master; + # done + # [ -s errors.txt ] && echo "There are problems with merging 'master' to branches:" || echo "Errors file is empty" + # cat errors.txt + # [ ! -s errors.txt ] diff --git a/.github/workflows/public-release-eth.yml b/.github/workflows/public-release-eth.yml new file mode 100644 index 000000000000..465327829df3 --- /dev/null +++ b/.github/workflows/public-release-eth.yml @@ -0,0 +1,63 @@ +name: Public Release for Ethereum + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Ethereum (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-ethereum:latest, ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum + + - name: Build and push Docker image for Ethereum (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=ethereum + diff --git a/.github/workflows/public-release-filecoin.yml b/.github/workflows/public-release-filecoin.yml new file mode 100644 index 000000000000..2f877f6f06f2 --- /dev/null +++ b/.github/workflows/public-release-filecoin.yml @@ -0,0 +1,62 @@ +name: Public Release for Filecoin + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Filecoin (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=filecoin + + - name: Build and push Docker image for Filecoin (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=filecoin diff --git a/.github/workflows/public-release-fuse.yml b/.github/workflows/public-release-fuse.yml new file mode 100644 index 000000000000..e9883e60ac1a --- /dev/null +++ b/.github/workflows/public-release-fuse.yml @@ -0,0 +1,62 @@ +name: Public Release for Fuse + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Fuse (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-fuse:latest, ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + + - name: Build and push Docker image for Fuse (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true diff --git a/.github/workflows/public-release-gnosis.yml b/.github/workflows/public-release-gnosis.yml new file mode 100644 index 000000000000..d6882a17f922 --- /dev/null +++ b/.github/workflows/public-release-gnosis.yml @@ -0,0 +1,64 @@ +name: Public Release for Gnosis Chain + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Gnosis chain (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai:latest, ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum + + - name: Build and push Docker image for Gnosis chain (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum diff --git a/.github/workflows/public-release-optimism.yml b/.github/workflows/public-release-optimism.yml new file mode 100644 index 000000000000..06f1ef9d2557 --- /dev/null +++ b/.github/workflows/public-release-optimism.yml @@ -0,0 +1,62 @@ +name: Public Release for Optimism + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Optimism (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-optimism:latest, ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + + - name: Build and push Docker image for Optimism (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism diff --git a/.github/workflows/public-release-polygon-zkevm.yml b/.github/workflows/public-release-polygon-zkevm.yml new file mode 100644 index 000000000000..c5a93bb8350d --- /dev/null +++ b/.github/workflows/public-release-polygon-zkevm.yml @@ -0,0 +1,63 @@ +name: Public Release for Polygon zkEVM + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Polygon zkEVM (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zkevm:latest, ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=polygon_zkevm + + - name: Build and push Docker image for Polygon zkEVM (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=polygon_zkevm + diff --git a/.github/workflows/public-release-rootstock.yml b/.github/workflows/public-release-rootstock.yml new file mode 100644 index 000000000000..2d624f9b5789 --- /dev/null +++ b/.github/workflows/public-release-rootstock.yml @@ -0,0 +1,63 @@ +name: Public Release for Rootstock + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Rootstock (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-rsk:latest, ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=rsk + + - name: Build and push Docker image for Rootstock (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=rsk + diff --git a/.github/workflows/public-release-scroll.yml b/.github/workflows/public-release-scroll.yml new file mode 100644 index 000000000000..6e61bf33f62f --- /dev/null +++ b/.github/workflows/public-release-scroll.yml @@ -0,0 +1,63 @@ +name: Public Release for Scroll + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Scroll (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-scroll:latest, ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=scroll + + - name: Build and push Docker image for Scroll (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=scroll + diff --git a/.github/workflows/public-release-suave.yml b/.github/workflows/public-release-suave.yml new file mode 100644 index 000000000000..7038416783f8 --- /dev/null +++ b/.github/workflows/public-release-suave.yml @@ -0,0 +1,62 @@ +name: Public Release for SUAVE + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for SUAVE (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-suave:latest, ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=suave + + - name: Build and push Docker image for SUAVE (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=suave diff --git a/.github/workflows/public-release-zetachain.yml b/.github/workflows/public-release-zetachain.yml new file mode 100644 index 000000000000..0219b31ec966 --- /dev/null +++ b/.github/workflows/public-release-zetachain.yml @@ -0,0 +1,62 @@ +name: Public Release for Zetachain + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Zetachain (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zetachain:latest, ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain + + - name: Build and push Docker image for Zetachain (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zetachain diff --git a/.github/workflows/public-release-zilliqa.yml b/.github/workflows/public-release-zilliqa.yml new file mode 100644 index 000000000000..6736e46dcfe2 --- /dev/null +++ b/.github/workflows/public-release-zilliqa.yml @@ -0,0 +1,64 @@ +name: Public Release for Zilliqa + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zilliqa:latest, ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zilliqa + + - name: Build and push Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zilliqa + + diff --git a/.github/workflows/public-release-zksync.yml b/.github/workflows/public-release-zksync.yml new file mode 100644 index 000000000000..c596894987e2 --- /dev/null +++ b/.github/workflows/public-release-zksync.yml @@ -0,0 +1,62 @@ +name: Public Release for ZkSync + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.0 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for ZkSync (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zksync:latest, ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zksync + + - name: Build and push Docker image for ZkSync (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=zksync From 68d650e3f6a25d33bd3ec4562a0a95e666c960a4 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Sep 2025 17:42:27 +0300 Subject: [PATCH 0302/1006] 9.1.0 CHANGELOG --- CHANGELOG.md | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3b94918808a..f76ca3de52de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,104 @@ # Changelog +## 9.1.0 + +### 🚀 Features + +- Improved proxy detection ([#12846](https://github.com/blockscout/blockscout/issues/12846)) +- Add `reputation` property where applicable ([#13070](https://github.com/blockscout/blockscout/issues/13070)) +- Add envs to configure RemoteIp lib usage ([#13082](https://github.com/blockscout/blockscout/issues/13082)) +- Add possibility to forward event notification to another DB ([#13064](https://github.com/blockscout/blockscout/issues/13064)) +- Add x-api-key header ([#13076](https://github.com/blockscout/blockscout/issues/13076)) +- Add token_type to token transfer API response ([#13038](https://github.com/blockscout/blockscout/issues/13038)) +- Export main page counters to Multichain service ([#13007](https://github.com/blockscout/blockscout/issues/13007)) +- Add methodId to txlist rpc method ([#13043](https://github.com/blockscout/blockscout/issues/13043)) +- Runtime config option to disable file logging ([#12805](https://github.com/blockscout/blockscout/issues/12805)) +- Add celo-cpecific APIv1 `getepoch` action ([#12853](https://github.com/blockscout/blockscout/issues/12853)) + +### 🐛 Bug Fixes + +- Replace get_constant_by_key with get_constant_value in get_last_processed_token_address_hash ([#13118](https://github.com/blockscout/blockscout/issues/13118)) +- Duplicates of smart contracts additional sources ([#13018](https://github.com/blockscout/blockscout/issues/13018)) +- Set for read ops in NFT backfillers ([#13116](https://github.com/blockscout/blockscout/issues/13116)) +- Return internal transactions for consensus blocks only in /api/v2/internal-transactions ([#13041](https://github.com/blockscout/blockscout/issues/13041)) +- Fix recv timeout option in Universal proxy config ([#13046](https://github.com/blockscout/blockscout/issues/13046)) +- Fix failing ETH RPC tests ([#13099](https://github.com/blockscout/blockscout/issues/13099)) +- Escape only significant characters in tokens ([#13078](https://github.com/blockscout/blockscout/issues/13078)) +- `/api/v2/addresses/:hash/token-transfers` returns 500 on celo ([#13050](https://github.com/blockscout/blockscout/issues/13050)) +- RuntimeEnvHelper usage in Auth0.Migrated ([#13075](https://github.com/blockscout/blockscout/issues/13075)) +- Fix no function clause matching in Explorer.Chain.Transaction.decoded_input_data/5 ([#13055](https://github.com/blockscout/blockscout/issues/13055)) +- Fix Postgres errors in Explorer.Migrator.BackfillMetadataURL ([#13063](https://github.com/blockscout/blockscout/issues/13063)) +- Fix multichain search queue export bug processing ([#13049](https://github.com/blockscout/blockscout/issues/13049)) +- Csv export for celo l2 epoch rewards on address ([#12815](https://github.com/blockscout/blockscout/issues/12815)) +- Change signed_authorizations chain_id type to numeric ([#13042](https://github.com/blockscout/blockscout/issues/13042)) +- Address api spec for `filecoin` and `zilliqa` chain types ([#12996](https://github.com/blockscout/blockscout/issues/12996)) +- Token type filtering to support multiple types with OR logic ([#13008](https://github.com/blockscout/blockscout/issues/13008)) +- Don't validate address hash for common blocks channels ([#13020](https://github.com/blockscout/blockscout/issues/13020)) +- Fix matching in current token balances import filter ([#12930](https://github.com/blockscout/blockscout/issues/12930)) +- Expand indexer timeout exception definition ([#12748](https://github.com/blockscout/blockscout/issues/12748)) + +### 🚜 Refactor + +- Remove public tags request functionality ([#13006](https://github.com/blockscout/blockscout/issues/13006)) + +### ⚡ Performance + +- Optimize maybe_hide_scam_addresses/3 query ([#12927](https://github.com/blockscout/blockscout/issues/12927)) + +### ⚙️ Miscellaneous Tasks + +- Change release workflow ([#13087](https://github.com/blockscout/blockscout/issues/13087)) +- Update and format pull request template ([#13028](https://github.com/blockscout/blockscout/issues/13028)) +- Add final check for ReindexDuplicatedInternalTransactions ([#13091](https://github.com/blockscout/blockscout/issues/13091)) +- Remove obsolete circleci config ([#13097](https://github.com/blockscout/blockscout/issues/13097)) +- Replace ReindexDuplicatedInternalTransactions grouping field ([#13084](https://github.com/blockscout/blockscout/issues/13084)) +- Bump default rps to 5 ([#13089](https://github.com/blockscout/blockscout/issues/13089)) +- Add `is_pending_update` flag to block and transaction API endpoints ([#13013](https://github.com/blockscout/blockscout/issues/13013)) +- Bump actions major versions ([#13077](https://github.com/blockscout/blockscout/issues/13077)) +- Move token transfers to a separate event handler ([#13068](https://github.com/blockscout/blockscout/issues/13068)) +- Remove Polygon Edge modules and chain type ([#13056](https://github.com/blockscout/blockscout/issues/13056)) +- Cover token info export to Multichain service by unit tests ([#12899](https://github.com/blockscout/blockscout/issues/12899)) +- Route left API DB requests from master to read DB replica ([#12896](https://github.com/blockscout/blockscout/issues/12896)) +- Refactor usage of delete_parameters_from_next_page_params/1 ([#13005](https://github.com/blockscout/blockscout/issues/13005)) +- Move address nonce updating to a separate process ([#12941](https://github.com/blockscout/blockscout/issues/12941)) +- Catchup fetcher various improvements ([#12866](https://github.com/blockscout/blockscout/issues/12866)) +- Add disconnect_on_error_codes param to repo config ([#12800](https://github.com/blockscout/blockscout/issues/12800)) +- Move addresses to a separate import stage ([#12857](https://github.com/blockscout/blockscout/issues/12857)) + +### New ENV variables + +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `DISABLE_FILE_LOGGING` | Disables file-based logging when set to `true`. When enabled, application logs will only be written to stdout/stderr. | Version: v9.1.0\+
Default: `false`
Applications: API, Indexer | +| `API_RATE_LIMIT_REMOTE_IP_HEADERS` | Comma separated list of HTTP headers to extract the real client IP address when Blockscout is behind a proxy for rate limiting purposes. Implemented in [#12386](https://github.com/blockscout/blockscout/pull/13082) | Version: v9.1.0\+
Default: `x-forwarded-for`
Applications: API | +| `API_RATE_LIMIT_REMOTE_IP_KNOWN_PROXIES` | Comma separated list of trusted proxy IP addresses or CIDR ranges that are allowed to set the client IP headers for rate limiting. Implemented in [#12386](https://github.com/blockscout/blockscout/pull/13082) | Version: v9.1.0\+
Default: `(empty)`
Applications: API | +| `INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT` | Disables exporting of interop messages to Multichain service. Implemented in [#13051](https://github.com/blockscout/blockscout/pull/13051). | Version: v9.1.0\+
Default: `true`
Applications: Indexer | +| `MICROSERVICE_MULTICHAIN_SEARCH_COUNTERS_CHUNK_SIZE` | Chunk size of counters while exporting to Multichain Search DB. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_FETCHER` | If `true`, multichain DB counters export fetcher doesn't run. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `false`
Applications: Indexer | +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_BATCH_SIZE` | Batch size for multichain DB counters export fetcher. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_CONCURRENCY` | Concurrency for multichain DB counters export fetcher. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `10`
Applications: Indexer | +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to multichain DB counters export queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1s`
Applications: Indexer | +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_MAX_QUEUE_SIZE` | Maximum size of multichain DB counters export queue. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT` | Limit of the init query for processing the counters export queue to the Multichain DB. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | + + + +### Deprecated ENV variables + +| Variable | Required | Description | Default | Version | Need recompile | Deprecated in Version | +| ----------------------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------- | -------------- | --------------------- | +| `INDEXER_POLYGON_EDGE_L1_RPC` | The RPC endpoint for L1 used to fetch deposit or withdrawal events. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L1_EXIT_HELPER_CONTRACT` | The address of ExitHelper contract on L1 (root chain) used to fetch withdrawal exits. Required for withdrawal events indexing. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L1_WITHDRAWALS_START_BLOCK` | The number of start block on L1 (root chain) to index withdrawal exits. If the table of withdrawal exits is not empty, the process will continue indexing from the last indexed message. If empty or not defined, the withdrawal exits are not indexed. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L1_STATE_SENDER_CONTRACT` | The address of StateSender contract on L1 (root chain) used to fetch deposits. Required for deposit events indexing. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L1_DEPOSITS_START_BLOCK` | The number of start block on L1 (root chain) to index deposits. If the table of deposits is not empty, the process will continue indexing from the last indexed message. If empty or not defined, the deposits are not indexed. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L2_STATE_SENDER_CONTRACT` | The address of L2StateSender contract on L2 (child chain) used to fetch withdrawals. Required for withdrawal events indexing. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L2_WITHDRAWALS_START_BLOCK` | The number of start block on L2 (child chain) to index withdrawals. If the table of withdrawals is not empty, the process will fill gaps and then continue indexing from the last indexed message. If empty or not defined, the withdrawals are not indexed. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L2_STATE_RECEIVER_CONTRACT` | The address of StateReceiver contract on L2 (child chain) used to fetch deposit executes. Required for deposit events indexing. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_L2_DEPOSITS_START_BLOCK` | The number of start block on L2 (child chain) to index deposit executes. If the table of deposit executes is not empty, the process will fill gaps and then continue indexing from the last indexed message. If empty or not defined, the deposit executes are not indexed. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE` | Block range size for eth\_getLogs request in Polygon Edge indexer modules. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | + + ## 9.0.2 ### 🐛 Bug Fixes @@ -111,6 +210,7 @@ - Remove `is_self_destructed` field in `/api/v2/smart-contracts/{address_hash}` response ([#12239](https://github.com/blockscout/blockscout/issues/12239)) - Set home directory for blockscout user ([#12337](https://github.com/blockscout/blockscout/issues/12337)) +### New ENV variables | Variable | Description | Parameters | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | From 8c89c1d6d9b57addfeff5265f32fd4f993da48a6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 11:26:03 +0300 Subject: [PATCH 0303/1006] Copy Bundle from Private to Public Repository GA workflow --- .github/workflows/copy-bundle.yml | 65 +++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 .github/workflows/copy-bundle.yml diff --git a/.github/workflows/copy-bundle.yml b/.github/workflows/copy-bundle.yml new file mode 100644 index 000000000000..1dca423e7c1a --- /dev/null +++ b/.github/workflows/copy-bundle.yml @@ -0,0 +1,65 @@ +name: Copy Bundle from Private to Public Repository + +on: + workflow_dispatch: + inputs: + release_number: + description: 'Release number (e.g., v9.1.0, latest)' + required: true + type: string + +jobs: + copy-bundle: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Set source and target tags + run: | + RELEASE_NUMBER="${{ github.event.inputs.release_number || 'latest' }}" + SOURCE_TAG="${RELEASE_NUMBER}" + TARGET_TAG="${RELEASE_NUMBER}" + echo "SOURCE_TAG=${SOURCE_TAG}" >> $GITHUB_ENV + echo "TARGET_TAG=${TARGET_TAG}" >> $GITHUB_ENV + + - name: Copy image from private to public repository + run: | + RELEASE_NUMBER="${{ github.event.inputs.release_number }}" + # Copy with the specific release number tag + docker buildx imagetools create \ + --tag ghcr.io/blockscout/blockscout-test:${TARGET_TAG} \ + ghcr.io/blockscout/blockscout-private:${SOURCE_TAG} + + # Also copy with latest tag + docker buildx imagetools create \ + --tag ghcr.io/blockscout/blockscout-test:latest \ + ghcr.io/blockscout/blockscout-private:latest + + - name: Verify deployment + run: | + RELEASE_NUMBER="${{ github.event.inputs.release_number }}" + echo "Bundle successfully copied from private to public repository" + echo "Source: ghcr.io/blockscout/blockscout-private:${SOURCE_TAG}" + echo "Target 1: ghcr.io/blockscout/blockscout-test:${TARGET_TAG}" + echo "Target 2: ghcr.io/blockscout/blockscout-test:latest" + + # Inspect both copied images + echo "Inspecting release-specific tag:" + docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:${TARGET_TAG} + echo "Inspecting latest tag:" + docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:latest From d06d987337dc581bc5e1905962ef88d77a7c0b24 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 11:28:52 +0300 Subject: [PATCH 0304/1006] Update: Copy Bundle from Private to Public Repository GA workflow --- .github/workflows/copy-bundle.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/copy-bundle.yml b/.github/workflows/copy-bundle.yml index 1dca423e7c1a..e1ffb904a01c 100644 --- a/.github/workflows/copy-bundle.yml +++ b/.github/workflows/copy-bundle.yml @@ -43,18 +43,18 @@ jobs: # Copy with the specific release number tag docker buildx imagetools create \ --tag ghcr.io/blockscout/blockscout-test:${TARGET_TAG} \ - ghcr.io/blockscout/blockscout-private:${SOURCE_TAG} + ghcr.io/blockscout/blockscout:${SOURCE_TAG} # Also copy with latest tag docker buildx imagetools create \ --tag ghcr.io/blockscout/blockscout-test:latest \ - ghcr.io/blockscout/blockscout-private:latest + ghcr.io/blockscout/blockscout:latest - name: Verify deployment run: | RELEASE_NUMBER="${{ github.event.inputs.release_number }}" echo "Bundle successfully copied from private to public repository" - echo "Source: ghcr.io/blockscout/blockscout-private:${SOURCE_TAG}" + echo "Source: ghcr.io/blockscout/blockscout:${SOURCE_TAG}" echo "Target 1: ghcr.io/blockscout/blockscout-test:${TARGET_TAG}" echo "Target 2: ghcr.io/blockscout/blockscout-test:latest" From a1a5cd7857e3a8f1e7c347186d3a30c16f7b1e1e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 11:47:11 +0300 Subject: [PATCH 0305/1006] Update: Copy Bundle from Private to Public Repository GA workflow for all chain types --- .github/workflows/copy-bundle.yml | 83 +++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 16 deletions(-) diff --git a/.github/workflows/copy-bundle.yml b/.github/workflows/copy-bundle.yml index e1ffb904a01c..8049038caed4 100644 --- a/.github/workflows/copy-bundle.yml +++ b/.github/workflows/copy-bundle.yml @@ -37,29 +37,80 @@ jobs: echo "SOURCE_TAG=${SOURCE_TAG}" >> $GITHUB_ENV echo "TARGET_TAG=${TARGET_TAG}" >> $GITHUB_ENV - - name: Copy image from private to public repository + - name: Copy chain-specific images from private to public repositories run: | RELEASE_NUMBER="${{ github.event.inputs.release_number }}" - # Copy with the specific release number tag - docker buildx imagetools create \ - --tag ghcr.io/blockscout/blockscout-test:${TARGET_TAG} \ - ghcr.io/blockscout/blockscout:${SOURCE_TAG} - # Also copy with latest tag - docker buildx imagetools create \ - --tag ghcr.io/blockscout/blockscout-test:latest \ - ghcr.io/blockscout/blockscout:latest + # Array of chain-specific repositories + CHAINS=( + "blockscout" + "blockscout-arbitrum" + "blockscout-celo" + "blockscout-ethereum" + "blockscout-filecoin" + "blockscout-fuse" + "blockscout-xdai" + "blockscout-optimism" + "blockscout-zkevm" + "blockscout-rsk" + "blockscout-scroll" + "blockscout-suave" + "blockscout-zetachain" + "blockscout-zilliqa" + "blockscout-zksync" + ) + + # Copy each chain-specific image + for CHAIN in "${CHAINS[@]}"; do + echo "Copying ${CHAIN}..." + + # Copy with the specific release number tag + docker buildx imagetools create \ + --tag ghcr.io/blockscout/${CHAIN}-test:${RELEASE_NUMBER} \ + ghcr.io/blockscout/${CHAIN}:${RELEASE_NUMBER} + + # Also copy with latest tag + docker buildx imagetools create \ + --tag ghcr.io/blockscout/${CHAIN}-test:latest \ + ghcr.io/blockscout/${CHAIN}:latest + + echo "✅ Completed copying ${CHAIN}" + done - name: Verify deployment run: | RELEASE_NUMBER="${{ github.event.inputs.release_number }}" - echo "Bundle successfully copied from private to public repository" - echo "Source: ghcr.io/blockscout/blockscout:${SOURCE_TAG}" - echo "Target 1: ghcr.io/blockscout/blockscout-test:${TARGET_TAG}" - echo "Target 2: ghcr.io/blockscout/blockscout-test:latest" + echo "🎉 All bundles successfully copied from private to public repositories" + echo "" + echo "📦 Main repository:" + echo " Source: ghcr.io/blockscout/blockscout:${SOURCE_TAG}" + echo " Target 1: ghcr.io/blockscout/blockscout-test:${TARGET_TAG}" + echo " Target 2: ghcr.io/blockscout/blockscout-test:latest" + echo "" + echo "🔗 Chain-specific repositories copied:" + + CHAINS=( + "blockscout-arbitrum" + "blockscout-celo" + "blockscout-ethereum" + "blockscout-filecoin" + "blockscout-fuse" + "blockscout-xdai" + "blockscout-optimism" + "blockscout-zkevm" + "blockscout-rsk" + "blockscout-scroll" + "blockscout-suave" + "blockscout-zetachain" + "blockscout-zilliqa" + "blockscout-zksync" + ) + + for CHAIN in "${CHAINS[@]}"; do + echo " ✅ ${CHAIN} -> ${CHAIN}-test (tags: ${RELEASE_NUMBER}, latest)" + done - # Inspect both copied images - echo "Inspecting release-specific tag:" + echo "" + echo "🔍 Inspecting main repository images:" docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:${TARGET_TAG} - echo "Inspecting latest tag:" docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:latest From ae0d73d67460d6cfba50bdac774c6f2ef9d5dde3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 12:09:26 +0300 Subject: [PATCH 0306/1006] Single public release workflow --- .github/workflows/public-release-arbitrum.yml | 62 ------- .github/workflows/public-release-celo.yml | 66 -------- .github/workflows/public-release-default.yml | 151 ------------------ .github/workflows/public-release-eth.yml | 63 -------- .github/workflows/public-release-filecoin.yml | 62 ------- .github/workflows/public-release-fuse.yml | 62 ------- .github/workflows/public-release-gnosis.yml | 64 -------- .github/workflows/public-release-optimism.yml | 62 ------- .../public-release-polygon-zkevm.yml | 63 -------- .../workflows/public-release-rootstock.yml | 63 -------- .github/workflows/public-release-scroll.yml | 63 -------- .github/workflows/public-release-suave.yml | 62 ------- .../workflows/public-release-zetachain.yml | 62 ------- .github/workflows/public-release-zilliqa.yml | 64 -------- .github/workflows/public-release-zksync.yml | 62 ------- .../{copy-bundle.yml => public-release.yml} | 24 ++- 16 files changed, 10 insertions(+), 1045 deletions(-) delete mode 100644 .github/workflows/public-release-arbitrum.yml delete mode 100644 .github/workflows/public-release-celo.yml delete mode 100644 .github/workflows/public-release-default.yml delete mode 100644 .github/workflows/public-release-eth.yml delete mode 100644 .github/workflows/public-release-filecoin.yml delete mode 100644 .github/workflows/public-release-fuse.yml delete mode 100644 .github/workflows/public-release-gnosis.yml delete mode 100644 .github/workflows/public-release-optimism.yml delete mode 100644 .github/workflows/public-release-polygon-zkevm.yml delete mode 100644 .github/workflows/public-release-rootstock.yml delete mode 100644 .github/workflows/public-release-scroll.yml delete mode 100644 .github/workflows/public-release-suave.yml delete mode 100644 .github/workflows/public-release-zetachain.yml delete mode 100644 .github/workflows/public-release-zilliqa.yml delete mode 100644 .github/workflows/public-release-zksync.yml rename .github/workflows/{copy-bundle.yml => public-release.yml} (80%) diff --git a/.github/workflows/public-release-arbitrum.yml b/.github/workflows/public-release-arbitrum.yml deleted file mode 100644 index 7530a2f384c6..000000000000 --- a/.github/workflows/public-release-arbitrum.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Arbitrum - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Arbitrum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:latest, ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=arbitrum - - - name: Build and push Docker image for Arbitrum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-arbitrum:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=arbitrum diff --git a/.github/workflows/public-release-celo.yml b/.github/workflows/public-release-celo.yml deleted file mode 100644 index 170e96a1de1b..000000000000 --- a/.github/workflows/public-release-celo.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Public Release for Celo - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - API_GRAPHQL_MAX_COMPLEXITY: 10400 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for CELO (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-celo:latest, ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo - - - name: Build and push Docker image for CELO (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-celo:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo - diff --git a/.github/workflows/public-release-default.yml b/.github/workflows/public-release-default.yml deleted file mode 100644 index de6132a33c1d..000000000000 --- a/.github/workflows/public-release-default.yml +++ /dev/null @@ -1,151 +0,0 @@ -name: Public Release - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build & Push Core Docker image (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:latest, ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - - name: Build & Push Core Docker image (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - - - name: Build & Push Docker image with an old UI (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/oldUI.Dockerfile - push: true - cache-from: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache - cache-to: type=registry,ref=ghcr.io/blockscout/blockscout:buildcache,mode=max - tags: ghcr.io/blockscout/blockscout:${{ env.RELEASE_VERSION }}-with-old-ui - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DECODE_NOT_A_CONTRACT_CALLS=false - MIXPANEL_URL= - MIXPANEL_TOKEN= - AMPLITUDE_URL= - AMPLITUDE_API_KEY= - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - - # - name: Send release announcement to Slack workflow - # id: slack - # uses: slackapi/slack-github-action@v1.24.0 - # with: - # payload: | - # { - # "release-version": "${{ env.RELEASE_VERSION }}", - # "release-link": "https://github.com/blockscout/blockscout/releases/tag/v${{ env.RELEASE_VERSION }}" - # } - # env: - # SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - - # merge-master-after-release: - # name: Merge 'master' to specific branch after release - # runs-on: ubuntu-latest - # env: - # BRANCHES: | - # production-core - # production-sokol - # production-eth-experimental - # production-eth-goerli - # production-lukso - # production-xdai - # production-polygon-supernets - # production-rsk - # production-immutable - # steps: - # - uses: actions/checkout@v5 - # - name: Set Git config - # run: | - # git config --local user.email "actions@github.com" - # git config --local user.name "Github Actions" - # - name: Merge master back after release - # run: | - # git fetch --unshallow - # touch errors.txt - # for branch in $BRANCHES; - # do - # git reset --merge - # git checkout master - # git fetch origin - # echo $branch - # git ls-remote --exit-code --heads origin $branch || { echo $branch >> errors.txt; continue; } - # echo "Merge 'master' to $branch" - # git checkout $branch - # git pull || { echo $branch >> errors.txt; continue; } - # git merge --no-ff master -m "Auto-merge master back to $branch" || { echo $branch >> errors.txt; continue; } - # git push || { echo $branch >> errors.txt; continue; } - # git checkout master; - # done - # [ -s errors.txt ] && echo "There are problems with merging 'master' to branches:" || echo "Errors file is empty" - # cat errors.txt - # [ ! -s errors.txt ] diff --git a/.github/workflows/public-release-eth.yml b/.github/workflows/public-release-eth.yml deleted file mode 100644 index 465327829df3..000000000000 --- a/.github/workflows/public-release-eth.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Ethereum - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Ethereum (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-ethereum:latest, ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=ethereum - - - name: Build and push Docker image for Ethereum (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-ethereum:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=ethereum - diff --git a/.github/workflows/public-release-filecoin.yml b/.github/workflows/public-release-filecoin.yml deleted file mode 100644 index 2f877f6f06f2..000000000000 --- a/.github/workflows/public-release-filecoin.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Filecoin - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Filecoin (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-filecoin:latest, ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=filecoin - - - name: Build and push Docker image for Filecoin (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-filecoin:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=filecoin diff --git a/.github/workflows/public-release-fuse.yml b/.github/workflows/public-release-fuse.yml deleted file mode 100644 index e9883e60ac1a..000000000000 --- a/.github/workflows/public-release-fuse.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Fuse - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Fuse (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-fuse:latest, ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - - - name: Build and push Docker image for Fuse (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-fuse:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true diff --git a/.github/workflows/public-release-gnosis.yml b/.github/workflows/public-release-gnosis.yml deleted file mode 100644 index d6882a17f922..000000000000 --- a/.github/workflows/public-release-gnosis.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Public Release for Gnosis Chain - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Gnosis chain (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-xdai:latest, ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - CHAIN_TYPE=ethereum - - - name: Build and push Docker image for Gnosis chain (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-xdai:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - BRIDGED_TOKENS_ENABLED=true - CHAIN_TYPE=ethereum diff --git a/.github/workflows/public-release-optimism.yml b/.github/workflows/public-release-optimism.yml deleted file mode 100644 index 06f1ef9d2557..000000000000 --- a/.github/workflows/public-release-optimism.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Optimism - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Optimism (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-optimism:latest, ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism - - - name: Build and push Docker image for Optimism (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-optimism:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=optimism diff --git a/.github/workflows/public-release-polygon-zkevm.yml b/.github/workflows/public-release-polygon-zkevm.yml deleted file mode 100644 index c5a93bb8350d..000000000000 --- a/.github/workflows/public-release-polygon-zkevm.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Polygon zkEVM - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Polygon zkEVM (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zkevm:latest, ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=polygon_zkevm - - - name: Build and push Docker image for Polygon zkEVM (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zkevm:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=polygon_zkevm - diff --git a/.github/workflows/public-release-rootstock.yml b/.github/workflows/public-release-rootstock.yml deleted file mode 100644 index 2d624f9b5789..000000000000 --- a/.github/workflows/public-release-rootstock.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Rootstock - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Rootstock (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-rsk:latest, ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=rsk - - - name: Build and push Docker image for Rootstock (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-rsk:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=rsk - diff --git a/.github/workflows/public-release-scroll.yml b/.github/workflows/public-release-scroll.yml deleted file mode 100644 index 6e61bf33f62f..000000000000 --- a/.github/workflows/public-release-scroll.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: Public Release for Scroll - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Scroll (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-scroll:latest, ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=scroll - - - name: Build and push Docker image for Scroll (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-scroll:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=scroll - diff --git a/.github/workflows/public-release-suave.yml b/.github/workflows/public-release-suave.yml deleted file mode 100644 index 7038416783f8..000000000000 --- a/.github/workflows/public-release-suave.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for SUAVE - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for SUAVE (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave:latest, ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave - - - name: Build and push Docker image for SUAVE (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave diff --git a/.github/workflows/public-release-zetachain.yml b/.github/workflows/public-release-zetachain.yml deleted file mode 100644 index 0219b31ec966..000000000000 --- a/.github/workflows/public-release-zetachain.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for Zetachain - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for Zetachain (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zetachain:latest, ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zetachain - - - name: Build and push Docker image for Zetachain (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zetachain:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zetachain diff --git a/.github/workflows/public-release-zilliqa.yml b/.github/workflows/public-release-zilliqa.yml deleted file mode 100644 index 6736e46dcfe2..000000000000 --- a/.github/workflows/public-release-zilliqa.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Public Release for Zilliqa - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:latest, ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zilliqa - - - name: Build and push Docker image (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zilliqa:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zilliqa - - diff --git a/.github/workflows/public-release-zksync.yml b/.github/workflows/public-release-zksync.yml deleted file mode 100644 index c596894987e2..000000000000 --- a/.github/workflows/public-release-zksync.yml +++ /dev/null @@ -1,62 +0,0 @@ -name: Public Release for ZkSync - -on: - workflow_dispatch: - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.0 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for ZkSync (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zksync:latest, ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zksync - - - name: Build and push Docker image for ZkSync (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-zksync:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=zksync diff --git a/.github/workflows/copy-bundle.yml b/.github/workflows/public-release.yml similarity index 80% rename from .github/workflows/copy-bundle.yml rename to .github/workflows/public-release.yml index 8049038caed4..3ec4f0906f27 100644 --- a/.github/workflows/copy-bundle.yml +++ b/.github/workflows/public-release.yml @@ -1,4 +1,4 @@ -name: Copy Bundle from Private to Public Repository +name: Public release publishing on: workflow_dispatch: @@ -66,13 +66,13 @@ jobs: # Copy with the specific release number tag docker buildx imagetools create \ - --tag ghcr.io/blockscout/${CHAIN}-test:${RELEASE_NUMBER} \ - ghcr.io/blockscout/${CHAIN}:${RELEASE_NUMBER} + --tag ghcr.io/blockscout/${CHAIN}:${RELEASE_NUMBER} \ + ghcr.io/blockscout/${CHAIN}-private:${RELEASE_NUMBER} # Also copy with latest tag docker buildx imagetools create \ - --tag ghcr.io/blockscout/${CHAIN}-test:latest \ - ghcr.io/blockscout/${CHAIN}:latest + --tag ghcr.io/blockscout/${CHAIN}:latest \ + ghcr.io/blockscout/${CHAIN}-private:latest echo "✅ Completed copying ${CHAIN}" done @@ -82,14 +82,10 @@ jobs: RELEASE_NUMBER="${{ github.event.inputs.release_number }}" echo "🎉 All bundles successfully copied from private to public repositories" echo "" - echo "📦 Main repository:" - echo " Source: ghcr.io/blockscout/blockscout:${SOURCE_TAG}" - echo " Target 1: ghcr.io/blockscout/blockscout-test:${TARGET_TAG}" - echo " Target 2: ghcr.io/blockscout/blockscout-test:latest" - echo "" - echo "🔗 Chain-specific repositories copied:" + echo "🔗 All repositories copied:" CHAINS=( + "blockscout" "blockscout-arbitrum" "blockscout-celo" "blockscout-ethereum" @@ -107,10 +103,10 @@ jobs: ) for CHAIN in "${CHAINS[@]}"; do - echo " ✅ ${CHAIN} -> ${CHAIN}-test (tags: ${RELEASE_NUMBER}, latest)" + echo " ✅ ${CHAIN}-private -> ${CHAIN} (tags: ${RELEASE_NUMBER}, latest)" done echo "" echo "🔍 Inspecting main repository images:" - docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:${TARGET_TAG} - docker buildx imagetools inspect ghcr.io/blockscout/blockscout-test:latest + docker buildx imagetools inspect ghcr.io/blockscout/blockscout:${TARGET_TAG} + docker buildx imagetools inspect ghcr.io/blockscout/blockscout:latest From 646a66a712a99c36f048550fdf042f5774e15377 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 14:53:12 +0300 Subject: [PATCH 0307/1006] Fix cspell issues --- CHANGELOG.md | 2 +- cspell.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f76ca3de52de..03fab8d48568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Export main page counters to Multichain service ([#13007](https://github.com/blockscout/blockscout/issues/13007)) - Add methodId to txlist rpc method ([#13043](https://github.com/blockscout/blockscout/issues/13043)) - Runtime config option to disable file logging ([#12805](https://github.com/blockscout/blockscout/issues/12805)) -- Add celo-cpecific APIv1 `getepoch` action ([#12853](https://github.com/blockscout/blockscout/issues/12853)) +- Add celo-specific APIv1 `getepoch` action ([#12853](https://github.com/blockscout/blockscout/issues/12853)) ### 🐛 Bug Fixes diff --git a/cspell.json b/cspell.json index c67cef16cb17..b9b69015f981 100644 --- a/cspell.json +++ b/cspell.json @@ -60,6 +60,7 @@ "Averify", "awesomplete", "Backfiller", + "backfillers" "backoff", "badhash", "badnumber", From 933bb0425812778252181737f26bf9f96e1542bd Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Sep 2025 14:53:35 +0300 Subject: [PATCH 0308/1006] Fix cspell issues --- cspell.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index b9b69015f981..57bb07523062 100644 --- a/cspell.json +++ b/cspell.json @@ -60,7 +60,7 @@ "Averify", "awesomplete", "Backfiller", - "backfillers" + "backfillers", "backoff", "badhash", "badnumber", From 10301f181b80982c91dc2346355e032c6e2932ca Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:38:58 +0300 Subject: [PATCH 0309/1006] feat: on-demand bytecode fetching on smart contract verification requests (#13081) * Initial plan * Add on-demand bytecode fetching for verification requests Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Improve bytecode fetching to be more robust and reliable Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Refactor bytecode fetching to reuse existing ContractCode module and address code review feedback Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Use Enum.at instead of List.first for explicit indexing Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Refactor to use handle_call with existing fetch_contract_code logic to eliminate duplication Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * Refactor to eliminate Helper wrapper and reuse existing logic per code review feedback Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> * refactor: reuse the same functions from contract code fetcher * fix: rebase artifacts * feat: add rate limiter --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: fedor-ivn <34605001+fedor-ivn@users.noreply.github.com> Co-authored-by: Fedor Ivanov --- .../api/rpc/contract_controller.ex | 31 +++++-- .../api/v2/verification_controller.ex | 30 ++++--- .../api/v2/verification_controller_test.exs | 81 ++++++++++++++++++ .../fetcher/on_demand/contract_code.ex | 77 +++++++++++------ .../fetcher/on_demand/contract_code_test.exs | 84 +++++++++++++++++++ 5 files changed, 256 insertions(+), 47 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index 05a9ccfcecc9..5149417682b8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -17,6 +17,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher alias Explorer.ThirdPartyIntegrations.Sourcify + alias Indexer.Fetcher.OnDemand.ContractCode if @chain_type == :zksync do @optimization_runs "0" @@ -32,7 +33,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do @addresses_required "Query parameter contractaddresses is required" @contract_not_found "Smart-contract not found or is not verified" @restricted_access "Access to this address is restricted" - @not_a_smart_contract "The address is not a smart contract" + @not_a_smart_contract "Address is not a smart-contract" @addresses_limit 10 @api_true [api?: true] @@ -82,8 +83,11 @@ defmodule BlockScoutWeb.API.RPC.ContractController do {:format, {:ok, casted_address_hash}} <- to_address_hash(address_hash), {:params, external_libraries} <- {:params, fetch_external_libraries(params)}, - {:not_a_smart_contract, bytecode} when bytecode != "0x" <- - {:not_a_smart_contract, Chain.smart_contract_bytecode(casted_address_hash, @api_true)}, + {:not_a_smart_contract, {:ok, _bytecode}} <- + {:not_a_smart_contract, + conn + |> AccessHelper.conn_to_ip_string() + |> ContractCode.get_or_fetch_bytecode(casted_address_hash)}, {:publish, {:ok, _}} <- {:publish, Publisher.publish(address_hash, fetched_params, external_libraries)} do address = Contracts.address_hash_to_address_with_source_code(casted_address_hash) @@ -165,8 +169,11 @@ defmodule BlockScoutWeb.API.RPC.ContractController do {:check_verified_status, SmartContract.verified_with_full_match?(address_hash)}, {:format, {:ok, casted_address_hash}} <- to_address_hash(address_hash), {:params, {:ok, fetched_params}} <- {:params, fetch_verifysourcecode_params(params)}, - {:not_a_smart_contract, bytecode} when bytecode != "0x" <- - {:not_a_smart_contract, Chain.smart_contract_bytecode(casted_address_hash, @api_true)}, + {:not_a_smart_contract, {:ok, _bytecode}} <- + {:not_a_smart_contract, + conn + |> AccessHelper.conn_to_ip_string() + |> ContractCode.get_or_fetch_bytecode(casted_address_hash)}, uid <- VerificationStatus.generate_uid(address_hash) do Que.add(SolidityPublisherWorker, {"json_api", fetched_params, json_input, uid}) @@ -201,8 +208,11 @@ defmodule BlockScoutWeb.API.RPC.ContractController do {:check_verified_status, SmartContract.verified_with_full_match?(address_hash)}, {:format, {:ok, casted_address_hash}} <- to_address_hash(address_hash), {:params, {:ok, fetched_params}} <- {:params, fetch_verifysourcecode_solidity_single_file_params(params)}, - {:not_a_smart_contract, bytecode} when bytecode != "0x" <- - {:not_a_smart_contract, Chain.smart_contract_bytecode(casted_address_hash, @api_true)}, + {:not_a_smart_contract, {:ok, _bytecode}} <- + {:not_a_smart_contract, + conn + |> AccessHelper.conn_to_ip_string() + |> ContractCode.get_or_fetch_bytecode(casted_address_hash)}, external_libraries <- fetch_external_libraries_for_verifysourcecode(params), uid <- VerificationStatus.generate_uid(address_hash) do Que.add(SolidityPublisherWorker, {"flattened_api", fetched_params, external_libraries, uid}) @@ -439,8 +449,11 @@ defmodule BlockScoutWeb.API.RPC.ContractController do def verify_vyper_contract(conn, %{"addressHash" => address_hash} = params) do with {:params, {:ok, fetched_params}} <- {:params, fetch_vyper_verify_params(params)}, {:format, {:ok, casted_address_hash}} <- to_address_hash(address_hash), - {:not_a_smart_contract, bytecode} when bytecode != "0x" <- - {:not_a_smart_contract, Chain.smart_contract_bytecode(casted_address_hash, @api_true)}, + {:not_a_smart_contract, {:ok, _bytecode}} <- + {:not_a_smart_contract, + conn + |> AccessHelper.conn_to_ip_string() + |> ContractCode.get_or_fetch_bytecode(casted_address_hash)}, {:publish, {:ok, _}} <- {:publish, VyperPublisher.publish(address_hash, fetched_params)} do address = Contracts.address_hash_to_address_with_source_code(casted_address_hash) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex index 442b38f82c9e..a691ec638f69 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex @@ -15,6 +15,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do alias Explorer.SmartContract.Stylus.PublisherWorker, as: StylusPublisherWorker alias Explorer.SmartContract.Vyper.PublisherWorker, as: VyperPublisherWorker alias Explorer.SmartContract.{CompilerVersion, RustVerifierInterface, Solidity.CodeCompiler, StylusVerifierInterface} + alias Indexer.Fetcher.OnDemand.ContractCode action_fallback(BlockScoutWeb.API.V2.FallbackController) @@ -103,7 +104,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do ) do Logger.info("API v2 smart-contract #{address_hash_string} verification via flattened file") - with :validated <- validate_address(params) do + with :validated <- validate_address(conn, params) do verification_params = %{ "address_hash" => String.downcase(address_hash_string), @@ -138,7 +139,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do ) do Logger.info("API v2 smart-contract #{address_hash_string} verification via standard json input") - with {:json_input, json_input} <- validate_params_standard_json_input(params) do + with {:json_input, json_input} <- validate_params_standard_json_input(conn, params) do verification_params = %{ "address_hash" => String.downcase(address_hash_string), @@ -167,7 +168,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do with {:not_found, true} <- {:not_found, Application.get_env(:explorer, Explorer.ThirdPartyIntegrations.Sourcify)[:enabled]}, - :validated <- validate_address(params), + :validated <- validate_address(conn, params), files_array <- PublishHelper.prepare_files_array(files), {:no_json_file, %Plug.Upload{path: _path}} <- {:no_json_file, PublishHelper.get_one_json(files_array)}, @@ -194,7 +195,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do Logger.info("API v2 smart-contract #{address_hash_string} verification via multipart") with :verifier_enabled <- check_microservice(), - :validated <- validate_address(params), + :validated <- validate_address(conn, params), libraries <- Map.get(params, "libraries", "{}"), {:libs_format, {:ok, json}} <- {:libs_format, Jason.decode(libraries)} do verification_params = @@ -230,7 +231,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do %{"address_hash" => address_hash_string, "compiler_version" => compiler_version, "source_code" => source_code} = params ) do - with :validated <- validate_address(params) do + with :validated <- validate_address(conn, params) do verification_params = %{ "address_hash" => String.downcase(address_hash_string), @@ -258,7 +259,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do Logger.info("API v2 vyper smart-contract #{address_hash_string} verification") with :verifier_enabled <- check_microservice(), - :validated <- validate_address(params) do + :validated <- validate_address(conn, params) do interfaces = parse_interfaces(params["interfaces"]) verification_params = @@ -291,7 +292,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do Logger.info("API v2 vyper smart-contract #{address_hash_string} verification via standard json input") with :verifier_enabled <- check_microservice(), - {:json_input, json_input} <- validate_params_standard_json_input(params) do + {:json_input, json_input} <- validate_params_standard_json_input(conn, params) do verification_params = %{ "address_hash" => String.downcase(address_hash_string), "compiler_version" => compiler_version, @@ -351,7 +352,7 @@ defmodule BlockScoutWeb.API.V2.VerificationController do Logger.info("API v2 stylus smart-contract #{address_hash_string} verification via github repository") with {:not_found, true} <- {:not_found, StylusVerifierInterface.enabled?()}, - :validated <- validate_address(params) do + :validated <- validate_address(conn, params) do log_sc_verification_started(address_hash_string) Que.add(StylusPublisherWorker, {"github_repository", params}) @@ -383,8 +384,8 @@ defmodule BlockScoutWeb.API.V2.VerificationController do end # sobelow_skip ["Traversal.FileModule"] - defp validate_params_standard_json_input(%{"files" => files} = params) do - with :validated <- validate_address(params), + defp validate_params_standard_json_input(conn, %{"files" => files} = params) do + with :validated <- validate_address(conn, params), files_array <- PublishHelper.prepare_files_array(files), {:no_json_file, %Plug.Upload{path: path}} <- {:no_json_file, PublishHelper.get_one_json(files_array)}, @@ -393,10 +394,13 @@ defmodule BlockScoutWeb.API.V2.VerificationController do end end - defp validate_address(%{"address_hash" => address_hash_string} = params) do + defp validate_address(conn, %{"address_hash" => address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, - {:not_a_smart_contract, bytecode} when bytecode != "0x" <- - {:not_a_smart_contract, Chain.smart_contract_bytecode(address_hash, @api_true)}, + {:not_a_smart_contract, {:ok, _bytecode}} <- + {:not_a_smart_contract, + conn + |> AccessHelper.conn_to_ip_string() + |> ContractCode.get_or_fetch_bytecode(address_hash)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:already_verified, false} <- {:already_verified, SmartContract.verified_with_full_match?(address_hash, @api_true)} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs index 17eaf4021e4b..2aaf53019614 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -1,12 +1,17 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do use BlockScoutWeb.ConnCase use BlockScoutWeb.ChannelCase, async: false + # Provide JSON-RPC named arguments and Mox helpers for Ethereum JSON-RPC + use EthereumJSONRPC.Case, async: false alias BlockScoutWeb.V2.UserSocket alias Explorer.Chain.Address alias Explorer.TestHelper alias Tesla.Multipart alias Plug.Conn + import Mox + alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand + alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @moduletag timeout: :infinity @@ -36,6 +41,82 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do end end + describe "bytecode lookup on verification requests" do + # Set up Mox and start on-demand bytecode fetcher with mocked transport + setup :set_mox_global + setup :verify_on_exit! + + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!({ContractCodeOnDemand, [mocked_json_rpc_named_arguments, [name: ContractCodeOnDemand]]}) + start_supervised!({ContractCreatorOnDemand, name: ContractCreatorOnDemand}) + + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end + + test "proceeds when bytecode is fetched from RPC", %{conn: conn} do + address = insert(:address) + string_address_hash = to_string(address.hash) + + contract_code = "0x6080" + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: contract_code}]} + end) + + params = %{"compiler_version" => "", "source_code" => ""} + + request = + post( + conn, + "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", + params + ) + + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + end + + test "returns 404 when RPC returns empty code (EOA or not a contract)", %{conn: conn} do + address = insert(:address) + string_address_hash = to_string(address.hash) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: "0x"}]} + end) + + params = %{"compiler_version" => "", "source_code" => ""} + + request = + post( + conn, + "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", + params + ) + + assert %{"message" => "Address is not a smart-contract"} = json_response(request, 404) + end + end + if Application.compile_env(:explorer, :chain_type) !== :zksync do describe "/api/v2/smart-contracts/{address_hash}/verification/via/flattened-code" do test "get 200 for verified contract", %{conn: conn} do diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex index e77324ade626..8f19f6a4a25a 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex @@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do import EthereumJSONRPC, only: [fetch_codes: 2] alias Explorer.Chain - alias Explorer.Chain.{Address, Data} + alias Explorer.Chain.{Address, Data, Hash} alias Explorer.Chain.Cache.Accounts alias Explorer.Chain.Cache.Counters.Helper alias Explorer.Chain.Events.Publisher @@ -33,6 +33,26 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do end end + @doc """ + Checks database for bytecode first, then fetches from RPC if not found. + This function handles the complete verification flow including database fallback. + + Returns `{:ok, bytecode}` if bytecode is found, or `:error` otherwise. + """ + @spec get_or_fetch_bytecode(Hash.Address.t()) :: + {:ok, String.t()} | :error + def get_or_fetch_bytecode(caller \\ nil, address_hash) do + with {:ok, %Address{} = address} <- Chain.hash_to_address(address_hash, []), + fetch? = is_nil(address.contract_code) or Address.eoa_with_code?(address), + {true, _} <- {fetch?, address.contract_code}, + :allow <- RateLimiter.check_rate(caller, :on_demand) do + GenServer.call(__MODULE__, {:fetch, address}) + else + {false, bytecode} -> {:ok, bytecode} + _ -> :error + end + end + # Attempts to fetch the contract code for a given address. # # This function checks if the contract code needs to be fetched and if enough time @@ -47,7 +67,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do # `:ok` in all cases. @spec fetch_contract_code(Address.t(), %{ json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments() - }) :: :ok + }) :: {:ok, String.t() | nil} | :error defp fetch_contract_code(address, state) do with {:need_to_fetch, true} <- {:need_to_fetch, fetch?(address)}, {:retries_number, {retries_number, updated_at}} <- @@ -60,14 +80,13 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do fetch_and_broadcast_bytecode(address, state) else {:need_to_fetch, false} -> - :ok + :error {:retries_number, nil} -> fetch_and_broadcast_bytecode(address, state) - :ok {:retry, false} -> - :ok + :error end end @@ -98,7 +117,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do # `:ok` (the function always returns `:ok`, actual results are handled via side effects) @spec fetch_and_broadcast_bytecode(Address.t(), %{ json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments() - }) :: :ok + }) :: {:ok, String.t() | nil} | :error defp fetch_and_broadcast_bytecode(address, %{json_rpc_named_arguments: _} = state) do with {:fetched_code, {:ok, %EthereumJSONRPC.FetchedCodes{params_list: fetched_codes}}} <- {:fetched_code, @@ -110,42 +129,44 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do false <- is_nil(contract_code_object), {:ok, fetched_code} <- (contract_code_object.code == "0x" && {:ok, nil}) || Data.cast(contract_code_object.code), - true <- fetched_code != address.contract_code do - case Chain.import(%{ + true <- fetched_code != address.contract_code, + {:ok, %{addresses: addresses}} <- + Chain.import(%{ addresses: %{ params: [%{hash: address.hash, contract_code: fetched_code}], on_conflict: {:replace, [:contract_code, :updated_at]}, fields_to_update: [:contract_code] } }) do - {:ok, %{addresses: addresses}} -> - Accounts.drop(addresses) + Accounts.drop(addresses) - # Update EIP7702 proxy addresses to avoid inconsistencies between addresses and proxy_implementations tables. - # Other proxy types are not handled here, since their bytecode doesn't change the way EIP7702 bytecode does. - cond do - Address.smart_contract?(address) and !Address.eoa_with_code?(address) -> - :ok + # Update EIP7702 proxy addresses to avoid inconsistencies between addresses and proxy_implementations tables. + # Other proxy types are not handled here, since their bytecode doesn't change the way EIP7702 bytecode does. + cond do + Address.smart_contract?(address) and !Address.eoa_with_code?(address) -> + :ok - is_nil(fetched_code) -> - Implementation.delete_implementations([address.hash]) + is_nil(fetched_code) -> + Implementation.delete_implementations([address.hash]) - true -> - Implementation.upsert_eip7702_implementations(addresses) - end + true -> + Implementation.upsert_eip7702_implementations(addresses) + end - Publisher.broadcast(%{fetched_bytecode: [address.hash, contract_code_object.code]}, :on_demand) + Publisher.broadcast(%{fetched_bytecode: [address.hash, contract_code_object.code]}, :on_demand) - ContractCreatorOnDemand.trigger_fetch(address) + ContractCreatorOnDemand.trigger_fetch(address) - AddressContractCodeFetchAttempt.delete(address.hash) - end + AddressContractCodeFetchAttempt.delete(address.hash) + + {:ok, fetched_code} else {:fetched_code, {:error, _}} -> - :ok + :error _ -> AddressContractCodeFetchAttempt.insert_retries_number(address.hash) + :error end end @@ -165,6 +186,12 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do {:noreply, state} end + @impl true + def handle_call({:fetch, address}, _from, state) do + result = fetch_contract_code(address, state) + {:reply, result, state} + end + # An initial threshold to fetch smart-contract bytecode on-demand @spec update_threshold_ms() :: non_neg_integer() defp update_threshold_ms do diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs index a93b76daea64..209607688dd7 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs @@ -294,4 +294,88 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do |> to_string() end end + + describe "get_or_fetch_bytecode/1" do + test "returns code from DB without RPC when present" do + code = Data.cast("0x6080") |> elem(1) + address = insert(:address, contract_code: code) + + # No RPC expectations set: will fail if RPC is called. + assert {:ok, ^code} = ContractCodeOnDemand.get_or_fetch_bytecode(address.hash) + + # Ensure fetch attempts not created + assert is_nil(Repo.get(AddressContractCodeFetchAttempt, address.hash)) + + # Ensure code unchanged in DB + assert Repo.get(Address, address.hash).contract_code == code + end + + test "fetches from RPC when code not in DB and returns it, broadcasting event" do + Subscriber.to(:fetched_bytecode, :on_demand) + + address = insert(:address) + address_hash = address.hash + string_address_hash = to_string(address.hash) + + contract_code_hex = "0x6080" + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: contract_code_hex}]} + end) + + code = Data.cast(contract_code_hex) |> elem(1) + + assert {:ok, ^code} = ContractCodeOnDemand.get_or_fetch_bytecode(address_hash) + + # DB updated + assert Repo.get(Address, address_hash).contract_code == code + + # No attempts record left + assert is_nil(Repo.get(AddressContractCodeFetchAttempt, address_hash)) + + # Broadcast happened + assert_receive({:chain_event, :fetched_bytecode, :on_demand, [^address_hash, ^contract_code_hex]}) + end + + test "returns :error and increments attempts when RPC returns empty code" do + address = insert(:address) + address_hash = address.hash + string_address_hash = to_string(address.hash) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: "0x"}]} + end) + + assert :error = ContractCodeOnDemand.get_or_fetch_bytecode(address_hash) + + # DB not updated, attempts incremented + assert is_nil(Repo.get(Address, address_hash).contract_code) + attempts = Repo.get(AddressContractCodeFetchAttempt, address_hash) + assert attempts.retries_number == 1 + end + + test "returns :error when address is not found" do + # Build a random address hash that is not in DB + {:ok, random_hash} = Hash.Address.cast(<<1::160>>) + assert :error = ContractCodeOnDemand.get_or_fetch_bytecode(random_hash) + end + end end From a502a5b389939b24e47df4e4a2e4a2d5ed534cc2 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Sep 2025 13:40:26 +0300 Subject: [PATCH 0310/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03fab8d48568..f2cfb4b870ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- on-demand bytecode fetching on smart contract verification requests ([#10724](https://github.com/blockscout/blockscout/issues/10724)) - Improved proxy detection ([#12846](https://github.com/blockscout/blockscout/issues/12846)) - Add `reputation` property where applicable ([#13070](https://github.com/blockscout/blockscout/issues/13070)) - Add envs to configure RemoteIp lib usage ([#13082](https://github.com/blockscout/blockscout/issues/13082)) From 5ac2ce3108172019dcd6c5880a9538789af48f30 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:45:39 +0300 Subject: [PATCH 0311/1006] feat: beacon deposits (#12985) * feat: beacon deposits * Add counters * Fetch domain deposit and fork version from rpc [no ci] * Add spec and docs [no ci] * Add tests * Update config/runtime.exs * Update apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex * add `case: :mixed` option to `Base.decode16!/2` * fix processing of reorgs * fix typespecs * handle reorgs; add transaction endpoint * fix tests * Improve doc * Fix fetcher pagination Co-authored-by: Kirill Fedoseev --------- Co-authored-by: Victor Baranov Co-authored-by: Kirill Fedoseev --- .../controllers/api/v2/address_controller.ex | 96 ++- .../controllers/api/v2/block_controller.ex | 72 ++- .../api/v2/ethereum/deposit_controller.ex | 141 +++++ .../api/v2/transaction_controller.ex | 66 ++- .../lib/block_scout_web/routers/api_router.ex | 19 + .../schemas/api/v2/address/tabs_counters.ex | 3 +- .../schemas/api/v2/beacon/deposit.ex | 40 ++ .../schemas/api/v2/beacon/deposit/status.ex | 8 + .../block_scout_web/schemas/api/v2/block.ex | 6 +- .../block_scout_web/schemas/api/v2/general.ex | 7 + .../views/api/v2/ethereum/deposit_view.ex | 40 ++ .../views/api/v2/ethereum_view.ex | 3 + .../api/v2/address_controller_test.exs | 93 +++ .../api/v2/block_controller_test.exs | 93 +++ .../v2/ethereum/deposit_controller_test.exs | 72 +++ .../api/v2/transaction_controller_test.exs | 76 +++ .../lib/explorer/chain/address/counters.ex | 33 +- .../chain/address/metadata_preloader.ex | 29 + .../lib/explorer/chain/beacon/deposit.ex | 303 ++++++++++ apps/explorer/lib/explorer/chain/block.ex | 2 + .../counters/address_tabs_elements_count.ex | 1 + .../explorer/chain/import/runner/blocks.ex | 11 +- .../20250813001523_create_deposits.exs | 43 ++ .../explorer/chain/beacon/deposit_test.exs | 55 ++ .../market/market_history_cache_test.exs | 1 - apps/explorer/test/support/factory.ex | 130 ++++- .../lib/indexer/fetcher/beacon/client.ex | 16 +- .../lib/indexer/fetcher/beacon/deposit.ex | 411 +++++++++++++ .../indexer/fetcher/beacon/deposit/status.ex | 92 +++ apps/indexer/lib/indexer/supervisor.ex | 2 + apps/indexer/mix.exs | 1 + .../fetcher/beacon/deposit/status_test.exs | 129 ++++ .../indexer/fetcher/beacon/deposit_test.exs | 550 ++++++++++++++++++ .../beacon_deposit_status_supervisor_test.ex | 9 + .../fetcher/beacon_deposit_supervisor_case.ex | 9 + config/runtime.exs | 20 + cspell.json | 7 + docker-compose/envs/common-blockscout.env | 6 + mix.lock | 1 + 39 files changed, 2682 insertions(+), 14 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/status.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum/deposit_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs create mode 100644 apps/explorer/lib/explorer/chain/beacon/deposit.ex create mode 100644 apps/explorer/priv/beacon/migrations/20250813001523_create_deposits.exs create mode 100644 apps/explorer/test/explorer/chain/beacon/deposit_test.exs create mode 100644 apps/indexer/lib/indexer/fetcher/beacon/deposit.ex create mode 100644 apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex create mode 100644 apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs create mode 100644 apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs create mode 100644 apps/indexer/test/support/indexer/fetcher/beacon_deposit_status_supervisor_test.ex create mode 100644 apps/indexer/test/support/indexer/fetcher/beacon_deposit_supervisor_case.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 6f9b620443ef..b8c149d3e3ef 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -30,9 +30,17 @@ defmodule BlockScoutWeb.API.V2.AddressController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] alias BlockScoutWeb.AccessHelper - alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} + + alias BlockScoutWeb.API.V2.{ + BlockView, + Ethereum.DepositController, + Ethereum.DepositView, + TransactionView, + WithdrawalView + } + alias Explorer.{Chain, Market, PagingOptions} - alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction} + alias Explorer.Chain.{Address, Beacon.Deposit, Hash, InternalTransaction, Transaction} alias Explorer.Chain.Address.{CoinBalance, Counters} alias Explorer.Chain.Token.Instance @@ -1089,7 +1097,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do logs: :logs_count, withdrawals: :withdrawals_count, internal_transactions: :internal_transactions_count, - celo_election_rewards: :celo_election_rewards_count + celo_election_rewards: :celo_election_rewards_count, + beacon_deposits: :beacon_deposits_count } case Chain.hash_to_address(address_hash, @address_options) do @@ -1387,6 +1396,87 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + operation :beacon_deposits, + summary: "List Beacon Deposits for a specific address", + description: "Retrieves Beacon deposits for a specific address.", + parameters: + base_params() ++ + [address_hash_param()] ++ + define_paging_params(["deposit_index", "items_count"]), + responses: [ + ok: + {"Beacon deposits for the specified address.", "application/json", + paginated_response( + items: Schemas.Beacon.Deposit, + next_page_params_example: %{ + "index" => 123, + "items_count" => 50 + }, + title_prefix: "AddressBeaconDeposits" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + + @doc """ + Handles `api/v2/addresses/:address_hash/beacon/deposits` endpoint. + Fetches beacon deposits for a given address with pagination support. + + This endpoint retrieves all beacon deposits originating from the specified + address. The results include preloaded associations for both the from_address + and withdrawal_address, including scam badges, names, smart contracts, and + proxy implementations. The response is paginated and may include ENS and + metadata enrichment if those services are enabled. + + ## Parameters + - `conn`: The Plug connection. + - `params`: A map containing: + - `address_hash_param`: The address hash string to fetch deposits for. + - Optional pagination parameter: + - `index`: non-negative integer, the starting index for pagination. + + ## Returns + - `{:format, :error}` - If the address hash format is invalid. + - `{:restricted_access, true}` - If the address is restricted from access. + - `Plug.Conn.t()` - A 200 response with rendered deposits and pagination + information when successful. + """ + @spec beacon_deposits(Plug.Conn.t(), map()) :: + {:format, :error} | {:restricted_access, true} | Plug.Conn.t() + def beacon_deposits(conn, %{address_hash_param: address_hash_param} = params) do + with {:ok, address_hash} <- validate_address_hash(address_hash_param, params) do + full_options = + [ + necessity_by_association: %{ + [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [withdrawal_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => + :optional + }, + api?: true + ] + |> Keyword.merge(DepositController.paging_options(params)) + + deposit_plus_one = Deposit.from_address_hash(address_hash, full_options) + {deposits, next_page} = split_list_by_page(deposit_plus_one) + + next_page_params = + next_page + |> next_page_params( + deposits, + params, + DepositController.paging_function() + ) + + conn + |> put_status(200) + |> put_view(DepositView) + |> render(:deposits, %{ + deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(), + next_page_params: next_page_params + }) + end + end + # Checks if this address hash string is valid, and this address is not prohibited. # Returns the `{:ok, address_hash}` if address hash passed all the checks. # Returns {:ok, _} response even if the address is not present in the database. diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 08dc3deda324..26dabe0d4cad 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -25,12 +25,15 @@ defmodule BlockScoutWeb.API.V2.BlockController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] alias BlockScoutWeb.API.V2.{ + Ethereum.DepositController, + Ethereum.DepositView, TransactionView, WithdrawalView } alias Explorer.Chain alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader + alias Explorer.Chain.Beacon.Deposit alias Explorer.Chain.Cache.{BlockNumber, Counters.AverageBlockTime} alias Explorer.Chain.InternalTransaction alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch @@ -43,7 +46,8 @@ defmodule BlockScoutWeb.API.V2.BlockController do :beacon_blob_transaction => :optional } @chain_type_block_necessity_by_association %{ - [transactions: :beacon_blob_transaction] => :optional + [transactions: :beacon_blob_transaction] => :optional, + :beacon_deposits => :optional } :optimism -> @@ -409,6 +413,72 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end + @doc """ + Handles `api/v2/blocks/:block_hash_or_number/beacon/deposits` endpoint. + Fetches beacon deposits included in a specific block with pagination support. + + This endpoint retrieves all beacon deposits that were included in the + specified block. The block can be identified by either its hash or number. + The results include preloaded associations for both the from_address and + withdrawal_address, including scam badges, names, smart contracts, and proxy + implementations. The response is paginated and may include ENS and metadata + enrichment if those services are enabled. + + ## Parameters + - `conn`: The Plug connection. + - `params`: A map containing: + - `"block_hash_or_number"`: The block identifier (hash or number) to fetch + deposits from. + - Optional pagination parameter: + - `"index"`: non-negative integer, the starting index for pagination. + + ## Returns + - `{:error, :not_found}` - If the block is not found. + - `{:error, {:invalid, :hash | :number}}` - If the block identifier format is + invalid. + - `{:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}` + - If the block has lost consensus in the blockchain. + - `Plug.Conn.t()` - A 200 response with rendered deposits and pagination + information when successful. + """ + @spec beacon_deposits(Plug.Conn.t(), map()) :: + {:error, :not_found | {:invalid, :hash | :number}} + | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}} + | Plug.Conn.t() + def beacon_deposits(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do + with {:ok, block} <- block_param_to_block(block_hash_or_number) do + full_options = + [ + necessity_by_association: %{ + [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [withdrawal_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => + :optional + }, + api?: true + ] + |> Keyword.merge(DepositController.paging_options(params)) + + deposit_plus_one = Deposit.from_block_hash(block.hash, full_options) + {deposits, next_page} = split_list_by_page(deposit_plus_one) + + next_page_params = + next_page + |> next_page_params( + deposits, + params, + DepositController.paging_function() + ) + + conn + |> put_status(200) + |> put_view(DepositView) + |> render(:deposits, %{ + deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(), + next_page_params: next_page_params + }) + end + end + defp block_param_to_block(block_hash_or_number, options \\ @api_true) do with {:ok, type, value} <- parse_block_hash_or_number_param(block_hash_or_number) do fetch_block(type, value, options) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex new file mode 100644 index 000000000000..5d5824e60a24 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex @@ -0,0 +1,141 @@ +defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do + use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs + + import BlockScoutWeb.Chain, only: [next_page_params: 4, split_list_by_page: 1] + import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] + import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + + alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.Beacon.Deposit + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags ["beacon_deposits"] + + operation :list, + summary: "Lists all beacon deposits", + description: "Retrieves a paginated list of all beacon deposits.", + parameters: base_params() ++ define_paging_params(["deposit_index", "items_count"]), + responses: [ + ok: + {"List of Beacon Deposits, with pagination.", "application/json", + paginated_response( + items: Schemas.Beacon.Deposit, + next_page_params_example: %{ + "index" => 123, + "items_count" => 50 + }, + title_prefix: "BeaconDeposits" + )}, + forbidden: ForbiddenResponse.response() + ] + + @doc """ + Handles `api/v2/beacon/deposits` endpoint. + Lists all beacon deposits with pagination support. + + This endpoint retrieves all beacon deposits from the blockchain in a + paginated format. The results include preloaded associations for both the + from_address and withdrawal_address, including scam badges, names, smart + contracts, and proxy implementations. The response may include ENS and + metadata enrichment if those services are enabled. + + ## Parameters + - `conn`: The Plug connection. + - `params`: A map containing optional pagination parameters: + - `"index"`: non-negative integer, the starting index for pagination. + + ## Returns + - `Plug.Conn.t()` - A 200 response with rendered deposits and pagination + information. + """ + @spec list(Plug.Conn.t(), map()) :: Plug.Conn.t() + def list(conn, params) do + full_options = + [ + necessity_by_association: %{ + [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [withdrawal_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional + }, + api?: true + ] + |> Keyword.merge(paging_options(params)) + + deposit_plus_one = Deposit.all(full_options) + {deposits, next_page} = split_list_by_page(deposit_plus_one) + + next_page_params = + next_page + |> next_page_params(deposits, params, paging_function()) + + conn + |> put_status(200) + |> render(:deposits, %{ + deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(), + next_page_params: next_page_params + }) + end + + operation :count, + summary: "Gets total count of beacon deposits", + description: "Retrieves the total count of beacon deposits.", + responses: [ + ok: + {"Total count of beacon deposits.", "application/json", + %Schema{ + title: "TotalBeaconDepositsCount", + type: :object, + properties: %{ + deposits_count: %Schema{type: :integer, nullable: false} + }, + required: [:deposits_count] + }}, + forbidden: ForbiddenResponse.response() + ] + + @doc """ + Handles `api/v2/beacon/deposits/count` endpoint. + Returns the total count of beacon deposits. + + This endpoint calculates the total number of beacon deposits by retrieving + the latest deposit's index. Since deposit indices are 0-based and sequential, + the total count equals the highest index plus one. If no deposits exist, the + count is 0. + + ## Parameters + - `conn`: The Plug connection. + + ## Returns + - `Plug.Conn.t()` - A JSON response containing: + - `deposits_count`: The total number of beacon deposits (integer). + """ + @spec count(Plug.Conn.t(), map()) :: Plug.Conn.t() + def count(conn, _params) do + last_deposit = Deposit.get_latest_deposit(api?: true) || %{index: -1} + + conn |> json(%{deposits_count: last_deposit.index + 1}) + end + + @spec paging_options(map()) :: [Chain.paging_options()] + def paging_options(%{"index" => index}) do + case Integer.parse(index) do + {index, ""} -> [paging_options: %{PagingOptions.default_paging_options() | key: %{index: index}}] + _ -> [paging_options: PagingOptions.default_paging_options()] + end + end + + def paging_options(%{index: index}) do + [paging_options: %{PagingOptions.default_paging_options() | key: %{index: index}}] + end + + def paging_options(_), do: [paging_options: PagingOptions.default_paging_options()] + + @spec paging_function() :: (Deposit.t() -> %{index: non_neg_integer()}) + def paging_function, + do: fn deposit -> + %{index: deposit.index} + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 7d1731cd867c..9970157cfe07 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -3,11 +3,11 @@ defmodule BlockScoutWeb.API.V2.TransactionController do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - alias BlockScoutWeb.API.V2.BlobView import BlockScoutWeb.Chain, only: [ next_page_params: 3, + next_page_params: 4, put_key_value_to_paging_options: 3, token_transfers_next_page_params: 3, paging_options: 1, @@ -37,10 +37,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do require Logger alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.API.V2.{BlobView, Ethereum.DepositController, Ethereum.DepositView} alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.Chain.Beacon.Reader, as: BeaconReader alias Explorer.Chain.Cache.Counters.{NewPendingTransactionsCount, Transactions24hCount} alias Explorer.Chain.{Hash, InternalTransaction, Transaction} @@ -707,6 +709,68 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ) end + @doc """ + Handles `api/v2/transactions/:transaction_hash_param/beacon/deposits` endpoint. + Fetches beacon deposits included in a specific transaction with pagination support. + + This endpoint retrieves all beacon deposits that were included in the + specified transactions. The results include preloaded associations for both the from_address and + withdrawal_address, including scam badges, names, smart contracts, and proxy + implementations. The response is paginated and may include ENS and metadata + enrichment if those services are enabled. + + ## Parameters + - `conn`: The Plug connection. + - `params`: A map containing: + - `"transaction_hash_param"`: The transaction hash to fetch deposits from. + - Optional pagination parameter: + - `"index"`: non-negative integer, the starting index for pagination. + + ## Returns + - `{:error, :not_found}` - If the transaction is not found. + - `{:error, {:invalid, :hash}}` - If the transaction hash format is invalid. + - `Plug.Conn.t()` - A 200 response with rendered deposits and pagination + information when successful. + """ + @spec beacon_deposits(Plug.Conn.t(), map()) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | Plug.Conn.t() + def beacon_deposits(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do + full_options = + [ + necessity_by_association: %{ + [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [withdrawal_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => + :optional + }, + api?: true + ] + |> Keyword.merge(DepositController.paging_options(params)) + + deposit_plus_one = BeaconDeposit.from_transaction_hash(transaction_hash, full_options) + {deposits, next_page} = split_list_by_page(deposit_plus_one) + + next_page_params = + next_page + |> next_page_params( + deposits, + params, + DepositController.paging_function() + ) + + conn + |> put_status(200) + |> put_view(DepositView) + |> render(:deposits, %{ + deposits: deposits |> maybe_preload_ens() |> maybe_preload_metadata(), + next_page_params: next_page_params + }) + end + end + @doc """ Checks if this valid transaction hash string, and this transaction doesn't belong to prohibited address """ diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 8e1c781e4720..8e0fa13031ea 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -190,6 +190,10 @@ defmodule BlockScoutWeb.Routers.ApiRouter do if @chain_type == :ethereum do get("/:transaction_hash_param/blobs", V2.TransactionController, :blobs) end + + chain_scope :ethereum do + get("/:transaction_hash_param/beacon/deposits", V2.TransactionController, :beacon_deposits) + end end scope "/token-transfers" do @@ -219,6 +223,10 @@ defmodule BlockScoutWeb.Routers.ApiRouter do if @chain_type == :scroll do get("/scroll-batch/:batch_number", V2.BlockController, :scroll_batch) end + + chain_scope :ethereum do + get("/:block_hash_or_number/beacon/deposits", V2.BlockController, :beacon_deposits) + end end scope "/addresses" do @@ -247,6 +255,10 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/:address_hash_param/celo/election-rewards", V2.AddressController, :celo_election_rewards) get("/:address_hash_param/celo/election-rewards/csv", V2.CsvExportController, :celo_election_rewards_csv) end + + chain_scope :ethereum do + get("/:address_hash_param/beacon/deposits", V2.AddressController, :beacon_deposits) + end end scope "/main-page" do @@ -419,6 +431,13 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end + chain_scope :ethereum do + scope "/beacon" do + get("/deposits", V2.Ethereum.DepositController, :list) + get("/deposits/count", V2.Ethereum.DepositController, :count) + end + end + scope "/blobs" do if @chain_type == :ethereum do get("/:blob_hash_param", V2.BlobController, :blob) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex index 3291980daaaf..e41d567a34c4 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex @@ -18,7 +18,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TabsCounters do withdrawals_count: %Schema{type: :integer, nullable: false}, internal_transactions_count: %Schema{type: :integer, nullable: false}, validations_count: %Schema{type: :integer, nullable: false}, - celo_election_rewards_count: %Schema{type: :integer, nullable: false} + celo_election_rewards_count: %Schema{type: :integer, nullable: false}, + beacon_deposits_count: %Schema{type: :integer, nullable: false} } }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex new file mode 100644 index 000000000000..8f0b61620fe2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex @@ -0,0 +1,40 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit do + @moduledoc """ + This module defines the schema for the Beacon.Deposit struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, Beacon.Deposit.Status, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + index: %Schema{type: :integer, nullable: false}, + transaction_hash: General.FullHash, + block_hash: General.FullHash, + block_number: %Schema{type: :integer, nullable: false}, + block_timestamp: General.Timestamp, + pubkey: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{96})$", nullable: false}, + withdrawal_credentials: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: false}, + withdrawal_address: %Schema{allOf: [Address], nullable: true}, + amount: General.IntegerString, + signature: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{192})$", nullable: false}, + status: Status, + from_address: Address + }, + required: [ + :index, + :transaction_hash, + :block_hash, + :block_number, + :block_timestamp, + :pubkey, + :withdrawal_credentials, + :amount, + :signature, + :status, + :from_address + ] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/status.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/status.ex new file mode 100644 index 000000000000..6e8b041d508a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/status.ex @@ -0,0 +1,8 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit.Status do + @moduledoc false + require OpenApiSpex + + alias Explorer.Chain.Beacon.Deposit + + OpenApiSpex.schema(%{type: :string, nullable: false, enum: Deposit.statuses()}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex index dbca67752072..7d25f609ded8 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -228,7 +228,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do |> put_in([:properties, :excess_blob_gas], General.IntegerStringNullable) |> put_in([:properties, :blob_gas_price], General.IntegerString) |> put_in([:properties, :burnt_blob_fees], General.IntegerString) - |> update_in([:required], &([:blob_transactions_count, :blob_gas_used, :excess_blob_gas] ++ &1)) + |> put_in([:properties, :beacon_deposits_count], %Schema{type: :integer, nullable: true}) + |> update_in( + [:required], + &([:blob_transactions_count, :blob_gas_used, :excess_blob_gas, :beacon_deposits_count] ++ &1) + ) :celo -> schema diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 343b762113f6..bdfb206e6ab2 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -676,6 +676,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: "Type for paging", name: :type + }, + "deposit_index" => %Parameter{ + in: :query, + schema: %Schema{type: :integer, minimum: 0, maximum: 9_223_372_036_854_775_807}, + required: false, + description: "Deposit index for paging", + name: :index } } diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum/deposit_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum/deposit_view.ex new file mode 100644 index 000000000000..92495c340f7d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum/deposit_view.ex @@ -0,0 +1,40 @@ +defmodule BlockScoutWeb.API.V2.Ethereum.DepositView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain.Beacon.Deposit + + def render("deposits.json", %{deposits: deposits, next_page_params: next_page_params}) do + %{"items" => Enum.map(deposits, &prepare_deposit/1), "next_page_params" => next_page_params} + end + + @spec prepare_deposit(Deposit.t()) :: map() + def prepare_deposit(deposit) do + %{ + "index" => deposit.index, + "transaction_hash" => deposit.transaction_hash, + "block_hash" => deposit.block_hash, + "block_number" => deposit.block_number, + "block_timestamp" => deposit.block_timestamp, + "pubkey" => deposit.pubkey, + "withdrawal_credentials" => deposit.withdrawal_credentials, + "withdrawal_address" => + Helper.address_with_info( + nil, + deposit.withdrawal_address, + deposit.withdrawal_address_hash, + false + ), + "amount" => deposit.amount, + "signature" => deposit.signature, + "status" => deposit.status, + "from_address" => + Helper.address_with_info( + nil, + deposit.from_address, + deposit.from_address_hash, + false + ) + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex index dbf5a0237b6b..5ed6506cdcd5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/ethereum_view.ex @@ -22,6 +22,7 @@ defmodule BlockScoutWeb.API.V2.EthereumView do def extend_block_json_response(out_json, %Block{} = block, single_block?) do blob_gas_used = Map.get(block, :blob_gas_used) excess_blob_gas = Map.get(block, :excess_blob_gas) + beacon_deposits = Map.get(block, :beacon_deposits, []) blob_transaction_count = block.blob_transactions_count @@ -38,8 +39,10 @@ defmodule BlockScoutWeb.API.V2.EthereumView do extended_out_json |> Map.put("blob_gas_price", blob_gas_price) |> Map.put("burnt_blob_fees", burnt_blob_transaction_fees) + |> Map.put("beacon_deposits_count", Enum.count(beacon_deposits)) else extended_out_json + |> Map.put("beacon_deposits_count", nil) end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 4acde86ee732..f8e2869c11f0 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -24,6 +24,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do alias Explorer.Account.{Identity, WatchlistAddress} alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand alias Plug.Conn @@ -5503,6 +5504,59 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end end + describe "/addresses/{address_hash}/beacon/deposits" do + if Application.compile_env(:explorer, :chain_type) == :ethereum do + test "get empty list on non-existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits") + response = json_response(request, 200) + + assert %{"items" => [], "next_page_params" => nil} = response + assert_schema(response, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/invalid/beacon/deposits") + response = json_response(request, 422) + + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = response + + assert_schema(response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get deposits", %{conn: conn} do + address = insert(:address) + + deposits = insert_list(51, :beacon_deposit, from_address: address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, deposits) + assert_schema(response, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + assert_schema(response_2nd_page, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + else + test "returns an error about chain type", %{conn: conn} do + request = get(conn, "/api/v2/addresses/address/beacon/deposits") + assert response = json_response(request, 404) + assert %{"message" => "Endpoint not available for current chain type"} = response + end + end + end + defp compare_item(%Address{} = address, json) do assert Address.checksum(address.hash) == json["hash"] assert to_string(address.transactions_count) == json["transactions_count"] @@ -5613,6 +5667,45 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } = json end + defp compare_item(%BeaconDeposit{} = deposit, json) do + index = deposit.index + transaction_hash = to_string(deposit.transaction_hash) + block_hash = to_string(deposit.block_hash) + block_number = deposit.block_number + pubkey = to_string(deposit.pubkey) + withdrawal_credentials = to_string(deposit.withdrawal_credentials) + signature = to_string(deposit.signature) + from_address_hash = Address.checksum(deposit.from_address_hash) + + if deposit.withdrawal_address_hash do + withdrawal_address_hash = Address.checksum(deposit.withdrawal_address_hash) + + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => %{"hash" => ^withdrawal_address_hash}, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + else + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => nil, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + end + end + defp compare_item({token, amount, token_instances}, json) do token_type = token.type token_address_hash = Address.checksum(token.contract_address_hash) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index a3e0f52f8dbd..842b9d6dcb26 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do use BlockScoutWeb.ConnCase alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Withdrawal} + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) @@ -517,6 +518,59 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do end end + describe "blocks/{block_hash_or_number}/beacon/deposits" do + if Application.compile_env(:explorer, :chain_type) == :ethereum do + test "get 404 on non-existing block", %{conn: conn} do + block = build(:block) + + request = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits") + response = json_response(request, 404) + end + + # test "get 422 on invalid block", %{conn: conn} do + # request = get(conn, "/api/v2/blocks/invalid/beacon/deposits") + # response = json_response(request, 422) + + # assert %{ + # "errors" => [ + # %{ + # "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + # "source" => %{"pointer" => "/address_hash_param"}, + # "title" => "Invalid value" + # } + # ] + # } = response + + # assert_schema(response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) + # end + + test "get deposits", %{conn: conn} do + block = insert(:block) + + deposits = insert_list(51, :beacon_deposit, block: block) + + insert(:beacon_deposit) + + request = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, deposits) + # assert_schema(response, "BlockBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + # assert_schema(response_2nd_page, "BlockBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + else + test "returns an error about chain type", %{conn: conn} do + block = insert(:block) + request = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits") + assert response = json_response(request, 404) + assert %{"message" => "Endpoint not available for current chain type"} = response + end + end + end + defp compare_item(%Block{} = block, json) do assert to_string(block.hash) == json["hash"] assert block.number == json["height"] @@ -543,6 +597,45 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] end + defp compare_item(%BeaconDeposit{} = deposit, json) do + index = deposit.index + transaction_hash = to_string(deposit.transaction_hash) + block_hash = to_string(deposit.block_hash) + block_number = deposit.block_number + pubkey = to_string(deposit.pubkey) + withdrawal_credentials = to_string(deposit.withdrawal_credentials) + signature = to_string(deposit.signature) + from_address_hash = Address.checksum(deposit.from_address_hash) + + if deposit.withdrawal_address_hash do + withdrawal_address_hash = Address.checksum(deposit.withdrawal_address_hash) + + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => %{"hash" => ^withdrawal_address_hash}, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + else + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => nil, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + end + end + defp check_paginated_response(first_page_resp, second_page_resp, list) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs new file mode 100644 index 000000000000..7584fa9ce458 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs @@ -0,0 +1,72 @@ +defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Repo + + import OpenApiSpex.TestAssertions + + if Application.compile_env(:explorer, :chain_type) == :ethereum do + describe "/beacon/deposits" do + test "get empty list when no deposits exist", %{conn: conn} do + request = get(conn, "/api/v2/beacon/deposits") + assert response = json_response(request, 200) + assert %{"items" => []} = response + assert_schema(response, "BeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + + test "get deposits", %{conn: conn} do + deposits = insert_list(51, :beacon_deposit) + + request = get(conn, "/api/v2/beacon/deposits") + assert response = json_response(request, 200) + assert %{"items" => deposits_json, "next_page_params" => next_page_params} = response + request_2nd_page = get(conn, "/api/v2/beacon/deposits", next_page_params) + assert response_2nd_page = json_response(request_2nd_page, 200) + assert %{"items" => deposits_json_2nd_page} = response_2nd_page + + assert deposits_json + |> Kernel.++(deposits_json_2nd_page) + |> Enum.map(&{&1["index"], &1["transaction_hash"], &1["block_hash"]}) == + deposits + |> Enum.reverse() + |> Enum.map(&{&1.index, to_string(&1.transaction_hash), to_string(&1.block_hash)}) + + assert_schema(response, "BeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + end + + describe "/beacon/deposits/count" do + test "returns 0 when no deposits exist", %{conn: conn} do + request = get(conn, "/api/v2/beacon/deposits/count") + assert response = json_response(request, 200) + assert %{"deposits_count" => 0} = response + end + + test "returns deposit count", %{conn: conn} do + insert_list(3, :beacon_deposit) + + deposits_count = Repo.aggregate(Explorer.Chain.Beacon.Deposit, :count, :index) + + request = get(conn, "/api/v2/beacon/deposits/count") + assert response = json_response(request, 200) + assert %{"deposits_count" => deposits_count} = response + end + end + else + describe "/beacon/deposits" do + test "returns an error about chain type", %{conn: conn} do + request = get(conn, "/api/v2/beacon/deposits/count") + assert response = json_response(request, 404) + assert %{"message" => "Endpoint not available for current chain type"} = response + end + end + + describe "/beacon/deposits/count" do + test "returns an error about chain type", %{conn: conn} do + request = get(conn, "/api/v2/beacon/deposits/count") + assert response = json_response(request, 404) + assert %{"message" => "Endpoint not available for current chain type"} = response + end + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index c53f3e74b828..dc0313a9162c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do alias Explorer.Account.{Identity, WatchlistAddress} alias Explorer.Chain.{Address, Data, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.{Repo, TestHelper} use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] @@ -1902,6 +1903,42 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end end + describe "transactions/{transaction_hash}/beacon/deposits" do + if Application.compile_env(:explorer, :chain_type) == :ethereum do + test "get 404 on non-existing transaction", %{conn: conn} do + transaction = build(:transaction) + + request = get(conn, "/api/v2/transactions/#{transaction.hash}/beacon/deposits") + response = json_response(request, 404) + end + + test "get deposits", %{conn: conn} do + transaction = insert(:transaction) + + deposits = insert_list(51, :beacon_deposit, transaction: transaction) + + insert(:beacon_deposit) + + request = get(conn, "/api/v2/transactions/#{transaction.hash}/beacon/deposits") + assert response = json_response(request, 200) + + request_2nd_page = + get(conn, "/api/v2/transactions/#{transaction.hash}/beacon/deposits", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, deposits) + end + else + test "returns an error about chain type", %{conn: conn} do + transaction = insert(:transaction) + request = get(conn, "/api/v2/transactions/#{transaction.hash}/beacon/deposits") + assert response = json_response(request, 404) + assert %{"message" => "Endpoint not available for current chain type"} = response + end + end + end + defp compare_item(%Transaction{} = transaction, json) do assert to_string(transaction.hash) == json["hash"] assert transaction.block_number == json["block_number"] @@ -1968,6 +2005,45 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do ) end + defp compare_item(%BeaconDeposit{} = deposit, json) do + index = deposit.index + transaction_hash = to_string(deposit.transaction_hash) + block_hash = to_string(deposit.block_hash) + block_number = deposit.block_number + pubkey = to_string(deposit.pubkey) + withdrawal_credentials = to_string(deposit.withdrawal_credentials) + signature = to_string(deposit.signature) + from_address_hash = Address.checksum(deposit.from_address_hash) + + if deposit.withdrawal_address_hash do + withdrawal_address_hash = Address.checksum(deposit.withdrawal_address_hash) + + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => %{"hash" => ^withdrawal_address_hash}, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + else + assert %{ + "index" => ^index, + "transaction_hash" => ^transaction_hash, + "block_hash" => ^block_hash, + "block_number" => ^block_number, + "pubkey" => ^pubkey, + "withdrawal_credentials" => ^withdrawal_credentials, + "withdrawal_address" => nil, + "signature" => ^signature, + "from_address" => %{"hash" => ^from_address_hash} + } = json + end + end + defp check_paginated_response(first_page_resp, second_page_resp, transactions) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index d11729f5271d..3f72d5c51df0 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -28,6 +28,7 @@ defmodule Explorer.Chain.Address.Counters do Withdrawal } + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward require Logger @@ -35,7 +36,16 @@ defmodule Explorer.Chain.Address.Counters do @typep counter :: non_neg_integer() | nil @counters_limit 51 - @types [:validations, :transactions, :token_transfers, :token_balances, :logs, :withdrawals, :internal_transactions] + @types [ + :validations, + :transactions, + :token_transfers, + :token_balances, + :logs, + :withdrawals, + :internal_transactions, + :beacon_deposits + ] @transactions_types [:transactions_from, :transactions_to, :transactions_contract] defp address_hash_to_logs_query(address_hash) do @@ -242,6 +252,13 @@ defmodule Explorer.Chain.Address.Counters do |> wrapped_union_subquery() end + defp address_hash_to_beacon_deposits_unordered_query(address_hash) do + from( + deposit in BeaconDeposit, + where: deposit.from_address_hash == ^address_hash + ) + end + def address_counters(address, options \\ []) do validation_count_task = Task.async(fn -> @@ -448,6 +465,17 @@ defmodule Explorer.Chain.Address.Counters do nil end + beacon_deposits_count_task = + if Application.get_env(:explorer, :chain_type) == :ethereum do + configure_task( + :beacon_deposits, + cached_counters, + address_hash_to_beacon_deposits_unordered_query(address_hash), + address_hash, + options + ) + end + map = [ validations_count_task, @@ -459,7 +487,8 @@ defmodule Explorer.Chain.Address.Counters do logs_count_task, withdrawals_count_task, internal_transactions_count_task, - celo_election_rewards_count_task + celo_election_rewards_count_task, + beacon_deposits_count_task ] |> Enum.reject(&is_nil/1) |> Task.yield_many(:timer.seconds(1)) diff --git a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex index c7307b9d5346..1f4b1632e48a 100644 --- a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex +++ b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.Address.MetadataPreloader do alias Explorer.Chain.{ Address, Address.CurrentTokenBalance, + Beacon.Deposit, Block, InternalTransaction, Log, @@ -188,6 +189,17 @@ defmodule Explorer.Chain.Address.MetadataPreloader do [to_string(owner_address_hash)] end + defp item_to_address_hash_strings(%Deposit{ + from_address_hash: from_address_hash, + withdrawal_address_hash: withdrawal_address_hash + }) do + if withdrawal_address_hash do + [to_string(withdrawal_address_hash), to_string(from_address_hash)] + else + [to_string(from_address_hash)] + end + end + defp put_ens_names(names, items) do Enum.map(items, &put_meta_to_item(&1, names, :ens_domain_name)) end @@ -292,6 +304,23 @@ defmodule Explorer.Chain.Address.MetadataPreloader do %Instance{instance | owner: alter_address(owner_address, owner_address_hash, names, field_to_put_info)} end + defp put_meta_to_item( + %Deposit{ + from_address: from_address, + from_address_hash: from_address_hash, + withdrawal_address: withdrawal_address, + withdrawal_address_hash: withdrawal_address_hash + } = deposit, + names, + field_to_put_info + ) do + %Deposit{ + deposit + | from_address: alter_address(from_address, from_address_hash, names, field_to_put_info), + withdrawal_address: alter_address(withdrawal_address, withdrawal_address_hash, names, field_to_put_info) + } + end + defp alter_address(address, nil, _names, _field), do: address defp alter_address(%NotLoaded{}, address_hash, names, field) do diff --git a/apps/explorer/lib/explorer/chain/beacon/deposit.ex b/apps/explorer/lib/explorer/chain/beacon/deposit.ex new file mode 100644 index 000000000000..e9aef4045ad2 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/beacon/deposit.ex @@ -0,0 +1,303 @@ +defmodule Explorer.Chain.Beacon.Deposit do + @moduledoc """ + Models a deposit in the beacon chain. + """ + + use Explorer.Schema + + alias Explorer.Chain.{Address, Block, Data, Hash, Log, Transaction, Wei} + alias Explorer.{Chain, Repo, SortingHelper} + + @deposit_event_signature "0x649BBC62D0E31342AFEA4E5CD82D4049E7E1EE912FC0889AA790803BE39038C5" + + @required_attrs ~w(pubkey withdrawal_credentials amount signature index block_number block_timestamp log_index status from_address_hash block_hash transaction_hash)a + + @statuses_enum ~w(invalid pending completed)a + + @primary_key false + typed_schema "beacon_deposits" do + field(:pubkey, Data, null: false) + field(:withdrawal_credentials, Data, null: false) + field(:amount, Wei, null: false) + field(:signature, Data, null: false) + field(:index, :integer, primary_key: true) + field(:block_number, :integer, null: false) + field(:block_timestamp, :utc_datetime_usec, null: false) + field(:log_index, :integer, null: false) + + field(:withdrawal_address_hash, Hash.Address, virtual: true) + + field(:status, Ecto.Enum, values: @statuses_enum, null: false) + + belongs_to(:withdrawal_address, Address, + foreign_key: :withdrawal_address_hash, + references: :hash, + define_field: false + ) + + belongs_to( + :from_address, + Address, + foreign_key: :from_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full, null: false) + + belongs_to(:transaction, Transaction, + foreign_key: :transaction_hash, + references: :hash, + type: Hash.Full, + null: false + ) + + timestamps() + end + + @doc """ + Validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() + def changeset(deposit, attrs) do + deposit + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + end + + @spec statuses :: [atom()] + def statuses, do: @statuses_enum + + @sorting [desc: :index] + + @doc """ + Fetches beacon deposits with pagination and association preloading. + + Retrieves beacon deposits sorted by index in descending order (newest first). + For each deposit, extracts the withdrawal address from the withdrawal + credentials if they have prefix 0x01 or 0x02, making it available as a + virtual field for association preloading. + + ## Parameters + - `options`: A keyword list of options: + - `:paging_options` - Pagination configuration (defaults to + `Chain.default_paging_options()`). + - `:necessity_by_association` - A map specifying which associations to + preload and whether they are `:required` or `:optional`. + - `:api?` - Boolean flag for API context. + + ## Returns + - A list of beacon deposits with requested associations preloaded and + withdrawal addresses extracted where applicable. + """ + @spec all([Chain.paging_options() | Chain.necessity_by_association() | Chain.api?()]) :: [t()] + def all(options \\ []) do + beacon_deposits_list_query(:all, nil, options) + end + + @doc """ + Fetches beacon deposits from a specific block with pagination and association preloading. + + Retrieves all beacon deposits that were included in the specified block, + sorted by index in descending order (newest first). For each deposit, + extracts the withdrawal address from the withdrawal credentials if they have + prefix 0x01 or 0x02, making it available as a virtual field for association + preloading. + + ## Parameters + - `block_hash`: The hash of the block to fetch deposits from. + - `options`: A keyword list of options: + - `:paging_options` - Pagination configuration (defaults to + `Chain.default_paging_options()`). + - `:necessity_by_association` - A map specifying which associations to + preload and whether they are `:required` or `:optional`. + - `:api?` - Boolean flag for API context. + + ## Returns + - A list of beacon deposits from the specified block with requested + associations preloaded and withdrawal addresses extracted where applicable. + """ + @spec from_block_hash(Hash.Full.t(), [Chain.paging_options() | Chain.necessity_by_association() | Chain.api?()]) :: [ + t() + ] + def from_block_hash(block_hash, options \\ []) do + beacon_deposits_list_query(:block_hash, block_hash, options) + end + + @doc """ + Fetches beacon deposits from a specific transaction with pagination and association preloading. + + Retrieves all beacon deposits that were included in the specified transaction, + sorted by index in descending order (newest first). For each deposit, + extracts the withdrawal address from the withdrawal credentials if they have + prefix 0x01 or 0x02, making it available as a virtual field for association + preloading. + + ## Parameters + - `transaction_hash`: The hash of the transaction to fetch deposits from. + - `options`: A keyword list of options: + - `:paging_options` - Pagination configuration (defaults to + `Chain.default_paging_options()`). + - `:necessity_by_association` - A map specifying which associations to + preload and whether they are `:required` or `:optional`. + - `:api?` - Boolean flag for API context. + + ## Returns + - A list of beacon deposits from the specified transaction with requested + associations preloaded and withdrawal addresses extracted where applicable. + """ + @spec from_transaction_hash(Hash.Full.t(), [Chain.paging_options() | Chain.necessity_by_association() | Chain.api?()]) :: + [ + t() + ] + def from_transaction_hash(transaction_hash, options \\ []) do + beacon_deposits_list_query(:transaction_hash, transaction_hash, options) + end + + @doc """ + Fetches beacon deposits from a specific address (`from_address`) with pagination and + association preloading. + + Retrieves all beacon deposits that were sent from the specified address, + sorted by index in descending order (newest first). For each deposit, + extracts the withdrawal address from the withdrawal credentials if they have + prefix 0x01 or 0x02, making it available as a virtual field for association + preloading. + + ## Parameters + - `from_address`: The address hash to fetch deposits from. + - `options`: A keyword list of options: + - `:paging_options` - Pagination configuration (defaults to + `Chain.default_paging_options()`). + - `:necessity_by_association` - A map specifying which associations to + preload and whether they are `:required` or `:optional`. + - `:api?` - Boolean flag for API context. + + ## Returns + - A list of beacon deposits from the specified address with requested + associations preloaded and withdrawal addresses extracted where applicable. + """ + @spec from_address_hash(Hash.Address.t(), [Chain.paging_options() | Chain.necessity_by_association() | Chain.api?()]) :: + [t()] + def from_address_hash(address_hash, options \\ []) do + beacon_deposits_list_query(:from_address_hash, address_hash, options) + end + + defp beacon_deposits_list_query(entity, hash, options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + {required_necessity_by_association, optional_necessity_by_association} = + options |> Keyword.get(:necessity_by_association, %{}) |> Enum.split_with(fn {_, v} -> v == :required end) + + __MODULE__ + |> then(fn q -> + case entity do + :block_hash -> where(q, [deposit], deposit.block_hash == ^hash) + :from_address_hash -> where(q, [deposit], deposit.from_address_hash == ^hash) + :transaction_hash -> where(q, [deposit], deposit.transaction_hash == ^hash) + :all -> q + end + end) + |> SortingHelper.apply_sorting(@sorting, []) + |> SortingHelper.page_with_sorting(paging_options, @sorting, []) + |> Chain.join_associations(Map.new(required_necessity_by_association)) + |> Chain.select_repo(options).all() + |> Enum.map(&put_withdrawal_address_hash/1) + |> Chain.select_repo(options).preload(optional_necessity_by_association |> Enum.map(&elem(&1, 0))) + end + + @doc """ + Retrieves the most recent beacon deposit by index. + + Fetches the beacon deposit with the highest index value, which represents + the most recently indexed deposit in the system. + + ## Parameters + - `options`: A keyword list of options: + - `:api?` - Boolean flag for API context, determines which repository to + use for the query. + + ## Returns + - The beacon deposit with the highest index. + - `nil` if no deposits exist. + """ + @spec get_latest_deposit([Chain.api?()]) :: t() | nil + def get_latest_deposit(options \\ []) do + Chain.select_repo(options).one(from(deposit in __MODULE__, order_by: [desc: deposit.index], limit: 1)) + end + + @doc """ + Fetches beacon deposit event logs from the deposit contract. + + Retrieves deposit event logs from the specified deposit contract address, + starting after the given block number and log index position. This function + is used for paginated retrieval of deposit events, ensuring only logs from + consensus blocks are included. + + ## Parameters + - `deposit_contract_address_hash`: The address hash of the deposit contract. + - `log_block_number`: The block number to start searching after (for + pagination). + - `log_index`: The log index within the block to start searching after (for + pagination). + - `limit`: The maximum number of logs to retrieve. + + ## Returns + - A list of deposit event logs with the following fields: + - Log fields: `first_topic`, `second_topic`, `third_topic`, + `fourth_topic`, `data`, `index`, `block_number`, `block_hash`, + `transaction_hash`. + - Transaction fields: `from_address_hash`, `block_timestamp`. + """ + @spec get_logs_with_deposits( + Hash.Address.t(), + non_neg_integer(), + non_neg_integer(), + non_neg_integer() + ) :: [ + %{ + first_topic: Hash.Full.t(), + data: Data.t(), + index: non_neg_integer(), + block_number: non_neg_integer(), + block_hash: Hash.Full.t(), + transaction_hash: Hash.Full.t(), + from_address_hash: Hash.Address.t(), + block_timestamp: DateTime.t() + } + ] + def get_logs_with_deposits(deposit_contract_address_hash, log_block_number, log_index, limit) do + query = + from(log in Log, + join: transaction in assoc(log, :transaction), + where: transaction.block_consensus == true, + where: log.block_hash == transaction.block_hash, + where: log.address_hash == ^deposit_contract_address_hash, + where: log.first_topic == ^@deposit_event_signature, + where: {log.block_number, log.index} > {^log_block_number, ^log_index}, + limit: ^limit, + select: + map( + log, + ^~w(first_topic data index block_number block_hash transaction_hash)a + ), + order_by: [asc: log.block_number, asc: log.index], + select_merge: map(transaction, ^~w(from_address_hash block_timestamp)a) + ) + + Repo.all(query) + end + + defp put_withdrawal_address_hash(deposit) do + case deposit.withdrawal_credentials do + %Data{bytes: <>} + when prefix in [0x01, 0x02] -> + {:ok, withdrawal_address_hash} = Hash.Address.cast(withdrawal_address_hash_bytes) + Map.put(deposit, :withdrawal_address_hash, withdrawal_address_hash) + + _ -> + deposit + end + end +end diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 22db19cc5cf5..06176d964a1b 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.Block.Schema do } alias Explorer.Chain.Arbitrum.BatchBlock, as: ArbitrumBatchBlock + alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} alias Explorer.Chain.Celo.Epoch, as: CeloEpoch alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch @@ -32,6 +33,7 @@ defmodule Explorer.Chain.Block.Schema do quote do field(:blob_gas_used, :decimal) field(:excess_blob_gas, :decimal) + has_many(:beacon_deposits, BeaconDeposit, foreign_key: :block_hash, references: :hash) end, 2 ) diff --git a/apps/explorer/lib/explorer/chain/cache/counters/address_tabs_elements_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/address_tabs_elements_count.ex index d7c99643b103..6c6e13de27d6 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/address_tabs_elements_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/address_tabs_elements_count.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.Cache.Counters.AddressTabsElementsCount do | :logs | :withdrawals | :internal_transactions + | :beacon_deposits @typep response_status :: :limit_value | :stale | :up_to_date @spec get_counter(counter_type, String.t()) :: {DateTime.t(), non_neg_integer(), response_status} | nil diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index fd2a0a2f9b2a..b4a6f30f6b21 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -542,6 +542,14 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) + removed_consensus_block_numbers = + removed_consensus_blocks + |> Enum.map(fn {number, _hash} -> number end) + + if not Enum.empty?(removed_consensus_block_numbers) do + GenServer.cast(Indexer.Fetcher.Beacon.Deposit, {:lost_consensus, removed_consensus_block_numbers |> Enum.min()}) + end + repo.update_all( from( transaction in Transaction, @@ -566,8 +574,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) - removed_consensus_blocks - |> Enum.map(fn {number, _hash} -> number end) + removed_consensus_block_numbers |> Enum.reject(&Enum.member?(consensus_block_numbers, &1)) |> MissingRangesManipulator.add_ranges_by_block_numbers() diff --git a/apps/explorer/priv/beacon/migrations/20250813001523_create_deposits.exs b/apps/explorer/priv/beacon/migrations/20250813001523_create_deposits.exs new file mode 100644 index 000000000000..4b95822f155e --- /dev/null +++ b/apps/explorer/priv/beacon/migrations/20250813001523_create_deposits.exs @@ -0,0 +1,43 @@ +defmodule Explorer.Repo.Beacon.Migrations.CreateDeposits do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE beacon_deposits_status AS ENUM ('invalid', 'pending', 'completed')", + "DROP TYPE beacon_deposits_status" + ) + + create table(:beacon_deposits, primary_key: false) do + add(:pubkey, :bytea, null: false) + add(:withdrawal_credentials, :bytea, null: false) + add(:amount, :decimal, precision: 100, scale: 0, null: false) + add(:signature, :bytea, null: false) + add(:index, :bigint, null: false, primary_key: true) + add(:block_number, :bigint, null: false) + add(:block_timestamp, :utc_datetime_usec, null: false) + add(:log_index, :integer, null: false) + add(:status, :beacon_deposits_status, null: false) + + add(:from_address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + + add(:block_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + + add(:transaction_hash, references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), + null: false + ) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create(index(:beacon_deposits, [:from_address_hash])) + create(index(:beacon_deposits, [:block_hash])) + create(index(:beacon_deposits, [:pubkey], where: "status != 'invalid'")) + + create( + index(:beacon_deposits, [:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp], + where: "status = 'pending'", + name: :beacon_deposits_composite_key_only_pending_index + ) + ) + end +end diff --git a/apps/explorer/test/explorer/chain/beacon/deposit_test.exs b/apps/explorer/test/explorer/chain/beacon/deposit_test.exs new file mode 100644 index 000000000000..514b6a3afe9b --- /dev/null +++ b/apps/explorer/test/explorer/chain/beacon/deposit_test.exs @@ -0,0 +1,55 @@ +defmodule Explorer.Chain.Beacon.DepositTest do + use Explorer.DataCase + + alias Explorer.Chain.Beacon.Deposit + + describe "get_logs_with_deposits/4" do + @deposit_event_signature "0x649BBC62D0E31342AFEA4E5CD82D4049E7E1EE912FC0889AA790803BE39038C5" + + test "filter out reorged blocks" do + deposit_contract = insert(:address) + reorg = insert(:block, consensus: false) + actual_block = insert(:block, consensus: true, number: reorg.number) + transaction = insert(:transaction) |> with_block(actual_block) + + reorged_logs = [ + insert(:log, + address: deposit_contract, + block: reorg, + transaction: transaction, + first_topic: @deposit_event_signature + ), + insert(:log, + address: deposit_contract, + block: reorg, + transaction: transaction, + first_topic: @deposit_event_signature + ) + ] + + actual_logs = [ + insert(:log, + address: deposit_contract, + block: actual_block, + transaction: transaction, + first_topic: @deposit_event_signature + ), + insert(:log, + address: deposit_contract, + block: actual_block, + transaction: transaction, + first_topic: @deposit_event_signature + ) + ] + + assert actual_logs |> Enum.map(&{&1.transaction_hash, &1.block_hash, &1.block_number, &1.data, &1.index}) == + Deposit.get_logs_with_deposits( + deposit_contract.hash, + -1, + -1, + 10 + ) + |> Enum.map(&{&1.transaction_hash, &1.block_hash, &1.block_number, &1.data, &1.index}) + end + end +end diff --git a/apps/explorer/test/explorer/market/market_history_cache_test.exs b/apps/explorer/test/explorer/market/market_history_cache_test.exs index ad33e0076559..bbc0b49db971 100644 --- a/apps/explorer/test/explorer/market/market_history_cache_test.exs +++ b/apps/explorer/test/explorer/market/market_history_cache_test.exs @@ -1,7 +1,6 @@ defmodule Explorer.Market.MarketHistoryCacheTest do use Explorer.DataCase - alias Explorer.Market alias Explorer.Market.MarketHistoryCache alias Explorer.Market.MarketHistory diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index e5ecc5a03601..9e0db04de2a4 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -22,7 +22,7 @@ defmodule Explorer.Factory do } alias Explorer.Admin.Administrator - alias Explorer.Chain.Beacon.{Blob, BlobTransaction} + alias Explorer.Chain.Beacon.{Blob, BlobTransaction, Deposit} alias Explorer.Chain.Block.{EmissionReward, Range, Reward} alias Explorer.Chain.Stability.Validator, as: ValidatorStability @@ -49,6 +49,7 @@ defmodule Explorer.Factory do TokenTransfer, Token.Instance, Transaction, + Wei, Withdrawal } @@ -1451,4 +1452,131 @@ defmodule Explorer.Factory do address_hash: insert(:address).hash } end + + @beacon_deposit_abi ABI.parse_specification( + [ + %{ + "anonymous" => false, + "inputs" => [ + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "pubkey", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "withdrawal_credentials", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "amount", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "signature", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "index", + "type" => "bytes" + } + ], + "name" => "DepositEvent", + "type" => "event" + } + ], + include_events?: true + ) + |> List.first() + + def beacon_deposit_log_factory(attrs) do + pubkey_raw = Map.get(attrs, :deposit_pubkey, sequence("beacon_deposit_pubkey", &<<&1::384>>)) + + withdrawal_credentials_raw = + Map.get( + attrs, + :deposit_withdrawal_credentials, + sequence("beacon_deposit_withdrawal_credentials", &<<&1::256>>) + ) + + amount = Map.get(attrs, :deposit_amount, sequence("beacon_deposit_amount", & &1)) + signature_raw = Map.get(attrs, :deposit_signature, sequence("beacon_deposit_signature", &<<&1::768>>)) + index = Map.get(attrs, :deposit_index, sequence("beacon_deposit_index", & &1)) + + <<_::bytes-4, data_raw::binary>> = + ABI.TypeEncoder.encode( + [ + pubkey_raw, + withdrawal_credentials_raw, + <>, + signature_raw, + <> + ], + @beacon_deposit_abi + ) + + build( + :log, + Map.merge( + %{ + first_topic: "0x649BBC62D0E31342AFEA4E5CD82D4049E7E1EE912FC0889AA790803BE39038C5", + second_topic: nil, + third_topic: nil, + fourth_topic: nil, + data: %Data{bytes: data_raw} + }, + attrs + |> Map.drop([ + :deposit_pubkey, + :deposit_withdrawal_credentials, + :deposit_amount, + :deposit_signature, + :deposit_index + ]) + ) + ) + end + + def beacon_deposit_factory do + pubkey_raw = sequence("beacon_deposit_pubkey", &<<&1::384>>) + withdrawal_credentials_raw = sequence("beacon_deposit_withdrawal_credentials", &<<&1::256>>) + amount = sequence("beacon_deposit_amount", & &1) + signature_raw = sequence("beacon_deposit_signature", &<<&1::768>>) + index = sequence("beacon_deposit_index", & &1) + + log = + insert(:beacon_deposit_log, + deposit_pubkey: pubkey_raw, + deposit_withdrawal_credentials: withdrawal_credentials_raw, + deposit_amount: amount, + deposit_signature: signature_raw, + deposit_index: index + ) + + block = insert(:block) + transaction = insert(:transaction) |> with_block(block) + + %Deposit{ + pubkey: %Data{bytes: pubkey_raw}, + withdrawal_credentials: %Data{bytes: withdrawal_credentials_raw}, + amount: amount |> Decimal.new() |> Wei.from(:wei), + signature: %Data{bytes: signature_raw}, + index: index, + block_number: transaction.block.number, + block_timestamp: transaction.block.timestamp, + log_index: log.index, + status: :pending, + from_address: insert(:address), + block: transaction.block, + transaction: transaction + } + end end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/client.ex b/apps/indexer/lib/indexer/fetcher/beacon/client.ex index d568d4637cb3..fd62ab60f952 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/client.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/client.ex @@ -8,7 +8,7 @@ defmodule Indexer.Fetcher.Beacon.Client do @request_error_msg "Error while sending request to beacon rpc" - def http_get_request(url) do + defp http_get_request(url) do case HttpClient.get(url) do {:ok, %{body: body, status_code: 200}} -> Jason.decode(body) @@ -79,10 +79,24 @@ defmodule Indexer.Fetcher.Beacon.Client do http_get_request(header_url(slot)) end + @spec get_spec :: {:error, any()} | {:ok, any()} + def get_spec do + http_get_request(spec_url()) + end + + @spec get_pending_deposits(integer() | String.t()) :: {:error, any()} | {:ok, any()} + def get_pending_deposits(slot) do + http_get_request(pending_deposits_url(slot)) + end + def blob_sidecars_url(slot), do: "#{base_url()}" <> "/eth/v1/beacon/blob_sidecars/" <> to_string(slot) def header_url(slot), do: "#{base_url()}" <> "/eth/v1/beacon/headers/" <> to_string(slot) + defp pending_deposits_url(epoch), do: "#{base_url()}/eth/v1/beacon/states/#{epoch}/pending_deposits" + + defp spec_url, do: "#{base_url()}/eth/v1/config/spec" + def base_url do Application.get_env(:indexer, Indexer.Fetcher.Beacon)[:beacon_rpc] end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex new file mode 100644 index 000000000000..37647f490543 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -0,0 +1,411 @@ +defmodule Indexer.Fetcher.Beacon.Deposit do + @moduledoc """ + Fetches deposit data from the beacon chain. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + alias ABI.Event + alias Explorer.Chain.Beacon.Deposit + alias Explorer.Chain.{Data, Wei} + alias Explorer.Repo + alias Indexer.Fetcher.Beacon.Client + + defstruct [ + :interval, + :batch_size, + :deposit_contract_address_hash, + :domain_deposit, + :genesis_fork_version, + :deposit_index, + :last_processed_log_block_number, + :last_processed_log_index + ] + + def start_link([init_opts, server_opts]) do + GenServer.start_link(__MODULE__, init_opts, server_opts) + end + + @impl GenServer + def init(_opts) do + Logger.metadata(fetcher: :beacon_deposit) + + {:ok, nil, {:continue, nil}} + end + + @impl GenServer + def handle_continue(nil, _state) do + chain_id = Application.get_env(:indexer, :chain_id) + + case Client.get_spec() do + {:ok, + %{ + "data" => %{ + "DEPOSIT_CHAIN_ID" => ^chain_id, + "DEPOSIT_CONTRACT_ADDRESS" => deposit_contract_address_hash, + "DOMAIN_DEPOSIT" => "0x" <> domain_deposit_hex, + "GENESIS_FORK_VERSION" => "0x" <> genesis_fork_version_hex + } + }} -> + last_processed_deposit = Deposit.get_latest_deposit() || %{index: -1, block_number: -1, log_index: -1} + + state = %__MODULE__{ + interval: Application.get_env(:indexer, __MODULE__)[:interval], + batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size], + deposit_contract_address_hash: deposit_contract_address_hash, + domain_deposit: Base.decode16!(domain_deposit_hex, case: :mixed), + genesis_fork_version: Base.decode16!(genesis_fork_version_hex, case: :mixed), + deposit_index: last_processed_deposit.index, + last_processed_log_block_number: last_processed_deposit.block_number, + last_processed_log_index: last_processed_deposit.log_index + } + + Process.send_after(self(), :process_logs, state.interval) + + {:noreply, state} + + {:ok, + %{ + "data" => %{ + "DEPOSIT_CHAIN_ID" => chain_id, + "DEPOSIT_CONTRACT_ADDRESS" => _deposit_contract_address_hash, + "DOMAIN_DEPOSIT" => "0x" <> _domain_deposit_hex, + "GENESIS_FORK_VERSION" => "0x" <> _genesis_fork_version_hex + } + }} -> + Logger.error("Misconfigured CHAIN_ID or INDEXER_BEACON_RPC_URL, CHAIN_ID from the node: #{inspect(chain_id)}") + {:stop, :wrong_chain_id, nil} + + {:ok, data} -> + Logger.error("Unexpected format on beacon spec endpoint: #{inspect(data)}") + {:stop, :unexpected_format, nil} + + {:error, reason} -> + Logger.error("Failed to fetch beacon spec: #{inspect(reason)}") + {:stop, :fetch_failed, nil} + end + end + + @impl GenServer + def handle_cast({:lost_consensus, block_number}, %__MODULE__{} = state) do + {_deleted_deposits_count, deleted_deposits} = + Repo.delete_all( + from(d in Deposit, where: d.block_number >= ^block_number, select: d.index), + timeout: :infinity + ) + + deposit_index = Enum.min(deleted_deposits, fn -> state.deposit_index + 1 end) + + {:noreply, + %{ + state + | deposit_index: deposit_index - 1, + last_processed_log_block_number: block_number, + last_processed_log_index: -1 + }} + rescue + postgrex_error in Postgrex.Error -> + Logger.error( + "Error while trying to delete reorged Beacon Deposits: #{Exception.format(:error, postgrex_error, __STACKTRACE__)}. Retrying." + ) + + GenServer.cast(self(), {:lost_consensus, block_number}) + {:noreply, state} + end + + @impl GenServer + def handle_info( + :process_logs, + %__MODULE__{ + interval: interval, + batch_size: batch_size, + deposit_contract_address_hash: deposit_contract_address_hash, + domain_deposit: domain_deposit, + genesis_fork_version: genesis_fork_version, + deposit_index: deposit_index, + last_processed_log_block_number: last_processed_log_block_number, + last_processed_log_index: last_processed_log_index + } = state + ) do + deposits = + deposit_contract_address_hash + |> Deposit.get_logs_with_deposits( + last_processed_log_block_number, + last_processed_log_index, + batch_size + ) + |> Enum.map(&log_to_deposit/1) + + case sequential?(deposit_index, deposits) do + {:error, prev, curr} -> + Logger.error("Non-sequential deposits detected: #{inspect(prev)} followed by #{inspect(curr)}") + Process.send_after(self(), :process_logs, interval * 10) + {:noreply, state} + + _ -> + {deposits_count, _} = + Repo.insert_all(Deposit, set_status(deposits, domain_deposit, genesis_fork_version), + on_conflict: :replace_all, + conflict_target: [:index] + ) + + if deposits_count < batch_size do + Process.send_after(self(), :process_logs, interval) + else + Process.send(self(), :process_logs, []) + end + + last_deposit = + List.last(deposits, %{ + index: state.deposit_index, + block_number: state.last_processed_log_block_number, + log_index: state.last_processed_log_index + }) + + {:noreply, + %__MODULE__{ + state + | deposit_index: last_deposit.index, + last_processed_log_block_number: last_deposit.block_number, + last_processed_log_index: last_deposit.log_index + }} + end + end + + @abi ABI.parse_specification( + [ + %{ + "anonymous" => false, + "inputs" => [ + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "pubkey", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "withdrawal_credentials", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "amount", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "signature", + "type" => "bytes" + }, + %{ + "indexed" => false, + "internalType" => "bytes", + "name" => "index", + "type" => "bytes" + } + ], + "name" => "DepositEvent", + "type" => "event" + } + ], + include_events?: true + ) + + defp log_to_deposit(log) do + {_, + [ + {"pubkey", "bytes", false, pubkey}, + {"withdrawal_credentials", "bytes", false, withdrawal_credentials}, + {"amount", "bytes", false, <>}, + {"signature", "bytes", false, signature}, + {"index", "bytes", false, <>} + ]} = + Event.find_and_decode( + @abi, + log.first_topic && log.first_topic.bytes, + nil, + nil, + nil, + log.data.bytes + ) + + %{ + pubkey: %Data{bytes: pubkey}, + withdrawal_credentials: %Data{bytes: withdrawal_credentials}, + amount: amount |> Decimal.new() |> Wei.from(:gwei), + signature: %Data{bytes: signature}, + index: index, + from_address_hash: log.from_address_hash, + transaction_hash: log.transaction_hash, + block_hash: log.block_hash, + block_number: log.block_number, + block_timestamp: log.block_timestamp, + log_index: log.index, + inserted_at: DateTime.utc_now(), + updated_at: DateTime.utc_now() + } + end + + defp sequential?(last_processed_deposit_index, deposits) do + Enum.reduce_while(deposits, %{index: last_processed_deposit_index}, fn + %{index: i}, %{index: prev} when i == prev + 1 -> + {:cont, %{index: i}} + + %{index: i}, %{index: prev} -> + {:halt, {:error, prev, i}} + end) + end + + defp set_status(deposits, domain_deposit, genesis_fork_version) do + {deposits_to_query, deposits_acc, _valid_pubkeys_acc} = + Enum.reduce(deposits, {[], [], MapSet.new()}, fn deposit, {deposit_to_query, deposits_acc, valid_pubkeys_acc} -> + valid_signature? = verify(deposit, domain_deposit, genesis_fork_version) + + new_valid_pubkeys_acc = + if valid_signature? do + MapSet.put(valid_pubkeys_acc, deposit.pubkey) + else + valid_pubkeys_acc + end + + if MapSet.member?(valid_pubkeys_acc, deposit.pubkey) or valid_signature? do + {deposit_to_query, [Map.put(deposit, :status, :pending) | deposits_acc], new_valid_pubkeys_acc} + else + {[deposit | deposit_to_query], deposits_acc, valid_pubkeys_acc} + end + end) + + deposits_to_query_pubkeys = Enum.map(deposits_to_query, & &1.pubkey) + + query = + from(deposit in Deposit, + where: deposit.status != :invalid, + where: deposit.pubkey in ^deposits_to_query_pubkeys, + select: deposit.pubkey + ) + + valid_pubkeys = query |> Repo.all() |> MapSet.new() + + deposits_with_status = + deposits_to_query + |> Enum.map(fn deposit -> + if MapSet.member?(valid_pubkeys, deposit.pubkey) do + Map.put(deposit, :status, :pending) + else + Map.put(deposit, :status, :invalid) + end + end) + + deposits_with_status ++ deposits_acc + end + + @zero_genesis_validators_root :binary.copy(<<0x00>>, 32) + + defp verify(deposit, domain_deposit, genesis_fork_version) do + deposit_message_root = + hash_tree_root_deposit_message( + deposit.pubkey.bytes, + deposit.withdrawal_credentials.bytes, + deposit.amount |> Wei.to(:gwei) |> Decimal.to_integer() + ) + + domain = + compute_domain( + domain_deposit, + genesis_fork_version, + @zero_genesis_validators_root + ) + + signing_root = compute_signing_root(deposit_message_root, domain) + + ExEthBls.verify(deposit.pubkey.bytes, signing_root, deposit.signature.bytes) + end + + defp hash_tree_root_deposit_message(pubkey, withdrawal_credentials, amount) do + pubkey_packed = pack_basic_type(pubkey) + pubkey_root = merkleize_chunks(pubkey_packed) + + wc_packed = pack_basic_type(withdrawal_credentials) + wc_root = merkleize_chunks(wc_packed) + + amount_bytes = <> + amount_packed = pack_basic_type(amount_bytes) + amount_root = merkleize_chunks(amount_packed) + + field_roots = [pubkey_root, wc_root, amount_root] + merkleize_chunks(field_roots) + end + + defp pack_basic_type(value) do + chunk_size = 32 + padding_needed = rem(chunk_size - rem(byte_size(value), chunk_size), chunk_size) + padded = value <> :binary.copy(<<0>>, padding_needed) + + for <>, do: chunk + end + + defp merkleize_chunks([chunk]), do: chunk + + defp merkleize_chunks(chunks) when is_list(chunks) do + padded_chunks = pad_to_next_power_of_two(chunks) + merkleize_recursive(padded_chunks) + end + + defp merkleize_recursive([single_chunk]), do: single_chunk + + defp merkleize_recursive(chunks) do + next_level = + chunks + |> Enum.chunk_every(2) + |> Enum.map(fn + [left, right] -> :crypto.hash(:sha256, left <> right) + [single] -> single + end) + + merkleize_recursive(next_level) + end + + defp pad_to_next_power_of_two(list) do + length = length(list) + next_power = next_power_of_two(length) + padding_needed = next_power - length + zero_chunk = :binary.copy(<<0>>, 32) + list ++ List.duplicate(zero_chunk, padding_needed) + end + + defp next_power_of_two(n) when n <= 1, do: 1 + + defp next_power_of_two(n) do + 2 |> :math.pow(:math.ceil(:math.log2(n))) |> round() + end + + defp compute_domain(domain_type, fork_version, genesis_validators_root) do + fork_data_root = compute_container_hash_tree_root([fork_version, genesis_validators_root]) + domain_type <> binary_part(fork_data_root, 0, 28) + end + + defp compute_signing_root(deposit_message_root, domain) do + compute_container_hash_tree_root([deposit_message_root, domain]) + end + + defp compute_container_hash_tree_root(field_values) do + field_roots = + field_values + |> Enum.map(fn value -> + value + |> pack_basic_type() + |> merkleize_chunks() + end) + + merkleize_chunks(field_roots) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex new file mode 100644 index 000000000000..6a780c913173 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex @@ -0,0 +1,92 @@ +defmodule Indexer.Fetcher.Beacon.Deposit.Status do + @moduledoc """ + Fetches the status of beacon deposits. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + alias Explorer.Chain.Beacon.Deposit + alias Explorer.Chain.{Data, Wei} + alias Explorer.{QueryHelper, Repo} + alias Indexer.Fetcher.Beacon.{Blob, Client} + + def start_link(arguments, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, arguments, gen_server_options) + end + + @impl GenServer + def init(_state) do + Logger.metadata(fetcher: :beacon_deposit_status) + + Process.send(self(), :fetch_queued_deposits, []) + + {:ok, nil} + end + + @impl GenServer + def handle_info(:fetch_queued_deposits, _state) do + case Client.get_pending_deposits("head") do + {:ok, %{"data" => pending_deposits}} -> mark_completed_deposits(pending_deposits) + {:error, reason} -> Logger.error("Failed to fetch pending deposits: #{inspect(reason)}") + end + + config = Application.get_env(:indexer, __MODULE__) + epoch_duration = config[:epoch_duration] + reference_timestamp = config[:reference_timestamp] + + current_time = System.os_time(:second) + epochs_elapsed = div(current_time - reference_timestamp, epoch_duration) + next_epoch_timestamp = (epochs_elapsed + 1) * epoch_duration + reference_timestamp + + timer = + Process.send_after( + self(), + :fetch_queued_deposits, + :timer.seconds(next_epoch_timestamp - current_time + 1) + ) + + {:noreply, timer} + end + + defp mark_completed_deposits(pending_deposits) do + ids = + pending_deposits + |> Enum.map(fn deposit -> + {:ok, pubkey} = Data.cast(deposit["pubkey"]) + {:ok, withdrawal_credentials} = Data.cast(deposit["withdrawal_credentials"]) + {amount, ""} = Integer.parse(deposit["amount"]) + {:ok, signature} = Data.cast(deposit["signature"]) + {slot, ""} = Integer.parse(deposit["slot"]) + + {pubkey.bytes, withdrawal_credentials.bytes, amount |> Decimal.new() |> Wei.from(:gwei) |> Wei.to(:wei), + signature.bytes, slot |> slot_to_timestamp() |> DateTime.from_unix!()} + end) + + tuple_not_in = + dynamic( + not (^QueryHelper.tuple_in([:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp], ids)) + ) + + query = + from( + deposit in Deposit, + where: deposit.status == :pending, + where: ^tuple_not_in + ) + + Repo.update_all(query, set: [status: "completed", updated_at: DateTime.utc_now()]) + end + + defp slot_to_timestamp(slot) do + config = Application.get_env(:indexer, Blob) + slot_duration = config[:slot_duration] + reference_slot = config[:reference_slot] + reference_timestamp = config[:reference_timestamp] + (slot - reference_slot) * slot_duration + reference_timestamp + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 5cf8549cb8b5..0d41e0f46293 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -256,6 +256,8 @@ defmodule Indexer.Supervisor do ]}, {Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.Beacon.Blob.Supervisor, [[memory_monitor: memory_monitor]]}, + {Indexer.Fetcher.Beacon.Deposit.Supervisor, []}, + {Indexer.Fetcher.Beacon.Deposit.Status.Supervisor, []}, {Indexer.Fetcher.SignedAuthorizationStatus.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 7734655ada05..5216047ae271 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -51,6 +51,7 @@ defmodule Indexer.MixProject do {:decorator, "~> 1.4"}, # JSONRPC access to Nethermind for `Explorer.Indexer` {:ethereum_jsonrpc, in_umbrella: true}, + {:ex_eth_bls, "~> 0.1.0"}, # Brotli compression/decompression {:ex_brotli, "~> 0.5.0"}, {:ex_keccak, "~> 0.7.5"}, diff --git a/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs b/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs new file mode 100644 index 000000000000..e6b1ff3113d2 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs @@ -0,0 +1,129 @@ +defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do + use Explorer.DataCase, async: false + + import Mox + import Ecto.Query + + alias Explorer.Chain.Beacon.Deposit + alias Explorer.Chain.Wei + alias Indexer.Fetcher.Beacon.Deposit.Status, as: StatusFetcher + alias Indexer.Fetcher.Beacon.Deposit.Status.Supervisor, as: StatusSupervisor + + setup :verify_on_exit! + setup :set_mox_global + + if Application.compile_env(:explorer, :chain_type) == :ethereum do + @epoch_duration 384 + + setup do + initial_supervisor_env = Application.get_env(:indexer, StatusSupervisor) + initial_fetcher_env = Application.get_env(:indexer, StatusFetcher) + + Application.put_env(:indexer, StatusSupervisor, initial_supervisor_env |> Keyword.put(:disabled?, false)) + + Application.put_env( + :indexer, + StatusFetcher, + initial_fetcher_env |> Keyword.merge(epoch_duration: @epoch_duration, reference_timestamp: 1_722_024_023) + ) + + on_exit(fn -> + Application.put_env(:indexer, StatusSupervisor, initial_supervisor_env) + Application.put_env(:indexer, StatusFetcher, initial_fetcher_env) + end) + end + + describe "handle_info(:fetch_queued_deposits, _state)" do + test "marks deposits as completed" do + # 12405630 + deposit_a = + insert(:beacon_deposit, + status: :invalid, + amount: 1 |> Decimal.new() |> Wei.from(:gwei), + block_timestamp: DateTime.from_unix!(1_755_691_583) + ) + + # 12405631 + deposit_b = + insert(:beacon_deposit, + status: :pending, + amount: 2 |> Decimal.new() |> Wei.from(:gwei), + block_timestamp: DateTime.from_unix!(1_755_691_595) + ) + + # 12405633 + deposit_c = + insert(:beacon_deposit, + status: :pending, + amount: 3 |> Decimal.new() |> Wei.from(:gwei), + block_timestamp: DateTime.from_unix!(1_755_691_619) + ) + + # 12405634 + _deposit_d = + insert(:beacon_deposit, + status: :pending, + amount: 4 |> Decimal.new() |> Wei.from(:gwei), + block_timestamp: DateTime.from_unix!(1_755_691_631) + ) + + pending_deposits_result = + """ + { + "execution_optimistic": false, + "finalized": false, + "data": [ + { + "pubkey": "#{deposit_a.pubkey}", + "withdrawal_credentials": "#{deposit_a.withdrawal_credentials}", + "amount": "#{deposit_a.amount |> Wei.to(:gwei)}", + "signature": "#{deposit_a.signature}", + "slot": "12405630" + }, + { + "pubkey": "#{deposit_b.pubkey}", + "withdrawal_credentials": "#{deposit_b.withdrawal_credentials}", + "amount": "#{deposit_b.amount |> Wei.to(:gwei)}", + "signature": "#{deposit_b.signature}", + "slot": "12405631" + }, + { + "pubkey": "#{deposit_c.pubkey}", + "withdrawal_credentials": "#{deposit_c.withdrawal_credentials}", + "amount": "#{deposit_c.amount |> Wei.to(:gwei)}", + "signature": "#{deposit_c.signature}", + "slot": "12405633" + }, + { + "pubkey": "0xb257656f0a024a5c3be175a3bafd96cfcc452544b0fc6a23bbc39381028a28c10e8bafe6119c34771b5d86c9cae559e5", + "withdrawal_credentials": "0x01000000000000000000000082ce3e15a02e6a2e5a677d9700fe1390efead8eb", + "amount": "32000000000", + "signature": "0xb12eb9bddaf201aac73fb5ba9972ed06093c95a8baa1b5adcf1936ae7f33b0033c34d9e4756567c0424529e2de6774230274b015dfbe848e24a962a8748ea6229a8a87825f979e17d5e66f0246fb5eeb180e318c6574c22339f1cbcf5408a8dd", + "slot": "12405634" + } + ] + } + """ + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:5052/eth/v1/beacon/states/head/pending_deposits"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: pending_deposits_result}} + end + ) + + {:noreply, timer} = StatusFetcher.handle_info(:fetch_queued_deposits, nil) + + # if next scheduled call is later than epoch duration something is wrong + assert Process.read_timer(timer) / 1000 <= @epoch_duration + 1 + + assert [ + %Deposit{status: :invalid}, + %Deposit{status: :pending}, + %Deposit{status: :pending}, + %Deposit{status: :completed} + ] = Repo.all(from(d in Deposit, order_by: [asc: d.index])) + end + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs new file mode 100644 index 000000000000..96cf17e22a48 --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs @@ -0,0 +1,550 @@ +defmodule Indexer.Fetcher.Beacon.DepositTest do + use Explorer.DataCase, async: false + + import ExUnit.CaptureLog, only: [capture_log: 1] + import Mox + import Ecto.Query + + alias Explorer.Chain.Beacon.Deposit + alias Indexer.Fetcher.Beacon.Deposit.Supervisor, as: DepositSupervisor + alias Indexer.Fetcher.Beacon.Deposit, as: DepositFetcher + + setup :verify_on_exit! + setup :set_mox_global + + if Application.compile_env(:explorer, :chain_type) == :ethereum do + setup do + initial_supervisor_env = Application.get_env(:indexer, DepositSupervisor) + initial_chain_id = Application.get_env(:indexer, :chain_id) + initial_fetcher_env = Application.get_env(:indexer, DepositFetcher) + + Application.put_env(:indexer, DepositSupervisor, initial_supervisor_env |> Keyword.put(:disabled?, false)) + Application.put_env(:indexer, :chain_id, "1") + Application.put_env(:indexer, DepositFetcher, initial_fetcher_env |> Keyword.merge(interval: 1, batch_size: 1)) + + on_exit(fn -> + Application.put_env(:indexer, DepositSupervisor, initial_supervisor_env) + Application.put_env(:indexer, :chain_id, initial_chain_id) + Application.put_env(:indexer, DepositFetcher, initial_fetcher_env) + end) + end + + @spec_result """ + { + "data": { + "CONFIG_NAME": "mainnet", + "PRESET_BASE": "mainnet", + "TERMINAL_TOTAL_DIFFICULTY": "58750000000000000000000", + "TERMINAL_BLOCK_HASH": "0x0000000000000000000000000000000000000000000000000000000000000000", + "TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH": "18446744073709551615", + "MIN_GENESIS_ACTIVE_VALIDATOR_COUNT": "16384", + "MIN_GENESIS_TIME": "1606824000", + "GENESIS_FORK_VERSION": "0x00000000", + "GENESIS_DELAY": "604800", + "ALTAIR_FORK_VERSION": "0x01000000", + "ALTAIR_FORK_EPOCH": "74240", + "BELLATRIX_FORK_VERSION": "0x02000000", + "BELLATRIX_FORK_EPOCH": "144896", + "CAPELLA_FORK_VERSION": "0x03000000", + "CAPELLA_FORK_EPOCH": "194048", + "DENEB_FORK_VERSION": "0x04000000", + "DENEB_FORK_EPOCH": "269568", + "ELECTRA_FORK_VERSION": "0x05000000", + "ELECTRA_FORK_EPOCH": "364032", + "FULU_FORK_VERSION": "0x06000000", + "FULU_FORK_EPOCH": "18446744073709551615", + "SECONDS_PER_SLOT": "12", + "SECONDS_PER_ETH1_BLOCK": "14", + "MIN_VALIDATOR_WITHDRAWABILITY_DELAY": "256", + "SHARD_COMMITTEE_PERIOD": "256", + "ETH1_FOLLOW_DISTANCE": "2048", + "SUBNETS_PER_NODE": "2", + "INACTIVITY_SCORE_BIAS": "4", + "INACTIVITY_SCORE_RECOVERY_RATE": "16", + "EJECTION_BALANCE": "16000000000", + "MIN_PER_EPOCH_CHURN_LIMIT": "4", + "MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT": "8", + "CHURN_LIMIT_QUOTIENT": "65536", + "PROPOSER_SCORE_BOOST": "40", + "DEPOSIT_CHAIN_ID": "1", + "DEPOSIT_NETWORK_ID": "1", + "DEPOSIT_CONTRACT_ADDRESS": "0x00000000219ab540356cbb839cbe05303d7705fa", + "GAS_LIMIT_ADJUSTMENT_FACTOR": "1024", + "MAX_PAYLOAD_SIZE": "10485760", + "MAX_REQUEST_BLOCKS": "1024", + "MIN_EPOCHS_FOR_BLOCK_REQUESTS": "33024", + "TTFB_TIMEOUT": "5", + "RESP_TIMEOUT": "10", + "ATTESTATION_PROPAGATION_SLOT_RANGE": "32", + "MAXIMUM_GOSSIP_CLOCK_DISPARITY_MILLIS": "500", + "MESSAGE_DOMAIN_INVALID_SNAPPY": "0x00000000", + "MESSAGE_DOMAIN_VALID_SNAPPY": "0x01000000", + "ATTESTATION_SUBNET_PREFIX_BITS": "6", + "MAX_REQUEST_BLOCKS_DENEB": "128", + "MAX_REQUEST_BLOB_SIDECARS": "768", + "MAX_REQUEST_DATA_COLUMN_SIDECARS": "16384", + "MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS": "4096", + "BLOB_SIDECAR_SUBNET_COUNT": "6", + "MAX_BLOBS_PER_BLOCK": "6", + "MIN_PER_EPOCH_CHURN_LIMIT_ELECTRA": "128000000000", + "MAX_PER_EPOCH_ACTIVATION_EXIT_CHURN_LIMIT": "256000000000", + "MAX_BLOBS_PER_BLOCK_ELECTRA": "9", + "BLOB_SIDECAR_SUBNET_COUNT_ELECTRA": "9", + "MAX_REQUEST_BLOB_SIDECARS_ELECTRA": "1152", + "NUMBER_OF_COLUMNS": "128", + "NUMBER_OF_CUSTODY_GROUPS": "128", + "DATA_COLUMN_SIDECAR_SUBNET_COUNT": "128", + "SAMPLES_PER_SLOT": "8", + "CUSTODY_REQUIREMENT": "4", + "MAX_COMMITTEES_PER_SLOT": "64", + "TARGET_COMMITTEE_SIZE": "128", + "MAX_VALIDATORS_PER_COMMITTEE": "2048", + "SHUFFLE_ROUND_COUNT": "90", + "HYSTERESIS_QUOTIENT": "4", + "HYSTERESIS_DOWNWARD_MULTIPLIER": "1", + "HYSTERESIS_UPWARD_MULTIPLIER": "5", + "MIN_DEPOSIT_AMOUNT": "1000000000", + "MAX_EFFECTIVE_BALANCE": "32000000000", + "EFFECTIVE_BALANCE_INCREMENT": "1000000000", + "MIN_ATTESTATION_INCLUSION_DELAY": "1", + "SLOTS_PER_EPOCH": "32", + "MIN_SEED_LOOKAHEAD": "1", + "MAX_SEED_LOOKAHEAD": "4", + "EPOCHS_PER_ETH1_VOTING_PERIOD": "64", + "SLOTS_PER_HISTORICAL_ROOT": "8192", + "MIN_EPOCHS_TO_INACTIVITY_PENALTY": "4", + "EPOCHS_PER_HISTORICAL_VECTOR": "65536", + "EPOCHS_PER_SLASHINGS_VECTOR": "8192", + "HISTORICAL_ROOTS_LIMIT": "16777216", + "VALIDATOR_REGISTRY_LIMIT": "1099511627776", + "BASE_REWARD_FACTOR": "64", + "WHISTLEBLOWER_REWARD_QUOTIENT": "512", + "PROPOSER_REWARD_QUOTIENT": "8", + "INACTIVITY_PENALTY_QUOTIENT": "67108864", + "MIN_SLASHING_PENALTY_QUOTIENT": "128", + "PROPORTIONAL_SLASHING_MULTIPLIER": "1", + "MAX_PROPOSER_SLASHINGS": "16", + "MAX_ATTESTER_SLASHINGS": "2", + "MAX_ATTESTATIONS": "128", + "MAX_DEPOSITS": "16", + "MAX_VOLUNTARY_EXITS": "16", + "INACTIVITY_PENALTY_QUOTIENT_ALTAIR": "50331648", + "MIN_SLASHING_PENALTY_QUOTIENT_ALTAIR": "64", + "PROPORTIONAL_SLASHING_MULTIPLIER_ALTAIR": "2", + "SYNC_COMMITTEE_SIZE": "512", + "EPOCHS_PER_SYNC_COMMITTEE_PERIOD": "256", + "MIN_SYNC_COMMITTEE_PARTICIPANTS": "1", + "INACTIVITY_PENALTY_QUOTIENT_BELLATRIX": "16777216", + "MIN_SLASHING_PENALTY_QUOTIENT_BELLATRIX": "32", + "PROPORTIONAL_SLASHING_MULTIPLIER_BELLATRIX": "3", + "MAX_BYTES_PER_TRANSACTION": "1073741824", + "MAX_TRANSACTIONS_PER_PAYLOAD": "1048576", + "BYTES_PER_LOGS_BLOOM": "256", + "MAX_EXTRA_DATA_BYTES": "32", + "MAX_BLS_TO_EXECUTION_CHANGES": "16", + "MAX_WITHDRAWALS_PER_PAYLOAD": "16", + "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP": "16384", + "MAX_BLOB_COMMITMENTS_PER_BLOCK": "4096", + "FIELD_ELEMENTS_PER_BLOB": "4096", + "MIN_ACTIVATION_BALANCE": "32000000000", + "MAX_EFFECTIVE_BALANCE_ELECTRA": "2048000000000", + "MIN_SLASHING_PENALTY_QUOTIENT_ELECTRA": "4096", + "WHISTLEBLOWER_REWARD_QUOTIENT_ELECTRA": "4096", + "PENDING_DEPOSITS_LIMIT": "134217728", + "PENDING_PARTIAL_WITHDRAWALS_LIMIT": "134217728", + "PENDING_CONSOLIDATIONS_LIMIT": "262144", + "MAX_ATTESTER_SLASHINGS_ELECTRA": "1", + "MAX_ATTESTATIONS_ELECTRA": "8", + "MAX_DEPOSIT_REQUESTS_PER_PAYLOAD": "8192", + "MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD": "16", + "MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD": "2", + "MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP": "8", + "MAX_PENDING_DEPOSITS_PER_EPOCH": "16", + "FIELD_ELEMENTS_PER_CELL": "64", + "FIELD_ELEMENTS_PER_EXT_BLOB": "8192", + "KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH": "4", + "DOMAIN_RANDAO": "0x02000000", + "DOMAIN_DEPOSIT": "0x03000000", + "FULL_EXIT_REQUEST_AMOUNT": "0", + "COMPOUNDING_WITHDRAWAL_PREFIX": "0x02", + "DOMAIN_SELECTION_PROOF": "0x05000000", + "SYNC_COMMITTEE_SUBNET_COUNT": "4", + "UNSET_DEPOSIT_REQUESTS_START_INDEX": "18446744073709551615", + "DOMAIN_BEACON_PROPOSER": "0x00000000", + "DOMAIN_VOLUNTARY_EXIT": "0x04000000", + "VERSIONED_HASH_VERSION_KZG": "1", + "DOMAIN_BEACON_ATTESTER": "0x01000000", + "TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE": "16", + "DOMAIN_APPLICATION_MASK": "0x00000001", + "DOMAIN_AGGREGATE_AND_PROOF": "0x06000000", + "ETH1_ADDRESS_WITHDRAWAL_PREFIX": "0x01", + "TARGET_AGGREGATORS_PER_COMMITTEE": "16", + "DOMAIN_SYNC_COMMITTEE": "0x07000000", + "BLS_WITHDRAWAL_PREFIX": "0x00", + "DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF": "0x08000000", + "DOMAIN_CONTRIBUTION_AND_PROOF": "0x09000000" + } + } + """ + + describe "init/1" do + test "fetches config and initializes state without deposits in the database" do + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:5052/eth/v1/config/spec"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: @spec_result}} + end + ) + + DepositSupervisor.Case.start_supervised!() + + {_, pid, _, _} = + Supervisor.which_children(DepositSupervisor) |> Enum.find(fn {name, _, _, _} -> name == DepositFetcher end) + + assert :sys.get_state(pid) == %DepositFetcher{ + interval: 1, + batch_size: 1, + deposit_contract_address_hash: "0x00000000219ab540356cbb839cbe05303d7705fa", + domain_deposit: <<3, 0, 0, 0>>, + genesis_fork_version: <<0, 0, 0, 0>>, + deposit_index: -1, + last_processed_log_block_number: -1, + last_processed_log_index: -1 + } + end + + test "fetches config and initializes state with deposits in the database" do + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:5052/eth/v1/config/spec"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: @spec_result}} + end + ) + + deposit = insert(:beacon_deposit) + + DepositSupervisor.Case.start_supervised!() + + {_, pid, _, _} = + Supervisor.which_children(DepositSupervisor) |> Enum.find(fn {name, _, _, _} -> name == DepositFetcher end) + + assert :sys.get_state(pid) == %DepositFetcher{ + interval: 1, + batch_size: 1, + deposit_contract_address_hash: "0x00000000219ab540356cbb839cbe05303d7705fa", + domain_deposit: <<3, 0, 0, 0>>, + genesis_fork_version: <<0, 0, 0, 0>>, + deposit_index: deposit.index, + last_processed_log_block_number: deposit.block_number, + last_processed_log_index: deposit.log_index + } + end + end + + describe "handle_info(:process_logs, state)" do + @state %DepositFetcher{ + interval: 1, + batch_size: 1, + deposit_contract_address_hash: "0x00000000219ab540356cbb839cbe05303d7705fa", + domain_deposit: <<3, 0, 0, 0>>, + genesis_fork_version: <<0, 0, 0, 0>>, + deposit_index: -1, + last_processed_log_block_number: -1, + last_processed_log_index: -1 + } + + test "processes logs (batch 1)" do + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + other_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fb") + + insert(:log) + transaction_a = insert(:transaction) |> with_block() + + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + log_a_transaction_hash = log_a.transaction_hash + transaction_b = insert(:transaction) |> with_block() + + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_b, + block: transaction_b.block + ) + + log_b_transaction_hash = log_b.transaction_hash + # ensure that logs from other contract or with other signature are ignored + _log_to_be_ignored_b = insert(:beacon_deposit_log, address: other_contract_address, deposit_index: 2) + + {:noreply, new_state} = DepositFetcher.handle_info(:process_logs, @state) + DepositFetcher.handle_info(:process_logs, new_state) + + assert [ + %Deposit{transaction_hash: ^log_a_transaction_hash}, + %Deposit{transaction_hash: ^log_b_transaction_hash} + ] = + Repo.all(from(d in Deposit, order_by: [asc: :index])) + end + + test "processes logs (batch 5)" do + state = Map.put(@state, :batch_size, 5) + + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + other_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fb") + + insert(:log) + transaction_a = insert(:transaction) |> with_block() + + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + log_a_transaction_hash = log_a.transaction_hash + + transaction_b = insert(:transaction) |> with_block() + + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_b, + block: transaction_b.block + ) + + log_b_transaction_hash = log_b.transaction_hash + # ensure that logs from other contract or with other signature are ignored + _log_to_be_ignored_b = insert(:beacon_deposit_log, address: other_contract_address, deposit_index: 2) + + DepositFetcher.handle_info(:process_logs, state) + + assert [ + %Deposit{transaction_hash: ^log_a_transaction_hash}, + %Deposit{transaction_hash: ^log_b_transaction_hash} + ] = + Repo.all(from(d in Deposit, order_by: [asc: :index])) + end + + test "fails to process non-sequential logs (logs starts not from 0, between batches)" do + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + + transaction_a_to_be_ignored = insert(:transaction) |> with_block() + + _log_to_be_ignored_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_a_to_be_ignored, + block: transaction_a_to_be_ignored.block + ) + + transaction_a = insert(:transaction) |> with_block() + + _log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_a, + block: transaction_a.block + ) + + transaction_b = insert(:transaction) |> with_block() + + _log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_b, + block: transaction_b.block + ) + + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, @state) end) + + assert log =~ "Non-sequential deposits detected:" + + assert [] == Repo.all(Deposit) + end + + test "fails to process non-sequential logs (non-sequential between, between batches)" do + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + + transaction_a = insert(:transaction) |> with_block() + + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + log_a_transaction_hash = log_a.transaction_hash + transaction_a_to_be_ignored = insert(:transaction) |> with_block() + + _log_to_be_ignored_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_a_to_be_ignored, + block: transaction_a_to_be_ignored.block + ) + + transaction_b = insert(:transaction) |> with_block() + + _log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_b, + block: transaction_b.block + ) + + {:noreply, new_state} = DepositFetcher.handle_info(:process_logs, @state) + + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, new_state) end) + + assert log =~ "Non-sequential deposits detected:" + + assert [%Deposit{transaction_hash: ^log_a_transaction_hash}] = Repo.all(Deposit) + end + + test "fails to process non-sequential logs (logs starts not from 0, inside batch)" do + state = Map.put(@state, :batch_size, 5) + + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + + transaction_a_to_be_ignored = insert(:transaction) |> with_block() + + _log_to_be_ignored_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_a_to_be_ignored, + block: transaction_a_to_be_ignored.block + ) + + transaction_a = insert(:transaction) |> with_block() + + _log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_a, + block: transaction_a.block + ) + + transaction_b = insert(:transaction) |> with_block() + + _log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_b, + block: transaction_b.block + ) + + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) + + assert log =~ "Non-sequential deposits detected:" + + assert [] == Repo.all(Deposit) + end + + test "fails to process non-sequential logs (non-sequential between, inside batch)" do + state = Map.put(@state, :batch_size, 5) + + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + + transaction_a = insert(:transaction) |> with_block() + + _log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + transaction_a_to_be_ignored = insert(:transaction) |> with_block() + + _log_to_be_ignored_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_a_to_be_ignored, + block: transaction_a_to_be_ignored.block + ) + + transaction_b = insert(:transaction) |> with_block() + + _log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_b, + block: transaction_b.block + ) + + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) + + assert log =~ "Non-sequential deposits detected:" + + assert [] = Repo.all(Deposit) + end + + test "signature verification" do + state = Map.put(@state, :batch_size, 5) + + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + + transaction_a = insert(:transaction) |> with_block() + + _valid_log = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_pubkey: + Base.decode16!( + "b325d901d41957b6746088a61f5a1562f09ccb184e1628fa63b6bdfe9a724645d536484e4d26e8c9b653aaf0501cefe1", + case: :mixed + ), + deposit_withdrawal_credentials: + Base.decode16!("0100000000000000000000001ed8b3e4278184675fefa6981dea36f4535df417", case: :mixed), + deposit_amount: 32_000_000_000, + deposit_signature: + Base.decode16!( + "a519a7ff525a6831a6099399033bb5a0b959ec7af022ad7f37aa869927bbb59e1271079cbdff416e7f8f6f0f8ea7173304f4abdabfa65923a6b0304d49c97cef0690d0017b39518e7b19848657e2a9f73601d5037c217c5252558be1a8176e3d", + case: :mixed + ), + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + transaction_b = insert(:transaction) |> with_block() + + _invalid_log = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_b, + block: transaction_b.block + ) + + DepositFetcher.handle_info(:process_logs, state) + + assert [%Deposit{status: :pending, index: 0}, %Deposit{status: :invalid, index: 1}] = + Repo.all(from(d in Deposit, order_by: [asc: :index])) + end + end + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/beacon_deposit_status_supervisor_test.ex b/apps/indexer/test/support/indexer/fetcher/beacon_deposit_status_supervisor_test.ex new file mode 100644 index 000000000000..c23e6d61607c --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/beacon_deposit_status_supervisor_test.ex @@ -0,0 +1,9 @@ +defmodule Indexer.Fetcher.Beacon.Deposit.Status.Supervisor.Case do + alias Indexer.Fetcher.Beacon.Deposit.Status + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + [fetcher_arguments] + |> Status.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/beacon_deposit_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/beacon_deposit_supervisor_case.ex new file mode 100644 index 000000000000..ef525dcc7650 --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/beacon_deposit_supervisor_case.ex @@ -0,0 +1,9 @@ +defmodule Indexer.Fetcher.Beacon.Deposit.Supervisor.Case do + alias Indexer.Fetcher.Beacon.Deposit + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + [fetcher_arguments] + |> Deposit.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/config/runtime.exs b/config/runtime.exs index cb20139d0bb1..c8298d50223a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -916,6 +916,7 @@ disable_multichain_search_db_export_counters_queue_fetcher = config :indexer, block_transformer: ConfigHelper.block_transformer(), + chain_id: System.get_env("CHAIN_ID"), metadata_updater_milliseconds_interval: ConfigHelper.parse_time_env_var("TOKEN_METADATA_UPDATE_INTERVAL", "48h"), block_ranges: block_ranges, first_block: first_block, @@ -1391,6 +1392,25 @@ config :indexer, Indexer.Fetcher.Beacon.Blob, start_block: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_START_BLOCK", 19_200_000), end_block: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_BLOB_FETCHER_END_BLOCK", 0) +config :indexer, Indexer.Fetcher.Beacon.Deposit.Supervisor, + disabled?: + ConfigHelper.chain_type() != :ethereum || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_BEACON_DEPOSIT_FETCHER") + +config :indexer, Indexer.Fetcher.Beacon.Deposit, + interval: ConfigHelper.parse_time_env_var("INDEXER_BEACON_DEPOSIT_FETCHER_INTERVAL", "6s"), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_DEPOSIT_FETCHER_BATCH_SIZE", 1_000) + +config :indexer, Indexer.Fetcher.Beacon.Deposit.Status.Supervisor, + disabled?: + ConfigHelper.chain_type() != :ethereum || + ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_BEACON_DEPOSIT_STATUS_FETCHER") + +config :indexer, Indexer.Fetcher.Beacon.Deposit.Status, + epoch_duration: ConfigHelper.parse_integer_env_var("INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_EPOCH_DURATION", 384), + reference_timestamp: + ConfigHelper.parse_integer_env_var("INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_REFERENCE_TIMESTAMP", 1_722_024_023) + config :indexer, Indexer.Fetcher.Shibarium.L1, rpc: System.get_env("INDEXER_SHIBARIUM_L1_RPC"), start_block: System.get_env("INDEXER_SHIBARIUM_L1_START_BLOCK"), diff --git a/cspell.json b/cspell.json index 57bb07523062..57c2968bc9f0 100644 --- a/cspell.json +++ b/cspell.json @@ -261,6 +261,7 @@ "fortawesome", "fsym", "fullwidth", + "FULU", "Fuov", "fvdskvjglav", "fwrite", @@ -384,6 +385,7 @@ "Menlo", "mergeable", "Merkle", + "merkleize", "metaprogramming", "metatags", "mfmmvq", @@ -512,6 +514,7 @@ "qwertyuioiuytrewertyuioiuytrertyuio", "racecar", "raisedbrow", + "RANDAO", "rangeright", "raquo", "rarr", @@ -655,8 +658,10 @@ "tsquery", "tsvector", "tsym", + "TTFB", "txid", "txlistinternal", + "txlsit", "Txns", "txpool", "txreceipt", @@ -721,6 +726,8 @@ "websockex", "whereis", "whiler", + "WHISTLEBLOWER", + "WITHDRAWABILITY", "wobserver", "wysdvjkizxonu", "xact", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 6aee91659a7f..22275bef597f 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -384,6 +384,12 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_BEACON_BLOB_FETCHER_REFERENCE_TIMESTAMP=1702824023 # INDEXER_BEACON_BLOB_FETCHER_START_BLOCK=19200000 # INDEXER_BEACON_BLOB_FETCHER_END_BLOCK=0 +# INDEXER_DISABLE_BEACON_DEPOSIT_FETCHER= +# INDEXER_BEACON_DEPOSIT_FETCHER_INTERVAL= +# INDEXER_BEACON_DEPOSIT_FETCHER_BATCH_SIZE= +# INDEXER_DISABLE_BEACON_DEPOSIT_STATUS_FETCHER= +# INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_EPOCH_DURATION= +# INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_REFERENCE_TIMESTAMP # MISSING_BALANCE_OF_TOKENS_WINDOW_SIZE= # INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE= # WEBAPP_URL= diff --git a/mix.lock b/mix.lock index 69acafb14e0a..ef6a4b68584f 100644 --- a/mix.lock +++ b/mix.lock @@ -61,6 +61,7 @@ "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"}, "ex_doc": {:hex, :ex_doc, "0.38.3", "ddafe36b8e9fe101c093620879f6604f6254861a95133022101c08e75e6c759a", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "ecaa785456a67f63b4e7d7f200e8832fa108279e7eb73fd9928e7e66215a01f9"}, + "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, "ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"}, From 7794b7456524e1dfede77ec56cc2af08e2fccfe8 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Sep 2025 13:46:25 +0300 Subject: [PATCH 0312/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2cfb4b870ae..3f6a289a57a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- beacon deposits ([#12985](https://github.com/blockscout/blockscout/pull/12985)) - on-demand bytecode fetching on smart contract verification requests ([#10724](https://github.com/blockscout/blockscout/issues/10724)) - Improved proxy detection ([#12846](https://github.com/blockscout/blockscout/issues/12846)) - Add `reputation` property where applicable ([#13070](https://github.com/blockscout/blockscout/issues/13070)) From c9c48feb1d5106fa010c891bfee98286048f23d5 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:22:32 +0300 Subject: [PATCH 0313/1006] fix: Add reputation to token, rework reputation preload (#13149) * fix: Add reputation to token, rework reputation preload * Fix tests + add websockets preload * Changelog --- CHANGELOG.md | 1 + .../controllers/api/v2/address_controller.ex | 22 +++++++--- .../api/v2/advanced_filter_controller.ex | 6 ++- .../controllers/api/v2/token_controller.ex | 12 ++++-- .../api/v2/transaction_controller.ex | 7 +++- .../transaction_interpretation.ex | 5 ++- .../lib/block_scout_web/notifier.ex | 3 +- .../views/api/v2/address_view.ex | 2 +- .../views/api/v2/advanced_filter_view.ex | 6 ++- .../views/api/v2/token_transfer_view.ex | 2 +- .../api/v2/address_controller_test.exs | 1 - .../api/v2/token_controller_test.exs | 2 +- apps/explorer/lib/explorer/chain.ex | 7 +++- apps/explorer/lib/explorer/chain/address.ex | 2 +- .../chain/address/current_token_balance.ex | 3 +- .../lib/explorer/chain/address/reputation.ex | 40 ++++++++++++++++++ .../lib/explorer/chain/advanced_filter.ex | 9 ++-- .../lib/explorer/chain/smart_contract.ex | 3 +- apps/explorer/lib/explorer/chain/token.ex | 13 ++++-- .../lib/explorer/chain/token/instance.ex | 21 ++++++++-- .../lib/explorer/chain/token_transfer.ex | 16 ++++---- apps/explorer/lib/explorer/helper.ex | 41 ++++++++++++------- 22 files changed, 169 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f6a289a57a8..e2918763b801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Add reputation to token, rework reputation preload ([#13149](https://github.com/blockscout/blockscout/pull/13149)) - Replace get_constant_by_key with get_constant_value in get_last_processed_token_address_hash ([#13118](https://github.com/blockscout/blockscout/issues/13118)) - Duplicates of smart contracts additional sources ([#13018](https://github.com/blockscout/blockscout/issues/13018)) - Set for read ops in NFT backfillers ([#13116](https://github.com/blockscout/blockscout/issues/13116)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index b8c149d3e3ef..ff0029e55951 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -28,6 +28,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1, maybe_preload_ens_to_address: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] alias BlockScoutWeb.AccessHelper @@ -82,7 +83,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, :block => :optional, :transaction => :optional, - :token => :optional + [token: reputation_association()] => :optional }, api?: true ] @@ -91,16 +92,16 @@ defmodule BlockScoutWeb.API.V2.AddressController do necessity_by_association: %{ :names => :optional, :scam_badge => :optional, - :token => :optional, :signed_authorization => :optional, - :smart_contract => :optional + :smart_contract => :optional, + [token: reputation_association()] => :optional }, api?: true ] @nft_necessity_by_association [ necessity_by_association: %{ - :token => :optional + [token: reputation_association()] => :optional } ] @@ -127,6 +128,12 @@ defmodule BlockScoutWeb.API.V2.AddressController do api?: true ] + @token_preload_options [ + necessity_by_association: %{ + [token: reputation_association()] => :optional + } + ] + @spec contract_address_preloads() :: [keyword()] defp contract_address_preloads do chain_type_associations = @@ -292,7 +299,11 @@ defmodule BlockScoutWeb.API.V2.AddressController do {:ok, _address} -> token_balances = address_hash - |> Chain.fetch_last_token_balances(@api_true |> fetch_scam_token_toggle(conn)) + |> Chain.fetch_last_token_balances( + @api_true + |> fetch_scam_token_toggle(conn) + |> Keyword.merge(@token_preload_options) + ) TokenBalanceOnDemand.trigger_fetch(ip, address_hash) @@ -900,6 +911,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> paging_options() |> Keyword.merge(token_transfers_types_options(params)) |> Keyword.merge(@api_true) + |> Keyword.merge(@token_preload_options) |> fetch_scam_token_toggle(conn) ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index ee71ac2f715f..ee2d7d3b65aa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do alias BlockScoutWeb.API.V2.{AdvancedFilterView, CsvExportController} alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{AdvancedFilter, ContractMethod, Data, Token, Transaction} + alias Explorer.Chain.{Address.Reputation, AdvancedFilter, ContractMethod, Data, Token, Transaction} alias Explorer.Chain.CsvExport.Helper, as: CsvHelper alias Plug.Conn @@ -46,6 +46,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do @methods_filter_limit 20 @tokens_filter_limit 20 + @token_options [api?: true, necessity_by_association: %{Reputation.reputation_association() => :optional}] + @doc """ Function responsible for `api/v2/advanced-filters/` endpoint. """ @@ -190,7 +192,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do |> Enum.reject(&(&1 == "native")) |> Enum.uniq() |> Enum.take(@tokens_filter_limit) - |> Token.get_by_contract_address_hashes(@api_true) + |> Token.get_by_contract_address_hashes(@token_options) |> Map.new(fn token -> {token.contract_address_hash, token} end) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 9a895ff3c191..384ce30b88f5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -11,6 +11,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenTotalSupply, as: TokenTotalSupplyOnDemand + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + import BlockScoutWeb.Chain, only: [ split_list_by_page: 1, @@ -37,12 +39,14 @@ defmodule BlockScoutWeb.API.V2.TokenController do @api_true [api?: true] + @token_options [api?: true, necessity_by_association: %{reputation_association() => :optional}] + def token(conn, %{"address_hash_param" => address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)} do TokenTotalSupplyOnDemand.trigger_fetch(ip, address_hash) conn @@ -138,7 +142,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {:ok, holder_address_hash}} <- {:format, Chain.string_to_address_hash(holder_address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(holder_address_hash_string, params) do @@ -179,7 +183,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do def instances(conn, %{"address_hash_param" => address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)} do results_plus_one = Instance.address_to_unique_tokens( token.contract_address_hash, @@ -205,7 +209,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do def instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, + {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)}, {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)}, {:ok, token_instance} <- diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 9970157cfe07..1ce31c80298b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -29,6 +29,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1, maybe_preload_metadata_to_transaction: 1] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + import Ecto.Query, only: [ preload: 2 @@ -104,13 +106,14 @@ defmodule BlockScoutWeb.API.V2.TransactionController do @token_transfers_necessity_by_association %{ [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, - [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional + [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [token: reputation_association()] => :optional } @token_transfers_in_transaction_necessity_by_association %{ [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, - token: :required + [token: reputation_association()] => :optional } @internal_transaction_necessity_by_association [ diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 2acbd97f8884..626722b69329 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -9,6 +9,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Data, InternalTransaction, Log, TokenTransfer, Transaction} + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0, proxy_implementations_smart_contracts_association: 0] @@ -199,7 +201,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do [ necessity_by_association: %{ [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, - [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional + [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [token: reputation_association()] => :optional } ] |> Keyword.merge(@api_true) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 4c7fa9f98de7..c5906f87a059 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -32,6 +32,7 @@ defmodule BlockScoutWeb.Notifier do alias Explorer.Chain.{ Address, Address.CoinBalance, + Address.Reputation, BlockNumberHelper, DenormalizationHelper, InternalTransaction, @@ -263,7 +264,7 @@ defmodule BlockScoutWeb.Notifier do all_token_transfers |> Repo.preload( DenormalizationHelper.extend_transaction_preload([ - :token, + [token: Reputation.reputation_association()], :transaction, from_address: [ :scam_badge, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 2d51ee5fa34e..ce79629a61bf 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -184,7 +184,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do defp prepare_nft(nft, token) do Map.merge( - %{"token_type" => token.type, "value" => value(token.type, nft), "reputation" => nft.reputation}, + %{"token_type" => token.type, "value" => value(token.type, nft), "reputation" => token.reputation}, TokenView.prepare_token_instance(nft, token) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 0e659b5f1de3..133cd3aa4283 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -174,7 +174,11 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do token_transfer_index: advanced_filter.token_transfer_index, token_transfer_batch_index: advanced_filter.token_transfer_batch_index, fee: advanced_filter.fee, - reputation: advanced_filter.reputation + reputation: + if(advanced_filter.type != "coin_transfer", + do: advanced_filter.reputation, + else: nil + ) } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex index e4c691773b36..f1b353c04f5b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex @@ -61,7 +61,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do "block_number" => token_transfer.block_number, "log_index" => token_transfer.log_index, "token_type" => token_transfer.token_type, - "reputation" => token_transfer.reputation + "reputation" => token_transfer.token.reputation } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index f8e2869c11f0..d16a91609e46 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -25,7 +25,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do alias Explorer.Account.{Identity, WatchlistAddress} alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit - alias Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand alias Plug.Conn diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index a7f234ec31bc..e46801ac0498 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1779,7 +1779,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "don't fetch token instance metadata for non-existent token instance", %{ conn: conn, - v2_secret_key: v2_secret_key + v2_secret_key: _v2_secret_key } do token = insert(:token, type: "ERC-721") token_id = 0 diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index ffd57417f398..7093f253c0ae 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3053,11 +3053,14 @@ defmodule Explorer.Chain do |> select_repo(options).all() end - @spec fetch_last_token_balances(Hash.Address.t(), [api?]) :: [] + @spec fetch_last_token_balances(Hash.Address.t(), [api? | necessity_by_association_option]) :: [] def fetch_last_token_balances(address_hash, options \\ []) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + address_hash |> CurrentTokenBalance.last_token_balances() |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> join_associations(necessity_by_association) |> select_repo(options).all() end @@ -3066,6 +3069,7 @@ defmodule Explorer.Chain do filter = Keyword.get(options, :token_type) options = Keyword.delete(options, :token_type) paging_options = Keyword.get(options, :paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) case paging_options do %PagingOptions{key: {nil, 0, _id}} -> @@ -3076,6 +3080,7 @@ defmodule Explorer.Chain do |> CurrentTokenBalance.last_token_balances(options, filter) |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> page_current_token_balances(paging_options) + |> join_associations(necessity_by_association) |> select_repo(options).all() end end diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index fa07c7fce2c3..b2d248b3b998 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -89,7 +89,6 @@ defmodule Explorer.Chain.Address.Schema do field(:gas_used, :integer) field(:ens_domain_name, :string, virtual: true) field(:metadata, :any, virtual: true) - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) has_one(:smart_contract, SmartContract, references: :hash) has_one(:token, Token, foreign_key: :contract_address_hash, references: :hash) @@ -112,6 +111,7 @@ defmodule Explorer.Chain.Address.Schema do has_many(:names, Address.Name, foreign_key: :address_hash, references: :hash) has_one(:scam_badge, Address.ScamBadgeToAddress, foreign_key: :address_hash, references: :hash) has_many(:withdrawals, Withdrawal, foreign_key: :address_hash, references: :hash) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :hash) # In practice, this is a one-to-many relationship, but we only need to check if any signed authorization # exists for a given address. This done this way to avoid loading all signed authorizations for an address. diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 8a06708a069e..2623da10cc35 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -40,7 +40,6 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do field(:distinct_token_instances_count, :integer, virtual: true) field(:token_ids, {:array, :decimal}, virtual: true) field(:preloaded_token_instances, {:array, :any}, virtual: true) - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) # A transient field for deriving token holder count deltas during address_current_token_balances upserts field(:old_value, :decimal) @@ -56,6 +55,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do null: false ) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) + timestamps() end diff --git a/apps/explorer/lib/explorer/chain/address/reputation.ex b/apps/explorer/lib/explorer/chain/address/reputation.ex index 8eb5d61e089a..30fa4247d0e0 100644 --- a/apps/explorer/lib/explorer/chain/address/reputation.ex +++ b/apps/explorer/lib/explorer/chain/address/reputation.ex @@ -2,7 +2,47 @@ defmodule Explorer.Chain.Address.Reputation do @moduledoc """ This module defines the reputation enum values. """ + use Explorer.Schema + + alias Explorer.Chain.Address.ScamBadgeToAddress + alias Explorer.Chain.Hash + alias Explorer.Repo @enum_values [:ok, :scam] def enum_values, do: @enum_values + + @primary_key false + typed_embedded_schema do + field(:address_hash, Hash.Address) + field(:reputation, Ecto.Enum, values: @enum_values) + end + + def preload_reputation(address_hashes) do + scam_badges = + if Application.get_env(:block_scout_web, :hide_scam_addresses) do + ScamBadgeToAddress + |> where([sb], sb.address_hash in ^address_hashes) + |> Repo.all() + |> Map.new(&{&1.address_hash, &1}) + else + %{} + end + + Enum.map(address_hashes, fn address_hash -> + case Map.get(scam_badges, address_hash) do + nil -> {address_hash, %__MODULE__{reputation: "ok"}} + _badge -> {address_hash, %__MODULE__{reputation: "scam"}} + end + end) + end + + def reputation_association do + [reputation: &__MODULE__.preload_reputation/1] + end +end + +defimpl Jason.Encoder, for: Explorer.Chain.Address.Reputation do + def encode(reputation, opts) do + Jason.Encode.string(reputation.reputation, opts) + end end diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index e6090c9c7461..69f845b0ba45 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -30,7 +30,6 @@ defmodule Explorer.Chain.AdvancedFilter do field(:type, :string) field(:input, Data) field(:timestamp, :utc_datetime_usec) - field(:reputation, Ecto.Enum, values: Reputation.enum_values()) belongs_to( :from_address, @@ -60,6 +59,8 @@ defmodule Explorer.Chain.AdvancedFilter do has_one(:token_transfer, TokenTransfer, foreign_key: :transaction_hash, references: :hash, null: true) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :hash) + field(:fee, :decimal) field(:block_number, :integer) @@ -238,7 +239,7 @@ defmodule Explorer.Chain.AdvancedFilter do transaction_index: token_transfer.transaction.index, token_transfer_index: token_transfer.log_index, token_transfer_batch_index: token_transfer.reverse_index_in_batch, - reputation: token_transfer.reputation + reputation: token_transfer.token.reputation } end @@ -518,11 +519,11 @@ defmodule Explorer.Chain.AdvancedFilter do |> page_token_transfers(paging_options) token_transfer_query - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) |> limit_query(paging_options) |> query_function.(false) |> limit_query(paging_options) - |> preload([:transaction, :token]) + |> preload([:transaction, [token: ^Reputation.reputation_association()]]) |> select_merge([token_transfer], %{token_ids: [token_transfer.token_id], amounts: [token_transfer.amount]}) end diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 76b9fbb2ea50..22ff3eb16237 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -64,7 +64,6 @@ defmodule Explorer.Chain.SmartContract.Schema do field(:certified, :boolean) field(:is_blueprint, :boolean) field(:language, Ecto.Enum, values: @languages_enum, default: :solidity) - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to( :address, @@ -80,6 +79,8 @@ defmodule Explorer.Chain.SmartContract.Schema do foreign_key: :address_hash ) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :address_hash) + timestamps() unquote_splicing(@chain_type_fields) diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index ac396c9568ee..507c125dbbca 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -34,7 +34,6 @@ defmodule Explorer.Chain.Token.Schema do field(:is_verified_via_admin_panel, :boolean) field(:volume_24h, :decimal) field(:transfer_count, :integer) - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to( :contract_address, @@ -46,6 +45,8 @@ defmodule Explorer.Chain.Token.Schema do null: false ) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :contract_address_hash) + unquote_splicing(@bridged_field) timestamps() @@ -385,9 +386,15 @@ defmodule Explorer.Chain.Token do @doc """ Gets tokens with given contract address hashes. """ - @spec get_by_contract_address_hashes([Hash.Address.t()], [Chain.api?()]) :: [Token.t()] + @spec get_by_contract_address_hashes([Hash.Address.t()], [Chain.api?() | Chain.necessity_by_association_option()]) :: + [Token.t()] def get_by_contract_address_hashes(hashes, options) do - Chain.select_repo(options).all(from(t in __MODULE__, where: t.contract_address_hash in ^hashes)) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + __MODULE__ + |> where([t], t.contract_address_hash in ^hashes) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() end @doc """ diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 9d24edf23c30..f9be39363c4d 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -53,7 +53,6 @@ defmodule Explorer.Chain.Token.Instance do field(:cdn_upload_error, :string) field(:metadata_url, :string) field(:skip_metadata_url, :boolean) - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to(:owner, Address, foreign_key: :owner_address_hash, references: :hash, type: Hash.Address) @@ -67,6 +66,8 @@ defmodule Explorer.Chain.Token.Instance do null: false ) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) + timestamps() end @@ -396,7 +397,14 @@ defmodule Explorer.Chain.Token.Instance do |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() |> Enum.map(&erc_1155_preload_nft(&1, address_hash, options)) - |> Helper.custom_preload(options, Token, :token_contract_address_hash, :contract_address_hash, :token) + |> Helper.custom_preload( + options, + Token, + :token_contract_address_hash, + :contract_address_hash, + :token, + Reputation.reputation_association() + ) end defp page_erc_1155_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-1155"}}) do @@ -429,7 +437,14 @@ defmodule Explorer.Chain.Token.Instance do |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() |> Enum.map(&erc_1155_preload_nft(&1, address_hash, options)) - |> Helper.custom_preload(options, Token, :token_contract_address_hash, :contract_address_hash, :token) + |> Helper.custom_preload( + options, + Token, + :token_contract_address_hash, + :contract_address_hash, + :token, + Reputation.reputation_association() + ) end defp page_erc_404_nft_collections(query, %PagingOptions{key: {contract_address_hash, "ERC-404"}}) do diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 4d6886f4ae86..df73252fa75f 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -61,7 +61,6 @@ defmodule Explorer.Chain.TokenTransfer.Schema do field(:token_type, :string) field(:block_consensus, :boolean) field(:token_instance, :any, virtual: true) :: Instance.t() | nil - field(:reputation, Ecto.Enum, values: Reputation.enum_values(), virtual: true) belongs_to(:from_address, Address, foreign_key: :from_address_hash, @@ -103,6 +102,8 @@ defmodule Explorer.Chain.TokenTransfer.Schema do has_one(:token, through: [:token_contract_address, :token]) + has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) + timestamps() unquote_splicing(@transaction_field) @@ -142,6 +143,7 @@ defmodule Explorer.Chain.TokenTransfer do require Explorer.Chain.TokenTransfer.Schema import Ecto.Changeset + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] alias Explorer.Chain alias Explorer.Chain.{DenormalizationHelper, Hash, Log, TokenTransfer} @@ -246,7 +248,7 @@ defmodule Explorer.Chain.TokenTransfer do preloads = DenormalizationHelper.extend_transaction_preload([ :transaction, - :token, + [token: reputation_association()], [from_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]], [to_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]] ]) @@ -273,7 +275,7 @@ defmodule Explorer.Chain.TokenTransfer do preloads = DenormalizationHelper.extend_transaction_preload([ :transaction, - :token, + [token: reputation_association()], [from_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]], [to_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]] ]) @@ -306,7 +308,7 @@ defmodule Explorer.Chain.TokenTransfer do preloads = DenormalizationHelper.extend_transaction_preload([ :transaction, - :token, + [token: reputation_association()], [from_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]], [to_address: [:scam_badge, :names, :smart_contract, Implementation.proxy_implementations_association()]] ]) @@ -577,7 +579,7 @@ defmodule Explorer.Chain.TokenTransfer do |> join(:inner, [tt], token in assoc(tt, :token), as: :token) |> preload([token: token], [{:token, token}]) |> filter_by_type(token_types) - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) |> handle_paging_options(paging_options) else to_address_hash_query = @@ -587,7 +589,7 @@ defmodule Explorer.Chain.TokenTransfer do |> filter_by_token_address_hash(token_address_hash) |> filter_by_type(token_types) |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) |> handle_paging_options(paging_options) |> Chain.wrapped_union_subquery() @@ -598,7 +600,7 @@ defmodule Explorer.Chain.TokenTransfer do |> filter_by_token_address_hash(token_address_hash) |> filter_by_type(token_types) |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) |> handle_paging_options(paging_options) |> Chain.wrapped_union_subquery() diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 4f23abc8ca2b..0f4eec9da4e5 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -5,7 +5,7 @@ defmodule Explorer.Helper do alias ABI.TypeDecoder alias Explorer.Chain - alias Explorer.Chain.{Address.ScamBadgeToAddress, Data, Hash, Wei} + alias Explorer.Chain.{Address.Reputation, Address.ScamBadgeToAddress, Data, Hash, Wei} import Ecto.Query import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @@ -156,12 +156,21 @@ defmodule Explorer.Helper do Results will be placed to `preload_field` """ @spec custom_preload(list(map()), keyword(), atom(), atom(), atom(), atom()) :: list() - def custom_preload(list, options, struct, foreign_key_field, references_field, preload_field) do + def custom_preload( + list, + options, + struct, + foreign_key_field, + references_field, + preload_field, + preload_field_association \\ [] + ) do to_fetch_from_db = list |> Enum.map(& &1[foreign_key_field]) |> Enum.uniq() associated_elements = struct |> where([t], field(t, ^references_field) in ^to_fetch_from_db) + |> preload(^preload_field_association) |> Chain.select_repo(options).all() |> Enum.reduce(%{}, fn el, acc -> Map.put(acc, Map.from_struct(el)[references_field], el) end) @@ -256,18 +265,20 @@ defmodule Explorer.Helper do query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) |> where([sabm: sabm], is_nil(sabm.address_hash)) - |> select_merge([q], %{reputation: "ok"}) + |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) |> select_merge([q, sabm: sabm], %{ - reputation: fragment("CASE WHEN ? THEN ? ELSE ? END", is_nil(sabm.address_hash), "ok", "scam") + reputation: %Reputation{ + reputation: fragment("CASE WHEN ? THEN ? ELSE ? END", is_nil(sabm.address_hash), "ok", "scam") + } }) true -> query - |> select_merge([q], %{reputation: "ok"}) + |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) end end @@ -309,24 +320,26 @@ defmodule Explorer.Helper do query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) |> where([sabm: sabm], is_nil(sabm.address_hash)) - |> select_merge([q], %{reputation: "ok"}) + |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) |> select_merge([q, sabm: sabm], %{ - reputation: - fragment( - "CASE WHEN MAX(CASE WHEN ? THEN 0 ELSE 1 END) = 0 THEN ? ELSE ? END", - is_nil(sabm.address_hash), - "ok", - "scam" - ) + reputation: %Reputation{ + reputation: + fragment( + "CASE WHEN MAX(CASE WHEN ? THEN 0 ELSE 1 END) = 0 THEN ? ELSE ? END", + is_nil(sabm.address_hash), + "ok", + "scam" + ) + } }) true -> query - |> select_merge([q], %{reputation: "ok"}) + |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) end end From f432885da4ef12432400b718adc00529e53aefe8 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:54:04 +0300 Subject: [PATCH 0314/1006] fix: Fix reputation preload for ERC-404 collections (#13174) * fix: Fix reputation preload for ERC-404 collections * Changelog --- CHANGELOG.md | 1 + .../api/v2/address_controller_test.exs | 150 +++++++++++++++++- .../lib/explorer/chain/token/instance.ex | 2 +- 3 files changed, 150 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2918763b801..39def16e544d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Fix reputation preload for ERC-404 collections ([#13174](https://github.com/blockscout/blockscout/pull/13174)) - Add reputation to token, rework reputation preload ([#13149](https://github.com/blockscout/blockscout/pull/13149)) - Replace get_constant_by_key with get_constant_value in get_last_processed_token_address_hash ([#13118](https://github.com/blockscout/blockscout/issues/13118)) - Duplicates of smart contracts additional sources ([#13018](https://github.com/blockscout/blockscout/issues/13018)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index d16a91609e46..2a88ffb78d04 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -4912,6 +4912,30 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %Instance{ti | current_token_balance: current_token_balance} end + token = insert(:token, type: "ERC-404") + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + request = conn |> put_req_cookie("show_scam_tokens", "true") @@ -4943,6 +4967,22 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> json_response(200) assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert response == + conn + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + |> json_response(200) + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get nft collections with scam reputation", %{conn: conn} do @@ -5003,6 +5043,31 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %Instance{ti | current_token_balance: current_token_balance} end + token = insert(:token, type: "ERC-404") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + request = conn |> put_req_cookie("show_scam_tokens", "true") @@ -5036,9 +5101,26 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["items"] == [] assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + + response = json_response(request, 200) + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert List.first(response["items"])["reputation"] == "scam" + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + response = json_response(request, 200) + + assert response["items"] == [] + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end - test "get smart-contract with ok reputation with hide_scam_addresses=false", %{conn: conn} do + test "get nft collections with ok reputation with hide_scam_addresses=false", %{conn: conn} do init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) Application.put_env(:block_scout_web, :hide_scam_addresses, false) on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) @@ -5093,6 +5175,30 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %Instance{ti | current_token_balance: current_token_balance} end + token = insert(:token, type: "ERC-404") + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) @@ -5106,9 +5212,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end - test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do + test "get nft collections with scam reputation with hide_scam_addresses=false", %{conn: conn} do init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) Application.put_env(:block_scout_web, :hide_scam_addresses, false) on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) @@ -5167,6 +5280,32 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %Instance{ti | current_token_balance: current_token_balance} end + token = insert(:token, type: "ERC-404") + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + amount = Enum.random(16..50) + + for _ <- 0..(amount - 1) do + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash, + owner_address_hash: address.hash + ) + |> Repo.preload([:token]) + + current_token_balance = + insert(:address_current_token_balance, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash, + value: Enum.random(1..100_000) + ) + |> Repo.preload([:token]) + + %Instance{ti | current_token_balance: current_token_balance} + end + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) @@ -5180,6 +5319,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) + response = json_response(request, 200) + + assert List.first(response["items"])["reputation"] == "ok" + + assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index f9be39363c4d..640455e08242 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -432,7 +432,7 @@ defmodule Explorer.Chain.Token.Instance do distinct_token_instances_count: fragment("COUNT(*)"), token_ids: fragment("array_agg(?)", ctb.token_id) }) - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_with_aggregate(:token_contract_address_hash, options) |> page_erc_404_nft_collections(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() From 5f2d3acf8b9793507d135952a4edf8bb51fb224a Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 9 Sep 2025 14:48:39 +0300 Subject: [PATCH 0315/1006] fix: pagination and filtering issues in `/addresses/:hash/nft` (#13175) * docs: add documentation for token_transfers_types_options function * fix: pagination and filtering issues in `/addresses/:hash/nft` * refactor: add doc and reduce complexity * fix: test name * test: add check for next_page_params to ensure original type filter is not leaked * chore: update CHANGELOG.md --- CHANGELOG.md | 1 + .../lib/block_scout_web/paging_helper.ex | 4 + .../api/v2/address_controller_test.exs | 182 +++++++++++++++++- apps/explorer/lib/explorer/chain/token.ex | 11 ++ .../lib/explorer/chain/token/instance.ex | 107 +++++++--- 5 files changed, 268 insertions(+), 37 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39def16e544d..21cc9bd82f6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Pagination and filtering issues in `/addresses/:hash/nft` ([#13175](https://github.com/blockscout/blockscout/pull/13175)) - Fix reputation preload for ERC-404 collections ([#13174](https://github.com/blockscout/blockscout/pull/13174)) - Add reputation to token, rework reputation preload ([#13149](https://github.com/blockscout/blockscout/pull/13149)) - Replace get_constant_by_key with get_constant_value in get_last_processed_token_address_hash ([#13118](https://github.com/blockscout/blockscout/issues/13118)) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index cfd72b9e216e..d6b0eaf79b17 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -73,6 +73,9 @@ defmodule BlockScoutWeb.PagingHelper do def stability_validators_state_options(_), do: [state: []] + @doc """ + Parse 'type' query parameter from request option map + """ @spec token_transfers_types_options(map()) :: [{:token_type, list}] def token_transfers_types_options(%{"type" => filters}) do [ @@ -228,6 +231,7 @@ defmodule BlockScoutWeb.PagingHelper do params |> Map.drop([ :address_hash_param, + :type, :apikey, "apikey", "block_hash_or_number", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 2a88ffb78d04..4d632b00c1b2 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -4639,6 +4639,40 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end + test "next_page_params does not leak original type filter", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + # Create a mix of ERC-1155 and ERC-404 items exceeding one page + insert_list(60, :address_current_token_balance_with_token_id) + + for _ <- 1..60 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, token_contract_address_hash: token.contract_address_hash) |> Repo.preload([:token]) + + ctb = + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + + %Instance{ti | current_token_balance: ctb} + end + + response = + conn + |> get(endpoint.(address.hash), %{type: "ERC-404,ERC-1155"}) + |> json_response(200) + + assert not is_nil(response["next_page_params"]) + refute Map.has_key?(response["next_page_params"], "type") + assert Map.has_key?(response["next_page_params"], "token_type") + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + end + test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do address = insert(:address) @@ -4837,20 +4871,154 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response_2nd_page["items"]) == 50 assert Enum.count(response_3rd_page["items"]) == 2 - compare_item(Enum.at(token_instances_721, 50), Enum.at(response["items"], 0)) - compare_item(Enum.at(token_instances_721, 1), Enum.at(response["items"], 49)) + compare_item(Enum.at(token_instances_1155, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(token_instances_1155, 1), Enum.at(response["items"], 49)) - compare_item(Enum.at(token_instances_721, 0), Enum.at(response_2nd_page["items"], 0)) - compare_item(Enum.at(token_instances_1155, 50), Enum.at(response_2nd_page["items"], 1)) - compare_item(Enum.at(token_instances_1155, 2), Enum.at(response_2nd_page["items"], 49)) + compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(token_instances_721, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(token_instances_721, 2), Enum.at(response_2nd_page["items"], 49)) - compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) - compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) + compare_item(Enum.at(token_instances_721, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(token_instances_721, 0), Enum.at(response_3rd_page["items"], 1)) assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_3rd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end + + test "paginates across types after intermediate type exhaustion (should include next type)", %{ + conn: conn, + endpoint: endpoint + } do + address = insert(:address) + + # Insert 30 ERC-1155 (with balances) + for _ <- 1..30 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + end + + # Insert 25 ERC-404 (with balances) + for _ <- 1..25 do + token = insert(:token, type: "ERC-404") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + end + + # Insert 10 ERC-721 (owned directly) + for _ <- 1..10 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + end + + # Page 1 + page1_resp = conn |> get(endpoint.(address.hash)) |> json_response(200) + assert Enum.count(page1_resp["items"]) == 50 + assert page1_resp["next_page_params"] != nil + + # Expect mixture of ERC-721 and ERC-1155 only on first page (by current logic order) + assert %{ + "ERC-1155" => 30, + "ERC-404" => 20 + } = Enum.frequencies_by(page1_resp["items"], & &1["token_type"]) + + page2_resp = conn |> get(endpoint.(address.hash), page1_resp["next_page_params"]) |> json_response(200) + + assert Enum.count(page2_resp["items"]) == 15 + assert page2_resp["next_page_params"] == nil + + assert %{ + "ERC-404" => 5, + "ERC-721" => 10 + } = Enum.frequencies_by(page2_resp["items"], & &1["token_type"]) + end + + test "multi-type filter includes only requested types", %{conn: conn, endpoint: endpoint} do + address = insert(:address) + + # ERC-721 tokens (should be excluded by filter) + for _ <- 1..5 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + end + + # ERC-1155 tokens (should be included) + for _ <- 1..5 do + token = insert(:token, type: "ERC-1155") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-1155", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + end + + # ERC-404 tokens (should be included) + for _ <- 1..5 do + token = insert(:token, type: "ERC-404") + + ti = + insert(:token_instance, + token_contract_address_hash: token.contract_address_hash + ) + |> Repo.preload([:token]) + + insert(:address_current_token_balance_with_token_id_and_fixed_token_type, + address: address, + token_type: "ERC-404", + token_id: ti.token_id, + token_contract_address_hash: token.contract_address_hash + ) + end + + filter = %{"type" => "ERC-404,ERC-1155"} + request = get(conn, endpoint.(address.hash), filter) + response = json_response(request, 200) + + assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) + + assert Enum.count(response["items"]) == 10 + assert Enum.all?(response["items"], fn item -> item["token_type"] in ["ERC-404", "ERC-1155"] end) + refute Enum.any?(response["items"], fn item -> item["token_type"] == "ERC-721" end) + end end describe "/addresses/{address_hash}/nft/collections" do diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 507c125dbbca..70659e29d8f5 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -143,6 +143,17 @@ defmodule Explorer.Chain.Token do @required_attrs ~w(contract_address_hash type)a @optional_attrs ~w(cataloged decimals name symbol total_supply skip_metadata total_supply_updated_at_block metadata_updated_at updated_at fiat_value circulating_market_cap icon_url is_verified_via_admin_panel volume_24h)a + @doc """ + Returns the list of allowed NFT type labels. + """ + @spec allowed_nft_type_labels() :: [String.t()] + def allowed_nft_type_labels, + do: [ + "ERC-1155", + "ERC-404", + "ERC-721" + ] + @doc false def changeset(%Token{} = token, params \\ %{}) do additional_attrs = if BridgedToken.enabled?(), do: [:bridged], else: [] diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 640455e08242..3ba7cb4cb39b 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -136,46 +136,93 @@ defmodule Explorer.Chain.Token.Instance do do: from(i in __MODULE__, where: i.token_contract_address_hash == ^token_contract_address and i.token_id == ^token_id) - @spec nft_list(binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] - def nft_list(address_hash, options \\ []) + @doc """ + Paginated NFT instances owned by an address. - def nft_list(address_hash, options) when is_list(options) do - nft_list(address_hash, Keyword.get(options, :token_type, []), options) - end + ## Notes + * Filter: `:token_type` list (empty or omitted = all). + * Pagination key: `{token_contract_address_hash, token_id, token_type}`. + * Resumes at the key's type; earlier types skipped. + * Fills one page sequentially across types; spillover allowed. - defp nft_list(address_hash, ["ERC-721"], options) do - erc_721_token_instances_by_owner_address_hash(address_hash, options) - end + ## Params + * `address_hash` - owner address (binary or `Hash.Address`). + * `options` (keyword): + - `:paging_options` (%PagingOptions{}; default `Chain.default_paging_options/0`). + - `:token_type` (list of labels) filter. + - `:necessity_by_association` pass-through for joins. - defp nft_list(address_hash, ["ERC-1155"], options) do - erc_1155_token_instances_by_address_hash(address_hash, options) - end + ## Returns + * list (<= page_size) of `%Explorer.Chain.Token.Instance{}`; empty list if none. + """ + @spec nft_list(binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] + def nft_list(address_hash, options) when is_list(options) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + page_size = paging_options.page_size + remaining_types = remaining_token_types(options) + + {acc, _filled} = + Enum.reduce_while(remaining_types, {[], 0}, fn type, {list, count} -> + options = + if Enum.empty?(list) do + options + else + Keyword.put(options, :paging_options, %PagingOptions{ + page_size: page_size - count + }) + end + + fetched = fetch_by_type_and_address_hash(type, address_hash, options) + new_list = list ++ fetched + new_count = count + length(fetched) + + if new_count >= page_size do + {:halt, {Enum.take(new_list, page_size), page_size}} + else + {:cont, {new_list, new_count}} + end + end) - defp nft_list(address_hash, ["ERC-404"], options) do - erc_404_token_instances_by_address_hash(address_hash, options) + acc end - defp nft_list(address_hash, _, options) do - paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) - - case paging_options do - %PagingOptions{key: {_contract_address_hash, _token_id, "ERC-1155"}} -> - erc_1155_token_instances_by_address_hash(address_hash, options) + # Drop all types until start type (so we continue where we left off) + # Accumulate results sequentially per type until we fill the page + @spec remaining_token_types(keyword()) :: [String.t()] + defp remaining_token_types(options) do + token_types = + options + |> Keyword.get(:token_type, []) + |> then(fn types -> + if Enum.empty?(types) do + Token.allowed_nft_type_labels() + else + types + end + end) + |> Enum.sort() - %PagingOptions{key: {_contract_address_hash, _token_id, "ERC-404"}} -> - erc_404_token_instances_by_address_hash(address_hash, options) + options + |> Keyword.get(:paging_options, Chain.default_paging_options()) + |> case do + %PagingOptions{ + key: {_contract_address_hash, _token_id, type} + } + when is_binary(type) -> + Enum.drop_while(token_types, &(&1 != type)) _ -> - erc_721 = erc_721_token_instances_by_owner_address_hash(address_hash, options) - - if length(erc_721) == paging_options.page_size do - erc_721 - else - erc_1155 = erc_1155_token_instances_by_address_hash(address_hash, options) - erc_404 = erc_404_token_instances_by_address_hash(address_hash, options) + token_types + end + end - (erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size) - end + @spec fetch_by_type_and_address_hash(binary(), binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] + defp fetch_by_type_and_address_hash(type, address_hash, options) do + case type do + "ERC-721" -> erc_721_token_instances_by_owner_address_hash(address_hash, options) + "ERC-1155" -> erc_1155_token_instances_by_address_hash(address_hash, options) + "ERC-404" -> erc_404_token_instances_by_address_hash(address_hash, options) + _ -> [] end end From cab58dccd77d50fb4e3b1c9ef5a926c86ea78d10 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 10 Sep 2025 17:00:04 +0300 Subject: [PATCH 0316/1006] Fix: Fix batch's number processing from the socket event (#13181) --- .../controllers/api/v2/optimism_controller.ex | 10 ++++---- .../lib/block_scout_web/notifiers/optimism.ex | 2 +- .../lib/block_scout_web/routers/api_router.ex | 2 +- .../views/api/v2/optimism_view.ex | 6 ++--- .../explorer/chain/optimism/frame_sequence.ex | 24 +++++++++---------- .../fetcher/optimism/transaction_batch.ex | 3 +-- 6 files changed, 23 insertions(+), 24 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 090b76ba1766..2faacc0a70ee 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -155,13 +155,13 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/batches/:internal_id` endpoint. + Function to handle GET requests to `/api/v2/optimism/batches/:number` endpoint. """ - @spec batch_by_internal_id(Plug.Conn.t(), map()) :: Plug.Conn.t() - def batch_by_internal_id(conn, %{"internal_id" => internal_id}) do - {internal_id, ""} = Integer.parse(internal_id) + @spec batch_by_number(Plug.Conn.t(), map()) :: Plug.Conn.t() + def batch_by_number(conn, %{"number" => number}) do + {number, ""} = Integer.parse(number) - batch = FrameSequence.batch_by_internal_id(internal_id, api?: true) + batch = FrameSequence.batch_by_number(number, api?: true) if is_nil(batch) do {:error, :not_found} diff --git a/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex b/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex index 13941eec610c..0d7576640b20 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifiers/optimism.ex @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.Notifiers.Optimism do def handle_event({:chain_event, :new_optimism_batches, :realtime, batches}) do batches - |> Enum.sort_by(& &1.internal_id, :asc) + |> Enum.sort_by(& &1.number, :asc) |> Enum.each(fn batch -> Endpoint.broadcast("optimism:new_batch", "new_optimism_batch", %{ batch: batch diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 8e0fa13031ea..218b3319adb5 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -306,7 +306,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/batches", V2.OptimismController, :batches) get("/batches/count", V2.OptimismController, :batches_count) get("/batches/da/celestia/:height/:commitment", V2.OptimismController, :batch_by_celestia_blob) - get("/batches/:internal_id", V2.OptimismController, :batch_by_internal_id) + get("/batches/:number", V2.OptimismController, :batch_by_number) get("/output-roots", V2.OptimismController, :output_roots) get("/output-roots/count", V2.OptimismController, :output_roots_count) get("/deposits", V2.OptimismController, :deposits) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index d1a7d4df2987..87c29cc85772 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -338,7 +338,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do # includes basic batch information. # # ## Parameters - # - `internal_id`: The internal ID of the batch. + # - `number`: The internal ID of the batch. # - `l2_block_number_from`: Start L2 block number of the batch block range. # - `l2_block_number_to`: End L2 block number of the batch block range. # - `transactions_count`: The L2 transaction count included into the blocks of the range. @@ -363,9 +363,9 @@ defmodule BlockScoutWeb.API.V2.OptimismView do :l1_transaction_hashes => list(), :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil } - defp render_base_info_for_batch(internal_id, l2_block_number_from, l2_block_number_to, transactions_count, batch) do + defp render_base_info_for_batch(number, l2_block_number_from, l2_block_number_to, transactions_count, batch) do FrameSequence.prepare_base_info_for_batch( - internal_id, + number, l2_block_number_from, l2_block_number_to, transactions_count, diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex index 7b5cb1a54f7c..fda6f9dbf1e2 100644 --- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex +++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex @@ -78,7 +78,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do frame_sequence_id = select_repo(options).one(query) if not is_nil(frame_sequence_id) do - batch_by_internal_id(frame_sequence_id, options) + batch_by_number(frame_sequence_id, options) end end @@ -87,7 +87,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do op_frame_sequence_blobs DB tables by the internal id of the batch. ## Parameters - - `internal_id`: Batch'es internal id. + - `number`: Batch'es number. - `options`: A keyword list of options that may include whether to use a replica database and/or whether to include blobs (true by default). @@ -95,30 +95,30 @@ defmodule Explorer.Chain.Optimism.FrameSequence do - A map with info about L1 batch having the specified id. - nil if the batch is not found. """ - @spec batch_by_internal_id(non_neg_integer(), list()) :: map() | nil - def batch_by_internal_id(internal_id, options \\ []) do + @spec batch_by_number(non_neg_integer(), list()) :: map() | nil + def batch_by_number(number, options \\ []) do query = from(fs in __MODULE__, - where: fs.id == ^internal_id and fs.view_ready == true + where: fs.id == ^number and fs.view_ready == true ) batch = select_repo(options).one(query) if not is_nil(batch) do - l2_block_number_from = TransactionBatch.edge_l2_block_number(internal_id, :min) - l2_block_number_to = TransactionBatch.edge_l2_block_number(internal_id, :max) + l2_block_number_from = TransactionBatch.edge_l2_block_number(number, :min) + l2_block_number_to = TransactionBatch.edge_l2_block_number(number, :max) transactions_count = Transaction.transaction_count_for_block_range(l2_block_number_from..l2_block_number_to) {batch_data_container, blobs} = if Keyword.get(options, :include_blobs?, true) do - FrameSequenceBlob.list(internal_id, options) + FrameSequenceBlob.list(number, options) else {nil, []} end result = prepare_base_info_for_batch( - internal_id, + number, l2_block_number_from, l2_block_number_to, transactions_count, @@ -141,7 +141,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do includes basic batch information. ## Parameters - - `internal_id`: The internal ID of the batch. + - `number`: Number of the batch. - `l2_block_number_from`: Start L2 block number of the batch block range. - `l2_block_number_to`: End L2 block number of the batch block range. - `transactions_count`: The L2 transaction count included into the blocks of the range. @@ -171,7 +171,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil } def prepare_base_info_for_batch( - internal_id, + number, l2_block_number_from, l2_block_number_to, transactions_count, @@ -179,7 +179,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do batch ) do %{ - :number => internal_id, + :number => number, :l1_timestamp => batch.l1_timestamp, :l2_start_block_number => l2_block_number_from, :l2_end_block_number => l2_block_number_to, diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index b98bbafd0ffe..86196f5a5d2b 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -310,8 +310,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do Publisher.broadcast( %{ - new_optimism_batches: - Enum.map(sequences, &FrameSequence.batch_by_internal_id(&1.id, include_blobs?: false)) + new_optimism_batches: Enum.map(sequences, &FrameSequence.batch_by_number(&1.id, include_blobs?: false)) }, :realtime ) From 428c740ae1c09db153c658743b28617df1d16663 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 10 Sep 2025 18:05:41 +0400 Subject: [PATCH 0317/1006] fix: Delete PTOs for forked transactions (#13145) --- .../explorer/chain/import/runner/blocks.ex | 4 +++ .../chain/import/runner/transactions.ex | 6 +++-- .../chain/pending_operations_helper.ex | 27 +++++++++++++++++-- .../switch_pending_operations_test.exs | 4 ++- 4 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index b4a6f30f6b21..e107a2453507 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -291,6 +291,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do {_num, transactions} = repo.update_all(update_query, [], timeout: timeout) + transactions + |> Enum.map(& &1.hash) + |> PendingOperationsHelper.delete_related_transaction_operations() + {:ok, transactions} rescue postgrex_error in Postgrex.Error -> diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 2959f9d55eca..7e9f4d2c34f3 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -699,13 +699,15 @@ defmodule Explorer.Chain.Import.Runner.Transactions do timeout: timeout ) - {_, _transactions_result} = + {_, transaction_hashes} = repo.update_all( - from(t in Transaction, join: s in subquery(transactions_query), on: t.hash == s.hash), + from(t in Transaction, join: s in subquery(transactions_query), on: t.hash == s.hash, select: t.hash), [set: transactions_replacements], timeout: timeout ) + PendingOperationsHelper.delete_related_transaction_operations(transaction_hashes) + MissingRangesManipulator.add_ranges_by_block_numbers(result) {:ok, result} diff --git a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex index eb64abc5fc1f..915449947729 100644 --- a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex +++ b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex @@ -23,6 +23,28 @@ defmodule Explorer.Chain.PendingOperationsHelper do end end + @doc """ + Deletes all entities from `PendingTransactionOperation` related to provided `transaction_hashes`. + """ + @spec delete_related_transaction_operations([Hash.Full.t()]) :: {non_neg_integer(), nil} + def delete_related_transaction_operations(transaction_hashes) do + pending_operations_query = + from( + pto in PendingTransactionOperation, + where: pto.transaction_hash in ^transaction_hashes, + order_by: [asc: :transaction_hash], + lock: "FOR UPDATE" + ) + + Repo.delete_all( + from( + pto in PendingTransactionOperation, + join: s in subquery(pending_operations_query), + on: pto.transaction_hash == s.transaction_hash + ) + ) + end + def actual_entity do case pending_operations_type() do "blocks" -> PendingBlockOperation @@ -68,7 +90,8 @@ defmodule Explorer.Chain.PendingOperationsHelper do :finish pbo_params -> - Repo.insert_all(PendingBlockOperation, Helper.add_timestamps(pbo_params), on_conflict: :nothing) + filtered_pbo_params = Enum.reject(pbo_params, &is_nil(&1.block_hash)) + Repo.insert_all(PendingBlockOperation, Helper.add_timestamps(filtered_pbo_params), on_conflict: :nothing) block_numbers_to_delete = Enum.map(pbo_params, & &1.block_number) @@ -76,7 +99,7 @@ defmodule Explorer.Chain.PendingOperationsHelper do from( pto in PendingTransactionOperation, join: t in assoc(pto, :transaction), - where: t.block_number in ^block_numbers_to_delete + where: is_nil(t.block_number) or t.block_number in ^block_numbers_to_delete ) Repo.delete_all(delete_query) diff --git a/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs b/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs index 2123e1a1ad42..64e5c23091f0 100644 --- a/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs +++ b/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs @@ -64,7 +64,9 @@ defmodule Explorer.Migrator.SwitchPendingOperationsTest do |> insert_list(:transaction) |> with_block(second_block) - Enum.each(transactions_1 ++ transactions_2, fn %{hash: transaction_hash} -> + pending_transactions = insert_list(4, :transaction) + + Enum.each(transactions_1 ++ transactions_2 ++ pending_transactions, fn %{hash: transaction_hash} -> insert(:pending_transaction_operation, transaction_hash: transaction_hash) end) From bde6f2f61d09760b37db2d3154f61862c54d7a5a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 10 Sep 2025 17:15:40 +0300 Subject: [PATCH 0318/1006] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21cc9bd82f6d..c70badddcdf1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ ### 🐛 Bug Fixes +- Fix batch's number processing from the socket event ([#13181](https://github.com/blockscout/blockscout/pull/13181)) +- Delete PTOs for forked transactions ([#13145](https://github.com/blockscout/blockscout/pull/13145)) - Pagination and filtering issues in `/addresses/:hash/nft` ([#13175](https://github.com/blockscout/blockscout/pull/13175)) - Fix reputation preload for ERC-404 collections ([#13174](https://github.com/blockscout/blockscout/pull/13174)) - Add reputation to token, rework reputation preload ([#13149](https://github.com/blockscout/blockscout/pull/13149)) From 7786d0e464a6ef4092184fe05a5a381c2695887d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Sep 2025 12:34:33 +0300 Subject: [PATCH 0319/1006] perf: Fix perf of finding non pending block in internal transactions related queries (#13189) * fix: Fix perf of finding non pending block in internal transactions related queries * Add CHANGELOG record --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain/internal_transaction.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c70badddcdf1..869bda68b9b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ ### ⚡ Performance - Optimize maybe_hide_scam_addresses/3 query ([#12927](https://github.com/blockscout/blockscout/issues/12927)) +- Fix perf of finding non pending block in internal transactions related queries ([#13189](https://github.com/blockscout/blockscout/pull/13189)) ### ⚙️ Miscellaneous Tasks diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index def767fe8bae..0e612b13d4cf 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -583,7 +583,7 @@ defmodule Explorer.Chain.InternalTransaction do (query || __MODULE__) |> where( [it], - fragment("(SELECT block_hash FROM pending_block_operations WHERE block_hash = ? LIMIT 1) IS NULL", it.block_hash) + fragment("NOT EXISTS (SELECT 1 FROM pending_block_operations WHERE block_hash = ?)", it.block_hash) ) end From 35f2bbed2529c90cdd9ca313b0bf356f8e53c05e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Sep 2025 15:41:25 +0300 Subject: [PATCH 0320/1006] chore: Remove quantile_estimator dep (#13190) --- CHANGELOG.md | 1 + mix.lock | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 869bda68b9b4..8dbc720baa08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ ### ⚙️ Miscellaneous Tasks +- Remove quantile_estimator dep ([#13190](https://github.com/blockscout/blockscout/pull/13190)) - Change release workflow ([#13087](https://github.com/blockscout/blockscout/issues/13087)) - Update and format pull request template ([#13028](https://github.com/blockscout/blockscout/issues/13028)) - Add final check for ReindexDuplicatedInternalTransactions ([#13091](https://github.com/blockscout/blockscout/issues/13091)) diff --git a/mix.lock b/mix.lock index ef6a4b68584f..1bf6939617ee 100644 --- a/mix.lock +++ b/mix.lock @@ -147,7 +147,6 @@ "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm", "0273a6483ccb936d79ca19b0ab629aef0dba958697c94782bb728b920dfc6a79"}, "prometheus_process_collector": {:git, "https://github.com/Phybbit/prometheus_process_collector.git", "3dc94dcff422d7b9cbd7ddf6bf2a896446705f3f", [ref: "3dc94dcff422d7b9cbd7ddf6bf2a896446705f3f"]}, "qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm", "a266b7fb7be0d3b713912055dde3575927eca920e5d604ded45cd534f6b7a447"}, - "quantile_estimator": {:hex, :quantile_estimator, "0.2.1", "ef50a361f11b5f26b5f16d0696e46a9e4661756492c981f7b2229ef42ff1cd15", [:rebar3], [], "hexpm", "282a8a323ca2a845c9e6f787d166348f776c1d4a41ede63046d72d422e3da946"}, "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, From a3c5ef1459de14160345eccdb274d75f62c83918 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Sep 2025 15:52:02 +0300 Subject: [PATCH 0321/1006] perf: Internal transactions REST API endpoint perf tradeoff (#13191) * perf: internal transactions REST API endpoint perf tradeoff * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../explorer/chain/internal_transaction.ex | 5 +- .../chain/internal_transaction_test.exs | 71 ++++++++++--------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dbc720baa08..01407fc7e113 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,7 @@ - Optimize maybe_hide_scam_addresses/3 query ([#12927](https://github.com/blockscout/blockscout/issues/12927)) - Fix perf of finding non pending block in internal transactions related queries ([#13189](https://github.com/blockscout/blockscout/pull/13189)) +- Internal transactions REST API endpoint perf tradeoff ([#13191](https://github.com/blockscout/blockscout/pull/13191)) ### ⚙️ Miscellaneous Tasks diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 0e612b13d4cf..fcac0709fd7e 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -997,7 +997,10 @@ defmodule Explorer.Chain.InternalTransaction do if DenormalizationHelper.transactions_denormalization_finished?() do query |> join(:inner, [internal_transaction], transaction in assoc(internal_transaction, :transaction)) - |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) + # todo: this additional check causes performance issues at /api/v2/internal-transactions endpoint + # In the future we plan to extract reorg data into separate tables, so the main tables will no longer contain + # reorged data, and this check will no longer be necessary.. + # |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) |> where([_internal_transaction, transaction], transaction.block_consensus == true) else query diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index b35407ad8de5..a6914eb064a9 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1138,39 +1138,42 @@ defmodule Explorer.Chain.InternalTransactionTest do assert Enum.member?(result, {second_transaction_hash, second_index}) end - test "with consensus transactions and blocks only" do - BackgroundMigrations.set_transactions_denormalization_finished(true) - block_non_consensus = insert(:block, number: 2000, consensus: false) - block_consensus = insert(:block, number: 3000) - - transaction = - :transaction - |> insert() - |> with_block(block_consensus) - - insert(:internal_transaction, - index: 1, - transaction: transaction, - block_number: transaction.block_number, - block_hash: block_non_consensus.hash, - block_index: 1, - transaction_index: transaction.index - ) - - consensus_it = - insert(:internal_transaction, - index: 2, - transaction: transaction, - block_number: transaction.block_number, - block_hash: block_consensus.hash, - block_index: 2, - transaction_index: transaction.index - ) - - assert [{consensus_it.transaction_hash, consensus_it.index, consensus_it.block_hash}] == - [] - |> InternalTransaction.fetch() - |> Enum.map(&{&1.transaction_hash, &1.index, &1.block_hash}) - end + # todo: This test is temporarily disabled because this check is removed for the sake of performance: + # |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) + # Return the test back when reorg data will be moved out from the main tables. + # test "with consensus transactions and blocks only" do + # BackgroundMigrations.set_transactions_denormalization_finished(true) + # block_non_consensus = insert(:block, number: 2000, consensus: false) + # block_consensus = insert(:block, number: 3000) + + # transaction = + # :transaction + # |> insert() + # |> with_block(block_consensus) + + # insert(:internal_transaction, + # index: 1, + # transaction: transaction, + # block_number: transaction.block_number, + # block_hash: block_non_consensus.hash, + # block_index: 1, + # transaction_index: transaction.index + # ) + + # consensus_it = + # insert(:internal_transaction, + # index: 2, + # transaction: transaction, + # block_number: transaction.block_number, + # block_hash: block_consensus.hash, + # block_index: 2, + # transaction_index: transaction.index + # ) + + # assert [{consensus_it.transaction_hash, consensus_it.index, consensus_it.block_hash}] == + # [] + # |> InternalTransaction.fetch() + # |> Enum.map(&{&1.transaction_hash, &1.index, &1.block_hash}) + # end end end From 61db50a61c82fc406d4a069860edcdf12e9e1298 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 11 Sep 2025 16:01:30 +0300 Subject: [PATCH 0322/1006] fix: adjustments in address nft and collections endpoints (#13192) * feat: add token types ordering * feat: apply same paging approach to nft collections * chore: update CHANGELOG.md --- CHANGELOG.md | 1 + .../api/v2/address_controller_test.exs | 48 +++--- apps/explorer/lib/explorer/chain/token.ex | 6 +- .../lib/explorer/chain/token/instance.ex | 146 ++++++++---------- 4 files changed, 96 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01407fc7e113..eb73ef2cf0e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Adjustments in address nft and collections endpoints ([#13192](https://github.com/blockscout/blockscout/pull/13192)) - Fix batch's number processing from the socket event ([#13181](https://github.com/blockscout/blockscout/pull/13181)) - Delete PTOs for forked transactions ([#13145](https://github.com/blockscout/blockscout/pull/13145)) - Pagination and filtering issues in `/addresses/:hash/nft` ([#13175](https://github.com/blockscout/blockscout/pull/13175)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 4d632b00c1b2..8853ed85bb2d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -4871,15 +4871,15 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response_2nd_page["items"]) == 50 assert Enum.count(response_3rd_page["items"]) == 2 - compare_item(Enum.at(token_instances_1155, 50), Enum.at(response["items"], 0)) - compare_item(Enum.at(token_instances_1155, 1), Enum.at(response["items"], 49)) + compare_item(Enum.at(token_instances_721, 50), Enum.at(response["items"], 0)) + compare_item(Enum.at(token_instances_721, 1), Enum.at(response["items"], 49)) - compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_2nd_page["items"], 0)) - compare_item(Enum.at(token_instances_721, 50), Enum.at(response_2nd_page["items"], 1)) - compare_item(Enum.at(token_instances_721, 2), Enum.at(response_2nd_page["items"], 49)) + compare_item(Enum.at(token_instances_721, 0), Enum.at(response_2nd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 50), Enum.at(response_2nd_page["items"], 1)) + compare_item(Enum.at(token_instances_1155, 2), Enum.at(response_2nd_page["items"], 49)) - compare_item(Enum.at(token_instances_721, 1), Enum.at(response_3rd_page["items"], 0)) - compare_item(Enum.at(token_instances_721, 0), Enum.at(response_3rd_page["items"], 1)) + compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) + compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) @@ -4892,8 +4892,18 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } do address = insert(:address) - # Insert 30 ERC-1155 (with balances) + # Insert 30 ERC-721 (owned directly) for _ <- 1..30 do + erc_721_token = insert(:token, type: "ERC-721") + + insert(:token_instance, + owner_address_hash: address.hash, + token_contract_address_hash: erc_721_token.contract_address_hash + ) + end + + # Insert 25 ERC-1155 (with balances) + for _ <- 1..25 do token = insert(:token, type: "ERC-1155") ti = @@ -4910,8 +4920,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do ) end - # Insert 25 ERC-404 (with balances) - for _ <- 1..25 do + # Insert 10 ERC-404 (with balances) + for _ <- 1..10 do token = insert(:token, type: "ERC-404") ti = @@ -4928,16 +4938,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do ) end - # Insert 10 ERC-721 (owned directly) - for _ <- 1..10 do - erc_721_token = insert(:token, type: "ERC-721") - - insert(:token_instance, - owner_address_hash: address.hash, - token_contract_address_hash: erc_721_token.contract_address_hash - ) - end - # Page 1 page1_resp = conn |> get(endpoint.(address.hash)) |> json_response(200) assert Enum.count(page1_resp["items"]) == 50 @@ -4945,8 +4945,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do # Expect mixture of ERC-721 and ERC-1155 only on first page (by current logic order) assert %{ - "ERC-1155" => 30, - "ERC-404" => 20 + "ERC-721" => 30, + "ERC-1155" => 20 } = Enum.frequencies_by(page1_resp["items"], & &1["token_type"]) page2_resp = conn |> get(endpoint.(address.hash), page1_resp["next_page_params"]) |> json_response(200) @@ -4955,8 +4955,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert page2_resp["next_page_params"] == nil assert %{ - "ERC-404" => 5, - "ERC-721" => 10 + "ERC-1155" => 5, + "ERC-404" => 10 } = Enum.frequencies_by(page2_resp["items"], & &1["token_type"]) end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 70659e29d8f5..bae1bbd0296d 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -144,14 +144,14 @@ defmodule Explorer.Chain.Token do @optional_attrs ~w(cataloged decimals name symbol total_supply skip_metadata total_supply_updated_at_block metadata_updated_at updated_at fiat_value circulating_market_cap icon_url is_verified_via_admin_panel volume_24h)a @doc """ - Returns the list of allowed NFT type labels. + Returns the **ordered** list of allowed NFT type labels. """ @spec allowed_nft_type_labels() :: [String.t()] def allowed_nft_type_labels, do: [ + "ERC-721", "ERC-1155", - "ERC-404", - "ERC-721" + "ERC-404" ] @doc false diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 3ba7cb4cb39b..ebfcd893947f 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -136,30 +136,21 @@ defmodule Explorer.Chain.Token.Instance do do: from(i in __MODULE__, where: i.token_contract_address_hash == ^token_contract_address and i.token_id == ^token_id) - @doc """ - Paginated NFT instances owned by an address. - - ## Notes - * Filter: `:token_type` list (empty or omitted = all). - * Pagination key: `{token_contract_address_hash, token_id, token_type}`. - * Resumes at the key's type; earlier types skipped. - * Fills one page sequentially across types; spillover allowed. - - ## Params - * `address_hash` - owner address (binary or `Hash.Address`). - * `options` (keyword): - - `:paging_options` (%PagingOptions{}; default `Chain.default_paging_options/0`). - - `:token_type` (list of labels) filter. - - `:necessity_by_association` pass-through for joins. - - ## Returns - * list (<= page_size) of `%Explorer.Chain.Token.Instance{}`; empty list if none. - """ - @spec nft_list(binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] - def nft_list(address_hash, options) when is_list(options) do + @spec page_nft_list( + binary() | Hash.Address.t(), + keyword(), + (PagingOptions.t() -> String.t() | nil), + %{ + String.t() => (binary() + | Hash.Address.t(), + keyword() -> + [any()]) + } + ) :: [any()] + defp page_nft_list(address_hash, options, get_type, type_to_fetch_func) when is_list(options) do paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) page_size = paging_options.page_size - remaining_types = remaining_token_types(options) + remaining_types = remaining_token_types(options, get_type) {acc, _filled} = Enum.reduce_while(remaining_types, {[], 0}, fn type, {list, count} -> @@ -172,7 +163,8 @@ defmodule Explorer.Chain.Token.Instance do }) end - fetched = fetch_by_type_and_address_hash(type, address_hash, options) + fetch = Map.get(type_to_fetch_func, type, fn _, _ -> [] end) + fetched = fetch.(address_hash, options) new_list = list ++ fetched new_count = count + length(fetched) @@ -186,10 +178,8 @@ defmodule Explorer.Chain.Token.Instance do acc end - # Drop all types until start type (so we continue where we left off) - # Accumulate results sequentially per type until we fill the page - @spec remaining_token_types(keyword()) :: [String.t()] - defp remaining_token_types(options) do + @spec remaining_token_types(keyword(), (PagingOptions.t() -> String.t() | nil)) :: [String.t()] + defp remaining_token_types(options, get_type) do token_types = options |> Keyword.get(:token_type, []) @@ -197,18 +187,15 @@ defmodule Explorer.Chain.Token.Instance do if Enum.empty?(types) do Token.allowed_nft_type_labels() else - types + Token.allowed_nft_type_labels() |> Enum.filter(&(&1 in types)) end end) - |> Enum.sort() options |> Keyword.get(:paging_options, Chain.default_paging_options()) + |> get_type.() |> case do - %PagingOptions{ - key: {_contract_address_hash, _token_id, type} - } - when is_binary(type) -> + type when is_binary(type) -> Enum.drop_while(token_types, &(&1 != type)) _ -> @@ -216,14 +203,40 @@ defmodule Explorer.Chain.Token.Instance do end end - @spec fetch_by_type_and_address_hash(binary(), binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] - defp fetch_by_type_and_address_hash(type, address_hash, options) do - case type do - "ERC-721" -> erc_721_token_instances_by_owner_address_hash(address_hash, options) - "ERC-1155" -> erc_1155_token_instances_by_address_hash(address_hash, options) - "ERC-404" -> erc_404_token_instances_by_address_hash(address_hash, options) - _ -> [] - end + @doc """ + Paginated NFT instances owned by an address. + + ## Notes + * Filter: `:token_type` list (empty or omitted = all). + * Pagination key: `{token_contract_address_hash, token_id, token_type}`. + * Resumes at the key's type; earlier types skipped. + * Fills one page sequentially across types; spillover allowed. + + ## Params + * `address_hash` - owner address (binary or `Hash.Address`). + * `options` (keyword): + - `:paging_options` (%PagingOptions{}; default `Chain.default_paging_options/0`). + - `:token_type` (list of labels) filter. + - `:necessity_by_association` pass-through for joins. + + ## Returns + * list (<= page_size) of `%Explorer.Chain.Token.Instance{}`; empty list if none. + """ + @spec nft_list(binary() | Hash.Address.t(), keyword()) :: [__MODULE__.t()] + def nft_list(address_hash, options) when is_list(options) do + page_nft_list( + address_hash, + options, + fn + %PagingOptions{key: {_, _, type}} -> type + _ -> nil + end, + %{ + "ERC-721" => &erc_721_token_instances_by_owner_address_hash/2, + "ERC-1155" => &erc_1155_token_instances_by_address_hash/2, + "ERC-404" => &erc_404_token_instances_by_address_hash/2 + } + ) end @doc """ @@ -358,44 +371,21 @@ defmodule Explorer.Chain.Token.Instance do @preloaded_nfts_limit 9 - @spec nft_collections(binary() | Hash.Address.t(), keyword) :: list - def nft_collections(address_hash, options \\ []) - + @spec nft_collections(binary() | Hash.Address.t(), keyword()) :: [CurrentTokenBalance.t()] def nft_collections(address_hash, options) when is_list(options) do - nft_collections(address_hash, Keyword.get(options, :token_type, []), options) - end - - defp nft_collections(address_hash, ["ERC-721"], options) do - erc_721_collections_by_address_hash(address_hash, options) - end - - defp nft_collections(address_hash, ["ERC-1155"], options) do - erc_1155_collections_by_address_hash(address_hash, options) - end - - defp nft_collections(address_hash, ["ERC-404"], options) do - erc_404_collections_by_address_hash(address_hash, options) - end - - defp nft_collections(address_hash, _, options) do - paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) - - case paging_options do - %PagingOptions{key: {_contract_address_hash, "ERC-1155"}} -> - erc_1155_collections_by_address_hash(address_hash, options) - - _ -> - erc_721 = erc_721_collections_by_address_hash(address_hash, options) - - if length(erc_721) == paging_options.page_size do - erc_721 - else - erc_1155 = erc_1155_collections_by_address_hash(address_hash, options) - erc_404 = erc_404_collections_by_address_hash(address_hash, options) - - (erc_721 ++ erc_1155 ++ erc_404) |> Enum.take(paging_options.page_size) - end - end + page_nft_list( + address_hash, + options, + fn + %PagingOptions{key: {_, type}} -> type + _ -> nil + end, + %{ + "ERC-721" => &erc_721_collections_by_address_hash/2, + "ERC-1155" => &erc_1155_collections_by_address_hash/2, + "ERC-404" => &erc_404_collections_by_address_hash/2 + } + ) end @spec erc_721_collections_by_address_hash(binary() | Hash.Address.t(), keyword) :: [CurrentTokenBalance.t()] From 4de99c406a60a7ce309741ee5f0ed7b4b063a1b0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Sep 2025 18:35:14 +0300 Subject: [PATCH 0323/1006] Update dependabot config to prevent npm packages update --- .github/dependabot.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7ff4b3ed8af5..90b61deae2b8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,18 +6,18 @@ updates: schedule: interval: "weekly" - - package-ecosystem: "npm" - directory: "/apps/block_scout_web/assets" - open-pull-requests-limit: 10 - schedule: - interval: "monthly" - ignore: - - dependency-name: "bootstrap" - - dependency-name: "web3" - versions: ["4.x"] + # - package-ecosystem: "npm" + # directory: "/apps/block_scout_web/assets" + # open-pull-requests-limit: 10 + # schedule: + # interval: "monthly" + # ignore: + # - dependency-name: "bootstrap" + # - dependency-name: "web3" + # versions: ["4.x"] - - package-ecosystem: "npm" - directory: "/apps/explorer" - open-pull-requests-limit: 10 - schedule: - interval: "monthly" + # - package-ecosystem: "npm" + # directory: "/apps/explorer" + # open-pull-requests-limit: 10 + # schedule: + # interval: "monthly" From 24243a60c60ca86920cf6b81bda3a2beb1bc12f0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 12 Sep 2025 11:33:03 +0300 Subject: [PATCH 0324/1006] Rollback version update in docker-compose --- bin/version_bump.sh | 4 ++-- docker-compose/docker-compose.yml | 4 ++-- docker-compose/no-services.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/version_bump.sh b/bin/version_bump.sh index 4ec967a3a9f6..1452ec47ce36 100755 --- a/bin/version_bump.sh +++ b/bin/version_bump.sh @@ -65,8 +65,8 @@ bump_version() { done sed -i '' "s/version: \"$current_version\"/version: \"$new_version\"/" "$CONFIG_FILE" - sed -i '' "s/RELEASE_VERSION: $current_version/RELEASE_VERSION: $new_version/" "$DOCKER_COMPOSE_FILE" - sed -i '' "s/RELEASE_VERSION: $current_version/RELEASE_VERSION: $new_version/" "$DOCKER_COMPOSE_NO_SERVICES_FILE" + # sed -i '' "s/RELEASE_VERSION: $current_version/RELEASE_VERSION: $new_version/" "$DOCKER_COMPOSE_FILE" + # sed -i '' "s/RELEASE_VERSION: $current_version/RELEASE_VERSION: $new_version/" "$DOCKER_COMPOSE_NO_SERVICES_FILE" sed -i '' "s/RELEASE_VERSION ?= '$current_version'/RELEASE_VERSION ?= '$new_version'/" "$MAKE_FILE" # Replace the old version with the new version in the GitHub workflows files diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index cf363b963cba..0babc774018b 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -30,7 +30,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.0.2 links: - db:database environment: @@ -49,7 +49,7 @@ services: context: .. dockerfile: ./docker/Dockerfile args: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.0.2 visualizer: extends: diff --git a/docker-compose/no-services.yml b/docker-compose/no-services.yml index 11d5703479c0..99b1ce18a614 100644 --- a/docker-compose/no-services.yml +++ b/docker-compose/no-services.yml @@ -34,7 +34,7 @@ services: DISABLE_WEBAPP: "false" API_V1_WRITE_METHODS_DISABLED: "false" ADMIN_PANEL_ENABLED: "" - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.0.2 links: - db:database environment: From 4b9a5db6d6b39aac87382b80bbe5b5defa490cc6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 14 Sep 2025 14:49:50 +0300 Subject: [PATCH 0325/1006] fix: Mark completed deposits in batches (#13210) * fix: Mark completed deposits in batches * Add CHANGELOG entry --- CHANGELOG.md | 1 + .../lib/indexer/fetcher/beacon/deposit/status.ex | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb73ef2cf0e0..4512c9296ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Mark completed deposits in batches ([#13210](https://github.com/blockscout/blockscout/pull/13210)) - Adjustments in address nft and collections endpoints ([#13192](https://github.com/blockscout/blockscout/pull/13192)) - Fix batch's number processing from the socket event ([#13181](https://github.com/blockscout/blockscout/pull/13181)) - Delete PTOs for forked transactions ([#13145](https://github.com/blockscout/blockscout/pull/13145)) diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex index 6a780c913173..9863d3210651 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex @@ -72,14 +72,25 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do not (^QueryHelper.tuple_in([:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp], ids)) ) + batch_size = 100 + query = from( deposit in Deposit, where: deposit.status == :pending, - where: ^tuple_not_in + where: ^tuple_not_in, + select: deposit.id, + order_by: [asc: deposit.id] ) - Repo.update_all(query, set: [status: "completed", updated_at: DateTime.utc_now()]) + query + |> Repo.stream() + |> Stream.chunk_every(batch_size) + |> Enum.each(fn batch_ids -> + Deposit + |> where([deposit], deposit.id in ^batch_ids) + |> Repo.update_all(set: [status: "completed", updated_at: DateTime.utc_now()]) + end) end defp slot_to_timestamp(slot) do From 6c885d231f487247115111ce79a77808b9907853 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 14 Sep 2025 18:13:34 +0300 Subject: [PATCH 0326/1006] Fix deposit.id -> deposit.index --- apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex index 9863d3210651..d174a1fc12ae 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex @@ -79,8 +79,8 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do deposit in Deposit, where: deposit.status == :pending, where: ^tuple_not_in, - select: deposit.id, - order_by: [asc: deposit.id] + select: deposit.index, + order_by: [asc: deposit.index] ) query @@ -88,7 +88,7 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do |> Stream.chunk_every(batch_size) |> Enum.each(fn batch_ids -> Deposit - |> where([deposit], deposit.id in ^batch_ids) + |> where([deposit], deposit.index in ^batch_ids) |> Repo.update_all(set: [status: "completed", updated_at: DateTime.utc_now()]) end) end From 364d2a430a703cf36dd9539dc5045961469d1184 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 14 Sep 2025 18:58:47 +0300 Subject: [PATCH 0327/1006] Beacon deposits: avoid Repo.stream --- .../indexer/lib/indexer/fetcher/beacon/deposit/status.ex | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex index d174a1fc12ae..abac57bca015 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex @@ -83,9 +83,12 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do order_by: [asc: deposit.index] ) - query - |> Repo.stream() - |> Stream.chunk_every(batch_size) + all_batch_ids = + query + |> Repo.all(timeout: :infinity) + + all_batch_ids + |> Enum.chunk_every(batch_size) |> Enum.each(fn batch_ids -> Deposit |> where([deposit], deposit.index in ^batch_ids) From 5030e453127f8df48c809dc9e3ca7adb69b07822 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 14 Sep 2025 19:44:45 +0300 Subject: [PATCH 0328/1006] Add beacon deposits management envs to CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4512c9296ef7..2c8753479fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,12 @@ | `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to multichain DB counters export queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1s`
Applications: Indexer | | `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_MAX_QUEUE_SIZE` | Maximum size of multichain DB counters export queue. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | | `INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT` | Limit of the init query for processing the counters export queue to the Multichain DB. Implemented in [#13007](https://github.com/blockscout/blockscout/pull/13007). | Version: v9.1.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_DISABLE_BEACON_DEPOSIT_FETCHER` | If `true`, the Beacon deposit fetcher won't be started. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `false`
Applications: Indexer | +| `INDEXER_BEACON_DEPOSIT_FETCHER_INTERVAL` | The interval indicating how often deposit events should be queried. [Time format](/setup/env-variables/backend-envs-chain-specific#time-format). Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `6s`
Applications: Indexer | +| `INDEXER_BEACON_DEPOSIT_FETCHER_BATCH_SIZE` | The batch size specifies how many events are retrieved in a single database query. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `1000`
Applications: Indexer | +| `INDEXER_DISABLE_BEACON_DEPOSIT_STATUS_FETCHER` | If `true`, the Beacon deposit status fetcher won't be started. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `false`
Applications: Indexer | +| `INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_EPOCH_DURATION` | Epoch duration in the Beacon chain in seconds. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `384`
Applications: Indexer | +| `INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_REFERENCE_TIMESTAMP` | Any past finalized Beacon Chain epoch UTC timestamp. Used as reference for status fetcher scheduling. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `1722024023`
Applications: Indexer | From ed644f42c48b006c8d7d4894ef54d25192235b5f Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:59:56 +0300 Subject: [PATCH 0329/1006] fix: Add reputation preload for celo gas_token (#13200) * fix: Add reputation preload for celo gas_token * Fix sanitize_duplicated_log_index_logs * Fix tests --- .../controllers/api/v2/address_controller.ex | 2 +- .../controllers/api/v2/block_controller.ex | 3 +- .../api/v2/main_page_controller.ex | 3 +- .../api/v2/transaction_controller.ex | 2 +- .../lib/block_scout_web/notifier.ex | 2 +- .../views/api/v2/stability_view.ex | 8 +- .../api/v2/search_controller_test.exs | 25 ++-- .../api/v2/verification_controller_test.exs | 126 +++++++++--------- apps/explorer/lib/explorer/chain/token.ex | 7 +- .../sanitize_duplicated_log_index_logs.ex | 2 + 10 files changed, 98 insertions(+), 82 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index ff0029e55951..d563b9ec4c18 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -57,7 +57,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do case @chain_type do :celo -> @chain_type_transaction_necessity_by_association %{ - :gas_token => :optional + [gas_token: reputation_association()] => :optional } _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 26dabe0d4cad..7adf7e610d59 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -23,6 +23,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] alias BlockScoutWeb.API.V2.{ Ethereum.DepositController, @@ -67,7 +68,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do :celo -> @chain_type_transaction_necessity_by_association %{ - :gas_token => :optional + [gas_token: reputation_association()] => :optional } @chain_type_block_necessity_by_association %{} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 74cf80675e82..0770404624fa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -10,11 +10,12 @@ defmodule BlockScoutWeb.API.V2.MainPageController do import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] case @chain_type do :celo -> @chain_type_transaction_necessity_by_association %{ - :gas_token => :optional + [gas_token: reputation_association()] => :optional } _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 1ce31c80298b..b9b51f1deccd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -66,7 +66,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do :celo -> @chain_type_transaction_necessity_by_association %{ - :gas_token => :optional + [gas_token: reputation_association()] => :optional } _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index c5906f87a059..6ab5522df342 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -66,7 +66,7 @@ defmodule BlockScoutWeb.Notifier do case @chain_type do :celo -> @chain_type_transaction_associations [ - :gas_token + gas_token: Reputation.reputation_association() ] _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex index 3bd0c2a6f1e7..14176e656a6b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex @@ -1,6 +1,6 @@ defmodule BlockScoutWeb.API.V2.StabilityView do alias BlockScoutWeb.API.V2.{Helper, TokenView} - alias Explorer.Chain.{Log, Token, Transaction} + alias Explorer.Chain.{Address.Reputation, Log, Token, Transaction} @api_true [api?: true] @transaction_fee_event_signature "0x99e7b0ba56da2819c37c047f0511fd2bf6c9b4e27b4a979a19d6da0f74be8155" @@ -122,7 +122,11 @@ defmodule BlockScoutWeb.API.V2.StabilityView do if Map.has_key?(tokens_acc, token_address_hash) do {tokens_acc[token_address_hash], tokens_acc} else - token = Token.get_by_contract_address_hash(token_address_hash, @api_true) + token = + Token.get_by_contract_address_hash(token_address_hash, + api?: true, + necessity_by_association: %{Reputation.reputation_association() => :optional} + ) {token, Map.put(tokens_acc, token_address_hash, token)} end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 40db80adb2da..907a696afdc6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias Explorer.Chain.{Address, Block} alias Explorer.Tags.AddressTag @@ -2435,18 +2436,20 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do request = get(conn, "/api/v2/search/quick?q=#{address_hash}") assert response = json_response(request, 200) + address_result = %{ + "address_hash" => address_hash, + "certified" => false, + "ens_info" => nil, + "is_smart_contract_verified" => false, + "name" => nil, + "priority" => 0, + "type" => "address", + "url" => "/address/#{address_hash}", + "reputation" => "ok" + } + correct_response = [ - %{ - "address_hash" => address_hash, - "certified" => false, - "ens_info" => nil, - "is_smart_contract_verified" => false, - "name" => nil, - "priority" => 0, - "type" => "address", - "url" => "/address/#{address_hash}", - "reputation" => "ok" - } + if(@chain_type == :filecoin, do: Map.put(address_result, "filecoin_robust_address", nil), else: address_result) | for( i <- 0..48, do: %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs index 2aaf53019614..f48e003ef6ab 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -41,83 +41,83 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do end end - describe "bytecode lookup on verification requests" do - # Set up Mox and start on-demand bytecode fetcher with mocked transport - setup :set_mox_global - setup :verify_on_exit! + if Application.compile_env(:explorer, :chain_type) !== :zksync do + describe "bytecode lookup on verification requests" do + # Set up Mox and start on-demand bytecode fetcher with mocked transport + setup :set_mox_global + setup :verify_on_exit! - setup %{json_rpc_named_arguments: json_rpc_named_arguments} do - mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) + setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox) - start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) - start_supervised!({ContractCodeOnDemand, [mocked_json_rpc_named_arguments, [name: ContractCodeOnDemand]]}) - start_supervised!({ContractCreatorOnDemand, name: ContractCreatorOnDemand}) + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!({ContractCodeOnDemand, [mocked_json_rpc_named_arguments, [name: ContractCodeOnDemand]]}) + start_supervised!({ContractCreatorOnDemand, name: ContractCreatorOnDemand}) - %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} - end + %{json_rpc_named_arguments: mocked_json_rpc_named_arguments} + end - test "proceeds when bytecode is fetched from RPC", %{conn: conn} do - address = insert(:address) - string_address_hash = to_string(address.hash) + test "proceeds when bytecode is fetched from RPC", %{conn: conn} do + address = insert(:address) + string_address_hash = to_string(address.hash) - contract_code = "0x6080" + contract_code = "0x6080" - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: contract_code}]} - end) + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: contract_code}]} + end) - params = %{"compiler_version" => "", "source_code" => ""} + params = %{"compiler_version" => "", "source_code" => ""} - request = - post( - conn, - "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", - params - ) + request = + post( + conn, + "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", + params + ) - assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) - end + assert %{"message" => "Smart-contract verification started"} = json_response(request, 200) + end - test "returns 404 when RPC returns empty code (EOA or not a contract)", %{conn: conn} do - address = insert(:address) - string_address_hash = to_string(address.hash) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: "0x"}]} - end) - - params = %{"compiler_version" => "", "source_code" => ""} + test "returns 404 when RPC returns empty code (EOA or not a contract)", %{conn: conn} do + address = insert(:address) + string_address_hash = to_string(address.hash) - request = - post( - conn, - "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", - params - ) + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: "0x"}]} + end) + + params = %{"compiler_version" => "", "source_code" => ""} - assert %{"message" => "Address is not a smart-contract"} = json_response(request, 404) + request = + post( + conn, + "/api/v2/smart-contracts/#{address.hash}/verification/via/flattened-code", + params + ) + + assert %{"message" => "Address is not a smart-contract"} = json_response(request, 404) + end end - end - if Application.compile_env(:explorer, :chain_type) !== :zksync do describe "/api/v2/smart-contracts/{address_hash}/verification/via/flattened-code" do test "get 200 for verified contract", %{conn: conn} do contract = insert(:smart_contract) diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index bae1bbd0296d..10e0f1fb1295 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -391,7 +391,12 @@ defmodule Explorer.Chain.Token do end def get_by_contract_address_hash(hash, options) do - Chain.select_repo(options).get_by(__MODULE__, contract_address_hash: hash) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + __MODULE__ + |> where([t], t.contract_address_hash == ^hash) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).one() end @doc """ diff --git a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex index 3e5c11fd7975..d85faf2fa9c2 100644 --- a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex +++ b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex @@ -125,6 +125,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do :token, :transaction, :token_instance, + :reputation, :__meta__ ]) end) @@ -160,6 +161,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do :is_unique, :owner, :token, + :reputation, :__meta__ ]) end) From 0303b7df5fcb104cea933507d1977dd630286b02 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:44:03 +0300 Subject: [PATCH 0330/1006] fix: Fix api/v2/addresses/{hash}/celo/election-rewards pagination (#13215) --- .../block_scout_web/controllers/api/v2/address_controller.ex | 5 +++-- .../schemas/api/v2/celo/election_reward/type.ex | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index d563b9ec4c18..6450c3bfa22c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -1308,14 +1308,15 @@ defmodule BlockScoutWeb.API.V2.AddressController do parameters: base_params() ++ [address_hash_param()] ++ - define_paging_params(["epoch_number", "amount", "associated_account_address_hash", "type"]), + define_paging_params(["items_count", "epoch_number", "amount", "associated_account_address_hash", "type"]), responses: [ ok: {"Celo election rewards for the specified address.", "application/json", paginated_response( items: Schemas.Celo.ElectionReward, next_page_params_example: %{ - "block_number" => 100, + "epoch_number" => 100, + "items_count" => 50, "amount" => "1000000000000000000", "associated_account_address_hash" => "0x1234567890123456789012345678901234567890", "type" => "validator" diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex index 7a4e6f59599e..eb9befd2e984 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward/type.ex @@ -4,5 +4,5 @@ defmodule BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type do alias Explorer.Chain.Celo.ElectionReward - OpenApiSpex.schema(%{type: :string, nullable: false, enum: ElectionReward.types()}) + OpenApiSpex.schema(%{type: :string, nullable: false, enum: ElectionReward.types(), title: "CeloElectionRewardType"}) end From 26dd21adc7157b7011391477bfda3705a44c62c8 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 15 Sep 2025 11:16:19 +0300 Subject: [PATCH 0331/1006] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8753479fc4..4524a9f8e483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ ### 🐛 Bug Fixes +- Fix api/v2/addresses/{hash}/celo/election-rewards pagination ([#13215](https://github.com/blockscout/blockscout/pull/13215)) +- Add reputation preload for celo gas_token ([#13200](https://github.com/blockscout/blockscout/pull/13200)) - Mark completed deposits in batches ([#13210](https://github.com/blockscout/blockscout/pull/13210)) - Adjustments in address nft and collections endpoints ([#13192](https://github.com/blockscout/blockscout/pull/13192)) - Fix batch's number processing from the socket event ([#13181](https://github.com/blockscout/blockscout/pull/13181)) From b19d630be1e8e7884ad48429774214340f1215a2 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 15 Sep 2025 12:29:09 +0300 Subject: [PATCH 0332/1006] fix: errors in celo epochs endpoints (#13201) * fix: error in `/api/v2/celo/epochs/:number` * fix: update election reward and epoch modules to use reputation association * fix: load reputation associations for reward tokens * fix: discrepancies in celo epochs schema * fix: apply suggestion by @nikitosing * fix: fix credo --------- Co-authored-by: Victor Baranov --- .../controllers/api/v2/address_controller.ex | 11 +++-- .../controllers/api/v2/celo_controller.ex | 2 +- .../block_scout_web/schemas/api/v2/general.ex | 2 +- .../explorer/chain/celo/election_reward.ex | 45 ++++++++++++++++--- .../explorer/lib/explorer/chain/celo/epoch.ex | 22 +++++++-- 5 files changed, 69 insertions(+), 13 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 6450c3bfa22c..937645732a4f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -1316,10 +1316,10 @@ defmodule BlockScoutWeb.API.V2.AddressController do items: Schemas.Celo.ElectionReward, next_page_params_example: %{ "epoch_number" => 100, - "items_count" => 50, "amount" => "1000000000000000000", "associated_account_address_hash" => "0x1234567890123456789012345678901234567890", - "type" => "validator" + "type" => "validator", + "items_count" => 50 }, title_prefix: "AddressCeloElectionRewards" )}, @@ -1393,7 +1393,12 @@ defmodule BlockScoutWeb.API.V2.AddressController do {amount, ""} <- Decimal.parse(amount_string), {:ok, associated_account_address_hash} <- Hash.Address.cast(associated_account_address_hash_string), - {:ok, type} <- CeloElectionReward.type_from_string(type_string) do + sanitized_type_string <- + type_string + |> String.trim() + |> String.downcase() + |> String.replace("-", "_"), + {:ok, type} <- CeloElectionReward.type_from_string(sanitized_type_string) do %{ PagingOptions.default_paging_options() | key: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index 1d99c7ab76b9..53c54d2e9179 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -79,7 +79,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do with {:ok, number} <- parse_epoch_number(number_string), {:ok, epoch} <- Epoch.from_number(number, options) do - aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, options) + aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, api?: true) conn |> render(:celo_epoch, %{ diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index bdfb206e6ab2..f386d04c5889 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -672,7 +672,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do }, "type" => %Parameter{ in: :query, - schema: CeloElectionRewardType, + schema: CeloElectionRewardType.schema(), required: false, description: "Type for paging", name: :type diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index 1afbc4fd5855..c5994ce9207e 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -30,10 +30,11 @@ defmodule Explorer.Chain.Celo.ElectionReward do import Explorer.PagingOptions, only: [default_paging_options: 0] import Ecto.Query, only: [from: 2, where: 3, group_by: 3, select: 3] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.{Chain, SortingHelper} - alias Explorer.Chain.{Address, Celo.Epoch, Hash, Token, Wei} + alias Explorer.Chain.{Address, Address.Reputation, Celo.Epoch, Hash, Token, Wei} @type type :: :voter | :validator | :group | :delegated_payment @types_enum ~w(voter validator group delegated_payment)a @@ -201,7 +202,7 @@ defmodule Explorer.Chain.Celo.ElectionReward do {type, %{total: total, count: count}} end) - reward_type_to_token = election_reward_tokens_by_type(options) + reward_type_to_token = election_reward_tokens_by_type() @types_enum |> Map.new(&{&1, %{total: Decimal.new(0), count: 0}}) @@ -229,14 +230,19 @@ defmodule Explorer.Chain.Celo.ElectionReward do # ...> } # iex> Explorer.Chain.Celo.ElectionReward.election_reward_token_addresses_by_type(epoch_number) # %{voter_reward: %Token{}, ...} - @spec election_reward_tokens_by_type(Keyword.t()) :: %{atom() => Token.t() | nil} - defp election_reward_tokens_by_type(options) do + @spec election_reward_tokens_by_type :: %{atom() => Token.t() | nil} + defp election_reward_tokens_by_type do reward_type_to_token_address_hash = reward_type_to_token_address_hash() tokens = reward_type_to_token_address_hash |> Map.values() - |> Token.get_by_contract_address_hashes(options) + |> Token.get_by_contract_address_hashes( + api?: true, + necessity_by_association: %{ + reputation_association() => :optional + } + ) reward_type_to_token_address_hash |> Map.new(fn {type, address_hash} -> @@ -329,6 +335,7 @@ defmodule Explorer.Chain.Celo.ElectionReward do |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() + |> with_loaded_token_reputations() end defp reward_type_to_token_address_hash do @@ -396,6 +403,34 @@ defmodule Explorer.Chain.Celo.ElectionReward do ) end + # Ensures each reward token has its reputation association loaded to avoid JSON encoding errors. + @spec with_loaded_token_reputations([__MODULE__.t()]) :: [__MODULE__.t()] + defp with_loaded_token_reputations(rewards) when is_list(rewards) do + tokens = + rewards + |> Enum.map(& &1.token) + |> Enum.reject(&is_nil/1) + + hash_to_reputation = + tokens + |> Enum.map(& &1.contract_address_hash) + |> Reputation.preload_reputation() + |> Map.new() + + address_hash_to_token = + Enum.reduce(tokens, %{}, fn token, acc -> + reputation = Map.get(hash_to_reputation, token.contract_address_hash) + Map.put(acc, token.contract_address_hash, Map.put(token, :reputation, reputation)) + end) + + Enum.map(rewards, fn r -> + case r.token do + nil -> r + %{contract_address_hash: h} -> %{r | token: Map.get(address_hash_to_token, h)} + end + end) + end + @doc """ Custom filter for `ElectionReward`, inspired by `Explorer.Chain.Block.Reader.General.where_block_number_in_period/3` diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex index d851c0f1c849..05bc2c718eb8 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -10,6 +10,8 @@ defmodule Explorer.Chain.Celo.Epoch do import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + alias Explorer.{Chain, Repo, SortingHelper} alias Explorer.Chain.{ @@ -267,9 +269,23 @@ defmodule Explorer.Chain.Celo.Epoch do where: tt.log_index in ^log_indexes and tt.block_hash == ^block_hash, select: {tt.log_index, tt}, preload: [ - :token, - [from_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]], - [to_address: [:scam_badge, :names, :smart_contract, ^proxy_implementations_association()]] + [token: ^reputation_association()], + [ + from_address: [ + :scam_badge, + :names, + :smart_contract, + ^proxy_implementations_association() + ] + ], + [ + to_address: [ + :scam_badge, + :names, + :smart_contract, + ^proxy_implementations_association() + ] + ] ] ) From bee4b74712f6713f7b33c93b6e53763cb3367bc6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 15 Sep 2025 12:29:55 +0300 Subject: [PATCH 0333/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4524a9f8e483..d5ef46cf225b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### 🐛 Bug Fixes +- Fix errors in celo epochs endpoints([#13201](https://github.com/blockscout/blockscout/pull/13201)) - Fix api/v2/addresses/{hash}/celo/election-rewards pagination ([#13215](https://github.com/blockscout/blockscout/pull/13215)) - Add reputation preload for celo gas_token ([#13200](https://github.com/blockscout/blockscout/pull/13200)) - Mark completed deposits in batches ([#13210](https://github.com/blockscout/blockscout/pull/13210)) From 1262ae72f431039f1cc6587cb37e4292ea10b555 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 15 Sep 2025 21:34:18 +0300 Subject: [PATCH 0334/1006] Add ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL, ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY to deprecated env vars --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ef46cf225b..62bfa5de8ac5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,8 @@ | `INDEXER_POLYGON_EDGE_L2_STATE_RECEIVER_CONTRACT` | The address of StateReceiver contract on L2 (child chain) used to fetch deposit executes. Required for deposit events indexing. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | | `INDEXER_POLYGON_EDGE_L2_DEPOSITS_START_BLOCK` | The number of start block on L2 (child chain) to index deposit executes. If the table of deposit executes is not empty, the process will fill gaps and then continue indexing from the last indexed message. If empty or not defined, the deposit executes are not indexed. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | | `INDEXER_POLYGON_EDGE_ETH_GET_LOGS_RANGE_SIZE` | Block range size for eth\_getLogs request in Polygon Edge indexer modules. Implemented in [#8180](https://github.com/blockscout/blockscout/pull/8180). | | v5.3.0+ | | v9.1.0 | +| `ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL` | Airtable URL for public tag requests functionality | | v5.0.0+ | | v9.1.0 | +| `ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY` | Airtable API key for public tag requests functionality | | v5.0.0+ | | v9.1.0 | ## 9.0.2 From a648d2911a6c998333f89b3b64d71899a38ff178 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Tue, 16 Sep 2025 11:25:43 +0400 Subject: [PATCH 0335/1006] feat: auto assert_schema in tests (#13029) * feat: auto assert_schema in tests * chore: remove unused import * chore: try additionalProperties=false * chore: use additionalProperties=false * fix: compile error * chore: refactor chain_type_fields * fix: celo api schema discrepancies (#13069) * chore: minor schema fixes * chore: fix test compilation * fix: filecoin tests * chore: apply suggestions * fix: reputation schema * fix: beacon deposits schema tests * fix: logs migration test * chore: remove remaining assert_schema * fix: TAC tests --------- Co-authored-by: Fedor Ivanov --- .../controllers/api/v2/address_controller.ex | 59 +- .../api/v2/ethereum/deposit_controller.ex | 3 +- .../block_scout_web/schemas/api/v2/address.ex | 43 +- .../schemas/api/v2/address/counters.ex | 3 +- .../schemas/api/v2/address/response.ex | 110 +- .../schemas/api/v2/address/tabs_counters.ex | 3 +- .../schemas/api/v2/address/token_balance.ex | 3 +- .../schemas/api/v2/beacon/deposit.ex | 3 +- .../block_scout_web/schemas/api/v2/block.ex | 101 +- .../schemas/api/v2/celo/election_reward.ex | 6 +- .../schemas/api/v2/coin_balance.ex | 3 +- .../schemas/api/v2/coin_balance_by_day.ex | 3 +- .../schemas/api/v2/error_responses.ex | 3 +- .../block_scout_web/schemas/api/v2/general.ex | 42 +- .../schemas/api/v2/internal_transaction.ex | 3 +- .../lib/block_scout_web/schemas/api/v2/log.ex | 7 +- .../schemas/api/v2/nft_collection.ex | 28 +- .../schemas/api/v2/proxy/metadata.ex | 3 +- .../schemas/api/v2/proxy/metadata_tag.ex | 3 +- .../schemas/api/v2/signed_authorization.ex | 10 +- .../block_scout_web/schemas/api/v2/token.ex | 20 +- .../schemas/api/v2/token_instance.ex | 4 +- .../schemas/api/v2/token_instance_in_list.ex | 34 +- .../schemas/api/v2/token_transfer.ex | 16 +- .../schemas/api/v2/transaction.ex | 234 +- .../schemas/api/v2/transaction/fee.ex | 3 +- .../schemas/api/v2/withdrawal.ex | 3 +- .../lib/block_scout_web/schemas/helper.ex | 28 + .../views/api/v2/address_view.ex | 6 +- .../api/v2/address_controller_test.exs | 328 +-- .../api/v2/block_controller_test.exs | 25 +- .../v2/ethereum/deposit_controller_test.exs | 10 +- .../api/v2/search_controller_test.exs | 1991 +++++++++-------- .../test/support/api_schema_assertions.ex | 113 + .../block_scout_web/test/support/conn_case.ex | 3 +- apps/explorer/lib/explorer/application.ex | 2 +- ...anitize_duplicated_log_index_logs_test.exs | 14 + apps/explorer/test/support/factory.ex | 2 +- 38 files changed, 1613 insertions(+), 1662 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/helper.ex create mode 100644 apps/block_scout_web/test/support/api_schema_assertions.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 937645732a4f..cbeeb69e4f4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -40,6 +40,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do WithdrawalView } + alias BlockScoutWeb.Schemas.Helper, as: SchemasHelper alias Explorer.{Chain, Market, PagingOptions} alias Explorer.Chain.{Address, Beacon.Deposit, Hash, InternalTransaction, Transaction} alias Explorer.Chain.Address.{CoinBalance, Counters} @@ -816,7 +817,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do days: %Schema{type: :integer, nullable: false}, items: %Schema{type: :array, items: Schemas.CoinBalanceByDay} }, - nullable: false + nullable: false, + additionalProperties: false }}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -1012,28 +1014,39 @@ defmodule BlockScoutWeb.API.V2.AddressController do responses: [ ok: {"List of native coin holders with their balances, with pagination.", "application/json", - %Schema{ - title: "AddressesList", - allOf: [ - paginated_response( - items: Schemas.Address, - next_page_params_example: %{ - "fetched_coin_balance" => "124355417998347240251800", - "hash" => "0x59708733fbbf64378d9293ec56b977c011a08fd2", - "items_count" => 50, - "transactions_count" => nil - }, - title_prefix: "AddressList" - ), - %Schema{ - properties: %{ - exchange_rate: Schemas.General.FloatStringNullable, - total_supply: Schemas.General.FloatStringNullable - }, - required: [:exchange_rate, :total_supply] - } - ] - }}, + SchemasHelper.extend_schema( + paginated_response( + items: + Schemas.Address.schema() + |> SchemasHelper.extend_schema( + title: "AddressWithCoinBalanceAndTransactionsCount", + properties: %{ + coin_balance: Schemas.General.IntegerStringNullable, + transactions_count: %Schema{ + anyOf: [ + Schemas.General.IntegerString, + # TODO: replace empty string with null? + Schemas.General.EmptyString + ], + nullable: true + } + }, + required: [:coin_balance, :transactions_count] + ), + next_page_params_example: %{ + "fetched_coin_balance" => "124355417998347240251800", + "hash" => "0x59708733fbbf64378d9293ec56b977c011a08fd2", + "items_count" => 50, + "transactions_count" => nil + }, + title_prefix: "AddressList" + ), + properties: %{ + exchange_rate: Schemas.General.FloatStringNullable, + total_supply: Schemas.General.FloatStringNullable + }, + required: [:exchange_rate, :total_supply] + )}, forbidden: ForbiddenResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex index 5d5824e60a24..f8d63ed2746b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex @@ -91,7 +91,8 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do properties: %{ deposits_count: %Schema{type: :integer, nullable: false} }, - required: [:deposits_count] + required: [:deposits_count], + additionalProperties: false }}, forbidden: ForbiddenResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index e17a77798a43..5a1e7e2b946a 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do @moduledoc false require OpenApiSpex + alias BlockScoutWeb.Schemas.Helper alias Ecto.Enum, as: EctoEnum alias Explorer.Chain.Address alias OpenApiSpex.Schema @@ -17,25 +18,30 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do case Application.get_env(:explorer, :chain_type) do :filecoin -> schema - |> put_in([:properties, :filecoin], %Schema{ - type: :object, + |> Helper.extend_schema( properties: %{ - id: %Schema{ - type: :string, - description: "Short f0 Filecoin address that may change during chain reorgs", - example: "f03248220", - nullable: true - }, - robust: @filecoin_robust_address_schema, - actor_type: %Schema{ - type: :string, - description: "Type of actor associated with the Filecoin address", - enum: EctoEnum.values(Address, :filecoin_actor_type), - nullable: true + filecoin: %Schema{ + type: :object, + properties: %{ + id: %Schema{ + type: :string, + description: "Short f0 Filecoin address that may change during chain reorgs", + example: "f03248220", + nullable: true + }, + robust: @filecoin_robust_address_schema, + actor_type: %Schema{ + type: :string, + description: "Type of actor associated with the Filecoin address", + enum: EctoEnum.values(Address, :filecoin_actor_type), + nullable: true + } + }, + required: [:id, :robust, :actor_type], + additionalProperties: false } - }, - required: [:id, :robust, :actor_type] - }) + } + ) _ -> schema @@ -114,7 +120,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do :private_tags, :watchlist_names, :public_tags - ] + ], + additionalProperties: false } |> ChainTypeCustomizations.chain_type_fields() ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex index 6a82016c3dec..5c795ef3ba66 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/counters.ex @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Counters do gas_usage_count: General.IntegerString, validations_count: General.IntegerString }, - required: [:transactions_count, :token_transfers_count, :gas_usage_count, :validations_count] + required: [:transactions_count, :token_transfers_count, :gas_usage_count, :validations_count], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index 8bbd8b707a8e..36815e342598 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations @moduledoc false require OpenApiSpex + alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema @filecoin_robust_address_schema %Schema{ @@ -16,7 +17,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations properties: %{ is_scilla_contract: %Schema{type: :boolean, nullable: false} }, - required: [:is_scilla_contract] + required: [:is_scilla_contract], + additionalProperties: false } @doc """ @@ -33,13 +35,17 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations case Application.get_env(:explorer, :chain_type) do :filecoin -> schema - |> put_in([:properties, :creator_filecoin_robust_address], @filecoin_robust_address_schema) - |> update_in([:required], &[:creator_filecoin_robust_address | &1]) + |> Helper.extend_schema( + properties: %{creator_filecoin_robust_address: @filecoin_robust_address_schema}, + required: [:creator_filecoin_robust_address] + ) :zilliqa -> schema - |> put_in([:properties, :zilliqa], @zilliqa_schema) - |> update_in([:required], &[:zilliqa | &1]) + |> Helper.extend_schema( + properties: %{zilliqa: @zilliqa_schema}, + required: [:zilliqa] + ) _ -> schema @@ -54,57 +60,51 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response do require OpenApiSpex alias BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations - alias BlockScoutWeb.Schemas.API.V2.{General, Token} + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema - OpenApiSpex.schema(%{ - title: "AddressResponse", - description: "Address response", - type: :object, - allOf: [ - BlockScoutWeb.Schemas.API.V2.Address, - struct( - Schema, - %{ - type: :object, - properties: %{ - creator_address_hash: General.AddressHashNullable, - creation_transaction_hash: General.FullHashNullable, - token: %Schema{allOf: [Token], nullable: true}, - coin_balance: General.IntegerStringNullable, - exchange_rate: General.FloatStringNullable, - block_number_balance_updated_at: %Schema{type: :integer, minimum: 0, nullable: true}, - has_validated_blocks: %Schema{type: :boolean, nullable: false}, - has_logs: %Schema{type: :boolean, nullable: false}, - has_tokens: %Schema{type: :boolean, nullable: false}, - has_token_transfers: %Schema{type: :boolean, nullable: false}, - watchlist_address_id: %Schema{type: :integer, nullable: true}, - has_beacon_chain_withdrawals: %Schema{type: :boolean, nullable: false}, - creation_status: %Schema{ - type: :string, - description: "Creation status of the contract", - enum: ["success", "failed", "selfdestructed"], - nullable: true - } - }, - required: [ - :creator_address_hash, - :creation_transaction_hash, - :token, - :coin_balance, - :exchange_rate, - :block_number_balance_updated_at, - :has_validated_blocks, - :has_logs, - :has_tokens, - :has_token_transfers, - :watchlist_address_id, - :has_beacon_chain_withdrawals, - :creation_status - ] + OpenApiSpex.schema( + Address.schema() + |> Helper.extend_schema( + title: "AddressResponse", + description: "Address response", + properties: %{ + creator_address_hash: General.AddressHashNullable, + creation_transaction_hash: General.FullHashNullable, + token: %Schema{allOf: [Token], nullable: true}, + coin_balance: General.IntegerStringNullable, + exchange_rate: General.FloatStringNullable, + block_number_balance_updated_at: %Schema{type: :integer, minimum: 0, nullable: true}, + has_validated_blocks: %Schema{type: :boolean, nullable: false}, + has_logs: %Schema{type: :boolean, nullable: false}, + has_tokens: %Schema{type: :boolean, nullable: false}, + has_token_transfers: %Schema{type: :boolean, nullable: false}, + watchlist_address_id: %Schema{type: :integer, nullable: true}, + has_beacon_chain_withdrawals: %Schema{type: :boolean, nullable: false}, + creation_status: %Schema{ + type: :string, + description: "Creation status of the contract", + enum: ["success", "failed", "selfdestructed"], + nullable: true } - |> ChainTypeCustomizations.chain_type_fields() - ) - ] - }) + }, + required: [ + :creator_address_hash, + :creation_transaction_hash, + :token, + :coin_balance, + :exchange_rate, + :block_number_balance_updated_at, + :has_validated_blocks, + :has_logs, + :has_tokens, + :has_token_transfers, + :watchlist_address_id, + :has_beacon_chain_withdrawals, + :creation_status + ] + ) + |> ChainTypeCustomizations.chain_type_fields() + ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex index e41d567a34c4..d265c4e6f831 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/tabs_counters.ex @@ -20,6 +20,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TabsCounters do validations_count: %Schema{type: :integer, nullable: false}, celo_election_rewards_count: %Schema{type: :integer, nullable: false}, beacon_deposits_count: %Schema{type: :integer, nullable: false} - } + }, + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex index 683487d9bc6a..8b2d15b32cd0 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex @@ -22,6 +22,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do nullable: true } }, - required: [:value, :token, :token_id, :token_instance, :reputation] + required: [:value, :token, :token_id, :token_instance, :reputation], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex index 8f0b61620fe2..a86acc70cbf3 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex @@ -35,6 +35,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit do :signature, :status, :from_address - ] + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex index 7d25f609ded8..4e9d170837ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do @moduledoc false alias BlockScoutWeb.API.V2.ZkSyncView alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema @zksync_schema %Schema{ @@ -30,7 +31,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do :prove_transaction_timestamp, :execute_transaction_hash, :execute_transaction_timestamp - ] + ], + additionalProperties: false } @arbitrum_schema %Schema{ @@ -73,7 +75,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do send_count: %Schema{type: :integer, nullable: true}, send_root: General.FullHashNullable }, - required: [:batch_number, :status, :commitment_transaction, :confirmation_transaction] + required: [:batch_number, :status, :commitment_transaction, :confirmation_transaction], + additionalProperties: false } @blob4844_schema %Schema{ @@ -84,7 +87,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do l1_transaction_hash: General.FullHashNullable, l1_timestamp: General.TimestampNullable }, - required: [:hash, :l1_transaction_hash, :l1_timestamp] + required: [:hash, :l1_transaction_hash, :l1_timestamp], + additionalProperties: false } @celestia_schema %Schema{ @@ -97,7 +101,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do l1_transaction_hash: General.FullHashNullable, l1_timestamp: General.TimestampNullable }, - required: [:height, :namespace, :commitment, :l1_transaction_hash, :l1_timestamp] + required: [:height, :namespace, :commitment, :l1_transaction_hash, :l1_timestamp], + additionalProperties: false } @optimism_schema %Schema{ @@ -110,7 +115,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do batch_data_container: %Schema{type: :string, nullable: false, enum: ["in_blob4844", "in_celestia", "in_calldata"]}, blobs: %Schema{type: :array, items: %Schema{anyOf: [@blob4844_schema, @celestia_schema]}, nullable: false} }, - required: [:number, :l1_timestamp, :l1_transaction_hashes, :batch_data_container] + required: [:number, :l1_timestamp, :l1_transaction_hashes, :batch_data_container], + additionalProperties: false } @celo_schema %Schema{ @@ -119,6 +125,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do properties: %{ is_epoch_block: %Schema{type: :boolean, nullable: false}, epoch_number: %Schema{type: :integer, nullable: false}, + l1_era_finalized_epoch_number: %Schema{type: :integer, nullable: true}, base_fee: %Schema{ type: :object, nullable: true, @@ -135,15 +142,18 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do amount: General.IntegerString, percentage: %Schema{type: :number, nullable: false} }, - required: [:address, :amount, :percentage] + required: [:address, :amount, :percentage], + additionalProperties: false }, nullable: false } }, - required: [:recipient, :amount, :token, :breakdown] + required: [:recipient, :amount, :token, :breakdown], + additionalProperties: false } }, - required: [:is_epoch_block, :epoch_number] + required: [:is_epoch_block, :epoch_number, :l1_era_finalized_epoch_number], + additionalProperties: false } @zilliqa_schema %Schema{ @@ -159,7 +169,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do signature: General.HexString, signers: %Schema{type: :array, items: %Schema{type: :integer, nullable: false}, nullable: false} }, - required: [:view, :signature, :signers] + required: [:view, :signature, :signers], + additionalProperties: false }, aggregate_quorum_certificate: %Schema{ type: :object, @@ -178,15 +189,18 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do proposed_by_validator_index: %Schema{type: :integer, nullable: false}, signers: %Schema{type: :array, items: %Schema{type: :integer, nullable: false}, nullable: false} }, - required: [:view, :signature, :proposed_by_validator_index, :signers] + required: [:view, :signature, :proposed_by_validator_index, :signers], + additionalProperties: false }, nullable: false } }, - required: [:view, :signature, :signers, :nested_quorum_certificates] + required: [:view, :signature, :signers, :nested_quorum_certificates], + additionalProperties: false } }, - required: [:view] + required: [:view], + additionalProperties: false } @doc """ @@ -203,45 +217,44 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do case Application.get_env(:explorer, :chain_type) do :rsk -> schema - |> put_in([:properties, :minimum_gas_price], General.IntegerString) - |> put_in([:properties, :bitcoin_merged_mining_header], General.HexString) - |> put_in([:properties, :bitcoin_merged_mining_coinbase_transaction], General.HexString) - |> put_in([:properties, :bitcoin_merged_mining_merkle_proof], General.HexString) - |> put_in([:properties, :hash_for_merged_mining], General.FullHash) + |> Helper.extend_schema( + properties: %{ + minimum_gas_price: General.IntegerString, + bitcoin_merged_mining_header: General.HexString, + bitcoin_merged_mining_coinbase_transaction: General.HexString, + bitcoin_merged_mining_merkle_proof: General.HexString, + hash_for_merged_mining: General.FullHash + } + ) :optimism -> - schema - |> put_in([:properties, :optimism], @optimism_schema) + schema |> Helper.extend_schema(properties: %{optimism: @optimism_schema}) :zksync -> - schema - |> put_in([:properties, :zksync], @zksync_schema) + schema |> Helper.extend_schema(properties: %{zksync: @zksync_schema}) :arbitrum -> - schema - |> put_in([:properties, :arbitrum], @arbitrum_schema) + schema |> Helper.extend_schema(properties: %{arbitrum: @arbitrum_schema}) :ethereum -> schema - |> put_in([:properties, :blob_transactions_count], %Schema{type: :integer, nullable: false}) - |> put_in([:properties, :blob_gas_used], General.IntegerStringNullable) - |> put_in([:properties, :excess_blob_gas], General.IntegerStringNullable) - |> put_in([:properties, :blob_gas_price], General.IntegerString) - |> put_in([:properties, :burnt_blob_fees], General.IntegerString) - |> put_in([:properties, :beacon_deposits_count], %Schema{type: :integer, nullable: true}) - |> update_in( - [:required], - &([:blob_transactions_count, :blob_gas_used, :excess_blob_gas, :beacon_deposits_count] ++ &1) + |> Helper.extend_schema( + properties: %{ + blob_transactions_count: %Schema{type: :integer, nullable: false}, + blob_gas_used: General.IntegerStringNullable, + excess_blob_gas: General.IntegerStringNullable, + blob_gas_price: General.IntegerString, + burnt_blob_fees: General.IntegerString, + beacon_deposits_count: %Schema{type: :integer, nullable: true} + }, + required: [:blob_transactions_count, :blob_gas_used, :excess_blob_gas, :beacon_deposits_count] ) :celo -> - schema - |> put_in([:properties, :celo], @celo_schema) - |> update_in([:required], &[:celo | &1]) + schema |> Helper.extend_schema(properties: %{celo: @celo_schema}, required: [:celo]) :zilliqa -> - schema - |> put_in([:properties, :zilliqa], @zilliqa_schema) + schema |> Helper.extend_schema(properties: %{zilliqa: @zilliqa_schema}) _ -> schema @@ -280,7 +293,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do priority_fee: General.IntegerStringNullable, uncles_hashes: %Schema{ type: :array, - items: %Schema{type: :object, properties: %{hash: General.FullHash}, required: [:hash], nullable: false}, + items: %Schema{ + type: :object, + properties: %{hash: General.FullHash}, + required: [:hash], + nullable: false, + additionalProperties: false + }, nullable: false }, rewards: %Schema{ @@ -291,7 +310,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do type: %Schema{type: :string, nullable: false}, reward: General.IntegerString }, - required: [:type, :reward] + required: [:type, :reward], + additionalProperties: false }, nullable: false }, @@ -329,7 +349,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do :transaction_fees, :withdrawals_count, :is_pending_update - ] + ], + additionalProperties: false } |> ChainTypeCustomizations.chain_type_fields() ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex index c272bbd956fb..80bc4e1c8c63 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/celo/election_reward.ex @@ -23,7 +23,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward do required: [ :amount, :account, - :associated_account - ] + :associated_account, + :epoch_number + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex index bc7e297317a4..13e84f478b8e 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance.ex @@ -22,6 +22,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.CoinBalance do :delta, :value, :block_timestamp - ] + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex index 56b3885bcced..867fa471af90 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/coin_balance_by_day.ex @@ -13,6 +13,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.CoinBalanceByDay do date: %Schema{type: :string, format: :date, nullable: false}, value: General.IntegerString }, - required: [:date, :value] + required: [:date, :value], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex index d07b02ef6c08..919ff39aeaef 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex @@ -18,7 +18,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do description: "Error message indicating the user is not authorized to access the resource", example: "Restricted access" } - } + }, + additionalProperties: false }) @spec response() :: {String.t(), String.t(), module()} diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index f386d04c5889..528977c0b938 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -6,6 +6,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do alias BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type, as: CeloElectionRewardType alias BlockScoutWeb.Schemas.API.V2.Token.Type, as: TokenType + alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.{Parameter, Schema} defmodule AddressHash do @@ -68,15 +69,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do case Application.get_env(:explorer, :chain_type) do :filecoin -> schema - |> put_in( - [:properties, :filecoin_robust_address], - %Schema{ - type: :string, - example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", - nullable: true - } + |> Helper.extend_schema( + properties: %{ + filecoin_robust_address: %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + }, + required: [:filecoin_robust_address] ) - |> update_in([:required], &[:filecoin_robust_address | &1]) _ -> schema @@ -98,7 +100,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do address_hash: AddressHash, name: %Schema{type: :string, nullable: true} }, - required: [:address_hash, :name] + required: [:address_hash, :name], + additionalProperties: false } |> ChainTypeCustomizations.chain_type_fields() ) @@ -114,7 +117,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do display_name: %Schema{type: :string, nullable: false}, label: %Schema{type: :string, nullable: false} }, - required: [:address_hash, :display_name, :label] + required: [:address_hash, :display_name, :label], + additionalProperties: false }) end @@ -127,7 +131,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do display_name: %Schema{type: :string, nullable: false}, label: %Schema{type: :string, nullable: false} }, - required: [:display_name, :label] + required: [:display_name, :label], + additionalProperties: false }) end @@ -198,12 +203,14 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do nullable: false } }, - nullable: false + nullable: false, + additionalProperties: false } } }, required: [:method_id, :method_call, :parameters], - nullable: false + nullable: false, + additionalProperties: false }) end @@ -238,13 +245,15 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } }, required: [:name, :type, :indexed, :value], - nullable: false + nullable: false, + additionalProperties: false }, nullable: false } }, required: [:method_id, :method_call, :parameters], - nullable: false + nullable: false, + additionalProperties: false }) end @@ -479,7 +488,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } }, required: [:items, :next_page_params], - nullable: false + nullable: false, + additionalProperties: false } end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex index 776e54635337..78331fe33e1a 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex @@ -65,6 +65,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.InternalTransaction do :index, :gas_limit, :block_index - ] + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex index 72cffc212253..97fb02286e61 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex @@ -15,8 +15,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Log do topics: %Schema{type: :array, items: General.HexStringNullable, nullable: false}, data: General.HexString, index: %Schema{type: :integer, nullable: false}, - decoded: %Schema{allOf: [General.DecodedInput], nullable: true}, - smart_contract: %Schema{oneOf: [Address], nullable: true}, + decoded: %Schema{allOf: [General.DecodedLogInput], nullable: true}, + smart_contract: %Schema{allOf: [Address], nullable: true}, block_hash: General.FullHash, block_number: %Schema{type: :integer, nullable: false} }, @@ -30,6 +30,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Log do :smart_contract, :block_hash, :block_number - ] + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex index 6be7a4ca698e..2be2fd134b10 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex @@ -4,7 +4,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.NFTCollection do """ require OpenApiSpex - alias BlockScoutWeb.Schemas.API.V2.{General, Token, Token.Type, TokenInstance} + alias BlockScoutWeb.Schemas.API.V2.{General, Token, TokenInstanceInList} + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -14,24 +15,17 @@ defmodule BlockScoutWeb.Schemas.API.V2.NFTCollection do amount: General.IntegerStringNullable, token_instances: %Schema{ type: :array, - items: %Schema{ - allOf: [ - TokenInstance, - %Schema{ - type: :object, - nullable: false, - properties: %{ - token_type: Type, - value: General.IntegerStringNullable - }, - required: [:token_type, :value] - } - ], - nullable: true - }, + items: TokenInstanceInList, nullable: false + }, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the token collection", + nullable: true } }, - required: [:token, :amount, :token_instances] + required: [:token, :amount, :token_instances, :reputation], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex index 9d0503121aa3..f39e3c14e354 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata.ex @@ -13,6 +13,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Proxy.Metadata do properties: %{ tags: %Schema{description: "Metadata tags linked with the address", type: :array, items: MetadataTag} }, - required: [:tags] + required: [:tags], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex index 04072169cda8..5b54df3359b2 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/metadata_tag.ex @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Proxy.MetadataTag do ordinal: %Schema{type: :integer, nullable: false}, meta: %Schema{type: :object, nullable: false} }, - required: [:slug, :name, :tagType, :ordinal, :meta] + required: [:slug, :name, :tagType, :ordinal, :meta], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex index 41b587bd4837..2887be4d2f85 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/signed_authorization.ex @@ -16,8 +16,14 @@ defmodule BlockScoutWeb.Schemas.API.V2.SignedAuthorization do r: General.IntegerString, s: General.IntegerString, v: %Schema{type: :integer, nullable: false}, - authority: General.AddressHash + authority: General.AddressHash, + status: %Schema{ + type: :string, + enum: ["ok", "invalid_chain_id", "invalid_signature", "invalid_nonce"], + nullable: true + } }, - required: [:address_hash, :chain_id, :nonce, :r, :s, :v, :authority] + required: [:address_hash, :chain_id, :nonce, :r, :s, :v, :authority, :status], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index 7120c9c923d3..d744c42b1ed9 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -2,6 +2,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.ChainTypeCustomizations do @moduledoc false require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.Helper alias Explorer.Chain.BridgedToken alias OpenApiSpex.Schema @@ -15,8 +17,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.ChainTypeCustomizations do case Application.get_env(:explorer, :chain_type) do :filecoin -> schema - |> put_in([:properties, :filecoin_robust_address], @filecoin_robust_address_schema) - |> update_in([:required], &[:filecoin_robust_address | &1]) + |> Helper.extend_schema( + properties: %{filecoin_robust_address: @filecoin_robust_address_schema}, + required: [:filecoin_robust_address] + ) _ -> schema @@ -51,13 +55,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do type: :object, properties: %{ address_hash: General.AddressHash, - symbol: %Schema{type: :string, nullable: false}, - name: %Schema{type: :string, nullable: false}, + symbol: %Schema{type: :string, nullable: true}, + name: %Schema{type: :string, nullable: true}, decimals: General.IntegerStringNullable, - type: %Schema{ - anyOf: [Type], - nullable: true - }, + type: %Schema{allOf: [Type], nullable: true}, holders_count: General.IntegerStringNullable, exchange_rate: General.FloatStringNullable, volume_24h: General.FloatStringNullable, @@ -84,7 +85,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do :icon_url, :circulating_market_cap, :reputation - ] + ], + additionalProperties: false } |> ChainTypeCustomizations.chain_type_fields() |> ChainTypeCustomizations.maybe_append_bridged_info() diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex index 58214a8c2fe0..cd68c80a13ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex @@ -53,6 +53,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstance do :thumbnails, :media_type, :media_url - ] + ], + nullable: false, + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex index 9b0ef8c4e6ba..677f5a0cae76 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex @@ -5,21 +5,25 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstanceInList do require OpenApiSpex alias BlockScoutWeb.Schemas.API.V2.{General, Token.Type, TokenInstance} + alias BlockScoutWeb.Schemas.Helper + alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema - OpenApiSpex.schema(%{ - allOf: [ - TokenInstance, - %Schema{ - type: :object, - nullable: false, - properties: %{ - token_type: Type, - value: General.IntegerStringNullable - }, - required: [:token_type, :value] - } - ], - type: :object - }) + OpenApiSpex.schema( + TokenInstance.schema() + |> Helper.extend_schema( + title: "TokenInstanceInList", + properties: %{ + token_type: Type, + value: General.IntegerStringNullable, + reputation: %Schema{ + type: :string, + enum: Reputation.enum_values(), + description: "Reputation of the token instance", + nullable: true + } + }, + required: [:token_type, :value, :reputation] + ) + ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index aa2b80a3fb08..cb7fb177b1b7 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -77,7 +77,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do :log_index, :token_type, :reputation - ] + ], + additionalProperties: false }) end @@ -92,9 +93,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TotalERC721 do type: :object, properties: %{ token_id: General.IntegerStringNullable, - token_instance: %Schema{type: :object, anyOf: [TokenInstance], nullable: true} + token_instance: %Schema{allOf: [TokenInstance], nullable: true} }, - required: [:token_id, :token_instance] + required: [:token_id, :token_instance], + additionalProperties: false }) end @@ -111,9 +113,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TotalERC1155 do token_id: General.IntegerStringNullable, value: General.IntegerStringNullable, decimals: General.IntegerStringNullable, - token_instance: %Schema{type: :object, anyOf: [TokenInstance], nullable: true} + token_instance: %Schema{allOf: [TokenInstance], nullable: true} }, - required: [:token_id, :value, :decimals, :token_instance] + required: [:token_id, :value, :decimals, :token_instance], + additionalProperties: false }) end @@ -129,6 +132,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.Total do value: General.IntegerStringNullable, decimals: General.IntegerStringNullable }, - required: [:value, :decimals] + required: [:value, :decimals], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex index 017e742c2194..c017bda3eddf 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do alias BlockScoutWeb.API.V2.ZkSyncView alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} alias BlockScoutWeb.Schemas.API.V2.Transaction.Fee + alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema @zksync_schema %Schema{ @@ -31,7 +32,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do :prove_transaction_timestamp, :execute_transaction_hash, :execute_transaction_timestamp - ] + ], + additionalProperties: false } @arbitrum_commitment_transaction_schema %Schema{ @@ -42,7 +44,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do timestamp: General.TimestampNullable, status: %Schema{type: :string, enum: ["unfinalized", "finalized"], nullable: true} }, - required: [:hash, :timestamp, :status] + required: [:hash, :timestamp, :status], + additionalProperties: false } @arbitrum_schema %Schema{ @@ -76,14 +79,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do ], nullable: false } - } + }, + additionalProperties: false }, gas_used_for_l1: General.IntegerString, gas_used_for_l2: General.IntegerString, poster_fee: General.IntegerString, network_fee: General.IntegerString }, - required: [:gas_used_for_l1, :gas_used_for_l2, :poster_fee, :network_fee] + required: [:gas_used_for_l1, :gas_used_for_l2, :poster_fee, :network_fee], + additionalProperties: false } @optimism_withdrawal_schema %Schema{ @@ -94,7 +99,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do status: %Schema{type: :string, nullable: false}, l1_transaction_hash: General.FullHashNullable }, - required: [:nonce, :status, :l1_transaction_hash] + required: [:nonce, :status, :l1_transaction_hash], + additionalProperties: false } @scroll_schema %Schema{ @@ -127,7 +133,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do :l1_gas_used, :l2_fee, :l2_block_status - ] + ], + additionalProperties: false } @doc """ @@ -145,124 +152,154 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do case Application.get_env(:explorer, :chain_type) do :polygon_zkevm -> schema - |> put_in([:properties, :zkevm_batch_number], %Schema{type: :integer, nullable: true}) - |> put_in([:properties, :zkevm_sequence_hash], General.FullHash) - |> put_in([:properties, :zkevm_verify_hash], General.FullHash) - |> put_in([:properties, :zkevm_status], %Schema{ - type: :string, - enum: ["Confirmed by Sequencer", "L1 Confirmed"], - nullable: false - }) + |> Helper.extend_schema( + properties: %{ + zkevm_batch_number: %Schema{type: :integer, nullable: true}, + zkevm_sequence_hash: General.FullHash, + zkevm_verify_hash: General.FullHash, + zkevm_status: %Schema{ + type: :string, + enum: ["Confirmed by Sequencer", "L1 Confirmed"], + nullable: false + } + } + ) :zksync -> - schema - |> put_in([:properties, :zksync], @zksync_schema) + schema |> Helper.extend_schema(properties: %{zksync: @zksync_schema}) :arbitrum -> - schema - |> put_in([:properties, :arbitrum], @arbitrum_schema) + schema |> Helper.extend_schema(properties: %{arbitrum: @arbitrum_schema}) :optimism -> schema - |> put_in([:properties, :l1_fee], General.IntegerString) - |> put_in([:properties, :l1_fee_scalar], General.IntegerString) - |> put_in([:properties, :l1_gas_price], General.IntegerString) - |> put_in([:properties, :l1_gas_used], General.IntegerString) - |> put_in([:properties, :op_withdrawals], %Schema{ - type: :array, - items: @optimism_withdrawal_schema, - nullable: false - }) - |> put_in([:properties, :op_interop], %Schema{ - type: :object, - nullable: false, + |> Helper.extend_schema( properties: %{ - nonce: %Schema{type: :integer}, - status: %Schema{type: :string, nullable: false, enum: ["Sent", "Relayed", "Failed"]}, - sender_address_hash: General.AddressHashNullable, - target_address_hash: General.AddressHashNullable, - payload: General.HexString, - relay_chain: %Schema{type: :object, nullable: true}, - relay_transaction_hash: General.FullHashNullable, - init_chain: %Schema{type: :object, nullable: true}, - init_transaction_hash: General.FullHashNullable - }, - required: [:nonce, :status, :sender_address_hash, :target_address_hash, :payload] - }) + l1_fee: General.IntegerString, + l1_fee_scalar: General.IntegerString, + l1_gas_price: General.IntegerString, + l1_gas_used: General.IntegerString, + op_withdrawals: %Schema{ + type: :array, + items: @optimism_withdrawal_schema, + nullable: false + }, + op_interop: %Schema{ + type: :object, + nullable: false, + properties: %{ + nonce: %Schema{type: :integer}, + status: %Schema{type: :string, nullable: false, enum: ["Sent", "Relayed", "Failed"]}, + sender_address_hash: General.AddressHashNullable, + target_address_hash: General.AddressHashNullable, + payload: General.HexString, + relay_chain: %Schema{type: :object, nullable: true}, + relay_transaction_hash: General.FullHashNullable, + init_chain: %Schema{type: :object, nullable: true}, + init_transaction_hash: General.FullHashNullable + }, + required: [:nonce, :status, :sender_address_hash, :target_address_hash, :payload], + additionalProperties: false + } + } + ) :scroll -> - schema - |> put_in([:properties, :scroll], @scroll_schema) + schema |> Helper.extend_schema(properties: %{scroll: @scroll_schema}) :suave -> schema - |> put_in([:properties, :allowed_peekers], %Schema{type: :array, items: General.AddressHash, nullable: false}) - |> put_in([:properties, :execution_node], Address) - |> put_in([:properties, :wrapped], %Schema{ - type: :object, - nullable: false, + |> Helper.extend_schema( properties: %{ - type: %Schema{type: :integer}, - nonce: %Schema{type: :integer, nullable: true}, - to: Address, - gas_limit: General.IntegerStringNullable, - gas_price: General.IntegerStringNullable, - fee: Fee, - max_priority_fee_per_gas: General.IntegerStringNullable, - max_fee_per_gas: General.IntegerStringNullable, - value: General.IntegerStringNullable, - hash: General.FullHash, - method: General.MethodNameNullable, - decoded_input: %Schema{allOf: [General.DecodedInput], nullable: true}, - raw_input: General.HexString + allowed_peekers: %Schema{type: :array, items: General.AddressHash, nullable: false}, + execution_node: Address, + wrapped: %Schema{ + type: :object, + nullable: false, + properties: %{ + type: %Schema{type: :integer}, + nonce: %Schema{type: :integer, nullable: true}, + to: Address, + gas_limit: General.IntegerStringNullable, + gas_price: General.IntegerStringNullable, + fee: Fee, + max_priority_fee_per_gas: General.IntegerStringNullable, + max_fee_per_gas: General.IntegerStringNullable, + value: General.IntegerStringNullable, + hash: General.FullHash, + method: General.MethodNameNullable, + decoded_input: %Schema{allOf: [General.DecodedInput], nullable: true}, + raw_input: General.HexString + }, + additionalProperties: false + } } - }) + ) :stability -> schema - |> put_in([:properties, :stability_fee], %Schema{ - type: :object, - nullable: false, + |> Helper.extend_schema( properties: %{ - token: Token, - validator_address: Address, - dapp_address: Address, - total_fee: General.IntegerString, - dapp_fee: General.IntegerString, - validator_fee: General.IntegerString - }, - required: [:token, :validator_address, :dapp_address, :total_fee, :dapp_fee, :validator_fee] - }) + stability_fee: %Schema{ + type: :object, + nullable: false, + properties: %{ + token: Token, + validator_address: Address, + dapp_address: Address, + total_fee: General.IntegerString, + dapp_fee: General.IntegerString, + validator_fee: General.IntegerString + }, + required: [:token, :validator_address, :dapp_address, :total_fee, :dapp_fee, :validator_fee], + additionalProperties: false + } + } + ) :ethereum -> schema - |> put_in([:properties, :max_fee_per_blob_gas], General.IntegerString) - |> put_in([:properties, :blob_versioned_hashes], %Schema{type: :array, items: General.FullHash, nullable: false}) - |> put_in([:properties, :blob_gas_used], General.IntegerString) - |> put_in([:properties, :blob_gas_price], General.IntegerString) - |> put_in([:properties, :burnt_blob_fee], General.IntegerString) + |> Helper.extend_schema( + properties: %{ + max_fee_per_blob_gas: General.IntegerString, + blob_versioned_hashes: %Schema{type: :array, items: General.FullHash, nullable: false}, + blob_gas_used: General.IntegerString, + blob_gas_price: General.IntegerString, + burnt_blob_fee: General.IntegerString + } + ) :celo -> schema - |> put_in([:properties, :celo], %Schema{ - type: :object, - nullable: false, - properties: %{gas_token: %Schema{oneOf: [Token], nullable: true}}, - required: [:gas_token] - }) - |> update_in([:required], &[:celo | &1]) + |> Helper.extend_schema( + properties: %{ + celo: %Schema{ + type: :object, + nullable: false, + properties: %{gas_token: %Schema{allOf: [Token], nullable: true}}, + required: [:gas_token], + additionalProperties: false + } + }, + required: [:celo] + ) :zilliqa -> schema - |> put_in([:properties, :zilliqa], %Schema{ - type: :object, - nullable: false, + |> Helper.extend_schema( properties: %{ - is_scilla: %Schema{type: :boolean, nullable: false} + zilliqa: %Schema{ + type: :object, + nullable: false, + properties: %{ + is_scilla: %Schema{type: :boolean, nullable: false} + }, + required: [:is_scilla], + additionalProperties: false + } }, - required: [:is_scilla] - }) - |> update_in([:required], &[:zilliqa | &1]) + required: [:zilliqa] + ) _ -> schema @@ -360,7 +397,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction do type: :object, properties: %{raw: %Schema{anyOf: [General.HexString, %Schema{type: :string}], nullable: true}}, required: [:raw], - nullable: false + nullable: false, + additionalProperties: false } ], nullable: true @@ -390,7 +428,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction do description: "Transaction action details (json formatted)", nullable: false } - } + }, + additionalProperties: false }, nullable: true }, @@ -462,7 +501,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction do :has_error_in_internal_transactions, :authorization_list, :is_pending_update - ] + ], + additionalProperties: false } |> ChainTypeCustomizations.chain_type_fields() ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex index 299809cdebec..61b8b0f74f4d 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/fee.ex @@ -16,6 +16,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Fee do enum: ["maximum", "actual"] }, value: General.IntegerStringNullable - } + }, + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex index f2a1e81d9c29..2e9e4f4e0b6b 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal.ex @@ -20,6 +20,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Withdrawal do :index, :validator_index, :amount - ] + ], + additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex new file mode 100644 index 000000000000..6fc972895330 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex @@ -0,0 +1,28 @@ +defmodule BlockScoutWeb.Schemas.Helper do + @moduledoc false + + alias OpenApiSpex.Schema + + @doc """ + Extends a schema with additional properties and required fields. + """ + @spec extend_schema(Schema.t() | map(), Keyword.t()) :: Schema.t() | map() + def extend_schema(schema, options) do + updated_schema = + schema + |> Map.update!(:properties, &Map.merge(&1, Keyword.get(options, :properties, %{}))) + |> Map.update!(:required, &(&1 ++ Keyword.get(options, :required, []))) + + updated_schema_with_title = + if Keyword.has_key?(options, :title), + do: Map.put(updated_schema, :title, Keyword.get(options, :title)), + else: updated_schema + + updated_schema_with_description = + if Keyword.has_key?(options, :description), + do: Map.put(updated_schema_with_title, :description, Keyword.get(options, :description)), + else: updated_schema_with_title + + updated_schema_with_description + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index ce79629a61bf..810958a12af0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -195,15 +195,15 @@ defmodule BlockScoutWeb.API.V2.AddressView do "amount" => string_or_null(collection.distinct_token_instances_count || collection.value), "token_instances" => Enum.map(collection.preloaded_token_instances, fn instance -> - prepare_nft_for_collection(collection.token.type, instance) + prepare_nft_for_collection(collection.token.type, instance, collection.reputation) end), "reputation" => collection.reputation } end - defp prepare_nft_for_collection(token_type, instance) do + defp prepare_nft_for_collection(token_type, instance, reputation) do Map.merge( - %{"token_type" => token_type, "value" => value(token_type, instance)}, + %{"token_type" => token_type, "value" => value(token_type, instance), "reputation" => reputation}, TokenView.prepare_token_instance(instance, nil) ) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 8853ed85bb2d..80c7b9f96c48 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -30,7 +30,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox - import OpenApiSpex.TestAssertions @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" @instances_amount_in_collection 9 @@ -89,7 +88,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") json_response = json_response(request, 200) check_response(correct_response, json_response) - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -106,8 +104,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do @@ -146,12 +142,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") json_response = json_response(request, 200) check_response(correct_response, json_response) - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}") json_response = json_response(request, 200) check_response(correct_response, json_response) - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "returns successful creation transaction for a contract when both failed and successful transactions exist", @@ -181,8 +175,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["is_contract"] assert response["creation_transaction_hash"] == to_string(succeeded_transaction.hash) assert response["creation_status"] == "success" - - assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "returns failed creation transaction for a contract", @@ -204,8 +196,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["is_contract"] assert response["creation_transaction_hash"] == to_string(failed_transaction.hash) assert response["creation_status"] == "failed" - - assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end defp check_response(pattern_response, response) do @@ -334,8 +324,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get EIP-1967 proxy contract info", %{conn: conn} do @@ -390,8 +378,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get Resolved Delegate Proxy contract info", %{conn: conn} do @@ -449,8 +435,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "get watchlist id", %{conn: conn} do @@ -485,8 +469,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response = json_response(request, 200) assert response["watchlist_address_id"] == watchlist_address.id - - assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) end test "broadcasts fetched_bytecode event", %{conn: conn} do @@ -517,8 +499,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> subscribe_and_join(topic) request = get(conn, "/api/v2/addresses/#{address.hash}") - assert response = json_response(request, 200) - assert_schema(response, "AddressResponse", BlockScoutWeb.ApiSpec.spec()) + assert _response = json_response(request, 200) assert_receive %Phoenix.Socket.Message{ payload: %{fetched_bytecode: ^contract_code}, @@ -543,8 +524,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "gas_usage_count" => "0", "validations_count" => "0" } = json_response - - assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -561,8 +540,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get counters with 0s", %{conn: conn} do @@ -578,8 +555,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "gas_usage_count" => "0", "validations_count" => "0" } = json_response - - assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end test "get counters", %{conn: conn} do @@ -619,8 +594,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "gas_usage_count" => ^gas_used, "validations_count" => "1" } = json_response - - assert_schema(json_response, "AddressCounters", BlockScoutWeb.ApiSpec.spec()) end end @@ -632,8 +605,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do json_response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = json_response - - assert_schema(json_response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -650,8 +621,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant transaction", %{conn: conn} do @@ -668,7 +637,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(transaction, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get pending transaction", %{conn: conn} do @@ -688,7 +656,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(pending_transaction, Enum.at(response["items"], 0)) compare_item(transaction, Enum.at(response["items"], 1)) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :to transaction", %{conn: conn} do @@ -704,7 +671,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(transaction, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :from transactions", %{conn: conn} do @@ -720,7 +686,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(transaction, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "validated transactions can paginate", %{conn: conn} do @@ -735,8 +700,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "pending transactions can paginate", %{conn: conn} do @@ -751,7 +714,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "pending + validated transactions can paginate", %{conn: conn} do @@ -763,13 +725,9 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/transactions") assert response = json_response(request, 200) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"]) assert response_2nd_page = json_response(request_2nd_page, 200) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert Enum.count(response["items"]) == 50 assert response["next_page_params"] != nil compare_item(Enum.at(transactions_pending, 50), Enum.at(response["items"], 0)) @@ -789,8 +747,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response, transactions_validated ++ [Enum.at(transactions_pending, 0)] ) - - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":to transactions can paginate", %{conn: conn} do @@ -809,8 +765,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from transactions can paginate", %{conn: conn} do @@ -829,8 +783,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from + :to transactions can paginate", %{conn: conn} do @@ -862,10 +814,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_3rd_page = json_response(request_3rd_page, 200) check_paginated_response(response_2nd_page, response_3rd_page, transactions_from ++ [Enum.at(transactions_to, 0)]) - - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_3rd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "422 on wrong ordering params", %{conn: conn} do @@ -890,8 +838,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "backward compatible with legacy paging params", %{conn: conn} do @@ -915,8 +861,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "backward compatible with legacy paging params for pending transactions", %{conn: conn} do @@ -942,8 +886,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page_pending = json_response(request_2nd_page_pending, 200) check_paginated_response(response, response_2nd_page_pending, transactions) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page_pending, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by fee ascending", %{conn: conn} do @@ -983,8 +925,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by fee descending", %{conn: conn} do @@ -1024,8 +964,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by value ascending", %{conn: conn} do @@ -1060,8 +998,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by value descending", %{conn: conn} do @@ -1096,8 +1032,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by block number ascending", %{conn: conn} do @@ -1136,8 +1070,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "can order and paginate by block number descending", %{conn: conn} do @@ -1176,8 +1108,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(transactions, 50), Enum.at(response_2nd_page["items"], 0)) check_paginated_response(response, response_2nd_page, transactions |> Enum.reverse()) - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "regression test for decoding issue", %{conn: conn} do @@ -1250,8 +1180,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil - assert_schema(response, "AddressTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - transaction = Enum.at(response["items"], 0) assert transaction["to"]["ens_domain_name"] == "test.eth" @@ -1300,8 +1228,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") |> json_response(200) - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token transfers with scam reputation", %{conn: conn} do @@ -1327,7 +1253,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-transfers") response = json_response(request, 200) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" @@ -1335,8 +1260,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert response["items"] == [] - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token transfers with ok reputation with hide_scam_addresses=false", %{conn: conn} do @@ -1359,8 +1282,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -1386,8 +1307,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 200 on non existing address", %{conn: conn} do @@ -1397,7 +1316,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do json_response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = json_response - assert_schema(json_response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -1414,8 +1332,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 200 on non existing address of token", %{conn: conn} do @@ -1427,7 +1343,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do json_response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = json_response - assert_schema(json_response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid token address hash", %{conn: conn} do @@ -1446,8 +1361,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant token transfer", %{conn: conn} do @@ -1476,7 +1389,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "method in token transfer could be decoded", %{conn: conn} do @@ -1528,8 +1440,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) assert Enum.at(response["items"], 0)["method"] == "mint" - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant token transfer filtered by token", %{conn: conn} do @@ -1571,7 +1481,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "token transfers by token can paginate", %{conn: conn} do @@ -1609,9 +1518,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :to token transfer", %{conn: conn} do @@ -1641,7 +1547,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get only :from token transfer", %{conn: conn} do @@ -1671,7 +1576,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0)) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "token transfers can paginate", %{conn: conn} do @@ -1696,8 +1600,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":to token transfers can paginate", %{conn: conn} do @@ -1736,8 +1638,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from token transfers can paginate", %{conn: conn} do @@ -1776,8 +1676,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test ":from + :to tt can paginate", %{conn: conn} do @@ -1830,9 +1728,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_3rd_page = json_response(request_3rd_page, 200) check_paginated_response(response_2nd_page, response_3rd_page, tt_from ++ [Enum.at(tt_to, 0)]) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_3rd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check token type filters", %{conn: conn} do @@ -1899,8 +1794,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_20_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # -- ERC-721 -- @@ -1914,8 +1807,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_721_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # -- ERC-1155 -- @@ -1929,8 +1820,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_1155_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- # two filters simultaneously @@ -1966,9 +1855,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_3rd_page["next_page_params"] == nil compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1)) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_3rd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # -- ------ -- end @@ -2027,8 +1913,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_721_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) assert response = json_response(request, 200) @@ -2039,8 +1923,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_721_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) @@ -2052,8 +1934,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, erc_20_tt) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that same token_ids within batch squashes", %{conn: conn} do @@ -2094,8 +1974,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works for 721 tokens", %{conn: conn} do @@ -2126,8 +2004,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #1 (large batch) + check filters", %{conn: conn} do @@ -2164,16 +2040,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155", "filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter) assert response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #2 some batches on the first page and one on the second", @@ -2240,8 +2112,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2 ++ [tt_3]) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "check that pagination works fine with 1155 batches #3", %{conn: conn} do @@ -2295,8 +2165,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end if @chain_type == :celo do @@ -2319,7 +2187,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(token_transfer, Enum.at(response["items"], 0), true) - assert_schema(response, "AddressTokenTransfersPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end end @@ -2332,7 +2199,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2349,8 +2215,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get internal transaction and filter working", %{conn: conn} do @@ -2391,7 +2255,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(internal_transaction_from, Enum.at(response["items"], 1)) compare_item(internal_transaction_to, Enum.at(response["items"], 0)) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"}) @@ -2400,7 +2263,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(internal_transaction_from, Enum.at(response["items"], 0)) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"}) @@ -2408,7 +2270,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.count(response["items"]) == 1 assert response["next_page_params"] == nil compare_item(internal_transaction_to, Enum.at(response["items"], 0)) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "internal transactions can paginate", %{conn: conn} do @@ -2441,8 +2302,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_from) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) internal_transactions_to = for i <- 52..102 do @@ -2471,8 +2330,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_to) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"filter" => "from"} request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter) @@ -2488,8 +2345,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, internal_transactions_from) - assert_schema(response, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressInternalTransactionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2500,7 +2355,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated") assert response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2517,8 +2371,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get relevant block validated", %{conn: conn} do @@ -2533,7 +2385,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(block, Enum.at(response["items"], 0)) - assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "blocks validated can be paginated", %{conn: conn} do @@ -2548,8 +2399,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, blocks) - assert_schema(response, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressBlocksValidatedPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2571,8 +2420,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response)["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-balances") |> json_response(200) - - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get token balances with scam reputation", %{conn: conn} do @@ -2590,7 +2437,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/token-balances") response = json_response(request, 200) - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) assert List.first(response)["reputation"] == "scam" @@ -2598,8 +2444,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert response == [] - - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get smart-contract with ok reputation with hide_scam_addresses=false", %{conn: conn} do @@ -2615,8 +2459,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response)["reputation"] == "ok" - - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -2634,8 +2476,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response)["reputation"] == "ok" - - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get empty list on non existing address", %{conn: conn} do @@ -2644,7 +2484,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances") assert response = json_response(request, 200) assert response == [] - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2661,8 +2500,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token balance", %{conn: conn} do @@ -2681,8 +2518,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do for i <- 0..50 do compare_item(Enum.at(ctbs, i), Enum.at(response, i)) end - - assert_schema(response, "AddressTokenBalances", BlockScoutWeb.ApiSpec.spec()) end end @@ -2694,7 +2529,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2711,8 +2545,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance history", %{conn: conn} do @@ -2728,7 +2560,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(acb, Enum.at(response["items"], 0)) - assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance with transaction", %{conn: conn} do @@ -2794,8 +2625,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, acbs) - assert_schema(response, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressCoinBalanceHistoryPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -2814,8 +2643,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "days" => ^days_count, "items" => [] } = response - - assert_schema(response, "AddressCoinBalanceHistoryByDay", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2832,8 +2659,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get coin balance history by day", %{conn: conn} do @@ -2858,8 +2683,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do %{"date" => _, "value" => "1000"} ] } = response - - assert_schema(response, "AddressCoinBalanceHistoryByDay", BlockScoutWeb.ApiSpec.spec()) end end @@ -2870,7 +2693,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, "/api/v2/addresses/#{address.hash}/logs") response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -2887,8 +2709,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get log", %{conn: conn} do @@ -2915,7 +2735,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(log, Enum.at(response["items"], 0)) - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end # for some reasons test does not work if run as single test @@ -2944,8 +2763,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/logs", response["next_page_params"]) assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, logs) - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end # https://github.com/blockscout/blockscout/issues/9926 @@ -3029,7 +2846,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(log, Enum.at(response["items"], 0)) - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) log = Enum.at(response["items"], 0) assert log["address"]["ens_domain_name"] == "test.eth" @@ -3091,7 +2907,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["next_page_params"] == nil compare_item(log, Enum.at(response["items"], 0)) - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "log could be decoded via verified implementation", %{conn: conn} do @@ -3191,8 +3006,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } - - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test corner case, when preload functions face absent smart contract", %{conn: conn} do @@ -3235,7 +3048,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do log_from_api = Enum.at(response["items"], 0) compare_item(log, log_from_api) - assert_schema(response, "AddressLogsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "ignore logs without topics when trying to decode with sig provider", %{conn: conn} do @@ -3321,8 +3133,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/tokens") |> json_response(200) - - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token balances with scam reputation", %{conn: conn} do @@ -3343,7 +3153,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" @@ -3351,8 +3160,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert response["items"] == [] - - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token balances with ok reputation with hide_scam_addresses=false", %{conn: conn} do @@ -3372,8 +3179,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token balances with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -3396,8 +3201,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get empty list on non existing address", %{conn: conn} do @@ -3407,7 +3210,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -3424,8 +3226,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get tokens", %{conn: conn} do @@ -3474,8 +3274,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_20) - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-721"} request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) @@ -3487,8 +3285,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_721) - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, "/api/v2/addresses/#{address.hash}/tokens", filter) @@ -3500,8 +3296,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs_erc_1155) - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) # Test multiple token types (the fix for the original issue) filter = %{"type" => "ERC-721,ERC-1155"} @@ -3516,7 +3310,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> Enum.sort() assert response_token_types == ["ERC-1155", "ERC-721"] - assert_schema(response, "AddressTokensPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -3809,7 +3602,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -3826,8 +3618,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get withdrawals", %{conn: conn} do @@ -3841,8 +3631,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, address.withdrawals) - assert_schema(response, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressWithdrawalsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -3858,7 +3646,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert pattern_response["items"] == response["items"] assert pattern_response["next_page_params"] == response["next_page_params"] assert pattern_response["total_supply"] == response["total_supply"] - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check pagination", %{conn: conn} do @@ -3878,9 +3665,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert Enum.at(response["items"], 0)["coin_balance"] == to_string(Enum.at(addresses, 50).fetched_coin_balance.value) - - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check nil", %{conn: conn} do @@ -3891,7 +3675,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert %{"items" => [address_json], "next_page_params" => nil} = response compare_item(address, address_json) - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check smart contract preload", %{conn: conn} do @@ -3904,7 +3687,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert String.downcase(address["hash"]) == to_string(smart_contract.address_hash) assert address["is_contract"] == true assert address["is_verified"] == true - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by balance asc", %{conn: conn} do @@ -3926,9 +3708,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response_2nd_page, Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :desc) ) - - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by transactions count asc", %{conn: conn} do @@ -3946,9 +3725,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :desc)) - - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by balance desc", %{conn: conn} do @@ -3970,9 +3746,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response_2nd_page, Enum.sort_by(addresses, &Decimal.to_integer(&1.fetched_coin_balance.value), :asc) ) - - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end test "check sorting by transactions count desc", %{conn: conn} do @@ -3990,9 +3763,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, Enum.sort_by(addresses, & &1.transactions_count, :asc)) - - assert_schema(response, "AddressesList", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressesList", BlockScoutWeb.ApiSpec.spec()) end end @@ -4013,8 +3783,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "internal_transactions_count" => 0, "celo_election_rewards_count" => 0 } = response - - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -4031,8 +3799,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get counters with 0s", %{conn: conn} do @@ -4050,8 +3816,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "withdrawals_count" => 0, "internal_transactions_count" => 0 } = response - - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "get counters and check that cache works", %{conn: conn} do @@ -4126,8 +3890,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "internal_transactions_count" => 2 } = response - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) - for x <- 3..4 do insert(:internal_transaction, transaction: transaction, @@ -4152,8 +3914,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "withdrawals_count" => 51, "internal_transactions_count" => 2 } = response - - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) end test "check counters cache ttl", %{conn: conn} do @@ -4228,8 +3988,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "internal_transactions_count" => 2 } = response - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) - old_env = Application.get_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount) Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, ttl: 200) :timer.sleep(200) @@ -4262,8 +4020,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "internal_transactions_count" => 4 } = response - assert_schema(response, "AddressTabsCounters", BlockScoutWeb.ApiSpec.spec()) - Application.put_env(:explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, old_env) end end @@ -4280,7 +4036,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do @@ -4297,8 +4052,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get token with ok reputation", %{conn: conn, endpoint: endpoint} do @@ -4353,7 +4106,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" @@ -4363,7 +4115,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" @@ -4373,7 +4124,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" @@ -4434,13 +4184,11 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert response["items"] == [] @@ -4449,13 +4197,11 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert response["items"] == [] @@ -4464,13 +4210,11 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> put_req_cookie("show_scam_tokens", "true") |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert response["items"] == [] end @@ -4523,19 +4267,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" end @@ -4593,19 +4334,16 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "ok" end @@ -4635,8 +4373,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "next_page_params does not leak original type filter", %{conn: conn, endpoint: endpoint} do @@ -4670,7 +4406,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert not is_nil(response["next_page_params"]) refute Map.has_key?(response["next_page_params"], "type") assert Map.has_key?(response["next_page_params"], "token_type") - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated ERC-1155 nft", %{conn: conn, endpoint: endpoint} do @@ -4707,8 +4442,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated ERC-404 nft", %{conn: conn, endpoint: endpoint} do @@ -4745,8 +4478,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test filters", %{conn: conn, endpoint: endpoint} do @@ -4798,8 +4529,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances_721) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, endpoint.(address.hash), filter) @@ -4809,8 +4538,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, token_instances_1155) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "return all token instances", %{conn: conn, endpoint: endpoint} do @@ -4880,10 +4607,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(token_instances_1155, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(token_instances_1155, 0), Enum.at(response_3rd_page["items"], 1)) - - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_3rd_page, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "paginates across types after intermediate type exhaustion (should include next type)", %{ @@ -5013,8 +4736,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = get(conn, endpoint.(address.hash), filter) response = json_response(request, 200) - assert_schema(response, "AddressNFTsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert Enum.count(response["items"]) == 10 assert Enum.all?(response["items"], fn item -> item["token_type"] in ["ERC-404", "ERC-1155"] end) refute Enum.any?(response["items"], fn item -> item["token_type"] == "ERC-721" end) @@ -5118,8 +4839,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) |> json_response(200) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> put_req_cookie("show_scam_tokens", "true") @@ -5134,8 +4853,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) |> json_response(200) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> put_req_cookie("show_scam_tokens", "true") @@ -5149,8 +4866,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) |> json_response(200) - - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get nft collections with scam reputation", %{conn: conn} do @@ -5242,7 +4957,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" @@ -5251,15 +4965,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["items"] == [] - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" @@ -5268,15 +4979,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response["items"] == [] - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) assert List.first(response["items"])["reputation"] == "scam" @@ -5284,8 +4992,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert response["items"] == [] - - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get nft collections with ok reputation with hide_scam_addresses=false", %{conn: conn} do @@ -5372,21 +5078,15 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get nft collections with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -5479,21 +5179,15 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert List.first(response["items"])["reputation"] == "ok" - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) assert List.first(response["items"])["reputation"] == "ok" - - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do @@ -5503,8 +5197,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn, endpoint: endpoint} do @@ -5521,8 +5213,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = json_response - - assert_schema(json_response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated erc-721 collection", %{conn: conn, endpoint: endpoint} do @@ -5570,8 +5260,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get paginated erc-1155 collection", %{conn: conn, endpoint: endpoint} do @@ -5618,8 +5306,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, collections) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "test filters", %{conn: conn, endpoint: endpoint} do @@ -5700,8 +5386,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, ctbs) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) filter = %{"type" => "ERC-1155"} request = get(conn, endpoint.(address.hash), filter) @@ -5711,8 +5395,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, collections) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "return all collections", %{conn: conn, endpoint: endpoint} do @@ -5811,9 +5493,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(Enum.at(collections_1155, 1), Enum.at(response_3rd_page["items"], 0)) compare_item(Enum.at(collections_1155, 0), Enum.at(response_3rd_page["items"], 1)) - assert_schema(response, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_3rd_page, "AddressNFTCollectionsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -5826,7 +5505,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) assert %{"items" => [], "next_page_params" => nil} = response - assert_schema(response, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get 422 on invalid address", %{conn: conn} do @@ -5842,8 +5520,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do } ] } = response - - assert_schema(response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) end test "get deposits", %{conn: conn} do @@ -5858,8 +5534,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, deposits) - assert_schema(response, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - assert_schema(response_2nd_page, "AddressBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end else test "returns an error about chain type", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index 842b9d6dcb26..a847e41c41ef 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -524,25 +524,16 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do block = build(:block) request = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits") - response = json_response(request, 404) + json_response(request, 404) end - # test "get 422 on invalid block", %{conn: conn} do - # request = get(conn, "/api/v2/blocks/invalid/beacon/deposits") - # response = json_response(request, 422) + test "get 422 on invalid block", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/beacon/deposits") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) - # assert %{ - # "errors" => [ - # %{ - # "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", - # "source" => %{"pointer" => "/address_hash_param"}, - # "title" => "Invalid value" - # } - # ] - # } = response - - # assert_schema(response, "JsonErrorResponse", BlockScoutWeb.ApiSpec.spec()) - # end + request_2 = get(conn, "/api/v2/blocks/123qwe/beacon/deposits") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end test "get deposits", %{conn: conn} do block = insert(:block) @@ -558,8 +549,6 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert response_2nd_page = json_response(request_2nd_page, 200) check_paginated_response(response, response_2nd_page, deposits) - # assert_schema(response, "BlockBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) - # assert_schema(response_2nd_page, "BlockBeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end else test "returns an error about chain type", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs index 7584fa9ce458..62b6a509afbb 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs @@ -3,15 +3,12 @@ defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do alias Explorer.Repo - import OpenApiSpex.TestAssertions - if Application.compile_env(:explorer, :chain_type) == :ethereum do describe "/beacon/deposits" do test "get empty list when no deposits exist", %{conn: conn} do request = get(conn, "/api/v2/beacon/deposits") assert response = json_response(request, 200) assert %{"items" => []} = response - assert_schema(response, "BeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end test "get deposits", %{conn: conn} do @@ -30,8 +27,6 @@ defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do deposits |> Enum.reverse() |> Enum.map(&{&1.index, to_string(&1.transaction_hash), to_string(&1.block_hash)}) - - assert_schema(response, "BeaconDepositsPaginatedResponse", BlockScoutWeb.ApiSpec.spec()) end end @@ -43,13 +38,16 @@ defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do end test "returns deposit count", %{conn: conn} do + Repo.delete_all(Explorer.Chain.Beacon.Deposit) + ExMachina.Sequence.reset("beacon_deposit_index") + insert_list(3, :beacon_deposit) deposits_count = Repo.aggregate(Explorer.Chain.Beacon.Deposit, :count, :index) request = get(conn, "/api/v2/beacon/deposits/count") assert response = json_response(request, 200) - assert %{"deposits_count" => deposits_count} = response + assert %{"deposits_count" => ^deposits_count} = response end end else diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 907a696afdc6..17964f96544f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -1193,629 +1193,643 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert ens["ens_info"]["name"] == name end - test "finds a TAC operation", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + if Application.compile_env(:explorer, :chain_type) == :default do + test "finds a TAC operation", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" - tac_response = """ - { - "items": [ + tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ - - Bypass.expect_once( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) - - request = get(conn, "/api/v2/search?q=#{operation_id}") - - assert %{ - "items" => [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } - ], - "next_page_params" => nil - } == json_response(request, 200) - end + """ - test "handles no results from TAC microservice", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + Bypass.expect_once( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + request = get(conn, "/api/v2/search?q=#{operation_id}") + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(request, 200) + end + + test "handles no results from TAC microservice", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" - tac_response = """ - { - "items": [], - "next_page_params": null - } - """ + tac_response = """ + { + "items": [], + "next_page_params": null + } + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - request = get(conn, "/api/v2/search?q=#{operation_id}") + request = get(conn, "/api/v2/search?q=#{operation_id}") - assert %{ - "items" => [], - "next_page_params" => nil - } == json_response(request, 200) - end + assert %{ + "items" => [], + "next_page_params" => nil + } == json_response(request, 200) + end - test "finds a TAC operation with transaction", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + test "finds a TAC operation with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - transaction = insert(:transaction) |> with_block() + transaction = insert(:transaction) |> with_block() - operation_id = "#{transaction.hash}" + operation_id = "#{transaction.hash}" - tac_response = """ - { - "items": [ + tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - request = get(conn, "/api/v2/search?q=#{operation_id}") - assert response = json_response(request, 200) + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) - # tl to check order - assert %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } in tl(response["items"]) - - assert %{ - "priority" => 0, - "transaction_hash" => "#{transaction.hash}", - "type" => "transaction", - "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), - "url" => "/tx/#{transaction.hash}" - } in response["items"] - end + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } in tl(response["items"]) - test "finds a TAC operation with block", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + assert %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } in response["items"] + end + + test "finds a TAC operation with block", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - transaction = insert(:transaction) |> with_block() + transaction = insert(:transaction) |> with_block() - operation_id = "#{transaction.block_hash}" + operation_id = "#{transaction.block_hash}" - tac_response = """ - { - "items": [ + tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ - - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) - - request = get(conn, "/api/v2/search?q=#{operation_id}") - assert response = json_response(request, 200) + """ - # tl to check order - assert %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } in tl(response["items"]) - - assert %{ - "block_hash" => "#{transaction.block_hash}", - "block_number" => transaction.block_number, - "block_type" => "block", - "priority" => 3, - "type" => "block", - "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), - "url" => "/block/#{transaction.block_hash}" - } in response["items"] - end + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - test "finds TAC operations by sender and paginates", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } in tl(response["items"]) + + assert %{ + "block_hash" => "#{transaction.block_hash}", + "block_number" => transaction.block_number, + "block_type" => "block", + "priority" => 3, + "type" => "block", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/block/#{transaction.block_hash}" + } in response["items"] + end + + test "finds TAC operations by sender and paginates", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - address_hash = Address.checksum(insert(:address).hash) - operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + address_hash = Address.checksum(insert(:address).hash) + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" - tac_first_response = """ - { - "items": [ - #{for i <- 10..59, do: """ + tac_first_response = """ { - "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" - }#{if i == 59, do: "", else: ","} - """} - ], - "next_page_params": { - "page_token": 1747250219, - "page_size": 50 + "items": [ + #{for i <- 10..59, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 59, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250219, + "page_size": 50 + } } - } - """ + """ - tac_second_response = """ - { - "items": [ - #{for i <- 10..59, do: """ + tac_second_response = """ { - "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "#{if i == 0, do: "2025-05-14T19:16:59.000Z", else: "2025-05-14T19:17:#{i}.000Z"}", - "type": "TON_TAC_TON" - }#{if i == 59, do: "", else: ","} - """} - ], - "next_page_params": { - "page_token": 1747250279, - "page_size": 50 + "items": [ + #{for i <- 10..59, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "#{if i == 0, do: "2025-05-14T19:16:59.000Z", else: "2025-05-14T19:17:#{i}.000Z"}", + "type": "TON_TAC_TON" + }#{if i == 59, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250279, + "page_size": 50 + } } - } - """ + """ - tac_third_response = """ - { - "items": [ + tac_third_response = """ { - "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "2025-05-14T19:18:01.000Z", - "type": "TON_TAC_TON" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:18:01.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - case conn.params["page_token"] do - nil -> Plug.Conn.resp(conn, 200, tac_first_response) - "1747250218" -> Plug.Conn.resp(conn, 200, tac_second_response) - "1747250279" -> Plug.Conn.resp(conn, 200, tac_third_response) - _ -> raise "Unexpected page_token" + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, tac_first_response) + "1747250218" -> Plug.Conn.resp(conn, 200, tac_second_response) + "1747250279" -> Plug.Conn.resp(conn, 200, tac_third_response) + _ -> raise "Unexpected page_token" + end end - end - ) + ) - request = get(conn, "/api/v2/search?q=#{address_hash}") - assert response = json_response(request, 200) + request = get(conn, "/api/v2/search?q=#{address_hash}") + assert response = json_response(request, 200) + + second_page_request = + get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + + second_page_response = json_response(second_page_request, 200) + + third_page_request = + get(conn, "/api/v2/search", second_page_response["next_page_params"] |> Query.encode() |> Query.decode()) + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => address_hash, + "timestamp" => "2025-05-14T19:18:01.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(third_page_request, 200) + end + + test "finds TAC operations by TON sender", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - second_page_request = - get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - second_page_response = json_response(second_page_request, 200) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - third_page_request = - get(conn, "/api/v2/search", second_page_response["next_page_params"] |> Query.encode() |> Query.decode()) + tac_response = """ + { + "items": [ + { + "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type": "ROLLBACK", + "timestamp": "2025-06-05T12:21:11.000Z", + "sender": { + "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain": "TON" + } + } + ], + "next_page_params": null + } + """ + + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["q"] do + expected_q + when expected_q in [ + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", + "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", + "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", + "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", + "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" + ] -> + Plug.Conn.resp(conn, 200, tac_response) + + q -> + raise "Unexpected 'q' parameter #{inspect(q)}" + end + end + ) - assert %{ - "items" => [ + request = + get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") + + assert response = json_response(request, 200) + + assert [ %{ "priority" => 0, "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => address_hash, - "timestamp" => "2025-05-14T19:18:01.000Z", - "type" => "TON_TAC_TON" + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, "type" => "tac_operation" } - ], - "next_page_params" => nil - } == json_response(third_page_request, 200) - end - - test "finds TAC operations by TON sender", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + ] == response["items"] - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + request = + get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + assert response = json_response(request, 200) - tac_response = """ - { - "items": [ - { - "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "type": "ROLLBACK", - "timestamp": "2025-06-05T12:21:11.000Z", - "sender": { - "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain": "TON" - } - } - ], - "next_page_params": null - } - """ + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" + }, + "type" => "tac_operation" + } + ] == response["items"] - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - case conn.params["q"] do - expected_q - when expected_q in [ - "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", - "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", - "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", - "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", - "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" - ] -> - Plug.Conn.resp(conn, 200, tac_response) - - q -> - raise "Unexpected 'q' parameter #{inspect(q)}" - end - end - ) + request = + get(conn, "/api/v2/search?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") - request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] - - request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") - assert response = json_response(request, 200) + "type" => "tac_operation" + } + ] == response["items"] - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" - }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] + request = + get(conn, "/api/v2/search?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") - request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] + "type" => "tac_operation" + } + ] == response["items"] - request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") - assert response = json_response(request, 200) + request = + get(conn, "/api/v2/search?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" - }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] + assert response = json_response(request, 200) - request = get(conn, "/api/v2/search?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") - assert response = json_response(request, 200) - - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] + "type" => "tac_operation" + } + ] == response["items"] - request = - get( - conn, - "/api/v2/search?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" - ) + request = + get( + conn, + "/api/v2/search?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" + ) - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response["items"] - end + "type" => "tac_operation" + } + ] == response["items"] + end - test "finds TAC operations with transaction and paginates", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + test "finds TAC operations with transaction and paginates", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" - insert(:transaction, hash: operation_id) + insert(:transaction, hash: operation_id) - tac_first_response = """ - { - "items": [ - #{for i <- 0..49, do: """ - { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" - }#{if i == 49, do: "", else: ","} - """} - ], - "next_page_params": { - "page_token": 1747250209, - "page_size": 50 - } - } - """ - - tac_second_response = """ - { - "items": [ + tac_first_response = """ { + "items": [ + #{for i <- 0..49, do: """ + { "operation_id": "#{operation_id}", "sender": null, - "timestamp": "2025-05-14T19:16:50.000Z", + "timestamp": "2025-05-14T19:16:#{i}.000Z", "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": 1747250209, + "page_size": 50 + } } - ], - "next_page_params": null - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - case conn.params["page_token"] do - nil -> Plug.Conn.resp(conn, 200, tac_first_response) - "1747250208" -> Plug.Conn.resp(conn, 200, tac_second_response) - _ -> raise "Unexpected page_token" - end - end - ) + tac_second_response = """ + { + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:50.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null + } + """ - request = get(conn, "/api/v2/search?q=#{operation_id}") - assert response = json_response(request, 200) - next_page_request = get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["page_token"] do + nil -> Plug.Conn.resp(conn, 200, tac_first_response) + "1747250208" -> Plug.Conn.resp(conn, 200, tac_second_response) + _ -> raise "Unexpected page_token" + end + end + ) - assert %{ - "items" => [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:50.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } - ], - "next_page_params" => nil - } == json_response(next_page_request, 200) + request = get(conn, "/api/v2/search?q=#{operation_id}") + assert response = json_response(request, 200) + + next_page_request = + get(conn, "/api/v2/search", response["next_page_params"] |> Query.encode() |> Query.decode()) + + assert %{ + "items" => [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:50.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ], + "next_page_params" => nil + } == json_response(next_page_request, 200) + end end end @@ -2183,557 +2197,558 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end) end - test "finds a TAC operation", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) - - operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" - - tac_response = """ - { - "items": [ - { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" - } - ], - "next_page_params": null - } - """ - - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) - - request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + if Application.compile_env(:explorer, :chain_type) == :default do + test "finds a TAC operation", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } - ] == json_response(request, 200) - end - - test "finds a TAC operation with transaction", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) - - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - transaction = insert(:transaction) |> with_block() + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - operation_id = "#{transaction.hash}" + operation_id = "0xd06b6d3dbefcd1e4a5bb5806d0fdad87ae963bcc7d48d9a39ed361167958c09b" - tac_response = """ - { - "items": [ + tac_response = """ { + "items": [ + { "operation_id": "#{operation_id}", "sender": null, "timestamp": "2025-05-14T19:16:38.000Z", "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ - - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) - - request = get(conn, "/api/v2/search/quick?q=#{operation_id}") - assert response = json_response(request, 200) + """ - # tl to check order - assert %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } in tl(response) - - assert %{ - "priority" => 0, - "transaction_hash" => "#{transaction.hash}", - "type" => "transaction", - "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), - "url" => "/tx/#{transaction.hash}" - } in response - end + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - test "finds a TAC operation with block", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ] == json_response(request, 200) + end - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + test "finds a TAC operation with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - transaction = insert(:transaction) |> with_block() + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - operation_id = "#{transaction.block_hash}" + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - tac_response = """ - { - "items": [ + transaction = insert(:transaction) |> with_block() + + operation_id = "#{transaction.hash}" + + tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:38.000Z", - "type": "TON_TAC_TON" + "items": [ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - request = get(conn, "/api/v2/search/quick?q=#{operation_id}") - assert response = json_response(request, 200) + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) - # tl to check order - assert %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:38.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } in tl(response) - - assert %{ - "block_hash" => "#{transaction.block_hash}", - "block_number" => transaction.block_number, - "block_type" => "block", - "priority" => 3, - "type" => "block", - "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), - "url" => "/block/#{transaction.block_hash}" - } in response - end + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } in tl(response) - test "finds a lot of TAC operations with address", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + assert %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } in response + end + + test "finds a TAC operation with block", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + transaction = insert(:transaction) |> with_block() - address_hash = Address.checksum(insert(:address).hash) - operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + operation_id = "#{transaction.block_hash}" - tac_response = - """ + tac_response = """ { - "items": [ - #{for i <- 0..49, do: """ + "items": [ { - "operation_id": "#{operation_id}", - "sender": "#{address_hash}", - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" - }#{if i == 49, do: "", else: ","} - """} - ], - "next_page_params": { - "page_token": "2025-05-14T19:16:49.000Z", - "page_size": 50 + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:38.000Z", + "type": "TON_TAC_TON" } + ], + "next_page_params": null } """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == address_hash + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id + Plug.Conn.resp(conn, 200, tac_response) + end + ) - Plug.Conn.resp(conn, 200, tac_response) - end - ) + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) - request = get(conn, "/api/v2/search/quick?q=#{address_hash}") - assert response = json_response(request, 200) + # tl to check order + assert %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:38.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } in tl(response) + + assert %{ + "block_hash" => "#{transaction.block_hash}", + "block_number" => transaction.block_number, + "block_type" => "block", + "priority" => 3, + "type" => "block", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/block/#{transaction.block_hash}" + } in response + end + + test "finds a lot of TAC operations with address", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - address_result = %{ - "address_hash" => address_hash, - "certified" => false, - "ens_info" => nil, - "is_smart_contract_verified" => false, - "name" => nil, - "priority" => 0, - "type" => "address", - "url" => "/address/#{address_hash}", - "reputation" => "ok" - } + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - correct_response = [ - if(@chain_type == :filecoin, do: Map.put(address_result, "filecoin_robust_address", nil), else: address_result) - | for( - i <- 0..48, - do: %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => address_hash, - "timestamp" => "2025-05-14T19:16:#{i}.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + address_hash = Address.checksum(insert(:address).hash) + operation_id = "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2" + + tac_response = + """ + { + "items": [ + #{for i <- 0..49, do: """ + { + "operation_id": "#{operation_id}", + "sender": "#{address_hash}", + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": "2025-05-14T19:16:49.000Z", + "page_size": 50 } - ) - ] + } + """ - assert correct_response == response - end + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == address_hash - test "finds a TAC operation with TON address", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + Plug.Conn.resp(conn, 200, tac_response) + end + ) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + request = get(conn, "/api/v2/search/quick?q=#{address_hash}") + assert response = json_response(request, 200) + + correct_response = [ + %{ + "address_hash" => address_hash, + "certified" => false, + "ens_info" => nil, + "is_smart_contract_verified" => false, + "name" => nil, + "priority" => 0, + "type" => "address", + "url" => "/address/#{address_hash}", + "reputation" => "ok" + } + | for( + i <- 0..48, + do: %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => address_hash, + "timestamp" => "2025-05-14T19:16:#{i}.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ) + ] - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + assert correct_response == response + end - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + test "finds a TAC operation with TON address", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - tac_response = """ - { - "items": [ + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + tac_response = """ { - "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "type": "ROLLBACK", - "timestamp": "2025-06-05T12:21:11.000Z", - "sender": { - "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain": "TON" - } + "items": [ + { + "operation_id": "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "type": "ROLLBACK", + "timestamp": "2025-06-05T12:21:11.000Z", + "sender": { + "address": "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain": "TON" + } + } + ], + "next_page_params": null } - ], - "next_page_params": null - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - case conn.params["q"] do - expected_q - when expected_q in [ - "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", - "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", - "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", - "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", - "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" - ] -> - Plug.Conn.resp(conn, 200, tac_response) - - q -> - raise "Unexpected 'q' parameter #{inspect(q)}" + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + case conn.params["q"] do + expected_q + when expected_q in [ + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt", + "UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko", + "kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n", + "0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi", + "0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d" + ] -> + Plug.Conn.resp(conn, 200, tac_response) + + q -> + raise "Unexpected 'q' parameter #{inspect(q)}" + end end - end - ) + ) - request = - get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response + "type" => "tac_operation" + } + ] == response - request = - get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnXTt")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response + "type" => "tac_operation" + } + ] == response - request = - get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("UQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnSko")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response + "type" => "tac_operation" + } + ] == response - request = - get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("kQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnc9n")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response + "type" => "tac_operation" + } + ] == response - request = - get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") + request = + get(conn, "/api/v2/search/quick?q=#{URI.encode_www_form("0QBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2-Zw8yaoxnZKi")}") - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response + "type" => "tac_operation" + } + ] == response - request = - get( - conn, - "/api/v2/search/quick?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" - ) + request = + get( + conn, + "/api/v2/search/quick?q=#{URI.encode_www_form("0:67560e31eae4c26bc8e5ae1f185f25a99c9277a31c6b741436f99c3cc9aa319d")}" + ) - assert response = json_response(request, 200) + assert response = json_response(request, 200) - assert [ - %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", - "sender" => %{ - "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", - "blockchain" => "TON" + assert [ + %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => "0xcdbc69a2d42c796bb8d6c2db76f366baa93f0ce5badcf8ed766f686b0f734612", + "sender" => %{ + "address" => "EQBnVg4x6uTCa8jlrh8YXyWpnJJ3oxxrdBQ2+Zw8yaoxnXTt", + "blockchain" => "TON" + }, + "timestamp" => "2025-06-05T12:21:11.000Z", + "type" => "ROLLBACK" }, - "timestamp" => "2025-06-05T12:21:11.000Z", - "type" => "ROLLBACK" - }, - "type" => "tac_operation" - } - ] == response - end + "type" => "tac_operation" + } + ] == response + end - test "finds a lot if TAC operations with transaction", %{conn: conn} do - bypass = Bypass.open() - tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) + test "finds a lot if TAC operations with transaction", %{conn: conn} do + bypass = Bypass.open() + tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, - service_url: "http://localhost:#{bypass.port}", - enabled: true - ) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, + service_url: "http://localhost:#{bypass.port}", + enabled: true + ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - on_exit(fn -> - Bypass.down(bypass) - Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + on_exit(fn -> + Bypass.down(bypass) + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, tac_envs) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - transaction = - insert(:transaction, hash: "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2") |> with_block() + transaction = + insert(:transaction, hash: "0x07d74803dd6fd1a684b50494c09e366f3a6be20cd09928ebdf80d178ce41b5a2") + |> with_block() - operation_id = "#{transaction.hash}" + operation_id = "#{transaction.hash}" - tac_response = """ - { - "items": [ - #{for i <- 0..49, do: """ + tac_response = """ { - "operation_id": "#{operation_id}", - "sender": null, - "timestamp": "2025-05-14T19:16:#{i}.000Z", - "type": "TON_TAC_TON" - }#{if i == 49, do: "", else: ","} - """} - ], - "next_page_params": { - "page_token": "2025-05-14T19:16:49.000Z", - "page_size": 50 + "items": [ + #{for i <- 0..49, do: """ + { + "operation_id": "#{operation_id}", + "sender": null, + "timestamp": "2025-05-14T19:16:#{i}.000Z", + "type": "TON_TAC_TON" + }#{if i == 49, do: "", else: ","} + """} + ], + "next_page_params": { + "page_token": "2025-05-14T19:16:49.000Z", + "page_size": 50 + } } - } - """ + """ - Bypass.expect( - bypass, - "GET", - "/api/v1/tac/operations", - fn conn -> - assert conn.params["q"] == operation_id + Bypass.expect( + bypass, + "GET", + "/api/v1/tac/operations", + fn conn -> + assert conn.params["q"] == operation_id - Plug.Conn.resp(conn, 200, tac_response) - end - ) + Plug.Conn.resp(conn, 200, tac_response) + end + ) - request = get(conn, "/api/v2/search/quick?q=#{operation_id}") - assert response = json_response(request, 200) + request = get(conn, "/api/v2/search/quick?q=#{operation_id}") + assert response = json_response(request, 200) - correct_response = [ - %{ - "priority" => 0, - "transaction_hash" => "#{transaction.hash}", - "type" => "transaction", - "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), - "url" => "/tx/#{transaction.hash}" - } - | for( - i <- 0..48, - do: %{ - "priority" => 0, - "tac_operation" => %{ - "operation_id" => operation_id, - "sender" => nil, - "timestamp" => "2025-05-14T19:16:#{i}.000Z", - "type" => "TON_TAC_TON" - }, - "type" => "tac_operation" - } - ) - ] + correct_response = [ + %{ + "priority" => 0, + "transaction_hash" => "#{transaction.hash}", + "type" => "transaction", + "timestamp" => "#{transaction.block_timestamp}" |> String.replace(" ", "T"), + "url" => "/tx/#{transaction.hash}" + } + | for( + i <- 0..48, + do: %{ + "priority" => 0, + "tac_operation" => %{ + "operation_id" => operation_id, + "sender" => nil, + "timestamp" => "2025-05-14T19:16:#{i}.000Z", + "type" => "TON_TAC_TON" + }, + "type" => "tac_operation" + } + ) + ] - assert correct_response == response + assert correct_response == response + end end test "returns empty list and don't crash", %{conn: conn} do diff --git a/apps/block_scout_web/test/support/api_schema_assertions.ex b/apps/block_scout_web/test/support/api_schema_assertions.ex new file mode 100644 index 000000000000..0d4465cc88a6 --- /dev/null +++ b/apps/block_scout_web/test/support/api_schema_assertions.ex @@ -0,0 +1,113 @@ +defmodule BlockScoutWeb.TestApiSchemaAssertions do + @moduledoc """ + Test helper that automatically validates JSON responses against the OpenAPI schema + for every GET request to `/api/*` endpoints. + + It wraps `Phoenix.ConnTest.json_response/2` to perform schema validation using + `OpenApiSpex.TestAssertions.assert_schema/3` based on the current request path, + HTTP method and status code. + """ + + require Logger + alias OpenApiSpex.{Operation, PathItem} + + @spec json_response(Plug.Conn.t(), non_neg_integer()) :: map() | list() + def json_response(%Plug.Conn{} = conn, status_code) when is_integer(status_code) do + json = Phoenix.ConnTest.json_response(conn, status_code) + + maybe_assert_schema(conn, status_code, json) + + json + end + + defp maybe_assert_schema(%Plug.Conn{method: "GET", request_path: request_path} = _conn, status_code, json) + when is_integer(status_code) and is_binary(request_path) do + if String.starts_with?(request_path, "/api/") do + spec = BlockScoutWeb.ApiSpec.spec() + + with {:path_item, {:ok, %PathItem{} = path_item}} <- {:path_item, find_path_item(spec, request_path)}, + {:operation, %Operation{} = operation} <- {:operation, Map.get(path_item, :get)}, + {:schema, {:ok, schema}} <- {:schema, find_response_schema(operation, status_code)} do + Logger.info("Validated response against schema for path: #{request_path} and status code: #{status_code}") + + OpenApiSpex.TestAssertions.assert_raw_schema(json, schema, spec) + else + {:path_item, :error} -> + Logger.warning("No schema found for path: #{request_path}") + :ok + + {:operation, _} -> + Logger.warning("No GET operation found for path: #{request_path}") + :ok + + {:schema, :error} -> + Logger.warning("No schema found for path: #{request_path} and status code: #{status_code}") + :ok + end + end + end + + defp maybe_assert_schema(_conn, _status_code, _json), do: :ok + + defp find_path_item(%{paths: paths} = _spec, request_path) when is_map(paths) do + api_relative = strip_api_prefix(request_path) + + with {:ok, {_, path_item}} <- match_template_path(paths, api_relative) do + {:ok, path_item} + else + _ -> :error + end + end + + defp strip_api_prefix("/api" <> rest), do: rest + defp strip_api_prefix(path), do: path + + defp match_template_path(paths_map, actual_path) do + actual_segments = split_path(actual_path) + + Enum.find_value(paths_map, fn {template_path, %PathItem{} = item} -> + template_segments = split_path(template_path) + + if segments_match?(template_segments, actual_segments) do + {:ok, {template_path, item}} + else + false + end + end) || :error + end + + defp split_path(path) when is_binary(path) do + path + |> String.split("?", parts: 2) + |> hd() + |> String.split("/", trim: true) + end + + defp segments_match?(template_segments, actual_segments) when length(template_segments) == length(actual_segments) do + Enum.zip(template_segments, actual_segments) + |> Enum.all?(fn {t, a} -> dynamic_segment?(t) or t == a end) + end + + defp segments_match?(_template_segments, _actual_segments), do: false + + defp dynamic_segment?(segment) when is_binary(segment) do + String.starts_with?(segment, "{") and String.ends_with?(segment, "}") + end + + defp find_response_schema(%Operation{responses: responses}, status_code) when is_map(responses) do + key = Integer.to_string(status_code) + + response = Map.get(responses, key) || Map.get(responses, status_code) || Map.get(responses, "default") + + case response do + %OpenApiSpex.Response{content: %{"application/json" => %OpenApiSpex.MediaType{schema: schema}}} -> + {:ok, schema} + + %OpenApiSpex.Reference{} = ref -> + {:ok, ref} + + _ -> + :error + end + end +end diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex index d122090b4a20..fc86ff109701 100644 --- a/apps/block_scout_web/test/support/conn_case.ex +++ b/apps/block_scout_web/test/support/conn_case.ex @@ -19,7 +19,8 @@ defmodule BlockScoutWeb.ConnCase do quote do # Import conveniences for testing with connections import Plug.Conn - import Phoenix.ConnTest + import Phoenix.ConnTest, except: [json_response: 2] + import BlockScoutWeb.TestApiSchemaAssertions, only: [json_response: 2] import BlockScoutWeb.Router.Helpers import BlockScoutWeb.Routers.WebRouter.Helpers, except: [static_path: 2] import BlockScoutWeb.Routers.AccountRouter.Helpers, except: [static_path: 2] diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 547c2b8bb440..757c61c389ae 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -178,7 +178,7 @@ defmodule Explorer.Application do ]), configure_chain_type_dependent_con_cache(), Explorer.Migrator.SanitizeDuplicatedLogIndexLogs - |> configure() + |> configure_mode_dependent_process(:indexer) |> configure_chain_type_dependent_process([ :polygon_zkevm, :rsk, diff --git a/apps/explorer/test/explorer/migrator/sanitize_duplicated_log_index_logs_test.exs b/apps/explorer/test/explorer/migrator/sanitize_duplicated_log_index_logs_test.exs index 5fbdc25a1ad9..475a7808bbf1 100644 --- a/apps/explorer/test/explorer/migrator/sanitize_duplicated_log_index_logs_test.exs +++ b/apps/explorer/test/explorer/migrator/sanitize_duplicated_log_index_logs_test.exs @@ -9,6 +9,20 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogsTest do if Application.compile_env(:explorer, :chain_type) in [:polygon_zkevm, :rsk, :filecoin] do describe "Sanitize duplicated log index logs" do + setup do + configuration = Application.get_env(:explorer, SanitizeDuplicatedLogIndexLogs) + + Application.put_env( + :explorer, + SanitizeDuplicatedLogIndexLogs, + Keyword.merge(configuration, batch_size: 50_000, concurrency: 10) + ) + + on_exit(fn -> + Application.put_env(:explorer, SanitizeDuplicatedLogIndexLogs, configuration) + end) + end + test "correctly identifies and updates duplicated log index logs" do block = insert(:block) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 9e0db04de2a4..50c82c08a75c 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1509,7 +1509,7 @@ defmodule Explorer.Factory do amount = Map.get(attrs, :deposit_amount, sequence("beacon_deposit_amount", & &1)) signature_raw = Map.get(attrs, :deposit_signature, sequence("beacon_deposit_signature", &<<&1::768>>)) - index = Map.get(attrs, :deposit_index, sequence("beacon_deposit_index", & &1)) + index = Map.get_lazy(attrs, :deposit_index, fn -> sequence("beacon_deposit_index", & &1) end) <<_::bytes-4, data_raw::binary>> = ABI.TypeEncoder.encode( From 91fa8e7b49e00f42f029754e8f81e417249e8361 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 16 Sep 2025 14:17:53 +0300 Subject: [PATCH 0336/1006] Update 9.1.0 CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62bfa5de8ac5..447156084a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,9 @@ ### ⚙️ Miscellaneous Tasks - Remove quantile_estimator dep ([#13190](https://github.com/blockscout/blockscout/pull/13190)) +- Add support of Scroll codecv8 ([#13090](https://github.com/blockscout/blockscout/pull/13090)) - Change release workflow ([#13087](https://github.com/blockscout/blockscout/issues/13087)) +- Add INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT env variable ([#13051](https://github.com/blockscout/blockscout/pull/13051)) - Update and format pull request template ([#13028](https://github.com/blockscout/blockscout/issues/13028)) - Add final check for ReindexDuplicatedInternalTransactions ([#13091](https://github.com/blockscout/blockscout/issues/13091)) - Remove obsolete circleci config ([#13097](https://github.com/blockscout/blockscout/issues/13097)) From 5ddba2cc995634eed28ed33593733fce9e47ba33 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 16 Sep 2025 14:50:27 +0300 Subject: [PATCH 0337/1006] Update cpell config --- cspell.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cspell.json b/cspell.json index 57c2968bc9f0..38d62be97023 100644 --- a/cspell.json +++ b/cspell.json @@ -138,6 +138,7 @@ "Cldr", "clearfix", "clickover", + "codecv", "codeformat", "coef", "coeff", From dbdfa768e467faac717eca28a0da14680d3c064a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 07:39:45 +0300 Subject: [PATCH 0338/1006] Update public release workflow --- .github/workflows/public-release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/public-release.yml b/.github/workflows/public-release.yml index 3ec4f0906f27..9268ea23eaf0 100644 --- a/.github/workflows/public-release.yml +++ b/.github/workflows/public-release.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: release_number: - description: 'Release number (e.g., v9.1.0, latest)' + description: 'Release number (e.g., v9.1.0)' required: true type: string @@ -31,7 +31,7 @@ jobs: - name: Set source and target tags run: | - RELEASE_NUMBER="${{ github.event.inputs.release_number || 'latest' }}" + RELEASE_NUMBER="${{ github.event.inputs.release_number }}" SOURCE_TAG="${RELEASE_NUMBER}" TARGET_TAG="${RELEASE_NUMBER}" echo "SOURCE_TAG=${SOURCE_TAG}" >> $GITHUB_ENV From e54f00ea68bef40bfcc572b4f388f115c1700a12 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 17 Sep 2025 07:46:26 +0300 Subject: [PATCH 0339/1006] chore: Refactoring reputation (#13221) * chore: Refactoring reputation * Fix notifier and /summary * Fix credo * Update schema * Add regression tests --- .../transaction_interpretation.ex | 13 +- .../lib/block_scout_web/notifier.ex | 7 +- .../schemas/api/v2/address/token_balance.ex | 11 +- .../schemas/api/v2/nft_collection.ex | 9 +- .../schemas/api/v2/token_instance_in_list.ex | 12 +- .../schemas/api/v2/token_transfer.ex | 12 +- .../views/api/v2/address_view.ex | 14 +- .../views/api/v2/advanced_filter_view.ex | 7 +- .../views/api/v2/token_transfer_view.ex | 3 +- .../api/v2/address_controller_test.exs | 72 ++-- .../v2/advanced_filter_controller_test.exs | 8 +- .../api/v2/token_transfer_controller_test.exs | 8 +- .../api/v2/transaction_controller_test.exs | 349 ++++++++++++++---- .../chain/address/current_token_balance.ex | 4 +- .../lib/explorer/chain/advanced_filter.ex | 7 +- apps/explorer/lib/explorer/chain/search.ex | 8 +- .../lib/explorer/chain/smart_contract.ex | 2 +- apps/explorer/lib/explorer/chain/token.ex | 2 +- .../lib/explorer/chain/token/instance.ex | 6 +- .../lib/explorer/chain/token_transfer.ex | 9 +- apps/explorer/lib/explorer/helper.ex | 34 +- .../sanitize_duplicated_log_index_logs.ex | 2 - 22 files changed, 382 insertions(+), 217 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 626722b69329..1be80e35a387 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -22,6 +22,12 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do @request_error_msg "Error while sending request to Transaction Interpretation Service" @api_true api?: true @items_limit 50 + @token_options [ + api?: true, + necessity_by_association: %{ + reputation_association() => :optional + } + ] @internal_transaction_necessity_by_association [ necessity_by_association: %{ [created_contract_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => @@ -316,13 +322,15 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do defp preload_template_variable(%{"type" => "token", "value" => %{"address" => address_hash_string} = value}), do: %{ "type" => "token", - "value" => address_hash_string |> Chain.token_from_address_hash(@api_true) |> token_from_db() |> Map.merge(value) + "value" => + address_hash_string |> Chain.token_from_address_hash(@token_options) |> token_from_db() |> Map.merge(value) } defp preload_template_variable(%{"type" => "token", "value" => %{"address_hash" => address_hash_string} = value}), do: %{ "type" => "token", - "value" => address_hash_string |> Chain.token_from_address_hash(@api_true) |> token_from_db() |> Map.merge(value) + "value" => + address_hash_string |> Chain.token_from_address_hash(@token_options) |> token_from_db() |> Map.merge(value) } defp preload_template_variable(%{"type" => "address", "value" => %{"hash" => address_hash_string} = value}), @@ -332,6 +340,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do address_hash_string |> Chain.hash_to_address( necessity_by_association: %{ + :scam_badge => :optional, :names => :optional, :smart_contract => :optional, proxy_implementations_association() => :optional diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 6ab5522df342..ab812e4f28c5 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -306,7 +306,10 @@ defmodule BlockScoutWeb.Notifier do to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()] ] - preloads = if API_V2.enabled?(), do: [:token_transfers | base_preloads], else: base_preloads + preloads = + if API_V2.enabled?(), + do: [{:token_transfers, [token: Reputation.reputation_association()]} | base_preloads], + else: base_preloads transactions |> Repo.preload(preloads) @@ -403,7 +406,7 @@ defmodule BlockScoutWeb.Notifier do ) when type in [:realtime, :on_demand] do address_current_token_balances - |> Repo.preload(:token) + |> Repo.preload(token: Reputation.reputation_association()) |> Enum.group_by(& &1.token_type) |> Enum.each(fn {token_type, balances} -> broadcast_token_balances(address_hash, token_type, balances) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex index 8b2d15b32cd0..4aacf99ac4bc 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/token_balance.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do require OpenApiSpex alias BlockScoutWeb.Schemas.API.V2.{General, Token, TokenInstance} - alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -14,15 +13,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.TokenBalance do value: General.IntegerString, token: %Schema{allOf: [Token], nullable: true}, token_id: General.IntegerStringNullable, - token_instance: %Schema{allOf: [TokenInstance], nullable: true}, - reputation: %Schema{ - type: :string, - enum: Reputation.enum_values(), - description: "Reputation of the token balance", - nullable: true - } + token_instance: %Schema{allOf: [TokenInstance], nullable: true} }, - required: [:value, :token, :token_id, :token_instance, :reputation], + required: [:value, :token, :token_id, :token_instance], additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex index 2be2fd134b10..fcf6848ce713 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/nft_collection.ex @@ -5,7 +5,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.NFTCollection do require OpenApiSpex alias BlockScoutWeb.Schemas.API.V2.{General, Token, TokenInstanceInList} - alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -17,15 +16,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.NFTCollection do type: :array, items: TokenInstanceInList, nullable: false - }, - reputation: %Schema{ - type: :string, - enum: Reputation.enum_values(), - description: "Reputation of the token collection", - nullable: true } }, - required: [:token, :amount, :token_instances, :reputation], + required: [:token, :amount, :token_instances], additionalProperties: false }) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex index 677f5a0cae76..ba78a22f1ce9 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance_in_list.ex @@ -6,8 +6,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstanceInList do alias BlockScoutWeb.Schemas.API.V2.{General, Token.Type, TokenInstance} alias BlockScoutWeb.Schemas.Helper - alias Explorer.Chain.Address.Reputation - alias OpenApiSpex.Schema OpenApiSpex.schema( TokenInstance.schema() @@ -15,15 +13,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstanceInList do title: "TokenInstanceInList", properties: %{ token_type: Type, - value: General.IntegerStringNullable, - reputation: %Schema{ - type: :string, - enum: Reputation.enum_values(), - description: "Reputation of the token instance", - nullable: true - } + value: General.IntegerStringNullable }, - required: [:token_type, :value, :reputation] + required: [:token_type, :value] ) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index cb7fb177b1b7..f57b1bcf6ca8 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -31,7 +31,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do TransactionHashCustomization } - alias Explorer.Chain.Address.Reputation alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -55,13 +54,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do block_hash: General.FullHash, block_number: %Schema{type: :integer, nullable: false}, log_index: %Schema{type: :integer, nullable: false}, - token_type: Token.Type, - reputation: %Schema{ - type: :string, - enum: Reputation.enum_values(), - description: "Reputation of the token transfer", - nullable: true - } + token_type: Token.Type }, required: [ :transaction_hash, @@ -75,8 +68,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do :block_hash, :block_number, :log_index, - :token_type, - :reputation + :token_type ], additionalProperties: false }) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 810958a12af0..a2da2fe63718 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -146,8 +146,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do token_balance.address_hash, token_balance ) - ), - "reputation" => token_balance.reputation + ) } end @@ -184,7 +183,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do defp prepare_nft(nft, token) do Map.merge( - %{"token_type" => token.type, "value" => value(token.type, nft), "reputation" => token.reputation}, + %{"token_type" => token.type, "value" => value(token.type, nft)}, TokenView.prepare_token_instance(nft, token) ) end @@ -195,15 +194,14 @@ defmodule BlockScoutWeb.API.V2.AddressView do "amount" => string_or_null(collection.distinct_token_instances_count || collection.value), "token_instances" => Enum.map(collection.preloaded_token_instances, fn instance -> - prepare_nft_for_collection(collection.token.type, instance, collection.reputation) - end), - "reputation" => collection.reputation + prepare_nft_for_collection(collection.token.type, instance) + end) } end - defp prepare_nft_for_collection(token_type, instance, reputation) do + defp prepare_nft_for_collection(token_type, instance) do Map.merge( - %{"token_type" => token_type, "value" => value(token_type, instance), "reputation" => reputation}, + %{"token_type" => token_type, "value" => value(token_type, instance)}, TokenView.prepare_token_instance(instance, nil) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 133cd3aa4283..f8985f4ebd53 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -173,12 +173,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do internal_transaction_index: advanced_filter.internal_transaction_index, token_transfer_index: advanced_filter.token_transfer_index, token_transfer_batch_index: advanced_filter.token_transfer_batch_index, - fee: advanced_filter.fee, - reputation: - if(advanced_filter.type != "coin_transfer", - do: advanced_filter.reputation, - else: nil - ) + fee: advanced_filter.fee } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex index f1b353c04f5b..7420c4192948 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_transfer_view.ex @@ -60,8 +60,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferView do "block_hash" => to_string(token_transfer.block_hash), "block_number" => token_transfer.block_number, "log_index" => token_transfer.log_index, - "token_type" => token_transfer.token_type, - "reputation" => token_transfer.token.reputation + "token_type" => token_transfer.token_type } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 80c7b9f96c48..1351698d0d6b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -1225,7 +1225,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") |> json_response(200) end @@ -1254,7 +1254,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") response = json_response(request, 200) @@ -1281,7 +1281,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -1306,7 +1306,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get 200 on non existing address", %{conn: conn} do @@ -2417,7 +2417,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response)["reputation"] == "ok" + assert List.first(response)["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/token-balances") |> json_response(200) end @@ -2438,7 +2438,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response)["reputation"] == "scam" + assert List.first(response)["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") response = json_response(request, 200) @@ -2458,7 +2458,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") response = json_response(request, 200) - assert List.first(response)["reputation"] == "ok" + assert List.first(response)["token"]["reputation"] == "ok" end test "get smart-contract with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -2475,7 +2475,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/token-balances") response = json_response(request, 200) - assert List.first(response)["reputation"] == "ok" + assert List.first(response)["token"]["reputation"] == "ok" end test "get empty list on non existing address", %{conn: conn} do @@ -3130,7 +3130,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/addresses/#{address.hash}/tokens") |> json_response(200) end @@ -3154,7 +3154,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) @@ -3178,7 +3178,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token balances with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -3200,7 +3200,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/tokens") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get empty list on non existing address", %{conn: conn} do @@ -4107,7 +4107,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) |> json_response(200) @@ -4116,7 +4116,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) |> json_response(200) @@ -4125,7 +4125,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) |> json_response(200) end @@ -4185,7 +4185,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) @@ -4198,7 +4198,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) @@ -4211,7 +4211,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) @@ -4268,17 +4268,17 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token with scam reputation with hide_scam_addresses=false", %{conn: conn, endpoint: endpoint} do @@ -4335,17 +4335,17 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-721"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get(endpoint.(address.hash), %{"type" => "ERC-404"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get paginated ERC-721 nft", %{conn: conn, endpoint: endpoint} do @@ -4832,7 +4832,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn @@ -4846,7 +4846,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn @@ -4860,7 +4860,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn @@ -4958,7 +4958,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) @@ -4972,7 +4972,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) @@ -4986,7 +4986,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) @@ -5076,17 +5076,17 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get nft collections with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -5177,17 +5177,17 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-721"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" request = conn |> get("/api/v2/addresses/#{address.hash}/nft/collections", %{type: "ERC-404"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get 200 on non existing address", %{conn: conn, endpoint: endpoint} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index 168114b870bc..aeef6eb025df 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -22,7 +22,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn @@ -47,7 +47,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) response = json_response(request, 200) @@ -67,7 +67,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do request = conn |> get("/api/v2/advanced-filters", %{"transaction_types" => "ERC-20,ERC-404,ERC-721,ERC-1155"}) response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "empty list", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs index 61b206db1b6e..d10867998a1a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_transfer_controller_test.exs @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/token-transfers") |> json_response(200) end @@ -40,7 +40,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/token-transfers") response = json_response(request, 200) @@ -63,7 +63,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do request = conn |> get("/api/v2/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferControllerTest do request = conn |> get("/api/v2/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "empty list", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index dc0313a9162c..548676a4a439 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -4,10 +4,9 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox - import Ecto.Query, only: [from: 2] alias Explorer.Account.{Identity, WatchlistAddress} - alias Explorer.Chain.{Address, Data, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.{Address, InternalTransaction, Log, Token, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.{Repo, TestHelper} @@ -231,7 +230,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/transactions/#{transaction.hash}") response = json_response(request, 200) - assert List.first(response["token_transfers"])["reputation"] == "ok" + assert List.first(response["token_transfers"])["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/transactions/#{transaction.hash}") |> json_response(200) end @@ -263,7 +262,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> put_req_cookie("show_scam_tokens", "true") |> get("/api/v2/transactions/#{transaction.hash}") response = json_response(request, 200) - assert List.first(response["token_transfers"])["reputation"] == "scam" + assert List.first(response["token_transfers"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/transactions/#{transaction.hash}") response = json_response(request, 200) @@ -296,7 +295,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> get("/api/v2/transactions/#{transaction.hash}") response = json_response(request, 200) - assert List.first(response["token_transfers"])["reputation"] == "ok" + assert List.first(response["token_transfers"])["token"]["reputation"] == "ok" end test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -326,7 +325,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> get("/api/v2/transactions/#{transaction.hash}") response = json_response(request, 200) - assert List.first(response["token_transfers"])["reputation"] == "ok" + assert List.first(response["token_transfers"])["token"]["reputation"] == "ok" end test "return 404 on non existing transaction", %{conn: conn} do @@ -796,7 +795,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" assert response == conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") |> json_response(200) end @@ -832,7 +831,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "scam" + assert List.first(response["items"])["token"]["reputation"] == "scam" request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") response = json_response(request, 200) @@ -865,7 +864,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "get token-transfers with scam reputation with hide_scam_addresses=false", %{conn: conn} do @@ -895,7 +894,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = conn |> get("/api/v2/transactions/#{transaction.hash}/token-transfers") response = json_response(request, 200) - assert List.first(response["items"])["reputation"] == "ok" + assert List.first(response["items"])["token"]["reputation"] == "ok" end test "return 404 on non existing transaction", %{conn: conn} do @@ -1975,36 +1974,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert check_total(Repo.preload(token_transfer, [{:token, :contract_address}]).token, json["total"], token_transfer) end - defp compare_item(%Transaction{} = transaction, json, wl_names) do - assert to_string(transaction.hash) == json["hash"] - assert transaction.block_number == json["block_number"] - assert to_string(transaction.value.value) == json["value"] - assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] - assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] - - assert json["to"]["watchlist_names"] == - if(wl_names[transaction.to_address_hash], - do: [ - %{ - "display_name" => wl_names[transaction.to_address_hash], - "label" => wl_names[transaction.to_address_hash] - } - ], - else: [] - ) - - assert json["from"]["watchlist_names"] == - if(wl_names[transaction.from_address_hash], - do: [ - %{ - "display_name" => wl_names[transaction.from_address_hash], - "label" => wl_names[transaction.from_address_hash] - } - ], - else: [] - ) - end - defp compare_item(%BeaconDeposit{} = deposit, json) do index = deposit.index transaction_hash = to_string(deposit.transaction_hash) @@ -2044,6 +2013,36 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end end + defp compare_item(%Transaction{} = transaction, json, wl_names) do + assert to_string(transaction.hash) == json["hash"] + assert transaction.block_number == json["block_number"] + assert to_string(transaction.value.value) == json["value"] + assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] + + assert json["to"]["watchlist_names"] == + if(wl_names[transaction.to_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.to_address_hash], + "label" => wl_names[transaction.to_address_hash] + } + ], + else: [] + ) + + assert json["from"]["watchlist_names"] == + if(wl_names[transaction.from_address_hash], + do: [ + %{ + "display_name" => wl_names[transaction.from_address_hash], + "label" => wl_names[transaction.from_address_hash] + } + ], + else: [] + ) + end + defp check_paginated_response(first_page_resp, second_page_resp, transactions) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil @@ -2413,18 +2412,17 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do transaction = :transaction |> insert() |> with_block() - log = - insert(:log, - transaction: transaction, - first_topic: TestHelper.topic(topic1), - second_topic: TestHelper.topic(topic2), - third_topic: nil, - fourth_topic: nil, - data: log_data, - address: address, - block: transaction.block, - block_number: transaction.block_number - ) + insert(:log, + transaction: transaction, + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address, + block: transaction.block, + block_number: transaction.block_number + ) insert(:proxy_implementation, proxy_address_hash: address.hash, @@ -2495,18 +2493,17 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do transaction = :transaction |> insert() |> with_block() - log = - insert(:log, - transaction: transaction, - first_topic: TestHelper.topic(topic1), - second_topic: TestHelper.topic(topic2), - third_topic: nil, - fourth_topic: nil, - data: log_data, - address: address, - block: transaction.block, - block_number: transaction.block_number - ) + insert(:log, + transaction: transaction, + first_topic: TestHelper.topic(topic1), + second_topic: TestHelper.topic(topic2), + third_topic: nil, + fourth_topic: nil, + data: log_data, + address: address, + block: transaction.block, + block_number: transaction.block_number + ) insert(:proxy_implementation, proxy_address_hash: address.hash, @@ -2527,9 +2524,229 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end end - if @chain_type == :neon do - import Ecto.Query, only: [from: 2] + describe "/transactions/{transaction_hash}/summary" do + setup do + original_config = + Application.get_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation) + + original_tesla_adapter = Application.get_env(:tesla, :adapter) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Application.put_env( + :block_scout_web, + BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + original_config + ) + + Application.put_env(:tesla, :adapter, original_tesla_adapter) + end) + end + + test "success preload template variables", %{conn: conn} do + bypass = Bypass.open() + + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: true, + service_url: "http://localhost:#{bypass.port}" + ) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + token = insert(:token) + address = insert(:address) + + tx_interpretation_response = """ + { + "success": true, + "data": { + "summaries": [ + { + "summary_template": "{action_type} {outgoing_amount} {native} for {incoming_amount} {incoming_token} on {market}", + "summary_template_variables": { + "action_type": { + "type": "string", + "value": "Swap" + }, + "outgoing_amount": { + "type": "currency", + "value": "0.3" + }, + "incoming_amount": { + "type": "currency", + "value": "9693.997876398680187001" + }, + "incoming_token": { + "type": "token", + "value": { + "address_hash": "#{token.contract_address_hash}" + } + }, + "rnd_address": { + "type": "address", + "value": { + "hash": "#{address.hash}" + } + }, + "market": { + "type": "dexTag", + "value": { + "name": "Uniswap V3", + "icon": "https://blockscout-content.s3.amazonaws.com/uniswap.png", + "url": "https://uniswap.org/?utm_source=Blockscout" + } + } + } + } + ] + } + } + """ + + Bypass.expect_once( + bypass, + "GET", + "/cache/#{to_string(transaction.hash)}", + fn conn -> + Plug.Conn.resp(conn, 404, "Not Found") + end + ) + + Bypass.expect_once( + bypass, + "POST", + "/transactions/summary", + fn conn -> + Plug.Conn.resp(conn, 200, tx_interpretation_response) + end + ) + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary") + assert response = json_response(request, 200) + + token_json = Enum.at(response["data"]["summaries"], 0)["summary_template_variables"]["incoming_token"]["value"] + assert token_json["address_hash"] == to_string(token.contract_address_hash) + assert token_json["symbol"] == token.symbol + assert token_json["reputation"] == "ok" + + address_json = Enum.at(response["data"]["summaries"], 0)["summary_template_variables"]["rnd_address"]["value"] + assert Map.has_key?(address_json, "ens_domain_name") + assert address_json["hash"] == to_string(address.hash) + + Bypass.down(bypass) + end + + test "success preload template variables when scam token", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + bypass = Bypass.open() + + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: true, + service_url: "http://localhost:#{bypass.port}" + ) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + token = insert(:token) + address = insert(:address) + + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + insert(:scam_badge_to_address, address_hash: address.hash) + + tx_interpretation_response = """ + { + "success": true, + "data": { + "summaries": [ + { + "summary_template": "{action_type} {outgoing_amount} {native} for {incoming_amount} {incoming_token} on {market}", + "summary_template_variables": { + "action_type": { + "type": "string", + "value": "Swap" + }, + "outgoing_amount": { + "type": "currency", + "value": "0.3" + }, + "incoming_amount": { + "type": "currency", + "value": "9693.997876398680187001" + }, + "incoming_token": { + "type": "token", + "value": { + "address_hash": "#{token.contract_address_hash}" + } + }, + "rnd_address": { + "type": "address", + "value": { + "hash": "#{address.hash}" + } + }, + "market": { + "type": "dexTag", + "value": { + "name": "Uniswap V3", + "icon": "https://blockscout-content.s3.amazonaws.com/uniswap.png", + "url": "https://uniswap.org/?utm_source=Blockscout" + } + } + } + } + ] + } + } + """ + + Bypass.expect_once( + bypass, + "GET", + "/cache/#{to_string(transaction.hash)}", + fn conn -> + Plug.Conn.resp(conn, 404, "Not Found") + end + ) + + Bypass.expect_once( + bypass, + "POST", + "/transactions/summary", + fn conn -> + Plug.Conn.resp(conn, 200, tx_interpretation_response) + end + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/summary") + assert response = json_response(request, 200) + + token_json = Enum.at(response["data"]["summaries"], 0)["summary_template_variables"]["incoming_token"]["value"] + dbg(token_json) + assert token_json["address_hash"] == to_string(token.contract_address_hash) + assert token_json["symbol"] == token.symbol + assert token_json["reputation"] == "scam" + + address_json = Enum.at(response["data"]["summaries"], 0)["summary_template_variables"]["rnd_address"]["value"] + assert Map.has_key?(address_json, "ens_domain_name") + assert address_json["hash"] == to_string(address.hash) + assert address_json["reputation"] == "scam" + assert address_json["is_scam"] == true + + Bypass.down(bypass) + end + end + + if @chain_type == :neon do describe "neon linked transactions service" do test "fetches data from the node and caches in the db", %{conn: conn} do transaction = insert(:transaction) diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 2623da10cc35..a692f5d7af62 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -15,7 +15,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, CurrencyHelper, Hash, Token} - alias Explorer.Chain.Address.{Reputation, TokenBalance} + alias Explorer.Chain.Address.TokenBalance @default_paging_options %PagingOptions{page_size: 50} @@ -55,8 +55,6 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do null: false ) - has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) - timestamps() end diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index 69f845b0ba45..d99a2f89717c 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -59,8 +59,6 @@ defmodule Explorer.Chain.AdvancedFilter do has_one(:token_transfer, TokenTransfer, foreign_key: :transaction_hash, references: :hash, null: true) - has_one(:reputation, Reputation, foreign_key: :address_hash, references: :hash) - field(:fee, :decimal) field(:block_number, :integer) @@ -238,8 +236,7 @@ defmodule Explorer.Chain.AdvancedFilter do block_number: token_transfer.block_number, transaction_index: token_transfer.transaction.index, token_transfer_index: token_transfer.log_index, - token_transfer_batch_index: token_transfer.reverse_index_in_batch, - reputation: token_transfer.token.reputation + token_transfer_batch_index: token_transfer.reverse_index_in_batch } end @@ -519,7 +516,7 @@ defmodule Explorer.Chain.AdvancedFilter do |> page_token_transfers(paging_options) token_transfer_query - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> limit_query(paging_options) |> query_function.(false) |> limit_query(paging_options) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index bbf815eb486f..0fa068a8505c 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -552,7 +552,7 @@ defmodule Explorer.Chain.Search do base_query |> apply_sorting([], @token_sorting) |> page_search_results(paging_options, "token") - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_for_search(:contract_address_hash, options) |> select(^(token_search_fields() |> add_reputation_to_search_fields(options))) end @@ -567,7 +567,7 @@ defmodule Explorer.Chain.Search do ) query - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_for_search(:contract_address_hash, options) |> select(^(token_search_fields() |> add_reputation_to_search_fields(options))) end @@ -590,7 +590,7 @@ defmodule Explorer.Chain.Search do ) base_query - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_for_search(:address_hash, options) |> select(^contract_search_fields) |> apply_sorting([], @contract_sorting) |> page_search_results(paging_options, "contract") @@ -755,7 +755,7 @@ defmodule Explorer.Chain.Search do as: :metadata_tag, on: address.hash == tag.address_hash ) - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_for_search(:hash, options) |> select(^(metadata_tags_search_fields() |> add_reputation_to_search_fields(options))) end diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 22ff3eb16237..9b9407285e27 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -1475,7 +1475,7 @@ defmodule Explorer.Chain.SmartContract do end addresses_query - |> ExplorerHelper.maybe_hide_scam_addresses(:hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_with_select(:hash, options) |> SortingHelper.apply_sorting(sorting_options, default_sorting_options) |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting_options) |> Chain.join_associations(necessity_by_association) diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 10e0f1fb1295..57e716584a5c 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -365,7 +365,7 @@ defmodule Explorer.Chain.Token do sorted_paginated_query = Token |> Chain.join_associations(necessity_by_association) - |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses_with_select(:contract_address_hash, options) |> apply_filter(token_type) |> SortingHelper.apply_sorting(sorting, @default_sorting) |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index ebfcd893947f..278bb2ec6ee0 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -66,8 +66,6 @@ defmodule Explorer.Chain.Token.Instance do null: false ) - has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) - timestamps() end @@ -429,7 +427,7 @@ defmodule Explorer.Chain.Token.Instance do distinct_token_instances_count: fragment("COUNT(*)"), token_ids: fragment("array_agg(?)", ctb.token_id) }) - |> ExplorerHelper.maybe_hide_scam_addresses_with_aggregate(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> page_erc_1155_nft_collections(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() @@ -469,7 +467,7 @@ defmodule Explorer.Chain.Token.Instance do distinct_token_instances_count: fragment("COUNT(*)"), token_ids: fragment("array_agg(?)", ctb.token_id) }) - |> ExplorerHelper.maybe_hide_scam_addresses_with_aggregate(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> page_erc_404_nft_collections(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index df73252fa75f..6e7694a7c57f 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -9,7 +9,6 @@ defmodule Explorer.Chain.TokenTransfer.Schema do alias Explorer.Chain.{ Address, - Address.Reputation, Block, Hash, Transaction @@ -102,8 +101,6 @@ defmodule Explorer.Chain.TokenTransfer.Schema do has_one(:token, through: [:token_contract_address, :token]) - has_one(:reputation, Reputation, foreign_key: :address_hash, references: :token_contract_address_hash) - timestamps() unquote_splicing(@transaction_field) @@ -579,7 +576,7 @@ defmodule Explorer.Chain.TokenTransfer do |> join(:inner, [tt], token in assoc(tt, :token), as: :token) |> preload([token: token], [{:token, token}]) |> filter_by_type(token_types) - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> handle_paging_options(paging_options) else to_address_hash_query = @@ -589,7 +586,7 @@ defmodule Explorer.Chain.TokenTransfer do |> filter_by_token_address_hash(token_address_hash) |> filter_by_type(token_types) |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> handle_paging_options(paging_options) |> Chain.wrapped_union_subquery() @@ -600,7 +597,7 @@ defmodule Explorer.Chain.TokenTransfer do |> filter_by_token_address_hash(token_address_hash) |> filter_by_type(token_types) |> order_by([tt], desc: tt.block_number, desc: tt.log_index) - |> ExplorerHelper.maybe_hide_scam_addresses_without_select(:token_contract_address_hash, options) + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) |> handle_paging_options(paging_options) |> Chain.wrapped_union_subquery() diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 0f4eec9da4e5..c6d6a327c9b8 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -254,12 +254,12 @@ defmodule Explorer.Helper do The modified query with scam addresses hidden, if applicable. """ - @spec maybe_hide_scam_addresses(nil | Ecto.Query.t(), atom(), [ + @spec maybe_hide_scam_addresses_with_select(nil | Ecto.Query.t(), atom(), [ Chain.paging_options() | Chain.api?() | Chain.show_scam_tokens?() ]) :: Ecto.Query.t() - def maybe_hide_scam_addresses(nil, _address_hash_key, _options), do: nil + def maybe_hide_scam_addresses_with_select(nil, _address_hash_key, _options), do: nil - def maybe_hide_scam_addresses(query, address_hash_key, options) do + def maybe_hide_scam_addresses_with_select(query, address_hash_key, options) do cond do Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> query @@ -285,12 +285,12 @@ defmodule Explorer.Helper do @doc """ Conditionally hides scam addresses in the given query, does not select the reputation field. """ - @spec maybe_hide_scam_addresses_without_select(nil | Ecto.Query.t(), atom(), [ + @spec maybe_hide_scam_addresses(nil | Ecto.Query.t(), atom(), [ Chain.paging_options() | Chain.api?() | Chain.show_scam_tokens?() ]) :: Ecto.Query.t() - def maybe_hide_scam_addresses_without_select(nil, _address_hash_key, _options), do: nil + def maybe_hide_scam_addresses(nil, _address_hash_key, _options), do: nil - def maybe_hide_scam_addresses_without_select(query, address_hash_key, options) do + def maybe_hide_scam_addresses(query, address_hash_key, options) do cond do Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> query @@ -299,7 +299,6 @@ defmodule Explorer.Helper do Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> query - |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) true -> query @@ -307,39 +306,26 @@ defmodule Explorer.Helper do end @doc """ - Conditionally hides scam addresses in the given query with aggregate functions. + Conditionally hides scam addresses in the given query, does not select the reputation field. """ - @spec maybe_hide_scam_addresses_with_aggregate(nil | Ecto.Query.t(), atom(), [ + @spec maybe_hide_scam_addresses_for_search(nil | Ecto.Query.t(), atom(), [ Chain.paging_options() | Chain.api?() | Chain.show_scam_tokens?() ]) :: Ecto.Query.t() - def maybe_hide_scam_addresses_with_aggregate(nil, _address_hash_key, _options), do: nil + def maybe_hide_scam_addresses_for_search(nil, _address_hash_key, _options), do: nil - def maybe_hide_scam_addresses_with_aggregate(query, address_hash_key, options) do + def maybe_hide_scam_addresses_for_search(query, address_hash_key, options) do cond do Application.get_env(:block_scout_web, :hide_scam_addresses) && !options[:show_scam_tokens?] -> query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) |> where([sabm: sabm], is_nil(sabm.address_hash)) - |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) Application.get_env(:block_scout_web, :hide_scam_addresses) && options[:show_scam_tokens?] -> query |> join(:left, [q], sabm in ScamBadgeToAddress, as: :sabm, on: sabm.address_hash == field(q, ^address_hash_key)) - |> select_merge([q, sabm: sabm], %{ - reputation: %Reputation{ - reputation: - fragment( - "CASE WHEN MAX(CASE WHEN ? THEN 0 ELSE 1 END) = 0 THEN ? ELSE ? END", - is_nil(sabm.address_hash), - "ok", - "scam" - ) - } - }) true -> query - |> select_merge([q], %{reputation: %Reputation{reputation: "ok"}}) end end diff --git a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex index d85faf2fa9c2..3e5c11fd7975 100644 --- a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex +++ b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex @@ -125,7 +125,6 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do :token, :transaction, :token_instance, - :reputation, :__meta__ ]) end) @@ -161,7 +160,6 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do :is_unique, :owner, :token, - :reputation, :__meta__ ]) end) From 62a1bca0c3ace6618386aa060e4ae6ed9e7079f3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 07:49:59 +0300 Subject: [PATCH 0340/1006] 9.1.1 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .../workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .../workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .../workflows/publish-docker-image-for-optimism.yml | 2 +- .../workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-suave.yml | 2 +- .../workflows/publish-docker-image-for-zetachain.yml | 2 +- .../workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../publish-docker-image-staging-on-demand.yml | 2 +- .../publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-suave.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 11 ++++++++++- apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 62 files changed, 71 insertions(+), 62 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index a3b8cb7ac4d6..4e6ba403c2a0 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 2fa9b5893b5c..943f6f9c05eb 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 2a198ad618e9..7e0d7e93ccba 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index d185fecf6815..2b79a1c1ce64 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 8db8e199ab0f..ad8117904960 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 846b9b2e8299..f2eb1187704e 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 5c4d73b49fcc..f5afcd02d2ac 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index d29cfe78d580..09314e950cef 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index b0445fbca73e..7c8aaf8745f5 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index fddb88a6c987..24d629962b62 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 70a209655844..b815fbdf180d 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 6c307ed3b8d8..b79a6a6e9cf6 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 84157a461218..d2ae5ff34532 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 2bf407689e3b..70d7c75f52a5 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index f45baf845a8a..527c856e4af5 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 4beeb74c2ab1..b091aa0fbe0b 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index f4778db7b069..6dca0b9b7d8e 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index d7343c52ab45..c022961dcce6 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index ff8fa48e8a2b..d70231efd308 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 372060c8dc60..ce1eb4e004c5 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 1f93285c2273..9ee91816faaf 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 63276b277b0c..aa0ae3543056 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index d160636dffcd..03a1b70a3e90 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 2720a06bd5df..40c23052eaa3 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 353912dec0e5..f3db7773e859 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index c6b4d07e5cdb..d7f760887046 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index ad12dd7a5c53..264a6096b6d0 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 4431a37725fe..85d2c8dcfc64 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml index 2f8a7e0509ec..54acde97ea4f 100644 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ b/.github/workflows/publish-docker-image-for-suave.yml @@ -17,7 +17,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: suave steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index 2035625de910..1b37fe779906 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 5cc1a8bf62d3..197283d09a99 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 8ee44fbc2b51..ec85a128e6cb 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 010f13a7c02e..8bf7c349f0e7 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 911e8a30e823..8fcff4f55dee 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index bd89045e48eb..52c5542c5b8d 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 4f4996f0e9c7..396ef86962c1 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 94d22807654a..2b8b288fe7f5 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index dcd3b1cec778..6545043871b2 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 6f65f1295e90..4084939f7ad4 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 7e0b07664c95..c713198e39f2 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 18b31b794483..398c4ef1fa4a 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index de19190fa275..2dc3e7ccae8b 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 9ca4c5edc812..84bcef7894a9 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index ac0adfc45259..29bc6823aa89 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index e8d338678d98..11cfaf5b15c6 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 409dff0afaa4..61240e588271 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index f054013c8c78..6fc570c2a9e1 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml index 3c7c125feb1d..d1b39430f100 100644 --- a/.github/workflows/release-suave.yml +++ b/.github/workflows/release-suave.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index bd401f9d5544..13c0584be6d3 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 9dc71c98f1f6..2c7a48273809 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 2ed8acb4f8a7..f2126fb38a28 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.0 + RELEASE_VERSION: 9.1.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 447156084a78..2365f71d2315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 9.1.1 + +### 🚀 Features + +- Auto assert_schema in tests ([#13029](https://github.com/blockscout/blockscout/issues/13029)) + +### ⚙️ Miscellaneous Tasks + +- Refactoring reputation ([#13221](https://github.com/blockscout/blockscout/issues/13221)) + ## 9.1.0 ### 🚀 Features @@ -105,7 +115,6 @@ | `INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_REFERENCE_TIMESTAMP` | Any past finalized Beacon Chain epoch UTC timestamp. Used as reference for status fetcher scheduling. Implemented in [#12985](https://github.com/blockscout/blockscout/pull/12985). | Version: v9.1.0+
Default: `1722024023`
Applications: Indexer | - ### Deprecated ENV variables | Variable | Required | Description | Default | Version | Need recompile | Deprecated in Version | diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index dfbb0cc270c4..9e6004d4394e 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.0", + version: "9.1.1", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 39a8d22e539e..4287614f0363 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.0" + version: "9.1.1" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index ff15b049b5da..98aa18aba1d3 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.1.0"}] + @default_headers [{"User-Agent", "blockscout-9.1.1"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 697ec2bb4519..f2c99e675cd6 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.0", + version: "9.1.1", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 5216047ae271..6390230a8dd1 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.1.0", + version: "9.1.1", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 0386675c2aaf..998415b6c4b3 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.1.0", + version: "9.1.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 30dab7424acd..f59093919e3b 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.1.0", + version: "9.1.1", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 98f767decf38..83e27f936fea 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.1.0' +RELEASE_VERSION ?= '9.1.1' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 7e46f36c36a9..e3d581a7a6bf 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.1.0", + version: "9.1.1", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 530794a9bf20..c04aeb19e51c 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.1.0" + set version: "9.1.1" set applications: [ :runtime_tools, block_scout_web: :permanent, From 518d945c439914b2336ae643b3821d2fd21f160c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 13:14:48 +0300 Subject: [PATCH 0341/1006] fix: Remove requirement for beacon deposit indexes to be sequential (#13228) --- CHANGELOG.md | 4 ++ .../lib/indexer/fetcher/beacon/deposit.ex | 59 ++++++++++--------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2365f71d2315..90d0737e2e13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ - Auto assert_schema in tests ([#13029](https://github.com/blockscout/blockscout/issues/13029)) +### 🐛 Bug Fixes + +- Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) + ### ⚙️ Miscellaneous Tasks - Refactoring reputation ([#13221](https://github.com/blockscout/blockscout/issues/13221)) diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index 37647f490543..942a6a6dff40 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -141,40 +141,45 @@ defmodule Indexer.Fetcher.Beacon.Deposit do ) |> Enum.map(&log_to_deposit/1) + # todo: sequential? check is removed as a hard requirement for deposits indexing + # since block ranges are found where node doesn't return deposits + # thus making the deposit index sequence non-sequential. + # We need a separate process which will monitor missed deposits + # after we check the nature of those missing deposit indexes. case sequential?(deposit_index, deposits) do {:error, prev, curr} -> Logger.error("Non-sequential deposits detected: #{inspect(prev)} followed by #{inspect(curr)}") - Process.send_after(self(), :process_logs, interval * 10) - {:noreply, state} _ -> - {deposits_count, _} = - Repo.insert_all(Deposit, set_status(deposits, domain_deposit, genesis_fork_version), - on_conflict: :replace_all, - conflict_target: [:index] - ) - - if deposits_count < batch_size do - Process.send_after(self(), :process_logs, interval) - else - Process.send(self(), :process_logs, []) - end + :ok + end + + {deposits_count, _} = + Repo.insert_all(Deposit, set_status(deposits, domain_deposit, genesis_fork_version), + on_conflict: :replace_all, + conflict_target: [:index] + ) - last_deposit = - List.last(deposits, %{ - index: state.deposit_index, - block_number: state.last_processed_log_block_number, - log_index: state.last_processed_log_index - }) - - {:noreply, - %__MODULE__{ - state - | deposit_index: last_deposit.index, - last_processed_log_block_number: last_deposit.block_number, - last_processed_log_index: last_deposit.log_index - }} + if deposits_count < batch_size do + Process.send_after(self(), :process_logs, interval) + else + Process.send(self(), :process_logs, []) end + + last_deposit = + List.last(deposits, %{ + index: state.deposit_index, + block_number: state.last_processed_log_block_number, + log_index: state.last_processed_log_index + }) + + {:noreply, + %__MODULE__{ + state + | deposit_index: last_deposit.index, + last_processed_log_block_number: last_deposit.block_number, + last_processed_log_index: last_deposit.log_index + }} end @abi ABI.parse_specification( From af5fbfaa0924081d5a8290f064ae6f895aedb991 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 14:01:21 +0300 Subject: [PATCH 0342/1006] fix: Fix logic of checking heavy DB index operation is finished (#13231) --- CHANGELOG.md | 1 + .../migrator/heavy_db_index_operation.ex | 26 +++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d0737e2e13..c4edb924e8c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Bug Fixes +- Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) - Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) ### ⚙️ Miscellaneous Tasks diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex index 2b154dfa3792..230b6d9a99ab 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex @@ -166,31 +166,25 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation do {:stop, :normal, state} migration_status -> - Process.send(self(), :check_if_db_operation_need_to_be_started, []) + Process.send(self(), :check_db_index_operation_progress, []) {:noreply, state} end end - @impl true - def handle_info(:check_if_db_operation_need_to_be_started, state) do - if db_operation_is_ready_to_start?() do - Process.send(self(), :check_db_index_operation_progress, []) - else - schedule_next_db_operation_readiness_check() - end - - {:noreply, state} - end - @impl true def handle_info(:check_db_index_operation_progress, state) do with {:index_operation_progress, status} when status in [:finished_or_not_started, :finished] <- {:index_operation_progress, check_db_index_operation_progress()}, {:db_index_operation_status, :not_initialized} <- {:db_index_operation_status, db_index_operation_status()} do - MigrationStatus.set_status(migration_name(), "started") - db_index_operation() - schedule_next_db_operation_status_check() + if db_operation_is_ready_to_start?() do + MigrationStatus.set_status(migration_name(), "started") + db_index_operation() + schedule_next_db_operation_status_check() + else + schedule_next_db_operation_readiness_check() + end + {:noreply, state} else {:index_operation_progress, _status} -> @@ -253,7 +247,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation do defp schedule_next_db_operation_readiness_check(timeout \\ nil) do Process.send_after( self(), - :check_if_db_operation_need_to_be_started, + :check_db_index_operation_progress, timeout || HeavyDbIndexOperationHelper.get_check_interval() ) end From 5d556e19eed23b26c34cd98da295c7f14e2eb9bf Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 15:02:42 +0300 Subject: [PATCH 0343/1006] perf: Improve perf of internal transactions retrieval from the DB (#13232) * perf: Improve perf of internal transactions retrieval from the DB, when pending_block_operations table has huge amount of rows * Add CHANGELOG entry * Add CHANGELOG entry --- CHANGELOG.md | 4 +++ .../explorer/chain/internal_transaction.ex | 27 +++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4edb924e8c9..0ea76e204508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ - Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) - Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) +### ⚡ Performance + +- Improve perf of internal transactions retrieval from the DB ([#13232](https://github.com/blockscout/blockscout/pull/13232)) + ### ⚙️ Miscellaneous Tasks - Refactoring reputation ([#13221](https://github.com/blockscout/blockscout/issues/13221)) diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index fcac0709fd7e..eb4ac7466714 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -6,6 +6,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral + alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{CallType, Type} @@ -580,11 +581,27 @@ defmodule Explorer.Chain.InternalTransaction do of internal_transactions """ def where_nonpending_block(query \\ nil) do - (query || __MODULE__) - |> where( - [it], - fragment("NOT EXISTS (SELECT 1 FROM pending_block_operations WHERE block_hash = ?)", it.block_hash) - ) + estimated_pbo_count = CacheCountersHelper.estimated_count_from("pending_block_operations") || 0 + + # NOT EXISTS query practically performs better on smaller tables + # while the indexed LEFT JOIN IS NULL query is better for larger tables + # The estimated count is used as a heuristic to decide which query to use + if estimated_pbo_count < 1_000 do + (query || __MODULE__) + |> where( + [it], + fragment("NOT EXISTS (SELECT 1 FROM pending_block_operations WHERE block_hash = ?)", it.block_hash) + ) + else + (query || __MODULE__) + |> where( + [it], + fragment( + "(SELECT block_hash FROM pending_block_operations WHERE block_hash = ? LIMIT 1) IS NULL", + it.block_hash + ) + ) + end end @doc """ From 63460d82b93f4fbc386efb14c3d25f0eadc1b7af Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 15:46:36 +0300 Subject: [PATCH 0344/1006] Add pre-release workflow for Gnosis Chain --- .github/workflows/pre-release-gnosis.yml | 69 ++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/pre-release-gnosis.yml diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml new file mode 100644 index 000000000000..a9e660db47b4 --- /dev/null +++ b/.github/workflows/pre-release-gnosis.yml @@ -0,0 +1,69 @@ +name: Pre-release for Gnosis Chain + +on: + workflow_dispatch: + inputs: + number: + type: number + description: Number of pre-release alpha iteration + required: true + +permissions: + contents: read + packages: write + +env: + OTP_VERSION: ${{ vars.OTP_VERSION }} + ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.1.1 + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image for Gnosis Chain (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum + + - name: Build and push Docker image for Gnosis Chain (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-xdai-private:${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + BRIDGED_TOKENS_ENABLED=true + CHAIN_TYPE=ethereum From d7e78147a1a27050f92aafe0771b6fa57429681a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 19:12:13 +0300 Subject: [PATCH 0345/1006] fix: Soften deposits deletion condition (#13234) --- CHANGELOG.md | 1 + apps/indexer/lib/indexer/fetcher/beacon/deposit.ex | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ea76e204508..c08848378891 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Bug Fixes +- Soften deposits deletion condition ([#13234](https://github.com/blockscout/blockscout/pull/13234)) - Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) - Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index 942a6a6dff40..895a63e237ca 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -93,9 +93,16 @@ defmodule Indexer.Fetcher.Beacon.Deposit do @impl GenServer def handle_cast({:lost_consensus, block_number}, %__MODULE__{} = state) do + max_reorg_depth_block_number = block_number + 64 + {_deleted_deposits_count, deleted_deposits} = Repo.delete_all( - from(d in Deposit, where: d.block_number >= ^block_number, select: d.index), + from( + d in Deposit, + where: d.block_number > ^block_number, + where: d.block_number <= ^max_reorg_depth_block_number, + select: d.index + ), timeout: :infinity ) From 269dbaf45cf4b753c2cc34c4c59ba814c24bc5c6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Sep 2025 21:49:40 +0300 Subject: [PATCH 0346/1006] chore: Do not modify deposit indexer state on reorgs (#13236) --- CHANGELOG.md | 1 + .../lib/indexer/fetcher/beacon/deposit.ex | 20 ++++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c08848378891..7aace2e46cfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### ⚙️ Miscellaneous Tasks +- Do not modify deposit indexer state on reorgs ([#13236](https://github.com/blockscout/blockscout/pull/13236)) - Refactoring reputation ([#13221](https://github.com/blockscout/blockscout/issues/13221)) ## 9.1.0 diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index 895a63e237ca..f6a74b72ad4d 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -106,15 +106,17 @@ defmodule Indexer.Fetcher.Beacon.Deposit do timeout: :infinity ) - deposit_index = Enum.min(deleted_deposits, fn -> state.deposit_index + 1 end) - - {:noreply, - %{ - state - | deposit_index: deposit_index - 1, - last_processed_log_block_number: block_number, - last_processed_log_index: -1 - }} + # todo: temporarily do not modify state of the indexer on reorgs. + # It should be handled by a separate process. + # deposit_index = Enum.min(deleted_deposits, fn -> state.deposit_index + 1 end) + # {:noreply, + # %{ + # state + # | deposit_index: deposit_index - 1, + # last_processed_log_block_number: block_number, + # last_processed_log_index: -1 + # }} + {:noreply, state} rescue postgrex_error in Postgrex.Error -> Logger.error( From 491189a4eeeed41e76a6486b87ede08bac763bb3 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:43:12 +0300 Subject: [PATCH 0347/1006] fix: Add reputation preload to state changes and bridged tokens (#13235) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + .../controllers/api/rpc/token_controller.ex | 6 +- .../models/transaction_state_helper.ex | 2 + .../api/v2/token_controller_test.exs | 174 +++++++++++++++++ .../api/v2/transaction_controller_test.exs | 184 +++++++++++++++++- .../lib/explorer/chain/bridged_token.ex | 3 +- 6 files changed, 366 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aace2e46cfb..a235872c0db5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Bug Fixes +- Add reputation preload to state changes and bridged tokens ([#13235](https://github.com/blockscout/blockscout/pull/13235)) - Soften deposits deletion condition ([#13234](https://github.com/blockscout/blockscout/pull/13234)) - Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) - Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex index f01730b48763..5d2bce55601d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/token_controller.ex @@ -65,8 +65,9 @@ defmodule BlockScoutWeb.API.RPC.TokenController do paging_options: 1 ] + # credo:disable-for-lines:2 Credo.Check.Design.AliasUsage bridged_tokens = - if BridgedToken.enabled?() do + if Explorer.Chain.BridgedToken.enabled?() do options = params |> paging_options() @@ -74,7 +75,8 @@ defmodule BlockScoutWeb.API.RPC.TokenController do |> Keyword.merge(tokens_sorting(params)) |> Keyword.merge(@api_true) - "" |> BridgedToken.list_top_bridged_tokens(options) + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + "" |> Explorer.Chain.BridgedToken.list_top_bridged_tokens(options) else [] end diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 7b7d05777fe0..f774c3ef0c48 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do Transaction state changes related functions """ + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] import Explorer.PagingOptions, only: [default_paging_options: 0] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] @@ -70,6 +71,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do |> Enum.find(&(&1.hash == transaction.hash)) |> Repo.preload( token_transfers: [ + token: reputation_association(), from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()], to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()] ], diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index e46801ac0498..bf202c19bc96 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -2,8 +2,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do use EthereumJSONRPC.Case, async: false use BlockScoutWeb.ConnCase use BlockScoutWeb.ChannelCase, async: false + use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] import Mox + import Ecto.Query, only: [from: 2] alias Explorer.{Repo, TestHelper} @@ -2483,4 +2485,176 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert second_page_resp["next_page_params"] == nil compare_holders_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) end + + if @bridged_tokens_enabled do + describe "/tokens/bridged" do + test "returns empty list when no bridged tokens", %{conn: conn} do + request = get(conn, "/api/v2/tokens/bridged") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + + test "returns bridged tokens list", %{conn: conn} do + # Create a token + token = insert(:token, %{total_supply: 1000}) + + # Update token to set bridged flag directly in the database + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token.contract_address_hash), + set: [bridged: true] + ) + + # Create a bridged token record + {:ok, _bridged_token} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token.contract_address_hash, + foreign_chain_id: 1, + foreign_token_contract_address_hash: build(:address).hash, + type: "omni", + exchange_rate: Decimal.new("1.5") + }) + + request = get(conn, "/api/v2/tokens/bridged") + + assert response = json_response(request, 200) + assert %{"items" => items, "next_page_params" => _} = response + assert length(items) == 1 + + item = List.first(items) + assert item["address_hash"] == Address.checksum(token.contract_address_hash) + assert item["name"] == token.name + assert item["symbol"] == token.symbol + assert item["bridge_type"] == "omni" + assert item["origin_chain_id"] == "1" + assert is_binary(item["foreign_address"]) + end + + test "filters bridged tokens by search query", %{conn: conn} do + # Create first token + token1 = insert(:token, %{total_supply: 1000, name: "TestToken", symbol: "TEST"}) + + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token1.contract_address_hash), + set: [bridged: true] + ) + + {:ok, _bridged_token1} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token1.contract_address_hash, + foreign_chain_id: 1, + foreign_token_contract_address_hash: build(:address).hash, + type: "omni" + }) + + # Create second token with different name + token2 = insert(:token, %{total_supply: 2000, name: "OtherToken", symbol: "OTHER"}) + + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token2.contract_address_hash), + set: [bridged: true] + ) + + {:ok, _bridged_token2} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token2.contract_address_hash, + foreign_chain_id: 1, + foreign_token_contract_address_hash: build(:address).hash, + type: "amb" + }) + + # Search for "Test" + request = get(conn, "/api/v2/tokens/bridged", %{"q" => "Test"}) + + assert response = json_response(request, 200) + assert %{"items" => items} = response + assert length(items) == 1 + + item = List.first(items) + assert item["name"] == "TestToken" + assert item["symbol"] == "TEST" + end + + test "filters bridged tokens by chain ids", %{conn: conn} do + # Create tokens on different chains + token1 = insert(:token, %{total_supply: 1000}) + + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token1.contract_address_hash), + set: [bridged: true] + ) + + {:ok, _bridged_token1} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token1.contract_address_hash, + foreign_chain_id: 1, + foreign_token_contract_address_hash: build(:address).hash, + type: "omni" + }) + + token2 = insert(:token, %{total_supply: 2000}) + + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token2.contract_address_hash), + set: [bridged: true] + ) + + {:ok, _bridged_token2} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token2.contract_address_hash, + foreign_chain_id: 56, + foreign_token_contract_address_hash: build(:address).hash, + type: "amb" + }) + + # Filter by chain id 1 + request = get(conn, "/api/v2/tokens/bridged", %{"chain_ids" => "1"}) + + assert response = json_response(request, 200) + assert %{"items" => items} = response + assert length(items) == 1 + + item = List.first(items) + assert item["origin_chain_id"] == "1" + end + + test "supports pagination", %{conn: conn} do + # Create 51 bridged tokens to trigger pagination (default page size is 50) + _tokens = + for i <- 1..51 do + token = insert(:token, %{total_supply: 1000 * i, name: "Token#{i}", symbol: "T#{i}"}) + + Explorer.Repo.update_all( + from(t in Explorer.Chain.Token, where: t.contract_address_hash == ^token.contract_address_hash), + set: [bridged: true] + ) + + {:ok, _bridged_token} = + Explorer.Repo.insert(%Explorer.Chain.BridgedToken{ + home_token_contract_address_hash: token.contract_address_hash, + foreign_chain_id: 1, + foreign_token_contract_address_hash: build(:address).hash, + type: "omni" + }) + + token + end + + # Test first page (should have 50 items and next_page_params) + request = get(conn, "/api/v2/tokens/bridged") + + assert response = json_response(request, 200) + assert %{"items" => items, "next_page_params" => next_page_params} = response + assert length(items) == 50 + assert next_page_params != nil + + # Test second page (should have 1 item and no next_page_params) + request = get(conn, "/api/v2/tokens/bridged", next_page_params) + + assert response = json_response(request, 200) + assert %{"items" => items, "next_page_params" => next_page_params} = response + assert length(items) == 1 + assert next_page_params == nil + end + end + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 548676a4a439..17e085f34593 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -1546,6 +1546,189 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert response = json_response(request, 200) assert Enum.count(response["items"]) == 5 end + + test "return state changes with token transfers and verify token is correctly loaded", %{conn: conn} do + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + token = insert(:token, type: "ERC-20", symbol: "TEST", name: "Test Token") + from_address = insert(:address) + to_address = insert(:address) + + # Create token transfer + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_contract_address_hash: token.contract_address_hash, + from_address: from_address, + from_address_hash: from_address.hash, + to_address: to_address, + to_address_hash: to_address.hash, + amount: Decimal.new(100), + token: token, + token_ids: nil + ) + + # Set up coin balances for transaction participants + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + # Set up token balances + insert(:address_current_token_balance, + address: from_address, + address_hash: from_address.hash, + token_contract_address_hash: token.contract_address_hash, + block_number: block_before.number, + value: Decimal.new(1000) + ) + + insert(:address_current_token_balance, + address: to_address, + address_hash: to_address.hash, + token_contract_address_hash: token.contract_address_hash, + block_number: block_before.number, + value: Decimal.new(0) + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert is_list(response["items"]) + + # Find the token state changes (should have at least from and to addresses) + token_state_changes = Enum.filter(response["items"], fn item -> item["type"] == "token" end) + assert length(token_state_changes) >= 2 + + # Verify token information is properly loaded in at least one state change + token_state_change = Enum.find(token_state_changes, fn item -> not is_nil(item["token"]) end) + assert token_state_change + + token_data = token_state_change["token"] + assert token_data["symbol"] == "TEST" + assert token_data["name"] == "Test Token" + assert token_data["type"] == "ERC-20" + assert token_data["address_hash"] == to_string(token.contract_address) + assert token_data["reputation"] == "ok" + end + + test "return state changes with scam token reputation properly set", %{conn: conn} do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + + block_before = insert(:block) + + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + # Create a token + token = insert(:token, type: "ERC-20", symbol: "SCAM", name: "Scam Token") + from_address = insert(:address) + to_address = insert(:address) + + # Create scam badge for the token address to mark it as scam + insert(:scam_badge_to_address, address_hash: token.contract_address_hash) + + # Create token transfer + insert(:token_transfer, + transaction: transaction, + block: transaction.block, + block_number: transaction.block_number, + token_contract_address: token.contract_address, + token_contract_address_hash: token.contract_address_hash, + from_address: from_address, + from_address_hash: from_address.hash, + to_address: to_address, + to_address_hash: to_address.hash, + amount: Decimal.new(100), + token: token, + token_ids: nil + ) + + # Set up coin balances for transaction participants + insert(:address_coin_balance, + address: transaction.from_address, + address_hash: transaction.from_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.to_address, + address_hash: transaction.to_address_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + insert(:address_coin_balance, + address: transaction.block.miner, + address_hash: transaction.block.miner_hash, + block_number: block_before.number, + value: %Wei{value: Decimal.new(1000)} + ) + + # Set up token balances + insert(:address_current_token_balance, + address: from_address, + address_hash: from_address.hash, + token_contract_address_hash: token.contract_address_hash, + block_number: block_before.number, + value: Decimal.new(1000) + ) + + insert(:address_current_token_balance, + address: to_address, + address_hash: to_address.hash, + token_contract_address_hash: token.contract_address_hash, + block_number: block_before.number, + value: Decimal.new(0) + ) + + request = get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes") + + assert response = json_response(request, 200) + assert is_list(response["items"]) + + # Find the token state changes + token_state_changes = Enum.filter(response["items"], fn item -> item["type"] == "token" end) + assert length(token_state_changes) >= 2 + + # Verify that the token has scam reputation + token_state_change = Enum.find(token_state_changes, fn item -> not is_nil(item["token"]) end) + assert token_state_change + + token_data = token_state_change["token"] + assert token_data["reputation"] == "scam" + assert token_data["symbol"] == "SCAM" + assert token_data["name"] == "Scam Token" + assert token_data["address_hash"] == to_string(token.contract_address) + end end if Application.compile_env(:explorer, :chain_type) == :celo do @@ -2731,7 +2914,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert response = json_response(request, 200) token_json = Enum.at(response["data"]["summaries"], 0)["summary_template_variables"]["incoming_token"]["value"] - dbg(token_json) assert token_json["address_hash"] == to_string(token.contract_address_hash) assert token_json["symbol"] == token.symbol assert token_json["reputation"] == "scam" diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex index 0cd88e5c7e7f..d82d8c9fd26f 100644 --- a/apps/explorer/lib/explorer/chain/bridged_token.ex +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -6,6 +6,7 @@ defmodule Explorer.Chain.BridgedToken do import Ecto.Changeset import EthereumJSONRPC, only: [json_rpc: 2] + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] import Ecto.Query, only: [ @@ -943,7 +944,7 @@ defmodule Explorer.Chain.BridgedToken do where: t.total_supply > ^0, where: t.bridged, select: {t, bt}, - preload: [:contract_address] + preload: [:contract_address, ^reputation_association()] ) base_query_with_paging = From 9a9beb6f9a8b443a644baf733f095d7e14291b6b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 18 Sep 2025 14:56:39 +0300 Subject: [PATCH 0348/1006] fix: Fix condition in Indexer.Fetcher.OnDemand.TokenTotalSupply fetcher (#13240) --- CHANGELOG.md | 1 + .../lib/indexer/fetcher/on_demand/token_total_supply.ex | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a235872c0db5..fce8332a15c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Bug Fixes +- Fix condition in Indexer.Fetcher.OnDemand.TokenTotalSupply fetcher ([#13240](https://github.com/blockscout/blockscout/pull/13240)) - Add reputation preload to state changes and bridged tokens ([#13235](https://github.com/blockscout/blockscout/pull/13235)) - Soften deposits deletion condition ([#13234](https://github.com/blockscout/blockscout/pull/13234)) - Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex index 0b9f14c76dc1..ca1406b71253 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_total_supply.ex @@ -49,7 +49,8 @@ defmodule Indexer.Fetcher.OnDemand.TokenTotalSupply do token = Repo.replica().get_by(Token, contract_address_hash: address_hash) if (token && !token.skip_metadata && is_nil(token.total_supply_updated_at_block)) or - BlockNumber.get_max() - token.total_supply_updated_at_block > @ttl_in_blocks do + (token && !is_nil(token.total_supply_updated_at_block) && + BlockNumber.get_max() - token.total_supply_updated_at_block > @ttl_in_blocks) do token_address_hash_string = to_string(address_hash) token_params = MetadataRetriever.get_total_supply_of(token_address_hash_string) From 72bc9cfea5f29a9a0cc7c25f5e02e86d130ac1b1 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 18 Sep 2025 14:58:29 +0300 Subject: [PATCH 0349/1006] fix: Add reputation preload for user op body for transaction interpreter (#13241) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + .../proxy/account_abstraction_controller.ex | 2 + .../transaction_interpretation.ex | 2 +- .../account_abstraction_controller_test.exs | 242 ++++++++++++++++++ apps/explorer/test/support/factory.ex | 34 ++- 5 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index fce8332a15c5..acb7b185871a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Bug Fixes +- Add reputation preload for user op body for transaction interpreter ([#13241](https://github.com/blockscout/blockscout/pull/13241)) - Fix condition in Indexer.Fetcher.OnDemand.TokenTotalSupply fetcher ([#13240](https://github.com/blockscout/blockscout/pull/13240)) - Add reputation preload to state changes and bridged tokens ([#13235](https://github.com/blockscout/blockscout/pull/13235)) - Soften deposits deletion condition ([#13234](https://github.com/blockscout/blockscout/pull/13234)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex index 22eab9056945..072baefea963 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex @@ -27,6 +27,8 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do {:error | :format | :transaction_interpreter_enabled | non_neg_integer(), any()} | Plug.Conn.t() def summary(conn, %{"operation_hash_param" => operation_hash_string, "just_request_body" => "true"}) do with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_full_hash(operation_hash_string)}, + {:transaction_interpreter_enabled, true} <- + {:transaction_interpreter_enabled, TransactionInterpretationService.enabled?()}, {200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do conn |> json(TransactionInterpretationService.get_user_op_request_body(user_op)) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 1be80e35a387..1acc2fe60dac 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -287,7 +287,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do necessity_by_association: %{ [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, - :token => :optional + [token: reputation_association()] => :optional } ] |> Keyword.merge(@api_true) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs new file mode 100644 index 000000000000..4e623dc253e2 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs @@ -0,0 +1,242 @@ +defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionControllerTest do + use BlockScoutWeb.ConnCase + use EthereumJSONRPC.Case, async: false + + # Helper function to create complete user operation data + defp create_user_op_data(operation_hash, block_hash, transaction_hash) do + %{ + "hash" => operation_hash, + "sender" => "0xc9f2b9AF320D92A7c9CD67BBbF0f3055F81B6d31", + "nonce" => "0x000000000000000000000000000000000000000000000001000000000000042e", + "call_data" => + "0xb61d27f600000000000000000000000047c4442562280196b54c640acd3af9f45c981f0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064541c9e4e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020c5561301aaa52dbc9fdcbf9fdae2ea1a929207270c909f2b6248a3bc80b042b200000000000000000000000000000000000000000000000000000000", + "call_gas_limit" => "633880", + "verification_gas_limit" => "34721", + "pre_verification_gas" => "48192", + "max_fee_per_gas" => "220000000", + "max_priority_fee_per_gas" => "2829647646", + "signature" => + "0xff002c1dc16510d46e607fc4f05f0bb3fe73b1e8102b6b982fcfd2d0d1eed241d69206ffa9d4914417a679fbfe89b36259c7b96d7f84239e5037d9e8cc5456afab131c", + "raw" => %{ + "nonce" => "18446744073709552686", + "pre_verification_gas" => "48192", + "paymaster_and_data" => + "0x2cc0c7981d846b9f2a16276556f6e8cb52bfb6330000000000000000000000000000788f00000000000000000000000000000000000000000000000068c81c5dc97f5c2402d4193ba68f29746742a77fad9571c3bb52056a877a98e369feab6c13f091314a3db58e01de16f79e459269577a54b17a7764f2728180b00d3a37281b", + "signature" => + "0xff002c1dc16510d46e607fc4f05f0bb3fe73b1e8102b6b982fcfd2d0d1eed241d69206ffa9d4914417a679fbfe89b36259c7b96d7f84239e5037d9e8cc5456afab131c", + "gas_fees" => "0x0000000000000000000000000d1cef00000000000000000000000000a8a8ff1e", + "account_gas_limits" => "0x000000000000000000000000000087a10000000000000000000000000009ac18", + "init_code" => "0x", + "call_data" => + "0xb61d27f600000000000000000000000047c4442562280196b54c640acd3af9f45c981f0c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000064541c9e4e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020c5561301aaa52dbc9fdcbf9fdae2ea1a929207270c909f2b6248a3bc80b042b200000000000000000000000000000000000000000000000000000000", + "sender" => "0xc9f2b9AF320D92A7c9CD67BBbF0f3055F81B6d31" + }, + "aggregator" => nil, + "aggregator_signature" => nil, + "entry_point" => "0x0000000071727De22E5E9d8BAf0edAc6f37da032", + "entry_point_version" => "v0.7", + "transaction_hash" => transaction_hash || "0xe0f96d979a2610a89a642744124e7caa087fe4771092286b763ea0d963a73fca", + "block_number" => "9208931", + "block_hash" => block_hash || "0xfe288e8d52d3148bda81194b9767e82d2238303a8808a1331b865cbb35f8bb35", + "bundler" => "0x92613ef2DF071255D6ccd554651e7e445e939A32", + "bundle_index" => 0, + "index" => 0, + "factory" => nil, + "paymaster" => "0x2cc0c7981D846b9F2a16276556f6e8cb52BfB633", + "status" => true, + "revert_reason" => nil, + "gas" => "747656", + "gas_price" => "1067209413", + "gas_used" => "416893", + "sponsor_type" => "paymaster_sponsor", + "user_logs_start_index" => 0, + "user_logs_count" => 100, + "fee" => "444912133813809", + "consensus" => true, + "timestamp" => "2025-09-15T13:52:12.000000Z", + "execute_target" => "0x47C4442562280196b54c640acD3AF9F45c981F0C", + "execute_call_data" => + "0x541c9e4e00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000020c5561301aaa52dbc9fdcbf9fdae2ea1a929207270c909f2b6248a3bc80b042b2" + } + end + + describe "/proxy/account-abstraction/operations/{operation_hash}/summary?just_request_body=true" do + setup do + # Setup for TransactionInterpretation service + original_ti_config = + Application.get_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation) + + # Setup for AccountAbstraction service + original_aa_config = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.AccountAbstraction) + + original_tesla_adapter = Application.get_env(:tesla, :adapter) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + aa_bypass = Bypass.open() + ti_bypass = Bypass.open() + + on_exit(fn -> + Application.put_env( + :block_scout_web, + BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + original_ti_config + ) + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, original_aa_config) + Application.put_env(:tesla, :adapter, original_tesla_adapter) + + Bypass.down(aa_bypass) + Bypass.down(ti_bypass) + end) + + # Return bypass instances for use in tests + %{ + aa_bypass: aa_bypass, + ti_bypass: ti_bypass + } + end + + test "return 422 on invalid operation hash", %{conn: conn} do + request = get(conn, "/api/v2/proxy/account-abstraction/operations/0x/summary?just_request_body=true") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "return 404 on non existing operation", %{conn: conn, aa_bypass: aa_bypass, ti_bypass: ti_bypass} do + operation_hash = "0x" <> String.duplicate("0", 64) + + # Setup AccountAbstraction service + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, + enabled: true, + service_url: "http://localhost:#{aa_bypass.port}" + ) + + # Setup TransactionInterpretation service + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: true, + service_url: "http://localhost:#{ti_bypass.port}" + ) + + Bypass.expect_once( + aa_bypass, + "GET", + "/api/v1/userOps/#{operation_hash}", + fn conn -> + Plug.Conn.resp(conn, 404, Jason.encode!(%{"error" => "Not found"})) + end + ) + + request = + get(conn, "/api/v2/proxy/account-abstraction/operations/#{operation_hash}/summary?just_request_body=true") + + assert %{"error" => "Not found"} = json_response(request, 404) + end + + test "return request body for existing operation", %{conn: conn, aa_bypass: aa_bypass, ti_bypass: ti_bypass} do + operation_hash = "0xcfb9123a6591d9f80ded2aec7e9842c2258c8e2a0d3c88f3a38a060aa10e9869" + + # Setup services + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, + enabled: true, + service_url: "http://localhost:#{aa_bypass.port}" + ) + + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: true, + service_url: "http://localhost:#{ti_bypass.port}" + ) + + transaction = + :transaction + |> insert() + |> with_block() + + logs = + insert_list(51, :token_transfer_log, + transaction: transaction, + block: transaction.block + ) + + for log <- logs do + insert(:token_transfer_with_predefined_params, + log: log, + block: transaction.block + ) + end + + user_op = create_user_op_data(operation_hash, transaction.block_hash, transaction.hash) + + Bypass.expect_once( + aa_bypass, + "GET", + "/api/v1/userOps/#{operation_hash}", + fn conn -> + Plug.Conn.resp(conn, 200, Jason.encode!(user_op)) + end + ) + + request = + get(conn, "/api/v2/proxy/account-abstraction/operations/#{operation_hash}/summary?just_request_body=true") + + assert response = json_response(request, 200) + + # Verify the structure of the request body + assert Map.has_key?(response, "data") + assert Map.has_key?(response, "logs_data") + assert Map.has_key?(response, "chain_id") + + # Verify data structure + data = response["data"] + assert Map.has_key?(data, "to") + assert Map.has_key?(data, "from") + assert Map.has_key?(data, "hash") + assert Map.has_key?(data, "type") + assert Map.has_key?(data, "value") + assert Map.has_key?(data, "method") + assert Map.has_key?(data, "status") + assert Map.has_key?(data, "transaction_types") + assert Map.has_key?(data, "raw_input") + assert Map.has_key?(data, "decoded_input") + assert Map.has_key?(data, "token_transfers") + + # Verify logs_data structure + logs_data = response["logs_data"] + assert Map.has_key?(logs_data, "items") + assert is_list(logs_data["items"]) + + # Verify chain_id is present and is an integer + assert is_integer(response["chain_id"]) + + # Verify operation data matches + assert operation_hash == data["hash"] + assert 0 == data["type"] + assert "0" == data["value"] + assert true == data["status"] + + assert Enum.count(data["token_transfers"]) == 50 + end + + test "return 403 when transaction interpretation service is disabled", %{ + conn: conn, + aa_bypass: aa_bypass + } do + operation_hash = "0xcfb9123a6591d9f80ded2aec7e9842c2258c8e2a0d3c88f3a38a060aa10e9869" + + # Setup AccountAbstraction service as enabled + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, + enabled: true, + service_url: "http://localhost:#{aa_bypass.port}" + ) + + # Setup TransactionInterpretation service as disabled + Application.put_env(:block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + enabled: false + ) + + request = + get(conn, "/api/v2/proxy/account-abstraction/operations/#{operation_hash}/summary?just_request_body=true") + + assert %{"message" => "Transaction Interpretation Service is disabled"} = json_response(request, 403) + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 50c82c08a75c..c54175ec7ec1 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -868,18 +868,20 @@ defmodule Explorer.Factory do end def token_transfer_log_factory do - token_contract_address = build(:address) to_address = build(:address) from_address = build(:address) - transaction = build(:transaction, to_address: token_contract_address, from_address: from_address) + contract_code = Map.fetch!(contract_code_info(), :bytecode) + token_address = insert(:contract_address, contract_code: contract_code) + + transaction = build(:transaction, to_address: token_address, from_address: from_address) log_params = %{ first_topic: TokenTransfer.constant(), second_topic: zero_padded_address_hash_string(from_address.hash), third_topic: zero_padded_address_hash_string(to_address.hash), - address_hash: token_contract_address.hash, - address: nil, + address_hash: token_address.hash, + address: token_address, data: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000", transaction: transaction } @@ -928,6 +930,30 @@ defmodule Explorer.Factory do } end + def token_transfer_with_predefined_params_factory(%{log: log, block: block}) do + to_address_hash = address_hash_from_zero_padded_hash_string(to_string(log.third_topic)) + from_address_hash = address_hash_from_zero_padded_hash_string(to_string(log.second_topic)) + + # `to_address` is the only thing that isn't created from the token_transfer_log_factory + to_address = build(:address, hash: to_address_hash) + from_address = build(:address, hash: from_address_hash) + + token = insert(:token, contract_address: log.address) + + %TokenTransfer{ + block: block, + amount: Decimal.new(1), + block_number: block.number, + from_address: from_address, + to_address: to_address, + token_contract_address: log.address, + token_type: token.type, + transaction: log.transaction, + log_index: log.index, + block_consensus: true + } + end + def market_history_factory do %MarketHistory{ closing_price: price(), From 1e866039f54b94a8d12b477dc457556cb7c04540 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Thu, 18 Sep 2025 15:59:21 +0400 Subject: [PATCH 0350/1006] fix: some flapping explorer/indexer tests (#13230) --- .../api/v2/search_controller_test.exs | 4 +- .../test/explorer/history/process_test.exs | 6 +- .../solidity/code_compiler_test.exs | 686 +++++++++--------- .../solidity/publisher_test.exs | 9 +- .../smart_contract/solidity/verifier_test.exs | 10 +- .../fetcher/on_demand/coin_balance_test.exs | 1 - .../fetcher/token_instance/realtime_test.exs | 2 +- 7 files changed, 362 insertions(+), 356 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 17964f96544f..e9fdaf5817a6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -1193,7 +1193,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do assert ens["ens_info"]["name"] == name end - if Application.compile_env(:explorer, :chain_type) == :default do + if @chain_type == :default do test "finds a TAC operation", %{conn: conn} do bypass = Bypass.open() tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) @@ -2197,7 +2197,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end) end - if Application.compile_env(:explorer, :chain_type) == :default do + if @chain_type == :default do test "finds a TAC operation", %{conn: conn} do bypass = Bypass.open() tac_envs = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle) diff --git a/apps/explorer/test/explorer/history/process_test.exs b/apps/explorer/test/explorer/history/process_test.exs index 890a007fc0f7..f2fbf7983e1b 100644 --- a/apps/explorer/test/explorer/history/process_test.exs +++ b/apps/explorer/test/explorer/history/process_test.exs @@ -58,8 +58,8 @@ defmodule Explorer.History.ProcessTest do state = %{historian: TestHistorian} # interval should be short enough that it doesn't slow down testing... - # ...but long enough to detect. 16ms should be detectable on the slowest dev machines - history_fetch_interval = 16 + # ...but long enough to detect. 100ms should be detectable on the slowest dev machines + history_fetch_interval = 100 now = DateTime.to_time(DateTime.utc_now()) time_to_fetch_at = now |> Time.add(history_fetch_interval, :millisecond) @@ -69,7 +69,7 @@ defmodule Explorer.History.ProcessTest do assert {:noreply, state} == HistoryProcess.handle_info({nil, {1, 0, {:ok, [record]}}}, state) # Message isn't sent before interval is up - refute_receive {:compile_historical_records, 2}, history_fetch_interval - 1 + refute_receive {:compile_historical_records, 2}, history_fetch_interval - 50 # Now message is sent assert_receive {:compile_historical_records, 2} diff --git a/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs index d66d3533b30c..8d518836771b 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/code_compiler_test.exs @@ -1,402 +1,406 @@ defmodule Explorer.SmartContract.Solidity.CodeCompilerTest do use ExUnit.Case, async: true - doctest Explorer.SmartContract.Solidity.CodeCompiler + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - @moduletag timeout: :infinity + if @chain_type == :default do + doctest Explorer.SmartContract.Solidity.CodeCompiler - alias Explorer.Factory - alias Explorer.SmartContract.Solidity.CodeCompiler + @moduletag timeout: :infinity - @compiler_tests "#{File.cwd!()}/test/support/fixture/smart_contract/compiler_tests.json" - |> File.read!() - |> Jason.decode!() + alias Explorer.Factory + alias Explorer.SmartContract.Solidity.CodeCompiler - describe "run/2" do - setup do - configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + @compiler_tests "#{File.cwd!()}/test/support/fixture/smart_contract/compiler_tests.json" + |> File.read!() + |> Jason.decode!() - on_exit(fn -> - Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) - Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) - end) + describe "run/2" do + setup do + configuration = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, enabled: false) + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - {:ok, - contract_code_info: Factory.contract_code_info(), - contract_code_info_modern_compiler: Factory.contract_code_info_modern_compiler()} - end + on_exit(fn -> + Application.put_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, configuration) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) - test "compiles the latest solidity version", %{contract_code_info: contract_code_info} do - response = - CodeCompiler.run( - name: contract_code_info.name, - compiler_version: contract_code_info.version, - code: contract_code_info.source_code, - optimize: contract_code_info.optimized, - evm_version: "byzantium" - ) - - assert {:ok, - %{ - "abi" => _, - "bytecode" => _, - "name" => _ - }} = response - end + {:ok, + contract_code_info: Factory.contract_code_info(), + contract_code_info_modern_compiler: Factory.contract_code_info_modern_compiler()} + end - test "compiles a optimized smart contract", %{ - contract_code_info_modern_compiler: contract_code_info_modern_compiler - } do - optimize = true - - response = - CodeCompiler.run( - name: contract_code_info_modern_compiler.name, - compiler_version: contract_code_info_modern_compiler.version, - code: contract_code_info_modern_compiler.source_code, - optimize: optimize, - evm_version: "byzantium" - ) - - assert {:ok, - %{ - "abi" => _, - "bytecode" => _, - "name" => _ - }} = response - end + test "compiles the latest solidity version", %{contract_code_info: contract_code_info} do + response = + CodeCompiler.run( + name: contract_code_info.name, + compiler_version: contract_code_info.version, + code: contract_code_info.source_code, + optimize: contract_code_info.optimized, + evm_version: "byzantium" + ) - test "compiles smart contract with default evm version", %{ - contract_code_info_modern_compiler: contract_code_info_modern_compiler - } do - optimize = true - - response = - CodeCompiler.run( - name: contract_code_info_modern_compiler.name, - compiler_version: contract_code_info_modern_compiler.version, - code: contract_code_info_modern_compiler.source_code, - optimize: optimize, - evm_version: "default" - ) - - assert {:ok, - %{ - "abi" => _, - "bytecode" => _, - "name" => _ - }} = response - end + assert {:ok, + %{ + "abi" => _, + "bytecode" => _, + "name" => _ + }} = response + end - test "compiles code with external libraries" do - Enum.each(@compiler_tests, fn compiler_test -> - compiler_version = compiler_test["compiler_version"] - external_libraries = compiler_test["external_libraries"] - name = compiler_test["name"] - optimize = compiler_test["optimize"] - contract = compiler_test["contract"] + test "compiles a optimized smart contract", %{ + contract_code_info_modern_compiler: contract_code_info_modern_compiler + } do + optimize = true - {:ok, result} = + response = CodeCompiler.run( - name: name, - compiler_version: compiler_version, - code: contract, + name: contract_code_info_modern_compiler.name, + compiler_version: contract_code_info_modern_compiler.version, + code: contract_code_info_modern_compiler.source_code, optimize: optimize, - evm_version: "byzantium", - external_libs: external_libraries + evm_version: "byzantium" ) - clean_result = remove_init_data_and_whisper_data(result["bytecode"]) - expected_result = remove_init_data_and_whisper_data(compiler_test["tx_input"]) - - assert expected_result == clean_result - end) - end - - test "compiles with constantinople evm version" do - optimize = false - name = "MyTest" - - code = """ - pragma solidity 0.5.2; - - contract MyTest { - constructor() public { - } - - mapping(address => bytes32) public myMapping; - - function contractHash(address _addr) public { - bytes32 hash; - assembly { hash := extcodehash(_addr) } - myMapping[_addr] = hash; - } - - function justHash(bytes memory _bytes) - public - pure - returns (bytes32) - { - return keccak256(_bytes); - } - } - """ - - version = "v0.5.2+commit.1df8f40c" - - evm_version = "constantinople" - - response = - CodeCompiler.run( - name: name, - compiler_version: version, - code: code, - optimize: optimize, - evm_version: evm_version - ) - - assert {:ok, - %{ - "abi" => _, - "bytecode" => _, - "name" => _ - }} = response - end - - test "compiles in an older solidity version" do - optimize = false - name = "SimpleStorage" - - code = """ - contract SimpleStorage { - uint storedData; + assert {:ok, + %{ + "abi" => _, + "bytecode" => _, + "name" => _ + }} = response + end - function set(uint x) public { - storedData = x; - } + test "compiles smart contract with default evm version", %{ + contract_code_info_modern_compiler: contract_code_info_modern_compiler + } do + optimize = true - function get() public constant returns (uint) { - return storedData; - } - } - """ + response = + CodeCompiler.run( + name: contract_code_info_modern_compiler.name, + compiler_version: contract_code_info_modern_compiler.version, + code: contract_code_info_modern_compiler.source_code, + optimize: optimize, + evm_version: "default" + ) - version = "v0.1.3+commit.028f561d" + assert {:ok, + %{ + "abi" => _, + "bytecode" => _, + "name" => _ + }} = response + end + + test "compiles code with external libraries" do + Enum.each(@compiler_tests, fn compiler_test -> + compiler_version = compiler_test["compiler_version"] + external_libraries = compiler_test["external_libraries"] + name = compiler_test["name"] + optimize = compiler_test["optimize"] + contract = compiler_test["contract"] + + {:ok, result} = + CodeCompiler.run( + name: name, + compiler_version: compiler_version, + code: contract, + optimize: optimize, + evm_version: "byzantium", + external_libs: external_libraries + ) + + clean_result = remove_init_data_and_whisper_data(result["bytecode"]) + expected_result = remove_init_data_and_whisper_data(compiler_test["tx_input"]) + + assert expected_result == clean_result + end) + end + + test "compiles with constantinople evm version" do + optimize = false + name = "MyTest" + + code = """ + pragma solidity 0.5.2; + + contract MyTest { + constructor() public { + } + + mapping(address => bytes32) public myMapping; + + function contractHash(address _addr) public { + bytes32 hash; + assembly { hash := extcodehash(_addr) } + myMapping[_addr] = hash; + } + + function justHash(bytes memory _bytes) + public + pure + returns (bytes32) + { + return keccak256(_bytes); + } + } + """ + + version = "v0.5.2+commit.1df8f40c" + + evm_version = "constantinople" + + response = + CodeCompiler.run( + name: name, + compiler_version: version, + code: code, + optimize: optimize, + evm_version: evm_version + ) - response = CodeCompiler.run(name: name, compiler_version: version, code: code, optimize: optimize) + assert {:ok, + %{ + "abi" => _, + "bytecode" => _, + "name" => _ + }} = response + end - assert {:ok, - %{ - "abi" => _, - "bytecode" => _, - "name" => _ - }} = response - end + test "compiles in an older solidity version" do + optimize = false + name = "SimpleStorage" - test "returns compilation error when compilation isn't possible", %{ - contract_code_info: contract_code_info - } do - wrong_code = "pragma solidity ^0.4.24; cont SimpleStorage { " - - response = - CodeCompiler.run( - name: contract_code_info.name, - compiler_version: contract_code_info.version, - code: wrong_code, - optimize: contract_code_info.optimized - ) - - assert {:error, :compilation, "Expected pragma, import directive or contract/interface/library definition."} = - response - end + code = """ + contract SimpleStorage { + uint storedData; - test "returns constructor in abi" do - code = """ - pragma solidity ^0.4.22; - - contract OwnedToken { - // TokenCreator is a contract type that is defined below. - // It is fine to reference it as long as it is not used - // to create a new contract. - TokenCreator creator; - address owner; - bytes32 name; - - // This is the constructor which registers the - // creator and the assigned name. - constructor(bytes32 _name) public { - // State variables are accessed via their name - // and not via e.g. this.owner. This also applies - // to functions and especially in the constructors, - // you can only call them like that ("internally"), - // because the contract itself does not exist yet. - owner = msg.sender; - // We do an explicit type conversion from `address` - // to `TokenCreator` and assume that the type of - // the calling contract is TokenCreator, there is - // no real way to check that. - creator = TokenCreator(msg.sender); - name = _name; + function set(uint x) public { + storedData = x; } - function changeName(bytes32 newName) public { - // Only the creator can alter the name -- - // the comparison is possible since contracts - // are implicitly convertible to addresses. - if (msg.sender == address(creator)) - name = newName; - } - - function transfer(address newOwner) public { - // Only the current owner can transfer the token. - if (msg.sender != owner) return; - // We also want to ask the creator if the transfer - // is fine. Note that this calls a function of the - // contract defined below. If the call fails (e.g. - // due to out-of-gas), the execution here stops - // immediately. - if (creator.isTokenTransferOK(owner, newOwner)) - owner = newOwner; + function get() public constant returns (uint) { + return storedData; } } + """ - contract TokenCreator { - function createToken(bytes32 name) - public - returns (OwnedToken tokenAddress) - { - // Create a new Token contract and return its address. - // From the JavaScript side, the return type is simply - // `address`, as this is the closest type available in - // the ABI. - return new OwnedToken(name); - } + version = "v0.1.3+commit.028f561d" - function changeName(OwnedToken tokenAddress, bytes32 name) public { - // Again, the external type of `tokenAddress` is - // simply `address`. - tokenAddress.changeName(name); - } + response = CodeCompiler.run(name: name, compiler_version: version, code: code, optimize: optimize) - function isTokenTransferOK(address currentOwner, address newOwner) - public - view - returns (bool ok) - { - // Check some arbitrary condition. - address tokenAddress = msg.sender; - return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); - } - } - """ + assert {:ok, + %{ + "abi" => _, + "bytecode" => _, + "name" => _ + }} = response + end - name = "OwnedToken" - compiler_version = "v0.4.22+commit.4cb486ee" + test "returns compilation error when compilation isn't possible", %{ + contract_code_info: contract_code_info + } do + wrong_code = "pragma solidity ^0.4.24; cont SimpleStorage { " - {:ok, %{"abi" => abi}} = - CodeCompiler.run( - name: name, - compiler_version: compiler_version, - code: code, - evm_version: "byzantium", - optimize: true - ) + response = + CodeCompiler.run( + name: contract_code_info.name, + compiler_version: contract_code_info.version, + code: wrong_code, + optimize: contract_code_info.optimized + ) - assert Enum.any?(abi, fn el -> el["type"] == "constructor" end) - end + assert {:error, :compilation, "Expected pragma, import directive or contract/interface/library definition."} = + response + end + + test "returns constructor in abi" do + code = """ + pragma solidity ^0.4.22; + + contract OwnedToken { + // TokenCreator is a contract type that is defined below. + // It is fine to reference it as long as it is not used + // to create a new contract. + TokenCreator creator; + address owner; + bytes32 name; + + // This is the constructor which registers the + // creator and the assigned name. + constructor(bytes32 _name) public { + // State variables are accessed via their name + // and not via e.g. this.owner. This also applies + // to functions and especially in the constructors, + // you can only call them like that ("internally"), + // because the contract itself does not exist yet. + owner = msg.sender; + // We do an explicit type conversion from `address` + // to `TokenCreator` and assume that the type of + // the calling contract is TokenCreator, there is + // no real way to check that. + creator = TokenCreator(msg.sender); + name = _name; + } + + function changeName(bytes32 newName) public { + // Only the creator can alter the name -- + // the comparison is possible since contracts + // are implicitly convertible to addresses. + if (msg.sender == address(creator)) + name = newName; + } + + function transfer(address newOwner) public { + // Only the current owner can transfer the token. + if (msg.sender != owner) return; + // We also want to ask the creator if the transfer + // is fine. Note that this calls a function of the + // contract defined below. If the call fails (e.g. + // due to out-of-gas), the execution here stops + // immediately. + if (creator.isTokenTransferOK(owner, newOwner)) + owner = newOwner; + } + } - test "can compile a large file" do - path = File.cwd!() <> "/test/support/fixture/smart_contract/large_smart_contract.sol" - contract = File.read!(path) - - assert {:ok, %{"abi" => _abi}} = - CodeCompiler.run( - name: "HomeWorkDeployer", - compiler_version: "v0.5.9+commit.e560f70d", - code: contract, - evm_version: "constantinople", - optimize: true - ) - end - end + contract TokenCreator { + function createToken(bytes32 name) + public + returns (OwnedToken tokenAddress) + { + // Create a new Token contract and return its address. + // From the JavaScript side, the return type is simply + // `address`, as this is the closest type available in + // the ABI. + return new OwnedToken(name); + } + + function changeName(OwnedToken tokenAddress, bytes32 name) public { + // Again, the external type of `tokenAddress` is + // simply `address`. + tokenAddress.changeName(name); + } + + function isTokenTransferOK(address currentOwner, address newOwner) + public + view + returns (bool ok) + { + // Check some arbitrary condition. + address tokenAddress = msg.sender; + return (keccak256(newOwner) & 0xff) == (bytes20(tokenAddress) & 0xff); + } + } + """ - describe "get_contract_info/1" do - test "return name error when the Contract name doesn't match" do - name = "Name" - different_name = "diff_name" + name = "OwnedToken" + compiler_version = "v0.4.22+commit.4cb486ee" - response = CodeCompiler.get_contract_info(%{name => %{}}, different_name) + {:ok, %{"abi" => abi}} = + CodeCompiler.run( + name: name, + compiler_version: compiler_version, + code: code, + evm_version: "byzantium", + optimize: true + ) - assert {:error, :name} == response + assert Enum.any?(abi, fn el -> el["type"] == "constructor" end) + end + + test "can compile a large file" do + path = File.cwd!() <> "/test/support/fixture/smart_contract/large_smart_contract.sol" + contract = File.read!(path) + + assert {:ok, %{"abi" => _abi}} = + CodeCompiler.run( + name: "HomeWorkDeployer", + compiler_version: "v0.5.9+commit.e560f70d", + code: contract, + evm_version: "constantinople", + optimize: true + ) + end end - test "returns compilation error for empty info" do - name = "Name" + describe "get_contract_info/1" do + test "return name error when the Contract name doesn't match" do + name = "Name" + different_name = "diff_name" - response = CodeCompiler.get_contract_info(%{}, name) + response = CodeCompiler.get_contract_info(%{name => %{}}, different_name) - assert {:error, :compilation} == response - end + assert {:error, :name} == response + end - test "the contract info is returned when the name matches" do - contract_inner_info = %{"abi" => %{}, "bytecode" => ""} - name = "Name" - contract_info = %{name => contract_inner_info} + test "returns compilation error for empty info" do + name = "Name" - response = CodeCompiler.get_contract_info(contract_info, name) + response = CodeCompiler.get_contract_info(%{}, name) - assert contract_inner_info == response - end + assert {:error, :compilation} == response + end + + test "the contract info is returned when the name matches" do + contract_inner_info = %{"abi" => %{}, "bytecode" => ""} + name = "Name" + contract_info = %{name => contract_inner_info} + + response = CodeCompiler.get_contract_info(contract_info, name) - test "the contract info is returned when the name matches with a `:` suffix" do - name = "Name" - name_with_suffix = ":Name" - contract_inner_info = %{"abi" => %{}, "bytecode" => ""} - contract_info = %{name_with_suffix => contract_inner_info} + assert contract_inner_info == response + end - response = CodeCompiler.get_contract_info(contract_info, name) + test "the contract info is returned when the name matches with a `:` suffix" do + name = "Name" + name_with_suffix = ":Name" + contract_inner_info = %{"abi" => %{}, "bytecode" => ""} + contract_info = %{name_with_suffix => contract_inner_info} - assert contract_inner_info == response + response = CodeCompiler.get_contract_info(contract_info, name) + + assert contract_inner_info == response + end end - end - # describe "allowed_solidity_evm_versions/0" do - # test "returns allowed evm versions defined by ALLOWED_EVM_VERSIONS env var" do - # Application.put_env(:explorer, :allowed_solidity_evm_versions, "CustomEVM1,CustomEVM2,CustomEVM3") - # response = CodeCompiler.evm_versions(:solidity) + # describe "allowed_solidity_evm_versions/0" do + # test "returns allowed evm versions defined by ALLOWED_EVM_VERSIONS env var" do + # Application.put_env(:explorer, :allowed_solidity_evm_versions, "CustomEVM1,CustomEVM2,CustomEVM3") + # response = CodeCompiler.evm_versions(:solidity) - # assert ["CustomEVM1", "CustomEVM2", "CustomEVM3"] = response - # end + # assert ["CustomEVM1", "CustomEVM2", "CustomEVM3"] = response + # end - # test "returns allowed evm versions defined by not trimmed ALLOWED_EVM_VERSIONS env var" do - # Application.put_env(:explorer, :allowed_solidity_evm_versions, "CustomEVM1, CustomEVM2, CustomEVM3") - # response = CodeCompiler.evm_versions(:solidity) + # test "returns allowed evm versions defined by not trimmed ALLOWED_EVM_VERSIONS env var" do + # Application.put_env(:explorer, :allowed_solidity_evm_versions, "CustomEVM1, CustomEVM2, CustomEVM3") + # response = CodeCompiler.evm_versions(:solidity) - # assert ["CustomEVM1", "CustomEVM2", "CustomEVM3"] = response - # end + # assert ["CustomEVM1", "CustomEVM2", "CustomEVM3"] = response + # end - # test "returns default_allowed_evm_versions" do - # Application.put_env( - # :explorer, - # :allowed_solidity_evm_versions, - # "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg" - # ) + # test "returns default_allowed_evm_versions" do + # Application.put_env( + # :explorer, + # :allowed_solidity_evm_versions, + # "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg" + # ) - # response = CodeCompiler.evm_versions(:solidity) + # response = CodeCompiler.evm_versions(:solidity) - # assert ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople", "petersburg"] = response - # end - # end + # assert ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople", "petersburg"] = response + # end + # end - defp remove_init_data_and_whisper_data(code) do - {res, _} = - code - |> String.split("0029") - |> List.first() - |> String.split_at(-64) + defp remove_init_data_and_whisper_data(code) do + {res, _} = + code + |> String.split("0029") + |> List.first() + |> String.split_at(-64) - res + res + end end end diff --git a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs index 0c8096ca294c..8dfa6e1ac767 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/publisher_test.exs @@ -1,9 +1,10 @@ -if Application.compile_env(:explorer, :chain_type) !== :zksync do - defmodule Explorer.SmartContract.Solidity.PublisherTest do - use ExUnit.Case, async: true +defmodule Explorer.SmartContract.Solidity.PublisherTest do + use ExUnit.Case, async: true + use Explorer.DataCase - use Explorer.DataCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + if @chain_type == :default do doctest Explorer.SmartContract.Solidity.Publisher @moduletag timeout: :infinity diff --git a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs index 7489a6c555c6..4880da3fba9c 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs @@ -1,8 +1,10 @@ -if Application.compile_env(:explorer, :chain_type) !== :zksync do - defmodule Explorer.SmartContract.Solidity.VerifierTest do - use ExUnit.Case, async: true - use Explorer.DataCase +defmodule Explorer.SmartContract.Solidity.VerifierTest do + use ExUnit.Case, async: true + use Explorer.DataCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + if @chain_type == :default do @moduletag timeout: :infinity doctest Explorer.SmartContract.Solidity.Verifier diff --git a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs index d11fc4910b01..840b28cac0a4 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs @@ -7,7 +7,6 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do import Mox import EthereumJSONRPC, only: [integer_to_quantity: 1] - alias Explorer.Chain alias Explorer.Chain.Address.CoinBalance alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.Wei diff --git a/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs index 698aae207791..b05310097186 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/realtime_test.exs @@ -113,7 +113,7 @@ defmodule Indexer.Fetcher.TokenInstance.RealtimeTest do %{token_contract_address_hash: token.contract_address_hash, token_ids: [Decimal.new(777)]} ]) - :timer.sleep(150) + :timer.sleep(250) [instance] = Repo.all(Instance) From e2cec487a2819e56e0021d236b67c063b0cc0770 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 18 Sep 2025 15:00:51 +0300 Subject: [PATCH 0351/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acb7b185871a..221e20d35371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add reputation preload to state changes and bridged tokens ([#13235](https://github.com/blockscout/blockscout/pull/13235)) - Soften deposits deletion condition ([#13234](https://github.com/blockscout/blockscout/pull/13234)) - Fix logic of checking finishing of heavy DB index operation ([#13231](https://github.com/blockscout/blockscout/pull/13231)) +- some flapping explorer/indexer tests ([#13230](https://github.com/blockscout/blockscout/pull/13230)) - Remove requirement for beacon deposit indexes to be sequential ([#13228](https://github.com/blockscout/blockscout/pull/13228)) ### ⚡ Performance From 33cc5ac8a24118e78d4edb056fab0b2c1a3c9b5c Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 18 Sep 2025 17:29:09 +0300 Subject: [PATCH 0352/1006] chore: Fix tests (#13244) --- apps/explorer/test/explorer/chain/token_transfer_test.exs | 3 ++- .../indexer/temporary/uncataloged_token_transfers_test.exs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 0ca06af2cb54..8e5db225259f 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -345,7 +345,8 @@ defmodule Explorer.Chain.TokenTransferTest do index: 0 ), block: block, - address_hash: address.hash + address_hash: address.hash, + address: address ) block_number = log.block_number diff --git a/apps/indexer/test/indexer/temporary/uncataloged_token_transfers_test.exs b/apps/indexer/test/indexer/temporary/uncataloged_token_transfers_test.exs index 9e1468916ecf..d5176747577a 100644 --- a/apps/indexer/test/indexer/temporary/uncataloged_token_transfers_test.exs +++ b/apps/indexer/test/indexer/temporary/uncataloged_token_transfers_test.exs @@ -46,6 +46,7 @@ defmodule Indexer.Temporary.UncatalogedTokenTransfersTest do insert(:token_transfer_log, transaction: transaction, address_hash: address.hash, + address: address, block: block ) From 2f4eb339a0836051648f986b9f7b82a9e1989ff7 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 18 Sep 2025 17:30:57 +0300 Subject: [PATCH 0353/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 221e20d35371..4ac9b5baeb62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### ⚙️ Miscellaneous Tasks +- Fix tests ([#13244](https://github.com/blockscout/blockscout/pull/13244)) - Do not modify deposit indexer state on reorgs ([#13236](https://github.com/blockscout/blockscout/pull/13236)) - Refactoring reputation ([#13221](https://github.com/blockscout/blockscout/issues/13221)) From fab90607cb0c87c1695f0585640de82848b12196 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 18 Sep 2025 17:44:32 +0300 Subject: [PATCH 0354/1006] Remove Suave GA workflows --- .github/workflows/config.yml | 3 +- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/public-release.yml | 2 - .../publish-docker-image-for-suave.yml | 47 -------------- .github/workflows/release-suave.yml | 64 ------------------- 5 files changed, 2 insertions(+), 116 deletions(-) delete mode 100644 .github/workflows/publish-docker-image-for-suave.yml delete mode 100644 .github/workflows/release-suave.yml diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 7f81adfcd609..7e23d0fa0550 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -15,7 +15,6 @@ on: - production-lukso - production-rsk - production-sokol - - production-suave - production-xdai - production-zkevm - production-zksync @@ -68,7 +67,7 @@ jobs: "neon" ]; - const extraChainTypes = ["suave"]; + const extraChainTypes = []; // Chain type matrix we use in master branch const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes); diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 4e6ba403c2a0..3cc77bf727f0 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -51,7 +51,7 @@ jobs: "neon" ]; - const extraChainTypes = ["suave"]; + const extraChainTypes = []; // Chain type matrix we use in master branch const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes); diff --git a/.github/workflows/public-release.yml b/.github/workflows/public-release.yml index 9268ea23eaf0..243ae60a2e50 100644 --- a/.github/workflows/public-release.yml +++ b/.github/workflows/public-release.yml @@ -54,7 +54,6 @@ jobs: "blockscout-zkevm" "blockscout-rsk" "blockscout-scroll" - "blockscout-suave" "blockscout-zetachain" "blockscout-zilliqa" "blockscout-zksync" @@ -96,7 +95,6 @@ jobs: "blockscout-zkevm" "blockscout-rsk" "blockscout-scroll" - "blockscout-suave" "blockscout-zetachain" "blockscout-zilliqa" "blockscout-zksync" diff --git a/.github/workflows/publish-docker-image-for-suave.yml b/.github/workflows/publish-docker-image-for-suave.yml deleted file mode 100644 index 54acde97ea4f..000000000000 --- a/.github/workflows/publish-docker-image-for-suave.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: SUAVE Publish Docker image - -on: - workflow_dispatch: - push: - branches: - - production-suave -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} -permissions: - contents: read - packages: write - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.1 - DOCKER_CHAIN_NAME: suave - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave diff --git a/.github/workflows/release-suave.yml b/.github/workflows/release-suave.yml deleted file mode 100644 index d1b39430f100..000000000000 --- a/.github/workflows/release-suave.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Release for SUAVE - -on: - workflow_dispatch: - release: - types: [published] - -permissions: - contents: read - packages: write - -env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - -jobs: - push_to_registry: - name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest - env: - RELEASE_VERSION: 9.1.1 - steps: - - uses: actions/checkout@v5 - - name: Setup repo - uses: ./.github/actions/setup-repo - id: setup - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - docker-remote-multi-platform: true - docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} - docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - - - name: Build and push Docker image for SUAVE (indexer + API) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave-private:latest, ghcr.io/blockscout/blockscout-suave-private:${{ env.RELEASE_VERSION }} - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave - - - name: Build and push Docker image for SUAVE (indexer) - uses: docker/build-push-action@v6 - with: - context: . - file: ./docker/Dockerfile - push: true - tags: ghcr.io/blockscout/blockscout-suave-private:${{ env.RELEASE_VERSION }}-indexer - labels: ${{ steps.setup.outputs.docker-labels }} - platforms: | - linux/amd64 - linux/arm64/v8 - build-args: | - DISABLE_API=true - BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} - RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=suave From 6d07e5be3faed15708a968bbc496a39c8e081d20 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 19 Sep 2025 14:52:33 +0300 Subject: [PATCH 0355/1006] fix: token transfer test for celo (#13250) --- apps/explorer/test/explorer/chain/token_transfer_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 8e5db225259f..70a2ecd3f959 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -383,7 +383,7 @@ defmodule Explorer.Chain.TokenTransferTest do |> to_string() |> String.replace_prefix("0x000000000000000000000000", "0x") - token_contract_address = insert(:address, hash: log.address_hash) + token_contract_address = log.address to_address = insert(:address, hash: to_address_hash) from_address = insert(:address, hash: from_address_hash) From 72d6791efe28fb58a8b54e12421495a75c56110a Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:04:37 +0300 Subject: [PATCH 0356/1006] fix: Add reputation preload to celo base fee (#13248) Co-authored-by: Victor Baranov --- CHANGELOG.md | 2 + .../block_scout_web/views/api/v2/celo_view.ex | 13 +- .../api/v2/block_controller_test.exs | 227 ++++++++++++++++++ 3 files changed, 239 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ac9b5baeb62..b6e7e1c941ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### 🐛 Bug Fixes +- Fix token transfer test for celo ([#13250](https://github.com/blockscout/blockscout/pull/13250)) +- Add reputation preload to celo base fee ([#13248](https://github.com/blockscout/blockscout/pull/13248)) - Add reputation preload for user op body for transaction interpreter ([#13241](https://github.com/blockscout/blockscout/pull/13241)) - Fix condition in Indexer.Fetcher.OnDemand.TokenTotalSupply fetcher ([#13240](https://github.com/blockscout/blockscout/pull/13240)) - Add reputation preload to state changes and bridged tokens ([#13235](https://github.com/blockscout/blockscout/pull/13235)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index 002d11234ef5..d3c123a823a3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do alias Explorer.Chain.Cache.{CeloCoreContracts, CeloEpochs} alias Explorer.Chain.Celo.Helper, as: CeloHelper alias Explorer.Chain.Celo.{Epoch, EpochReward} - alias Explorer.Chain.{Block, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.{Address.Reputation, Block, Token, TokenTransfer, Transaction, Wei} @address_params [ necessity_by_association: %{ @@ -24,6 +24,13 @@ defmodule BlockScoutWeb.API.V2.CeloView do api?: true ] + @token_params [ + necessity_by_association: %{ + Reputation.reputation_association() => :optional + }, + api?: true + ] + def render("celo_epochs.json", %{ epochs: epochs, next_page_params: next_page_params @@ -499,7 +506,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do } ) - celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, api?: true) + celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, @token_params) %{ recipient: fee_handler_contract_address_info, @@ -566,7 +573,7 @@ defmodule BlockScoutWeb.API.V2.CeloView do address_hash ) - celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, api?: true) + celo_token = Token.get_by_contract_address_hash(celo_token_address_hash, @token_params) %{ recipient: address_with_info, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index a847e41c41ef..39126b14dbc7 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do use BlockScoutWeb.ConnCase + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Withdrawal} alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit @@ -279,6 +280,232 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert refetch_block_response["is_pending_update"] == true assert no_refetch_block_response["is_pending_update"] == false end + + if @chain_type == :celo do + test "get block with Celo base fee information when chain type is celo", %{conn: conn} do + # Store original configuration + original_celo_config = Application.get_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts) + + # Set up Celo core contracts configuration for base fee + fee_handler_address = insert(:address) + governance_address = insert(:address) + celo_token_address = insert(:address) + + celo_config = [ + contracts: %{ + "addresses" => %{ + "FeeHandler" => [ + %{ + "address" => to_string(fee_handler_address.hash), + "updated_at_block_number" => 0 + } + ], + "Governance" => [ + %{ + "address" => to_string(governance_address.hash), + "updated_at_block_number" => 0 + } + ], + "GoldToken" => [ + %{ + "address" => to_string(celo_token_address.hash), + "updated_at_block_number" => 0 + } + ] + }, + "events" => %{ + "FeeHandler" => %{ + "FeeBeneficiarySet" => [ + %{ + "address_hash" => to_string(insert(:address).hash), + "updated_at_block_number" => 0 + } + ], + "BurnFractionSet" => [ + %{ + "value" => "500000000000000000000000", + "updated_at_block_number" => 0 + } + ] + } + } + } + ] + + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, celo_config) + + # Create a CELO token for the response + insert(:token, + contract_address_hash: celo_token_address.hash, + contract_address: celo_token_address, + symbol: "CELO", + name: "Celo", + type: "ERC-20" + ) + + # Create a block with base fee and transactions + block = + insert(:block, + # 1 gwei + base_fee_per_gas: 1_000_000_000 + ) + + # Create transactions for the block to calculate burnt fees + for index <- 0..2 do + insert(:transaction, + block_hash: block.hash, + block_number: block.number, + # 2 gwei + gas_price: 2_000_000_000, + gas_used: 21_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + cumulative_gas_used: 21_000, + index: index + ) + end + + # Make the request + request = get(conn, "/api/v2/blocks/#{block.hash}") + assert response = json_response(request, 200) + + # Verify basic block information + compare_item(block, response) + + # Verify Celo-specific information is present + assert Map.has_key?(response, "celo") + celo_info = response["celo"] + + # Verify epoch information + assert Map.has_key?(celo_info, "epoch_number") + assert Map.has_key?(celo_info, "is_epoch_block") + assert celo_info["is_epoch_block"] == false + + # Verify base fee information is present + assert Map.has_key?(celo_info, "base_fee") + assert base_fee_info = celo_info["base_fee"] + + # Verify base fee structure + assert Map.has_key?(base_fee_info, "recipient") + assert Map.has_key?(base_fee_info, "amount") + assert Map.has_key?(base_fee_info, "token") + assert Map.has_key?(base_fee_info, "breakdown") + + # Verify token information + token_info = base_fee_info["token"] + assert token_info["symbol"] == "CELO" + assert token_info["name"] == "Celo" + + # Verify recipient information + recipient = base_fee_info["recipient"] + assert Map.has_key?(recipient, "hash") + + # Verify breakdown structure + breakdown = base_fee_info["breakdown"] + assert is_list(breakdown) + + # Restore original configuration + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, original_celo_config) + end) + end + + test "get block with Celo governance base fee when fee handler is not available", %{conn: conn} do + # Store original configuration + original_celo_config = Application.get_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts) + + # Set up Celo core contracts configuration with only governance (no fee handler) + governance_address = insert(:address) + celo_token_address = insert(:address) + + celo_config = [ + contracts: %{ + "addresses" => %{ + "Governance" => [ + %{ + "address" => to_string(governance_address.hash), + "updated_at_block_number" => 0 + } + ], + "GoldToken" => [ + %{ + "address" => to_string(celo_token_address.hash), + "updated_at_block_number" => 0 + } + ] + } + } + ] + + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, celo_config) + + # Create a CELO token for the response + insert(:token, + contract_address_hash: celo_token_address.hash, + contract_address: celo_token_address, + symbol: "CELO", + name: "Celo", + type: "ERC-20" + ) + + # Create a block with base fee and transactions + block = + insert(:block, + # 1 gwei + base_fee_per_gas: 1_000_000_000 + ) + + # Create transactions for the block to calculate burnt fees + for index <- 0..2 do + insert(:transaction, + block_hash: block.hash, + block_number: block.number, + # 2 gwei + gas_price: 2_000_000_000, + gas_used: 21_000, + max_fee_per_gas: 2_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + cumulative_gas_used: 21_000, + index: index + ) + end + + # Make the request + request = get(conn, "/api/v2/blocks/#{block.hash}") + assert response = json_response(request, 200) + + # Verify basic block information + compare_item(block, response) + + # Verify Celo-specific information is present + assert Map.has_key?(response, "celo") + celo_info = response["celo"] + + # Verify epoch information + assert Map.has_key?(celo_info, "epoch_number") + assert Map.has_key?(celo_info, "is_epoch_block") + + # Verify base fee information is present (may be nil if governance fallback doesn't work) + assert Map.has_key?(celo_info, "base_fee") + assert base_fee_info = celo_info["base_fee"] + + # Verify base fee structure for governance case + assert Map.has_key?(base_fee_info, "recipient") + assert Map.has_key?(base_fee_info, "amount") + assert Map.has_key?(base_fee_info, "token") + assert Map.has_key?(base_fee_info, "breakdown") + + # For governance case, breakdown should be empty + breakdown = base_fee_info["breakdown"] + assert is_list(breakdown) + assert Enum.empty?(breakdown) + + # Restore original configuration + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, original_celo_config) + end) + end + end end describe "/blocks/{block_hash_or_number}/transactions" do From c2a8ca56b01152a77ed239b328161a68ba01c2b5 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Tue, 23 Sep 2025 04:10:53 -0600 Subject: [PATCH 0357/1006] feat: initial support of indexing EigenDA-grounded Arbitrum batches (#12915) --- .../controllers/api/v2/arbitrum_controller.ex | 14 +- .../lib/block_scout_web/routers/api_router.ex | 1 + .../block_scout_web/schemas/api/v2/block.ex | 2 +- .../views/api/v2/arbitrum_view.ex | 22 +- .../arbitrum/constants/contracts.ex | 145 +++++ .../chain/arbitrum/da_multi_purpose_record.ex | 18 + .../lib/explorer/chain/arbitrum/l1_batch.ex | 9 +- .../20250731001757_add_eigenda_batches.exs | 7 + .../lib/indexer/fetcher/arbitrum/da/README.md | 539 ++++++++++++++++++ .../lib/indexer/fetcher/arbitrum/da/common.ex | 59 +- .../indexer/fetcher/arbitrum/da/eigenda.ex | 196 +++++++ .../lib/indexer/fetcher/arbitrum/utils/rpc.ex | 48 +- .../arbitrum/workers/batches/README.md | 1 + .../arbitrum/workers/batches/discovery.ex | 13 +- .../fetcher/arbitrum/workers/batches/tasks.ex | 8 +- cspell.json | 2 + 16 files changed, 1040 insertions(+), 44 deletions(-) create mode 100644 apps/explorer/priv/arbitrum/migrations/20250731001757_add_eigenda_batches.exs create mode 100644 apps/indexer/lib/indexer/fetcher/arbitrum/da/README.md create mode 100644 apps/indexer/lib/indexer/fetcher/arbitrum/da/eigenda.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex index 54a18765eff3..510bf91e8011 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex @@ -138,23 +138,23 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do end @doc """ - Function to handle GET requests to `/api/v2/arbitrum/batches/da/:data_hash` or - `/api/v2/arbitrum/batches/da/:transaction_commitment/:height` endpoints. + Function to handle GET requests to `/api/v2/arbitrum/batches/da/.../:data_hash` or + `/api/v2/arbitrum/batches/da/celestia/:transaction_commitment/:height` endpoints. - For AnyTrust data hash, the function can be called in two ways: + For AnyTrust and EigenDA data hash, the function can be called in two ways: 1. Without type parameter - returns the most recent batch for the data hash 2. With type=all parameter - returns all batches for the data hash ## Parameters - `conn`: The connection struct - `params`: A map that may contain: - * `data_hash` - The AnyTrust data hash + * `data_hash` - The AnyTrust or EigenDA data hash * `transaction_commitment` and `height` - For Celestia data * `type` - Optional parameter to specify return type ("all" for all batches) """ @spec batch_by_data_availability_info(Plug.Conn.t(), map()) :: Plug.Conn.t() def batch_by_data_availability_info(conn, %{"data_hash" => data_hash} = params) do - # In case of AnyTrust, `data_key` is the hash of the data itself + # In case of AnyTrust or EigenDA, `data_key` is the hash of the data itself case Map.get(params, "type") do "all" -> all_batches_by_data_availability_info(conn, data_hash, params) _ -> one_batch_by_data_availability_info(conn, data_hash, params) @@ -185,7 +185,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do # # ## Parameters # - `conn`: The connection struct - # - `data_hash`: The AnyTrust data hash + # - `data_hash`: The AnyTrust or EigenDA data hash # - `params`: The original request parameters # # ## Returns @@ -205,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do # # ## Parameters # - `conn`: The connection struct - # - `data_hash`: The AnyTrust data hash + # - `data_hash`: The AnyTrust or EigenDA data hash # - `params`: The original request parameters (for pagination) # # ## Returns diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 218b3319adb5..d3c82d181714 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -498,6 +498,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/batches/count", V2.ArbitrumController, :batches_count) get("/batches/:batch_number", V2.ArbitrumController, :batch) get("/batches/da/anytrust/:data_hash", V2.ArbitrumController, :batch_by_data_availability_info) + get("/batches/da/eigenda/:data_hash", V2.ArbitrumController, :batch_by_data_availability_info) get( "/batches/da/celestia/:height/:transaction_commitment", diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex index 4e9d170837ec..1ea8176540d6 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -43,7 +43,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do batch_data_container: %Schema{ type: :string, nullable: true, - enum: ["in_blob4844", "in_calldata", "in_celestia", "in_anytrust"] + enum: ["in_blob4844", "in_calldata", "in_celestia", "in_anytrust", "in_eigenda"] }, status: %Schema{ type: :string, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex index 7feb96632540..46e33442c49b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex @@ -373,8 +373,9 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do # Adds data availability (DA) information to the given output JSON based on the batch container type. # # This function enriches the output JSON with data availability information based on - # the type of batch container. It handles different DA types, including AnyTrust and - # Celestia, and generates the appropriate DA data for inclusion in the output. + # the type of batch container. It handles different DA types, including AnyTrust, + # Celestia, and EigenDA, and generates the appropriate DA data for inclusion in the + # output. # # ## Parameters # - `out_json`: The initial JSON map to be enriched with DA information. @@ -384,7 +385,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do # - An updated JSON map containing the data availability information. @spec add_da_info(map(), %{ :__struct__ => L1Batch, - :batch_container => :in_anytrust | :in_celestia | atom() | nil, + :batch_container => :in_anytrust | :in_celestia | :in_eigenda | atom() | nil, :number => non_neg_integer(), optional(any()) => any() }) :: map() @@ -394,6 +395,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do nil -> %{"batch_data_container" => nil} :in_anytrust -> generate_anytrust_certificate(batch.number) :in_celestia -> generate_celestia_da_info(batch.number) + :in_eigenda -> generate_eigen_da_info(batch.number) value -> %{"batch_data_container" => to_string(value)} end @@ -471,6 +473,20 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do }) end + # Generates EigenDA information for the given batch number. + @spec generate_eigen_da_info(non_neg_integer()) :: map() + defp generate_eigen_da_info(batch_number) do + out = %{"batch_data_container" => "in_eigenda"} + + da_info = SettlementReader.get_da_info_by_batch_number(batch_number) + + out + |> Map.merge(%{ + "blob_header" => Map.get(da_info, "blob_header"), + "blob_verification_proof" => Map.get(da_info, "blob_verification_proof") + }) + end + # Augments an output JSON with commit and confirm transaction details and their statuses. @spec add_l1_transactions_info_and_status(map(), %{ optional(:commitment_transaction) => any(), diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/arbitrum/constants/contracts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/arbitrum/constants/contracts.ex index 1136a2bde325..32473ddd0c9a 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/arbitrum/constants/contracts.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/arbitrum/constants/contracts.ex @@ -6,6 +6,81 @@ defmodule EthereumJSONRPC.Arbitrum.Constants.Contracts do interacting with core Arbitrum protocol contracts including: """ + # EigenDA type definitions - extracted for reusability + @eigen_da_bn254_g1_point_type {:tuple, + [ + # X + {:uint, 256}, + # Y + {:uint, 256} + ]} + + @eigen_da_quorum_blob_param_type {:tuple, + [ + # quorumNumber + {:uint, 8}, + # adversaryThresholdPercentage + {:uint, 8}, + # confirmationThresholdPercentage + {:uint, 8}, + # chunkLength + {:uint, 32} + ]} + + @eigen_da_batch_header_type {:tuple, + [ + # blobHeadersRoot + {:bytes, 32}, + # quorumNumbers + :bytes, + # signedStakeForQuorums + :bytes, + # referenceBlockNumber + {:uint, 32} + ]} + + @eigen_da_batch_metadata_type {:tuple, + [ + # BatchHeader + @eigen_da_batch_header_type, + # signatoryRecordHash + {:bytes, 32}, + # confirmationBlockNumber + {:uint, 32} + ]} + + @eigen_da_blob_verification_proof_type {:tuple, + [ + # batchId + {:uint, 32}, + # blobIndex + {:uint, 32}, + # BatchMetadata + @eigen_da_batch_metadata_type, + # inclusionProof + :bytes, + # quorumIndices + :bytes + ]} + + @eigen_da_blob_header_type {:tuple, + [ + # BN254.G1Point commitment + @eigen_da_bn254_g1_point_type, + # dataLength + {:uint, 32}, + # QuorumBlobParam[] quorumBlobParams + {:array, @eigen_da_quorum_blob_param_type} + ]} + + @eigen_da_cert_type {:tuple, + [ + # BlobVerificationProof + @eigen_da_blob_verification_proof_type, + # BlobHeader + @eigen_da_blob_header_type + ]} + @selector_outbox "ce11e6ab" @selector_sequencer_inbox "ee35f327" @selector_bridge "e78cea92" @@ -546,4 +621,74 @@ defmodule EthereumJSONRPC.Arbitrum.Constants.Contracts do ]} ] } + + @doc """ + Returns selector with ABI (object of `ABI.FunctionSelector`) of the function: + + addSequencerL2BatchFromEigenDA( + uint256 sequenceNumber, + EigenDACert calldata cert, + IGasRefunder gasRefunder, + uint256 afterDelayedMessagesRead, + uint256 prevMessageCount, + uint256 newMessageCount + ) + """ + def add_sequencer_l2_batch_from_eigen_da_selector_with_abi, + do: %ABI.FunctionSelector{ + function: "addSequencerL2BatchFromEigenDA", + types: [ + {:uint, 256}, + # EigenDACert structure + @eigen_da_cert_type, + :address, + {:uint, 256}, + {:uint, 256}, + {:uint, 256} + ] + } + + @doc """ + Returns ABI definition for EigenDACert structure for encoding purposes. + + The EigenDACert contains BlobVerificationProof and BlobHeader with full nested structures. + """ + def eigen_da_cert_abi, + do: %ABI.FunctionSelector{ + function: nil, + types: [ + # EigenDACert structure + @eigen_da_cert_type + ] + } + + @doc """ + Returns ABI definition for BlobVerificationProof structure for encoding purposes. + + The BlobVerificationProof contains batchId, blobIndex, BatchMetadata, inclusionProof, and quorumIndices. + """ + def eigen_da_blob_verification_proof_abi, + # https://github.com/Layr-Labs/eigenda/blob/6c1deb51fced68efb1aaa585dd5ddb6a27f88637/contracts/src/core/libraries/v1/EigenDATypesV1.sol#L36-L55 + do: %ABI.FunctionSelector{ + function: nil, + types: [ + # BlobVerificationProof + @eigen_da_blob_verification_proof_type + ] + } + + @doc """ + Returns ABI definition for BlobHeader structure for encoding purposes. + + The BlobHeader contains BN254.G1Point commitment, dataLength, and quorumBlobParams array. + """ + def eigen_da_blob_header_abi, + # https://github.com/Layr-Labs/eigenda/blob/6c1deb51fced68efb1aaa585dd5ddb6a27f88637/contracts/src/core/libraries/v1/EigenDATypesV1.sol#L18-L29 + do: %ABI.FunctionSelector{ + function: nil, + types: [ + # BlobHeader + @eigen_da_blob_header_type + ] + } end diff --git a/apps/explorer/lib/explorer/chain/arbitrum/da_multi_purpose_record.ex b/apps/explorer/lib/explorer/chain/arbitrum/da_multi_purpose_record.ex index c7ef42e98e60..de14ed251628 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/da_multi_purpose_record.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/da_multi_purpose_record.ex @@ -103,4 +103,22 @@ defmodule Explorer.Chain.Arbitrum.DaMultiPurposeRecord.Helper do when is_integer(height) and is_binary(transaction_commitment) do :crypto.hash(:sha256, :binary.encode_unsigned(height) <> transaction_commitment) end + + @doc """ + Calculates the data key for `Explorer.Chain.Arbitrum.DaMultiPurposeRecord` that contains EigenDA blob data. + + ## Parameters + - `blob_header`: The binary blob header from EigenDA. + + ## Returns + - A binary representing the calculated data key for the record containing + EigenDA blob data. + """ + @spec calculate_eigenda_data_key(binary()) :: binary() + def calculate_eigenda_data_key(blob_header) when is_binary(blob_header) do + # The data key is calculated as the hash of the blob header as per + # https://layr-labs.github.io/eigenda/integration/spec/3-data-structs.html#blobheader + + ExKeccak.hash_256(blob_header) + end end diff --git a/apps/explorer/lib/explorer/chain/arbitrum/l1_batch.ex b/apps/explorer/lib/explorer/chain/arbitrum/l1_batch.ex index 624d75832750..15f40bca81fc 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/l1_batch.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/l1_batch.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.Arbitrum.L1Batch do Migrations: - Explorer.Repo.Arbitrum.Migrations.CreateArbitrumTables - Explorer.Repo.Arbitrum.Migrations.AddDaInfo + - Explorer.Repo.Arbitrum.Migrations.AddEigendaBatches """ use Explorer.Schema @@ -31,7 +32,7 @@ defmodule Explorer.Chain.Arbitrum.L1Batch do * `before_acc` - The hash of the state before the batch. * `after_acc` - The hash of the state after the batch. * `commitment_id` - The ID of the commitment L1 transaction from Explorer.Chain.Arbitrum.LifecycleTransaction. - * `batch_container` - The tag meaning the container of the batch data: `:in_blob4844`, `:in_calldata`, `:in_celestia`, `:in_anytrust` + * `batch_container` - The tag meaning the container of the batch data: `:in_blob4844`, `:in_calldata`, `:in_celestia`, `:in_anytrust`, `:in_eigenda` """ @type to_import :: %{ number: non_neg_integer(), @@ -41,7 +42,7 @@ defmodule Explorer.Chain.Arbitrum.L1Batch do before_acc: binary(), after_acc: binary(), commitment_id: non_neg_integer(), - batch_container: :in_blob4844 | :in_calldata | :in_celestia | :in_anytrust + batch_container: :in_blob4844 | :in_calldata | :in_celestia | :in_anytrust | :in_eigenda } @typedoc """ @@ -53,7 +54,7 @@ defmodule Explorer.Chain.Arbitrum.L1Batch do * `after_acc` - The hash of the state after the batch. * `commitment_id` - The ID of the commitment L1 transaction from `Explorer.Chain.Arbitrum.LifecycleTransaction`. * `commitment_transaction` - An instance of `Explorer.Chain.Arbitrum.LifecycleTransaction` referenced by `commitment_id`. - * `batch_container` - The tag meaning the container of the batch data: `:in_blob4844`, `:in_calldata`, `:in_celestia`, `:in_anytrust` + * `batch_container` - The tag meaning the container of the batch data: `:in_blob4844`, `:in_calldata`, `:in_celestia`, `:in_anytrust`, `:in_eigenda` """ @primary_key {:number, :integer, autogenerate: false} typed_schema "arbitrum_l1_batches" do @@ -69,7 +70,7 @@ defmodule Explorer.Chain.Arbitrum.L1Batch do type: :integer ) - field(:batch_container, Ecto.Enum, values: [:in_blob4844, :in_calldata, :in_celestia, :in_anytrust]) + field(:batch_container, Ecto.Enum, values: [:in_blob4844, :in_calldata, :in_celestia, :in_anytrust, :in_eigenda]) timestamps() end diff --git a/apps/explorer/priv/arbitrum/migrations/20250731001757_add_eigenda_batches.exs b/apps/explorer/priv/arbitrum/migrations/20250731001757_add_eigenda_batches.exs new file mode 100644 index 000000000000..2c4af82d268d --- /dev/null +++ b/apps/explorer/priv/arbitrum/migrations/20250731001757_add_eigenda_batches.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Arbitrum.Migrations.AddEigendaBatches do + use Ecto.Migration + + def change do + execute("ALTER TYPE arbitrum_da_containers_types ADD VALUE 'in_eigenda'") + end +end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/da/README.md b/apps/indexer/lib/indexer/fetcher/arbitrum/da/README.md new file mode 100644 index 000000000000..8b97b96cae4f --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/da/README.md @@ -0,0 +1,539 @@ +# Data Availability Solution Extension Guide for Arbitrum/Orbit Rollups + +This guide provides a comprehensive list of all files that need to be touched when extending the Blockscout indexer for Arbitrum-based (Orbit) rollups with support for new Data Availability (DA) solutions or updating existing DA solutions to newer versions. + +## Overview + +This document outlines the systematic approach required to add new DA solutions or update existing ones for Arbitrum-based rollups. The implementation typically follows a multi-phase approach to ensure stability and proper functionality. + +## Prerequisites for New DA Solution Implementation + +Before implementing support for a new DA solution, developers must research and understand the following requirements: + +### 1. Contract Method ABI +**Requirement**: Complete ABI definition of the method used to commit Orbit batches to the base (L1) layer. + +**Examples**: +- `addSequencerL2BatchFromEigenDA(uint256 sequenceNumber, EigenDACert calldata cert, ...)` +- `addSequencerL2BatchFromOrigin(uint256 sequenceNumber, bytes calldata data, ...)` + +**Investigation needed**: Contract source code, deployment documentation, or ABI extraction from verified contracts. + +### 2. DA Data Encoding Logic +**Requirement**: Understanding how DA-related data is encoded within the contract method parameters. + +**Two patterns**: +- **Independent encoding**: DA data is part of generic `data` parameter (like in `addSequencerL2BatchFromOrigin`) +- **Explicit encoding**: DA data has dedicated parameter with specific structure (like `cert` in `addSequencerL2BatchFromEigenDA`) + +**Investigation needed**: Contract implementation details, DA solution documentation, example transactions. + +### 3. Data Storage Requirements +**Requirement**: Determine which DA information must be decoded/accessible vs. stored as binary blobs. + +**Decoded information** (for API display): +- **Celestia**: Block height, transaction commitment +- **AnyTrust**: Data hash, timeout, signers mask, keyset information +- **EigenDA**: Blob metadata, certificate details + +**Binary blob storage** (for completeness): +- **Celestia**: `raw` field containing complete blob descriptor +- **EigenDA**: Blob header and verification proof as encoded bytes + +**Investigation needed**: What information do rollup operators and users need to see in the explorer UI? + +### 4. Supplementary Data Collection +**Requirement**: Identify any additional information that must be collected separately from the batch committing transaction. + +**Example**: AnyTrust keyset data +- **Primary DA record**: Certificate with keyset hash reference +- **Supplementary record**: Complete keyset details (threshold, committee member public keys) +- **Collection method**: Separate L1 event logs (`SetValidKeyset` events) + +**Investigation needed**: Dependencies, cross-references, external data sources required for complete DA information. + +### 5. Data Key Strategy +**Requirement**: Define how to uniquely identify DA records and link them with external explorers. + +**Strategies**: +- **Celestia**: Hash of (block height + transaction commitment) - enables `/da/celestia/:height/:commitment` endpoints +- **AnyTrust**: Direct data hash usage - enables `/da/anytrust/:data_hash` endpoints +- **EigenDA**: Hash of blob header - enables `/da/eigenda/:data_hash` endpoints + +**Investigation needed**: What identifiers do external DA explorers use? How should backlinks be constructed? + +### 6. Conflict Resolution Strategy +**Requirement**: Define how to handle duplicate DA records with the same identifier. + +**Strategies**: +- **Simple deduplication**: Database record wins (Celestia/EigenDA pattern) +- **Value-based resolution**: Compare fields and keep better record (AnyTrust timeout comparison) +- **Custom logic**: DA-specific rules based on solution characteristics + +**Investigation needed**: Are DA records immutable? Do they have quality metrics or validity periods? + +## Preparation Checklist + +Before starting implementation: +- [ ] Contract ABI obtained and validated +- [ ] DA encoding/decoding logic understood +- [ ] Data storage requirements defined +- [ ] Supplementary data sources identified +- [ ] External explorer integration requirements clarified +- [ ] Conflict resolution strategy determined +- [ ] Test data and scenarios prepared + +## Background: Understanding the Data Flow + +To understand why specific files need modification, it's essential to understand how batch data flows through the Blockscout indexer and how Data Availability (DA) information is processed, stored, and served. + +### Batch Discovery and Processing Flow + +The core batch indexing process follows this flow: + +```plaintext +Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery.perform + └─ Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery.handle_batches_from_logs + └─ Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery.execute_transaction_requests_parse_transactions_calldata + ├─ Indexer.Fetcher.Arbitrum.Utils.Rpc.parse_calldata_of_add_sequencer_l2_batch + └─ Indexer.Fetcher.Arbitrum.DA.Common.examine_batch_accompanying_data + └─ Indexer.Fetcher.Arbitrum.DA.Common.parse_data_availability_info + └─ Indexer.Fetcher.Arbitrum.DA.{Celestia,Anytrust,Eigenda}.parse_batch_accompanying_data +``` + +**Why this matters**: Each new DA solution requires updates at multiple points in this flow: + +1. **RPC Layer**: `parse_calldata_of_add_sequencer_l2_batch` must recognize new method selectors and decode new transaction structures +2. **DA Common Layer**: `examine_batch_accompanying_data` must handle new DA type identifiers and route to appropriate parsers +3. **DA Specific Layer**: New DA modules must implement parsing logic for their specific data structures + +### Data Import and Storage Flow + +After parsing, DA information follows this import flow: + +```plaintext +Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery.execute_transaction_requests_parse_transactions_calldata + └─ Indexer.Fetcher.Arbitrum.DA.Common.required_import? + └─ Indexer.Fetcher.Arbitrum.DA.Common.prepare_for_import + └─ Indexer.Fetcher.Arbitrum.DA.{Celestia,Anytrust,Eigenda}.prepare_for_import + ├─ [AnyTrust] check_if_new_keyset → get_keyset_info_from_l1 (RPC calls) + └─ [Others] Direct data preparation + └─ Indexer.Fetcher.Arbitrum.DA.Common.eliminate_conflicts + └─ Indexer.Fetcher.Arbitrum.DA.Common.process_records + └─ Indexer.Fetcher.Arbitrum.DA.{Celestia,Anytrust,Eigenda}.resolve_conflict +``` + +**Why this matters**: Each DA solution has different parsing and storage requirements: + +1. **Parsing Approaches**: + - **Celestia/AnyTrust**: Extract DA info during input parameter parsing + - **EigenDA**: Full ABI decoding of complex nested structures + +2. **Storage Requirements**: Not all DA information requires database storage + - **Celestia/EigenDA**: Basic display information only + - **AnyTrust**: Comprehensive data including committee member details and keysets + +3. **Supplementary Data Collection**: Some DA solutions require additional data gathering during `prepare_for_import` + - **AnyTrust**: Fetch keyset details via separate L1 RPC calls to retrieve `SetValidKeyset` events + - **Complex operations**: May involve multiple RPC requests, event log parsing, or third-party data provider communication + - **Performance impact**: Can significantly increase import processing time + +4. **Data Import Logic**: Each DA solution creates specific database records that must be prepared and conflict-resolved + +### API Rendering and Serving Flow + +When serving batch information via API, the flow varies by DA solution complexity: + +```plaintext +BlockScoutWeb.API.V2.ArbitrumController.batch + └─ Explorer.Chain.Arbitrum.Reader.API.Settlement.batch + └─ BlockScoutWeb.API.V2.ArbitrumView.render("arbitrum_batch.json", %{batch: batch}) + └─ BlockScoutWeb.API.V2.ArbitrumView.add_da_info + ├─ generate_celestia_da_info (basic blob info) + ├─ generate_anytrust_certificate (certificate + keyset lookup) + │ └─ Explorer.Chain.Arbitrum.Reader.API.Settlement.get_da_info_by_batch_number + │ └─ Fetch keyset details for committee information + └─ generate_eigen_da_info (basic certificate info) +``` + +**Why this matters**: DA solutions have different rendering complexity: + +1. **Basic Rendering** (Celestia/EigenDA): Simple metadata display + - Height, commitment, blob headers + - Direct mapping from database records + +2. **Complex Rendering** (AnyTrust): Multi-record aggregation + - Certificate data (data_type = 0) + - Keyset information (data_type = 1) for committee details + - Cross-reference between records using keyset hash + +3. **Schema Layer**: API response schemas must include new DA container types + +### Reverse Lookup Flow (Data Hash to Batch) + +DA solutions support different reverse lookup patterns based on their identifier schemes. These endpoints enable **backlink integration** with DA solution explorers (e.g., Celenium for Celestia, EigenDA Blob Explorer) to provide direct links to corresponding batch pages in Blockscout instances. + +**Single-Parameter Lookups** (AnyTrust, EigenDA): +```plaintext +/api/v2/arbitrum/batches/da/{anytrust|eigenda}/:data_hash + └─ BlockScoutWeb.API.V2.ArbitrumController.batch_by_data_availability_info + └─ BlockScoutWeb.API.V2.ArbitrumController.one_batch_by_data_availability_info + └─ Explorer.Chain.Arbitrum.Reader.API.Settlement.get_da_record_by_data_key + └─ BlockScoutWeb.API.V2.ArbitrumController.batch +``` + +**Multi-Parameter Lookups** (Celestia): +```plaintext +/api/v2/arbitrum/batches/da/celestia/:height/:transaction_commitment + └─ calculate_celestia_data_key(height, transaction_commitment) + └─ [same lookup flow as above] +``` + +**Why this matters**: DA solutions have different identifier requirements and integration needs: + +1. **Endpoint Design**: + - **AnyTrust/EigenDA**: Single hash parameter endpoints (`/batches/da/{solution}/:data_hash`) + - **Celestia**: Multi-parameter endpoints (`/batches/da/celestia/:height/:transaction_commitment`) + +2. **External Integration**: Enable DA solution explorers to provide backlinks + - **Celenium (Celestia Explorer)**: Links from blob data to corresponding Arbitrum batch + - **EigenDA Blob Explorer**: Links from blob certificates to rollup batch information + - **AnyTrust explorers**: Links from data hash to batch details + +3. **Data Key Calculation**: Helper functions must compute correct database keys + - **AnyTrust**: Direct data hash usage + - **Celestia**: Hash of (height + transaction_commitment) + - **EigenDA**: Hash of blob header + +4. **Database Indexing**: Efficient reverse lookups from computed keys to batch numbers + +### Contract Integration Flow + +The system must decode new contract method calls: + +```plaintext +Smart Contract Method Call (addSequencerL2BatchFrom{DASolution}) + └─ EthereumJSONRPC.Arbitrum.Constants.Contracts.add_sequencer_l2_batch_from_{da_solution}_selector_with_abi + └─ Indexer.Fetcher.Arbitrum.Utils.Rpc.parse_calldata_of_add_sequencer_l2_batch + └─ ABI.TypeDecoder.decode(calldata, abi) +``` + +**Why this matters**: Each new DA solution typically introduces: + +1. **New Contract Methods**: Different function signatures for batch submission +2. **Data Processing Variations**: Some DA solutions return decoded data as-is, others require preprocessing (e.g., EigenDA prepends a header byte flag) +3. **ABI Complexity**: Varies from simple parameters to complex nested structures requiring comprehensive ABI definitions + +### Database Entity Relationships + +DA information uses a flexible multi-purpose storage design: + +```plaintext +arbitrum_l1_batches (batch_container field - indicates DA type) + ├─ arbitrum_da_multi_purpose (main DA storage with type-based records) + │ ├─ DA Records (data_type = 0): certificates, proofs, blob descriptors + │ └─ Supplementary Records (data_type ≠ 0): keysets, additional metadata + └─ arbitrum_batches_to_da_blobs (many-to-one batch-DA relationships) + └─ Multiple batches can reference the same DA record when data is identical +``` + +**Why this matters**: The flexible design supports varying DA solution requirements: + +1. **Multi-Purpose Storage**: `arbitrum_da_multi_purpose` stores JSON data with different `data_type` values + - **Type 0**: Primary DA records (certificates, blob descriptors) + - **Type 1+**: Supplementary records (e.g., AnyTrust keysets) + +2. **Data Key Strategies**: Each DA solution uses different identifier schemes + - **AnyTrust**: Data hash of the certificate + - **Celestia**: Hash of (block height + transaction commitment) + - **EigenDA**: Hash of blob header + +3. **Storage Granularity**: Varies by DA solution based on display requirements + - **Celestia/EigenDA**: Basic information (height, commitment, blob metadata) + - **AnyTrust**: Comprehensive data (certificate + individual committee member contributions) + +4. **Batch Relationships**: `arbitrum_batches_to_da_blobs` handles multiple batches referencing the same DA record (many-to-one relationship when batches contain identical data) + +This interconnected architecture explains why implementing a new DA solution requires touching multiple layers of the system - from low-level contract parsing to high-level API responses. Each component serves a specific role in the overall data flow, and all must be updated cohesively to maintain system integrity. + +## Complete File List + +### 1. Core DA Module Files + +**Location**: `apps/indexer/lib/indexer/fetcher/arbitrum/da/` + +- `common.ex` - Core DA functionality that handles all DA types +- `{new_da_solution}.ex` - New DA-specific module (e.g., `eigenda.ex`, `celestia.ex`, `anytrust.ex`) + +**Purpose**: +- `common.ex` contains shared functionality for all DA solutions +- Individual DA modules handle parsing, preparation, and conflict resolution for specific DA types + +### 2. Database Schema and Migrations + +**Migration Files**: `apps/explorer/priv/arbitrum/migrations/` + +- `{timestamp}_add_{da_solution}_batches.exs` - Adds new DA container type to enum +- (Reference: `20250731001757_add_eigenda_batches.exs` for EigenDA) +- (Reference: `20240527212653_add_da_info.exs` for original DA types) + +**Schema Files**: `apps/explorer/lib/explorer/chain/arbitrum/` + +- `l1_batch.ex` - Updates batch container enum and type definitions +- `da_multi_purpose_record.ex` - May need helper functions for data key calculation + +**Purpose**: Define new DA container types and update database schema to support them. + +### 3. RPC and Contract Interaction + +**Files**: +- `apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/arbitrum/constants/contracts.ex` +- `apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex` + +**Purpose**: +- Add new method selectors and ABI definitions +- Update calldata parsing functions to handle new DA methods +- Define contract interaction patterns for new DA solutions + +### 4. Batch Discovery and Processing + +**Files**: +- `apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex` +- `apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex` +- `apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md` (documentation) + +**Purpose**: Handle batch discovery workflow and update documentation for new DA types. + +### 5. API Layer + +#### Controllers +**File**: `apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex` + +**Purpose**: Handle API endpoints for DA-specific batch lookups (e.g., `/batches/da/{da_solution}/:data_hash`) to enable integration with external DA explorers + +#### Views +**File**: `apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex` + +**Purpose**: +- Add new DA info generation functions (e.g., `generate_{da_solution}_da_info`) +- Update `add_da_info` function to handle new DA container types +- Implement rendering logic for DA-specific information + +#### Routing +**File**: `apps/block_scout_web/lib/block_scout_web/routers/api_router.ex` + +**Purpose**: Add new API endpoints for DA-specific batch lookups, enabling backlink integration with DA solution explorers (Celenium, EigenDA Blob Explorer, etc.) + +#### API Schemas +**File**: `apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex` + +**Purpose**: Update API response schemas to include new DA container types in enums + +### 6. Configuration and Documentation + +**Files**: +- `cspell.json` - Add new DA solution terms to spell check dictionary +- `apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md` - Update documentation + +## Implementation Phases + +### Phase 1: Basic Support (No Exception) +- Add method selector support in `contracts.ex` +- Update `parse_calldata_of_add_sequencer_l2_batch` in `rpc.ex` +- Return placeholder data to prevent indexing failures + +### Phase 2: Database Schema Extension +- Create migration to add new DA container type to enum +- Update `l1_batch.ex` schema definitions: + - Add new enum value to `batch_container` field definition + - Update type specifications to include new DA container type +- Update type specifications in `common.ex`: + - Add new DA type to `@spec` definitions that enumerate DA container types + - Update documentation comments that list supported DA types + +### Phase 3: DA Certificate/Data Parsing +- Implement appropriate parsing approach for the DA solution: + - **Simple parsing**: Extract basic parameters during input processing + - **Complex parsing**: Full ABI decoding of nested structures +- Create new DA module with parsing logic +- Update `parse_data_availability_info` in `common.ex`: + - Add new header flag case to route to the new DA module + - Ensure the function returns the correct DA type atom + +### Phase 4: Data Preparation and Import +- Implement `prepare_for_import` functionality based on storage requirements: + - **Basic storage**: Simple metadata for display + - **Comprehensive storage**: Multiple record types with supplementary data + - **Supplementary data collection**: Implement additional data gathering if required + - **Example**: AnyTrust keyset fetching via `check_if_new_keyset` → `get_keyset_info_from_l1` + - **RPC operations**: Block number retrieval, event log fetching, data decoding + - **Third-party integration**: May require external data provider APIs + - **Caching strategy**: Implement caching to avoid redundant external calls +- Update `common.ex` import logic: + - Add new DA type to `required_import?` function (if storage is needed) + - Add pattern matching for new DA type in `prepare_for_import` + - Include new DA type in `eliminate_conflicts` function +- Implement conflict resolution for both intra-batch and inter-batch scenarios: + - **Intra-batch deduplication**: Handle multiple batches in same processing chunk referencing same DA blob + - **Inter-batch resolution**: Implement `resolve_conflict/2` function to handle conflicts with existing database records + - **Simple deduplication** (Celestia/EigenDA pattern): Exclude candidates when data_key already exists in database + - **Value-based resolution** (AnyTrust pattern): Compare specific fields (e.g., timeout values) and keep the better record + - **Custom logic**: Define DA-specific rules for handling duplicate data scenarios + +### Phase 5: API Integration +- Add DA-specific rendering functions based on complexity: + - **Simple rendering**: Direct mapping from single database record + - **Complex rendering**: Multi-record aggregation with cross-references +- Update API response generation +- Implement DA-specific data retrieval + +### Phase 6: Endpoint Extension +- Add new API endpoints for DA-specific lookups to enable backlink integration: + - **Single-parameter**: `/batches/da/{solution}/:identifier` (AnyTrust, EigenDA) + - **Multi-parameter**: `/batches/da/{solution}/:param1/:param2` (Celestia) + - **Purpose**: Allow DA solution explorers to link directly to corresponding batch pages +- Update routing configuration + +### Phase 7: Schema Updates +- Update API response schemas +- Add new DA container types to API documentation + +## Key Patterns to Follow + +### 1. DA Module Structure +Each DA solution module should implement: +- `@enforce_keys` and `defstruct` for DA info structure +- `@type t` type definition +- `parse_batch_accompanying_data/2` function +- `prepare_for_import/2` function +- `resolve_conflict/2` function + +### 2. Common DA Integration Points +**Phase 2 Updates (Type Specifications):** +- Update `@spec` definitions in `common.ex` to include new DA container type +- Update documentation comments listing supported DA types + +**Phase 3 Updates (Parsing Logic):** +- Add new header flag case in `parse_data_availability_info` to route to new DA module +- Ensure correct DA type atom is returned + +**Phase 4 Updates (Import Logic):** +- Add new DA type to `required_import?` function based on storage needs +- Include new DA type in `prepare_for_import` pattern matching +- Add conflict resolution support in `eliminate_conflicts` + +### 3. RPC Parsing Patterns +Two main approaches for handling DA data in `parse_calldata_of_add_sequencer_l2_batch`: +```elixir +# Simple approach: return data as-is +{sequence_number, prev_message_count, new_message_count, data} + +# Complex approach: prepend header flag for DA routing +{sequence_number, prev_message_count, new_message_count, <> <> processed_data} +``` + +### 4. Database Migration Pattern +```elixir +def change do + execute("ALTER TYPE arbitrum_da_containers_types ADD VALUE 'in_{da_solution}'") +end +``` + +### 5. API View Pattern +```elixir +case batch.batch_container do + :in_{da_solution} -> generate_{da_solution}_da_info(batch.number) + # ... other cases +end +``` + +### 6. Data Key Calculation +Each DA solution needs a helper function in `da_multi_purpose_record.ex` to calculate the data key used for database storage and lookups. The calculation varies by DA solution: +- **Single-parameter**: Hash of primary identifier +- **Multi-parameter**: Hash of combined parameters + +### 7. Conflict Resolution Patterns +The `resolve_conflict/2` function handles duplicate data keys between database and candidate records. + +**What are these conflicts?** +There are **two types of conflicts** that must be handled: + +**1. Intra-Batch Conflicts** (within current import set): +- **Problem**: Multiple batches in the same processing chunk referencing the same DA blob +- **Issue**: Database uniqueness constraints fail when trying to insert duplicate `data_key` values +- **Solution**: Deduplicate within the current batch before database import +- **Example**: Batches B1 and B2 both reference the same AnyTrust data blob D1 + +**2. Inter-Batch Conflicts** (current vs existing database): +- **Problem**: Current batch references a DA blob that already exists in database +- **Issue**: Without proper handling, new batch associations can overwrite existing ones +- **Solution**: Check existing database records and resolve conflicts appropriately +- **Example**: Previously imported batch B1 with blob D1, now importing batch B2 with same blob D1 + +**Common scenarios**: +- Reprocessing the same batch data during indexer restarts +- Multiple batches referencing identical DA blobs (empty batches scenario) +- Chain reorganizations causing re-import of previously processed DA records + +**Resolution strategies:** + +**Simple Deduplication Pattern** (Celestia/EigenDA): +- Database record always wins - if `data_key` exists in database, exclude the candidate +- Used when DA records are immutable and duplicates are identical + +**Value-Based Resolution Pattern** (AnyTrust): +- Compare specific field values (e.g., timeout) between database and candidate records +- Keep the record with better characteristics (higher timeout = longer data availability) +- Used when DA records can have different quality metrics + +## Testing Considerations + +When implementing a new DA solution, ensure testing covers: +- Batch discovery and parsing +- **Supplementary data collection** (if applicable): + - RPC call handling and error scenarios + - Event log parsing and decoding + - Caching mechanism functionality + - Third-party data provider integration + - Performance impact of additional network calls +- Database import and conflict resolution scenarios: + - **Intra-batch conflicts**: Multiple batches in same processing chunk with same DA blob + - **Inter-batch conflicts**: Current batch vs existing database records + - Value-based conflict resolution (if applicable) + - Multiple batches referencing the same DA record + - Chain reorganization scenarios +- API endpoint functionality +- Schema validation +- Error handling for malformed data + +## Backward Compatibility + +Always ensure that: +- Existing DA solutions continue to work +- Database migrations are non-destructive +- API responses maintain backward compatibility +- New fields are properly nullable where appropriate + +## Dependencies + +New DA solutions may require: +- Additional Elixir dependencies for cryptographic functions +- ABI encoder/decoder updates +- External service integrations for DA layer communication +- **Supplementary data sources**: + - Enhanced RPC node access for event log retrieval + - Third-party data provider APIs for off-chain information + - Caching infrastructure for performance optimization of external calls + +## Implementation Example + +The EigenDA implementation ([PR #12915](https://github.com/blockscout/blockscout/pull/12915)) serves as a concrete example of following this systematic approach. The implementation demonstrates: + +- **Multi-phase rollout**: From basic method support to full API integration +- **Complex ABI handling**: Nested structures with BlobVerificationProof and BlobHeader +- **Header flag usage**: Custom flag (237) for DA type routing +- **Supplementary data**: While EigenDA doesn't require external data collection like AnyTrust keysets, it showcases complex certificate parsing +- **Database design**: Integration with the `arbitrum_batches_to_da_blobs` table structure +- **API endpoints**: Implementation of `/batches/da/eigenda/:data_hash` reverse lookup + +This example illustrates how the abstract patterns described in this guide translate into working code across all the identified file layers. \ No newline at end of file diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/da/common.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/da/common.ex index 7a334225ecea..40d6b7521841 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/da/common.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/da/common.ex @@ -6,7 +6,7 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_error: 1] - alias Indexer.Fetcher.Arbitrum.DA.{Anytrust, Celestia} + alias Indexer.Fetcher.Arbitrum.DA.{Anytrust, Celestia, Eigenda} alias Indexer.Fetcher.Arbitrum.Utils.Db.Settlement, as: Db alias Explorer.Chain.Arbitrum @@ -23,15 +23,16 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do ## Returns - `{status, da_type, da_info}` where `da_type` is one of `:in_blob4844`, - `:in_calldata`, `:in_celestia`, `:in_anytrust`, or `nil` if the accompanying - data cannot be parsed or is of an unsupported type. `da_info` contains the DA - info descriptor for Celestia or Anytrust. + `:in_calldata`, `:in_celestia`, `:in_anytrust`, `:in_eigenda`, or `nil` if + the accompanying data cannot be parsed or is of an unsupported type. + `da_info` contains the DA info descriptor for Celestia, Anytrust, or EigenDA. """ @spec examine_batch_accompanying_data(non_neg_integer(), binary()) :: {:ok, :in_blob4844, nil} | {:ok, :in_calldata, nil} | {:ok, :in_celestia, Celestia.t()} | {:ok, :in_anytrust, Anytrust.t()} + | {:ok, :in_eigenda, Eigenda.t()} | {:error, nil, nil} def examine_batch_accompanying_data(batch_number, batch_accompanying_data) do case batch_accompanying_data do @@ -43,9 +44,9 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do @doc """ Prepares data availability (DA) information for import. - This function processes a list of DA information, either from Celestia or Anytrust, - preparing it for database import. It handles deduplication of records within the same - processing chunk and against existing database records. + This function processes a list of DA information, either from Celestia, Anytrust, or + EigenDA, preparing it for database import. It handles deduplication of records + within the same processing chunk and against existing database records. ## Parameters - `da_info`: A list of DA information structs. @@ -58,7 +59,7 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do the current batch and against existing database records. - A list of batch-to-blob associations (`BatchToDaBlob`) ready for import. """ - @spec prepare_for_import([Celestia.t() | Anytrust.t() | map()], %{ + @spec prepare_for_import([Celestia.t() | Anytrust.t() | Eigenda.t() | map()], %{ :sequencer_inbox_address => String.t(), :json_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments() }) :: {[Arbitrum.DaMultiPurposeRecord.to_import()], [Arbitrum.BatchToDaBlob.to_import()]} @@ -69,7 +70,8 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do initial_acc = { %{ celestia: %{}, - anytrust: %{} + anytrust: %{}, + eigenda: %{} }, [], MapSet.new() @@ -105,6 +107,16 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do updated_cache } + %Eigenda{} -> + {updated_records, updated_batches} = + Eigenda.prepare_for_import({da_records_by_type.eigenda, batch_to_blob_acc}, info) + + { + %{da_records_by_type | eigenda: updated_records}, + updated_batches, + cache + } + _ -> {da_records_by_type, batch_to_blob_acc, cache} end @@ -123,19 +135,21 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do # # ## Parameters # - `da_records_by_type`: A map containing candidate records organized by DA type - # (`:celestia` and `:anytrust`), where each type has a map of records keyed by `data_key`. + # (`:celestia`, `:anytrust`, and `:eigenda`), where each type has a map of records keyed by `data_key`. # # ## Returns # - A list of `DaMultiPurposeRecord` records after conflict resolution, ready for import. @spec eliminate_conflicts(%{ celestia: %{binary() => Arbitrum.DaMultiPurposeRecord.to_import()}, - anytrust: %{binary() => Arbitrum.DaMultiPurposeRecord.to_import()} + anytrust: %{binary() => Arbitrum.DaMultiPurposeRecord.to_import()}, + eigenda: %{binary() => Arbitrum.DaMultiPurposeRecord.to_import()} }) :: [Arbitrum.DaMultiPurposeRecord.to_import()] defp eliminate_conflicts(da_records_by_type) do # Define the types and their corresponding resolution modules type_configs = [ {:celestia, da_records_by_type.celestia, &Celestia.resolve_conflict/2}, - {:anytrust, da_records_by_type.anytrust, &Anytrust.resolve_conflict/2} + {:anytrust, da_records_by_type.anytrust, &Anytrust.resolve_conflict/2}, + {:eigenda, da_records_by_type.eigenda, &Eigenda.resolve_conflict/2} ] # Process each type using the same pattern @@ -148,7 +162,7 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do # Processes candidate DA records using a type-specific resolution function. # # This function takes a map of candidate DA records and a resolution function specific - # to the DA type (Celestia or AnyTrust). It fetches any existing records from the + # to the DA type (Celestia, AnyTrust, or EigenDA). It fetches any existing records from the # database with matching data keys and uses the resolution function to determine # which records should be imported. # @@ -188,16 +202,16 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do ## Parameters - `da_type`: The type of data availability, which can be `:in_blob4844`, `:in_calldata`, - `:in_celestia`, `:in_anytrust`, or `nil`. + `:in_celestia`, `:in_anytrust`, `:in_eigenda`, or `nil`. ## Returns - - `true` if the DA type is `:in_celestia` or `:in_anytrust`, indicating that the data - requires import. + - `true` if the DA type is `:in_celestia`, `:in_anytrust`, or `:in_eigenda`, indicating + that the data requires import. - `false` for all other DA types, indicating that the data does not require import. """ - @spec required_import?(:in_blob4844 | :in_calldata | :in_celestia | :in_anytrust | nil) :: boolean() + @spec required_import?(:in_blob4844 | :in_calldata | :in_celestia | :in_anytrust | :in_eigenda | nil) :: boolean() def required_import?(da_type) do - da_type in [:in_celestia, :in_anytrust] + da_type in [:in_celestia, :in_anytrust, :in_eigenda] end # Parses data availability information based on the header flag. @@ -205,6 +219,7 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do {:ok, :in_calldata, nil} | {:ok, :in_celestia, Celestia.t()} | {:ok, :in_anytrust, Anytrust.t()} + | {:ok, :in_eigenda, Eigenda.t()} | {:error, nil, nil} defp parse_data_availability_info(batch_number, << header_flag::size(8), @@ -216,18 +231,26 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Common do {:ok, :in_calldata, nil} 32 -> + # 0x20 log_error("ZERO HEAVY messages are not supported.") {:error, nil, nil} 99 -> + # 0x63 Celestia.parse_batch_accompanying_data(batch_number, rest) 128 -> + # 0x80 Anytrust.parse_batch_accompanying_data(batch_number, rest) 136 -> + # 0x88 Anytrust.parse_batch_accompanying_data(batch_number, rest) + 237 -> + # 0xed + Eigenda.parse_batch_accompanying_data(batch_number, rest) + _ -> log_error("Unknown header flag found during an attempt to parse DA data: #{header_flag}") {:error, nil, nil} diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/da/eigenda.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/da/eigenda.ex new file mode 100644 index 000000000000..01596f9e3755 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/da/eigenda.ex @@ -0,0 +1,196 @@ +defmodule Indexer.Fetcher.Arbitrum.DA.Eigenda do + @moduledoc """ + Provides functionality for parsing EigenDA data availability information + associated with Arbitrum rollup batches. + """ + + import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_error: 1, log_info: 1] + import Explorer.Chain.Arbitrum.DaMultiPurposeRecord.Helper, only: [calculate_eigenda_data_key: 1] + + alias ABI.{TypeDecoder, TypeEncoder} + alias EthereumJSONRPC.Arbitrum.Constants.Contracts, as: ArbitrumContracts + alias Indexer.Fetcher.Arbitrum.Utils.Helper, as: ArbitrumHelper + + alias Explorer.Chain.Arbitrum + + @enforce_keys [:batch_number, :blob_verification_proof, :blob_header] + defstruct @enforce_keys + + @typedoc """ + EigenDA certificate struct: + * `batch_number` - The batch number in the Arbitrum rollup associated with the + EigenDA certificate. + * `blob_verification_proof` - The encoded binary data of the blob verification proof + containing batchId, blobIndex, BatchMetadata, inclusionProof, and quorumIndices. + * `blob_header` - The encoded binary data of the blob header containing commitment (BN254.G1Point), + dataLength, and quorumBlobParams array. + """ + @type t :: %__MODULE__{ + batch_number: non_neg_integer(), + blob_verification_proof: binary(), + blob_header: binary() + } + + @doc """ + Parses the batch accompanying data for EigenDA. + + This function extracts EigenDA certificate information from the binary input + associated with a given batch number by decoding the full EigenDACert structure, + then encoding the BlobVerificationProof and BlobHeader components back to binary + format for storage. The complex nested structures (BatchMetadata, BN254.G1Point, + QuorumBlobParams, etc.) are preserved in their encoded binary form. + + ## Parameters + - `batch_number`: The batch number in the Arbitrum rollup associated with the EigenDA data. + - `binary_data`: The binary data containing the encoded EigenDA certificate. + + ## Returns + - `{:ok, :in_eigenda, da_info}` if the data is successfully parsed. + - `{:error, nil, nil}` if the data cannot be parsed. + """ + @spec parse_batch_accompanying_data(non_neg_integer(), binary()) :: + {:ok, :in_eigenda, __MODULE__.t()} | {:error, nil, nil} + def parse_batch_accompanying_data(batch_number, binary_data) do + # This function implements a decode->encode pattern that may seem redundant but is + # architecturally necessary for the following reasons: + # + # 1. Data Validation: Decoding first ensures the EigenDA certificate is well-formed + # and contains valid data before storing it in the database. + # 2. Interface Compatibility: The DA pipeline requires individual BlobVerificationProof + # and BlobHeader components as separate binary fields, not as a single combined structure. + # 3. Library Reliability: Using TypeDecoder/TypeEncoder leverages battle-tested ABI + # handling instead of manual binary parsing, reducing the risk of encoding bugs. + # + # Alternative approaches (direct binary extraction) would require reimplementing + # complex ABI parsing logic and handling dynamic arrays/nested structures manually, + # introducing significant complexity and maintenance burden for minimal performance gain. + + # Decode the complex EigenDACert structure to get the tuple components + [{blob_verification_proof_tuple, blob_header_tuple}] = + TypeDecoder.decode( + binary_data, + ArbitrumContracts.eigen_da_cert_abi() + ) + + # Encode each component back to bytes for storage + blob_verification_proof_bytes = + TypeEncoder.encode([blob_verification_proof_tuple], ArbitrumContracts.eigen_da_blob_verification_proof_abi()) + + blob_header_bytes = + TypeEncoder.encode([blob_header_tuple], ArbitrumContracts.eigen_da_blob_header_abi()) + + {:ok, :in_eigenda, + %__MODULE__{ + batch_number: batch_number, + blob_verification_proof: blob_verification_proof_bytes, + blob_header: blob_header_bytes + }} + rescue + exception -> + log_error("Can not parse EigenDA certificate: #{inspect(exception)}") + {:error, nil, nil} + end + + @doc """ + Prepares EigenDA certificate data for import. + + ## Parameters + - A tuple containing: + - A map of already prepared DA records + - A list of already prepared batch-to-blob associations + - `da_info`: The EigenDA certificate struct containing blob header and verification proof. + + ## Returns + - A tuple containing: + - An updated map of `DaMultiPurposeRecord` structures ready for import in the DB, + where `data_key` maps to the record + - An updated list of `BatchToDaBlob` structures ready for import in the DB. + """ + @spec prepare_for_import( + {%{binary() => Arbitrum.DaMultiPurposeRecord.to_import()}, [Arbitrum.BatchToDaBlob.to_import()]}, + __MODULE__.t() + ) :: + {%{binary() => Arbitrum.DaMultiPurposeRecord.to_import()}, [Arbitrum.BatchToDaBlob.to_import()]} + def prepare_for_import({da_records_acc, batch_to_blob_acc}, %__MODULE__{} = da_info) do + blob_header_hex = ArbitrumHelper.bytes_to_hex_str(da_info.blob_header) + blob_verification_proof_hex = ArbitrumHelper.bytes_to_hex_str(da_info.blob_verification_proof) + + data = %{ + blob_header: blob_header_hex, + blob_verification_proof: blob_verification_proof_hex + } + + data_key = calculate_eigenda_data_key(da_info.blob_header) + + # Create record for arbitrum_da_multi_purpose table with batch_number set to nil + da_record = %{ + data_type: 0, + data_key: data_key, + data: data, + # TODO: This field must be removed as soon as migration to a separate table for Batch-to-DA-record associations is completed. + batch_number: nil + } + + # Create record for arbitrum_batches_to_da_blobs table + batch_to_blob_record = %{ + batch_number: da_info.batch_number, + data_blob_id: data_key + } + + # Only add the DA record if it doesn't already exist in the map + updated_da_records = + if Map.has_key?(da_records_acc, data_key) do + log_info("Found duplicate DA record #{ArbitrumHelper.bytes_to_hex_str(data_key)}") + # Record with this data_key already exists, keep existing record + da_records_acc + else + # No duplicate, add the new record + Map.put(da_records_acc, data_key, da_record) + end + + {updated_da_records, [batch_to_blob_record | batch_to_blob_acc]} + end + + @doc """ + Resolves conflicts between existing database records and candidate DA records. + + This function handles deduplication by comparing EigenDA data keys between database + records and candidate records. For EigenDA records, if a record with a matching data_key + already exists in the database, the candidate record is excluded from import. + + ## Parameters + - `db_records`: A list of `Arbitrum.DaMultiPurposeRecord` retrieved from the database + - `candidate_records`: A map where `data_key` maps to `Arbitrum.DaMultiPurposeRecord.to_import()` + + ## Returns + - A list of `Arbitrum.DaMultiPurposeRecord.to_import()` after resolving conflicts + """ + @spec resolve_conflict( + [Arbitrum.DaMultiPurposeRecord.t()], + %{binary() => Arbitrum.DaMultiPurposeRecord.to_import()} + ) :: [Arbitrum.DaMultiPurposeRecord.to_import()] + # When no database records to check against, simply return all candidate records + def resolve_conflict([], candidate_records) do + Map.values(candidate_records) + end + + def resolve_conflict(db_records, candidate_records) do + # Create a set of keys to exclude (those already present in DB) + keys_to_exclude = + Enum.reduce(db_records, MapSet.new(), fn db_record, acc -> + # Any key present in both DB and candidates should be excluded + if Map.has_key?(candidate_records, db_record.data_key) do + log_info("DA record #{ArbitrumHelper.bytes_to_hex_str(db_record.data_key)} already exists in DB") + + MapSet.put(acc, db_record.data_key) + else + acc + end + end) + + # Return only candidate records not in the exclude set + candidate_records + |> Enum.reject(fn {data_key, _record} -> MapSet.member?(keys_to_exclude, data_key) end) + |> Enum.map(fn {_data_key, record} -> record end) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex index b1e6a3858363..f18c5e90192d 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex @@ -5,7 +5,7 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do # TODO: Move the module under EthereumJSONRPC.Arbitrum. - alias ABI.TypeDecoder + alias ABI.{TypeDecoder, TypeEncoder} import EthereumJSONRPC, only: [json_rpc: 2, quantity_to_integer: 1, timestamp_to_datetime: 1] @@ -647,6 +647,15 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do - addSequencerL2BatchFromBlobsDelayProof - addSequencerL2BatchFromOriginDelayProof - addSequencerL2BatchDelayProof + - addSequencerL2BatchFromEigenDA + + Note: Although it is technically possible to implement a basic fallback to + avoid raising exceptions for unknown function selectors, this is + intentionally not implemented. Such a fallback would obscure issues where + batch data is not properly parsed and stored in the database. If this + occurred and was discovered later, a backfiller would need to be written to + re-process all discovered batches to assign them proper container types and + extract DA information correctly. ## Parameters - `calldata`: The raw calldata from the transaction as a binary string starting with "0x" @@ -764,6 +773,43 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do ) {sequence_number, prev_message_count, new_message_count, data} + + "0x283d8225" <> encoded_params -> + # addSequencerL2BatchFromEigenDA(uint256 sequenceNumber, EigenDACert calldata cert, IGasRefunder gasRefunder, uint256 afterDelayedMessagesRead, uint256 prevMessageCount, uint256 newMessageCount) + # https://github.com/Layr-Labs/nitro-contracts/blob/278fdbc39089fa86330f0c23f0a05aee61972c84/src/bridge/SequencerInbox.sol#L505-L512 + [ + sequence_number, + cert, + _gas_refunder, + _after_delayed_messages_read, + prev_message_count, + new_message_count + ] = + TypeDecoder.decode( + Base.decode16!(encoded_params, case: :lower), + ArbitrumContracts.add_sequencer_l2_batch_from_eigen_da_selector_with_abi() + ) + + # Encode the complex EigenDACert structure to bytes for interface compatibility. + # + # Why this encoding step is necessary: + # 1. TypeDecoder.decode() automatically unpacks the complex EigenDACert tuple structure + # into Elixir tuples when using the detailed ABI definition above. + # 2. However, the data availability parsing pipeline expects binary data that can be + # prefixed with a header flag (237 for EigenDA) and passed through the common interface. + # 3. Alternative approaches don't work: + # - Using :bytes in the function ABI gives ABI-encoded data with offset pointers, + # not the clean tuple encoding needed for later decoding + # - Changing the common interface to handle tuples would break compatibility with + # other DA types (Celestia, AnyTrust) that expect binary data + # 4. This encode step converts the decoded Elixir tuples back into clean ABI-encoded + # bytes that can be consistently decoded later in parse_batch_accompanying_data. + # + # This is not a performance issue as the encoding overhead is negligible compared to + # blockchain I/O operations, and it maintains clean separation of concerns. + cert_encoded = TypeEncoder.encode([cert], ArbitrumContracts.eigen_da_cert_abi()) + + {sequence_number, prev_message_count, new_message_count, <<237>> <> cert_encoded} end end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md index 34bfce7ca1fd..60c231ad5f8f 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/README.md @@ -17,6 +17,7 @@ The indexer supports multiple data storage mechanisms for batch data: - Data Availability (DA) blobs (EIP-4844) - AnyTrust solution - Celestia DA layer +- Eigen DA layer For each batch, the indexer: 1. Processes the `SequencerBatchDelivered` event diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex index 2f744d9414ec..4cf769aaff91 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex @@ -5,7 +5,8 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do The module's primary responsibilities include: * Processing `SequencerBatchDelivered` event logs to extract batch information * Building comprehensive data structures for batches and associated entities - * Handling Data Availability information for AnyTrust and Celestia solutions + * Handling Data Availability information for AnyTrust, Celestia, and EigenDA + solutions * Managing L2-to-L1 message status updates for committed messages * Importing discovered data into the database * Broadcasting new batch notifications for websocket clients @@ -22,7 +23,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do alias Explorer.Chain.Events.Publisher alias Indexer.Fetcher.Arbitrum.DA.Common, as: DataAvailabilityInfo - alias Indexer.Fetcher.Arbitrum.DA.{Anytrust, Celestia} + alias Indexer.Fetcher.Arbitrum.DA.{Anytrust, Celestia, Eigenda} alias Indexer.Fetcher.Arbitrum.Utils.Db.Messages, as: DbMessages alias Indexer.Fetcher.Arbitrum.Utils.Db.ParentChainTransactions, as: DbParentChainTransactions alias Indexer.Fetcher.Arbitrum.Utils.Db.Settlement, as: DbSettlement @@ -466,8 +467,8 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do # and decodes the calldata from the transactions to obtain batch details. It updates # the provided batch map with block ranges for new batches and constructs a map of # lifecycle transactions with their timestamps and finalization status. Additionally, - # it examines the data availability (DA) information for Anytrust or Celestia and - # constructs a list of DA info structs. + # it examines the data availability (DA) information for Anytrust, Celestia, or EigenDA + # and constructs a list of DA info structs. # # ## Parameters # - `transactions_requests`: The list of RPC requests to fetch transaction data. @@ -487,7 +488,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do # database import and require further processing. # - An updated map of batch descriptions with block ranges and data availability # information. - # - A list of data availability information structs for Anytrust or Celestia. + # - A list of data availability information structs for Anytrust, Celestia, or EigenDA. @spec execute_transaction_requests_parse_transactions_calldata( [EthereumJSONRPC.Transport.request()], non_neg_integer(), @@ -520,7 +521,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do :data_available => atom() | nil, optional(any()) => any() } - }, [Anytrust.t() | Celestia.t()]} + }, [Anytrust.t() | Celestia.t() | Eigenda.t()]} defp execute_transaction_requests_parse_transactions_calldata( transactions_requests, msg_to_block_shift, diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex index c8fc4824cb55..0fddd4f30b2f 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex @@ -10,10 +10,10 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Tasks do The module processes logs from the `SequencerBatchDelivered` events emitted by the Arbitrum `SequencerInbox` contract to extract batch details. It maintains linkages between batches and their corresponding rollup blocks and transactions. - For batches stored in Data Availability solutions like AnyTrust or Celestia, - it retrieves DA information to locate the batch data. The module also tracks - cross-chain messages initiated in rollup blocks associated with new batches, - updating their status to committed (`:sent`). + For batches stored in Data Availability solutions like AnyTrust, Celestia, or + EigenDA, it retrieves DA information to locate the batch data. The module also + tracks cross-chain messages initiated in rollup blocks associated with new + batches, updating their status to committed (`:sent`). For any blocks or transactions missing in the database, data is requested in chunks from the rollup RPC endpoint by `eth_getBlockByNumber`. Additionally, diff --git a/cspell.json b/cspell.json index 38d62be97023..72a75824c719 100644 --- a/cspell.json +++ b/cspell.json @@ -216,6 +216,8 @@ "EDCSA", "edhygl", "efkuga", + "Eigen", + "EigenDA", "einval", "Encryptor", "endregion", From 107c596787c7e50cdb401aa1b5840f66f22f1531 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 23 Sep 2025 13:13:22 +0300 Subject: [PATCH 0358/1006] chore: Phoenix update (#13147) --- apps/block_scout_web/lib/block_scout_web.ex | 7 +- .../account/api/v2/fallback_controller.ex | 2 +- .../controllers/api/v2/fallback_controller.ex | 2 +- .../controllers/api/v2/search_controller.ex | 2 +- .../controllers/api/v2/stats_controller.ex | 2 +- .../block_withdrawal_controller.ex | 4 +- .../templates/layout/app.html.eex | 106 +++++++++--------- .../views/abi_encoded_value_view.ex | 3 +- .../views/address_contract_view.ex | 2 +- .../lib/block_scout_web/views/error_helper.ex | 2 +- .../block_scout_web/views/script_helper.ex | 2 +- apps/block_scout_web/mix.exs | 9 +- .../views/error_helper_test.exs | 4 +- apps/explorer/mix.exs | 1 - mix.lock | 13 ++- 15 files changed, 83 insertions(+), 78 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web.ex b/apps/block_scout_web/lib/block_scout_web.ex index ddc521e086fa..379ff8b5eacc 100644 --- a/apps/block_scout_web/lib/block_scout_web.ex +++ b/apps/block_scout_web/lib/block_scout_web.ex @@ -50,11 +50,10 @@ defmodule BlockScoutWeb do root: "lib/block_scout_web/templates", namespace: BlockScoutWeb - # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 2, view_module: 1] - # Use all HTML functionality (forms, tags, etc) - use Phoenix.HTML + import Phoenix.HTML + import Phoenix.HTML.Form + use PhoenixHTMLHelpers import BlockScoutWeb.{ CurrencyHelper, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex index bf4bd39f9d6d..86e902e6c2f6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex @@ -1,5 +1,5 @@ defmodule BlockScoutWeb.Account.API.V2.FallbackController do - use Phoenix.Controller + use Phoenix.Controller, namespace: BlockScoutWeb alias BlockScoutWeb.Account.API.V2.UserView alias Ecto.Changeset diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 37b44cb23a04..e2343eb226cb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -1,5 +1,5 @@ defmodule BlockScoutWeb.API.V2.FallbackController do - use Phoenix.Controller + use Phoenix.Controller, namespace: BlockScoutWeb require Logger diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex index 1b9bb7ebd08a..14db522f73aa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex @@ -1,5 +1,5 @@ defmodule BlockScoutWeb.API.V2.SearchController do - use Phoenix.Controller + use Phoenix.Controller, namespace: BlockScoutWeb import BlockScoutWeb.Chain, only: [from_param: 1, fetch_scam_token_toggle: 2] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens_info_to_search_results: 1] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index d868faecbf97..5c0d51f03200 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -1,5 +1,5 @@ defmodule BlockScoutWeb.API.V2.StatsController do - use Phoenix.Controller + use Phoenix.Controller, namespace: BlockScoutWeb use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias BlockScoutWeb.API.V2.Helper diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex index c6044d84ca43..8ea4f731a28a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex @@ -57,8 +57,8 @@ defmodule BlockScoutWeb.BlockWithdrawalController do {:error, :not_found} -> conn |> put_status(:not_found) + |> put_view(BlockTransactionView) |> render( - BlockTransactionView, "404.html", block: nil, block_above_tip: block_above_tip(formatted_block_hash_or_number) @@ -95,8 +95,8 @@ defmodule BlockScoutWeb.BlockWithdrawalController do {:error, :not_found} -> conn |> put_status(:not_found) + |> put_view(BlockTransactionView) |> render( - BlockTransactionView, "404.html", block: nil, block_above_tip: block_above_tip(formatted_block_hash_or_number) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index 996abc8c2bc8..65ec79a5a044 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -5,7 +5,7 @@ - <%= case @view_module do %> + <%= case Phoenix.Controller.view_module(@conn) do %> <% Elixir.BlockScoutWeb.ChainView -> %> "> " as="script"> @@ -27,7 +27,7 @@ " as="image" crossorigin> " as="style" onload="this.onload=null;this.rel='stylesheet'"> "> - <%= render_existing(@view_module, "styles.html", assigns) %> + <%= render_existing(Phoenix.Controller.view_module(@conn), "styles.html", assigns) %> "> "> @@ -39,7 +39,7 @@ "> - <%= render_existing(@view_module, "_metatags.html", assigns) || render("_default_title.html") %> + <%= render_existing(Phoenix.Controller.view_module(@conn), "_metatags.html", assigns) || render("_default_title.html") %> <% end %> @@ -159,20 +159,20 @@ render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status end %> - <%= render_existing(@view_module, "scripts.html", assigns) %> - <%= if @view_module == Elixir.BlockScoutWeb.ChainView do %> + <%= render_existing(Phoenix.Controller.view_module(@conn), "scripts.html", assigns) %> + <%= if Phoenix.Controller.view_module(@conn) == Elixir.BlockScoutWeb.ChainView do %> <% end %> - <%= if @view_module == Elixir.BlockScoutWeb.TransactionView do %> + <%= if Phoenix.Controller.view_module(@conn) == Elixir.BlockScoutWeb.TransactionView do %> <% end %> - <%= if @view_module in [Elixir.BlockScoutWeb.AddressContractVerificationView, Elixir.BlockScoutWeb.AddressContractVerificationVyperView, Elixir.BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView] do %> + <%= if Phoenix.Controller.view_module(@conn) in [Elixir.BlockScoutWeb.AddressContractVerificationView, Elixir.BlockScoutWeb.AddressContractVerificationVyperView, Elixir.BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView] do %> <% end %> - <%= if @view_module in [Elixir.BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView, Elixir.BlockScoutWeb.AddressContractVerificationViaJsonView, Elixir.BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView] do %> + <%= if Phoenix.Controller.view_module(@conn) in [Elixir.BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView, Elixir.BlockScoutWeb.AddressContractVerificationViaJsonView, Elixir.BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView] do %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex index 55fc039bfe03..5cb142e1bd7f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/abi_encoded_value_view.ex @@ -6,8 +6,7 @@ defmodule BlockScoutWeb.ABIEncodedValueView do values via `
` tags, and that is hard to do in an eex template.
   """
   use BlockScoutWeb, :view
-
-  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
+  use Phoenix.LiveView
 
   alias ABI.FunctionSelector
   alias Phoenix.HTML
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
index 7d07052bb8e3..d3bb724bf960 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
@@ -1,10 +1,10 @@
 defmodule BlockScoutWeb.AddressContractView do
   use BlockScoutWeb, :view
+  use Phoenix.LiveView
 
   require Logger
 
   import Explorer.Helper, only: [decode_data: 2]
-  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
 
   alias ABI.FunctionSelector
   alias Explorer.Chain
diff --git a/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex
index 916746eb83d1..a29a482dd9bc 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex
@@ -3,7 +3,7 @@ defmodule BlockScoutWeb.ErrorHelper do
   Conveniences for translating and building error messages.
   """
 
-  use Phoenix.HTML
+  use PhoenixHTMLHelpers
 
   alias Ecto.Changeset
   alias Phoenix.HTML.Form
diff --git a/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex
index f3d0c6cc43e8..4293338abb9c 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/script_helper.ex
@@ -2,8 +2,8 @@ defmodule BlockScoutWeb.Views.ScriptHelper do
   @moduledoc """
   Helper for rendering view specific script tags.
   """
+  use Phoenix.LiveView
 
-  import Phoenix.LiveView.Helpers, only: [sigil_H: 2]
   import BlockScoutWeb.Router.Helpers, only: [static_path: 2]
 
   alias Phoenix.HTML.Safe
diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs
index 9e6004d4394e..6ab6c506ff8a 100644
--- a/apps/block_scout_web/mix.exs
+++ b/apps/block_scout_web/mix.exs
@@ -121,11 +121,12 @@ defmodule BlockScoutWeb.Mixfile do
       {:math, "~> 0.7.0"},
       {:mock, "~> 0.3.0", only: [:test], runtime: false},
       {:number, "~> 1.0.1"},
-      {:phoenix, "== 1.5.14"},
+      {:phoenix, "== 1.6.16"},
       {:phoenix_ecto, "~> 4.1"},
-      {:phoenix_html, "== 3.3.4"},
-      {:phoenix_live_reload, "~> 1.2", only: [:dev]},
-      {:phoenix_live_view, "~> 0.17"},
+      {:phoenix_html, "== 4.2.1"},
+      {:phoenix_html_helpers, "~> 1.0"},
+      {:phoenix_live_reload, "~> 1.6", only: [:dev]},
+      {:phoenix_live_view, "~> 1.1"},
       {:phoenix_pubsub, "~> 2.0"},
       {:prometheus_ex, "~> 5.0.0", override: true},
       # use `:cowboy` for WebServer with `:plug`
diff --git a/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs b/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs
index 7aac3cffee7d..7b376b55d0f6 100644
--- a/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/error_helper_test.exs
@@ -1,6 +1,8 @@
 defmodule BlockScoutWeb.ErrorHelperTest do
   use BlockScoutWeb.ConnCase, async: true
-  import Phoenix.HTML.Tag, only: [content_tag: 3]
+  import Phoenix.HTML
+  import Phoenix.HTML.Form
+  use PhoenixHTMLHelpers
 
   alias BlockScoutWeb.ErrorHelper
 
diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs
index f2c99e675cd6..e161e7911f4c 100644
--- a/apps/explorer/mix.exs
+++ b/apps/explorer/mix.exs
@@ -91,7 +91,6 @@ defmodule Explorer.Mixfile do
       {:math, "~> 0.7.0"},
       {:mock, "~> 0.3.0", only: [:test], runtime: false},
       {:mox, "~> 1.1.0"},
-      {:phoenix_html, "== 3.3.4"},
       {:poison, "~> 4.0.1"},
       {:nimble_csv, "~> 1.1"},
       {:postgrex, ">= 0.0.0"},
diff --git a/mix.lock b/mix.lock
index 1bf6939617ee..c4d9d9a0a11c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -128,12 +128,15 @@
   "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"},
   "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
   "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
-  "phoenix": {:hex, :phoenix, "1.5.14", "2d5db884be496eefa5157505ec0134e66187cb416c072272420c5509d67bf808", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "207f1aa5520320cbb7940d7ff2dde2342162cf513875848f88249ea0ba02fef7"},
+  "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"},
   "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"},
-  "phoenix_html": {:hex, :phoenix_html, "3.3.4", "42a09fc443bbc1da37e372a5c8e6755d046f22b9b11343bf885067357da21cb3", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0249d3abec3714aff3415e7ee3d9786cb325be3151e6c4b3021502c585bf53fb"},
-  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"},
+  "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
+  "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
+  "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.11", "1b4d8fa56898d93b6f528c89227198a3fce7c5b242819b22ed9e92b73c1bb077", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "266823602e11a54e562ac03a25b3d232d79de12514262db7cfcbb83fdfd8fd57"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
+  "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
+  "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
   "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
   "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
   "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
@@ -178,6 +181,8 @@
   "vix": {:hex, :vix, "0.35.0", "f6319b715e3b072e53eba456a21af5f2ff010a7a7b19b884600ea98a0609b18c", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "a3e80067a89d0631b6cf2b93594e03c1b303a2c7cddbbdd28040750d521984e5"},
   "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"},
   "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"},
+  "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+  "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"},
   "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"},
   "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"},
 }

From 400b00d2aac57cf8eda7c26398670cd821c71aee Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 23 Sep 2025 13:47:15 +0300
Subject: [PATCH 0359/1006] chore(deps): bump ex_cldr from 2.42.0 to 2.43.2
 (#13270)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index c4d9d9a0a11c..544c9c680b55 100644
--- a/mix.lock
+++ b/mix.lock
@@ -55,7 +55,7 @@
   "ex_aws": {:hex, :ex_aws, "2.5.11", "5646eaad701485505b78246b0cd406fde9b1619459a86e85b53398810d3d0bd3", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e16100ff93a118ef01c916d945969535cbe8d4ab6593fcf01d1cf854eb75345"},
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"},
   "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"},
-  "ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"},
+  "ex_cldr": {:hex, :ex_cldr, "2.43.2", "92370062f72d8efbeba3a7dc60a331fbb70f061cbc87041cb9f3c6701c6e2ddc", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "095137a7bd081166f77d23291b0649db2136ca013245cb73955fb0515031272a"},
   "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"},
   "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"},
   "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"},

From 177707b7b12d97a60abe6636a4150bd4cd353829 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 23 Sep 2025 13:48:34 +0300
Subject: [PATCH 0360/1006] chore(deps): bump ezstd from 1.2.2 to 1.2.3
 (#13267)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index 544c9c680b55..4c8b215c4939 100644
--- a/mix.lock
+++ b/mix.lock
@@ -74,7 +74,7 @@
   "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"},
   "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
   "exvcr": {:hex, :exvcr, "0.16.0", "11579f43c88ae81f57c82ce4f09e3ebda4c40117c859ed39e61a653c3a0b4ff4", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "8f576af22369942f7a1482baff1f31e2f45983cf6fac45d49d2bd2e84b4d5be8"},
-  "ezstd": {:hex, :ezstd, "1.2.2", "9d286f6727409260970bc08582cffec9a80a61d51cd9effa2b52fde5a333a2c6", [:rebar3], [], "hexpm", "6fcadd8bf64ab40e345c1e23f3c2aac12592813364c517f35a2139fab94e145e"},
+  "ezstd": {:hex, :ezstd, "1.2.3", "98748f4099e6e2a067f77ace43041ebaa53c13194b08ce22370e4c93079e9e16", [:rebar3], [], "hexpm", "de32e0b41ba36a9ed46db8215da74777d2f141bb75f67bfc05dbb4b7c3386dee"},
   "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"},
   "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
   "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},

From 572e2295c490a486ad1632bb880b2524c438ad56 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 23 Sep 2025 16:11:09 +0300
Subject: [PATCH 0361/1006] chore(deps): bump ecto from 3.13.2 to 3.13.3
 (#13271)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index 4c8b215c4939..c444838a03be 100644
--- a/mix.lock
+++ b/mix.lock
@@ -44,7 +44,7 @@
   "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"},
   "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"},
   "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
-  "ecto": {:hex, :ecto, "3.13.2", "7d0c0863f3fc8d71d17fc3ad3b9424beae13f02712ad84191a826c7169484f01", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "669d9291370513ff56e7b7e7081b7af3283d02e046cf3d403053c557894a0b3e"},
+  "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"},
   "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"},
   "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
   "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},

From 5d943f81a22801b6355645fe0b3977b0435a1d76 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 12:57:50 +0300
Subject: [PATCH 0362/1006] chore(deps-dev): bump ex_doc from 0.38.3 to 0.38.4
 (#13195)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index c444838a03be..246552aa02b9 100644
--- a/mix.lock
+++ b/mix.lock
@@ -60,7 +60,7 @@
   "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"},
   "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"},
   "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"},
-  "ex_doc": {:hex, :ex_doc, "0.38.3", "ddafe36b8e9fe101c093620879f6604f6254861a95133022101c08e75e6c759a", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "ecaa785456a67f63b4e7d7f200e8832fa108279e7eb73fd9928e7e66215a01f9"},
+  "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"},
   "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"},
   "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"},
   "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"},

From 12b1525cfdf52cfcb77e3639c544aa0f982cf902 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 13:11:43 +0300
Subject: [PATCH 0363/1006] chore(deps-dev): bump cowboy from 2.13.0 to 2.14.0
 (#13269)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/mix.lock b/mix.lock
index 246552aa02b9..9314d2078515 100644
--- a/mix.lock
+++ b/mix.lock
@@ -29,9 +29,9 @@
   "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"},
   "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
   "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
-  "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"},
+  "cowboy": {:hex, :cowboy, "2.14.0", "565dcf221ba99b1255b0adcec24d2d8dbe79e46ec79f30f8373cceadc6a41e2a", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "ea99769574550fe8a83225c752e8a62780a586770ef408816b82b6fe6d46476b"},
   "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
-  "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"},
+  "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
   "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
   "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"},
   "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"},

From 622364d6a1fb798f714a15e788fd6c293681541a Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 24 Sep 2025 15:35:39 +0300
Subject: [PATCH 0364/1006] chore(deps): bump ex_cldr_units from 3.19.0 to
 3.19.1 (#13194)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/mix.lock b/mix.lock
index 9314d2078515..c334466988cb 100644
--- a/mix.lock
+++ b/mix.lock
@@ -56,10 +56,10 @@
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"},
   "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"},
   "ex_cldr": {:hex, :ex_cldr, "2.43.2", "92370062f72d8efbeba3a7dc60a331fbb70f061cbc87041cb9f3c6701c6e2ddc", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "095137a7bd081166f77d23291b0649db2136ca013245cb73955fb0515031272a"},
-  "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.4", "d76770690699b6ba91f1fa253a299a905f9c22b45d91891b85f431b9dafa8b3b", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "46a67d1387f14e836b1a24d831fa5f0904663b4f386420736f40a7d534e3cb9e"},
+  "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"},
   "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"},
-  "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.1", "aa84601e604f6656b4bfb7f58a329328d2cbc13e4601da9071c14af38fc1d641", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "925c25debb0a4d27b3559885a79fb66dd4ebb6b72ce46b1e971db647042e28a0"},
-  "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.0", "9b03c90f53e1f93c33aa80923da7d78ac79f3aba11f2ed40218b6008cb8c0ecc", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "2795f250aaafd573d067e8de346fbfb6a33810182e1b61747d127867e83259fa"},
+  "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.2", "29c1b17732e089f5cd71623add92d7bbd584b2c4a586e51f782303a592f7eb81", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6db5fc81a7de7efe9e9bd66fb2f436b5f82cdbf88deea38f513e8432533856dd"},
+  "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.1", "11a99d0741e8786158a40f9b7a90f7bf055a642d277a4ea1bb2b7e90a108f09f", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "63023b6e5c1ec159d9e50f50c2d271127a1263a2316dff5a08b7b4a0f82bf1f8"},
   "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"},
   "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"},
   "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"},

From b253b605f57c6bb48ec27b81208501c68b59a46d Mon Sep 17 00:00:00 2001
From: nikitosing <32202610+nikitosing@users.noreply.github.com>
Date: Wed, 24 Sep 2025 18:56:43 +0300
Subject: [PATCH 0365/1006] chore: Add label for running tests with enabled
 bridged tokens (#13263)

---
 .github/workflows/config.yml | 29 ++++++++++++++++++++++++++---
 1 file changed, 26 insertions(+), 3 deletions(-)

diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index 7e23d0fa0550..c726d36b7063 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -83,7 +83,24 @@ jobs:
             // Chain type matrix we use in PRs to master branch
             const ciChainTypes = labeledChainTypes.length > 0 ? labeledChainTypes : defaultChainTypes;
 
-            const matrix = { "chain-type": ${{ github.event_name == 'pull_request' && 'ciChainTypes' || 'allChainTypes' }} };
+            // Check for bridged tokens label
+            const hasBridgedTokensLabel = ciLabels.includes("ci:bridged-tokens");
+
+            // Create matrix combinations
+            const targetChainTypes = ${{ github.event_name == 'pull_request' && 'ciChainTypes' || 'allChainTypes' }};
+            const bridgedTokensConfigs = hasBridgedTokensLabel ? [false, true] : [false];
+            
+            const matrixIncludes = [];
+            for (const chainType of targetChainTypes) {
+              for (const bridgedTokens of bridgedTokensConfigs) {
+                matrixIncludes.push({
+                  "chain-type": chainType,
+                  "bridged-tokens": bridgedTokens
+                });
+              }
+            }
+
+            const matrix = { "include": matrixIncludes };
             console.log(JSON.stringify(matrix));
           ')" >> $GITHUB_OUTPUT
 
@@ -243,9 +260,9 @@ jobs:
         id: dialyzer-cache
         with:
           path: priv/plts
-          key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-dialyzer-mixlockhash-${{ hashFiles('mix.lock') }}
+          key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-${{ matrix.bridged-tokens }}-dialyzer-mixlockhash-${{ hashFiles('mix.lock') }}
           restore-keys: |
-            ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-dialyzer-mixlockhash-
+            ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-${{ matrix.chain-type }}-${{ matrix.bridged-tokens }}-dialyzer-mixlockhash-
 
       - name: Conditionally build Dialyzer Cache
         if: steps.dialyzer-cache.output.cache-hit != 'true'
@@ -254,11 +271,13 @@ jobs:
           mix dialyzer --plt
         env:
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}
 
       - name: Run Dialyzer
         run: mix dialyzer --halt-exit-status
         env:
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}
 
   gettext:
     name: Missing translation keys check
@@ -553,6 +572,7 @@ jobs:
           ETHEREUM_JSONRPC_CASE: "EthereumJSONRPC.Case.Nethermind.Mox"
           ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox"
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}
 
   test_nethermind_mox_explorer:
     strategy:
@@ -629,6 +649,7 @@ jobs:
           ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox"
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
           WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true"
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}
 
   test_nethermind_mox_indexer:
     strategy:
@@ -697,6 +718,7 @@ jobs:
           ETHEREUM_JSONRPC_WEB_SOCKET_CASE: "EthereumJSONRPC.WebSocket.Case.Mox"
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
           WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true"
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}
 
   test_nethermind_mox_block_scout_web:
     strategy:
@@ -799,3 +821,4 @@ jobs:
           SOURCIFY_INTEGRATION_ENABLED: "true"
           CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }}
           WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true"
+          BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }}

From d8c17346c8e1f665e32406cc177057a98f2e5aaf Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 25 Sep 2025 14:40:52 +0300
Subject: [PATCH 0366/1006] chore(deps): bump poison from 4.0.1 to 5.0.0
 (#13268)

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Victor Baranov 
---
 apps/block_scout_web/lib/block_scout_web/chain.ex        | 9 ---------
 apps/block_scout_web/mix.exs                             | 2 +-
 apps/block_scout_web/test/block_scout_web/chain_test.exs | 4 ++--
 apps/explorer/mix.exs                                    | 2 +-
 apps/indexer/lib/indexer/fetcher/beacon/deposit.ex       | 2 +-
 mix.lock                                                 | 2 +-
 6 files changed, 6 insertions(+), 15 deletions(-)

diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex
index 032e1274bce9..5ff84100fda0 100644
--- a/apps/block_scout_web/lib/block_scout_web/chain.ex
+++ b/apps/block_scout_web/lib/block_scout_web/chain.ex
@@ -57,15 +57,6 @@ defmodule BlockScoutWeb.Chain do
   alias Explorer.Chain.Scroll.Bridge, as: ScrollBridge
   alias Explorer.PagingOptions
 
-  defimpl Poison.Encoder, for: Decimal do
-    def encode(value, _opts) do
-      # silence the xref warning
-      decimal = Decimal
-
-      [?\", decimal.to_string(value), ?\"]
-    end
-  end
-
   @page_size page_size()
   @default_paging_options default_paging_options()
   @address_hash_len 40
diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs
index 6ab6c506ff8a..c6061b0d2ca8 100644
--- a/apps/block_scout_web/mix.exs
+++ b/apps/block_scout_web/mix.exs
@@ -133,7 +133,7 @@ defmodule BlockScoutWeb.Mixfile do
       {:plug_cowboy, "~> 2.2"},
       # Waiting for the Pretty Print to be implemented at the Jason lib
       # https://github.com/michalmuskala/jason/issues/15
-      {:poison, "~> 4.0.1"},
+      {:poison, "~> 5.0.0"},
       {:postgrex, ">= 0.0.0"},
       {:prometheus, "~> 6.0", override: true},
       # Gather methods for Phoenix requests
diff --git a/apps/block_scout_web/test/block_scout_web/chain_test.exs b/apps/block_scout_web/test/block_scout_web/chain_test.exs
index 4b941780ed46..b0192d46b29e 100644
--- a/apps/block_scout_web/test/block_scout_web/chain_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/chain_test.exs
@@ -130,11 +130,11 @@ defmodule BlockScoutWeb.ChainTest do
     end
   end
 
-  describe "Poison.encode!" do
+  describe "Jason.encode!" do
     test "correctly encodes decimal values" do
       val = Decimal.from_float(5.55)
 
-      assert "\"5.55\"" == Poison.encode!(val)
+      assert "\"5.55\"" == Jason.encode!(val)
     end
   end
 end
diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs
index e161e7911f4c..06c13df06d09 100644
--- a/apps/explorer/mix.exs
+++ b/apps/explorer/mix.exs
@@ -91,7 +91,7 @@ defmodule Explorer.Mixfile do
       {:math, "~> 0.7.0"},
       {:mock, "~> 0.3.0", only: [:test], runtime: false},
       {:mox, "~> 1.1.0"},
-      {:poison, "~> 4.0.1"},
+      {:poison, "~> 5.0.0"},
       {:nimble_csv, "~> 1.1"},
       {:postgrex, ">= 0.0.0"},
       {:prometheus, "~> 6.0", override: true},
diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex
index f6a74b72ad4d..a006db6a16a6 100644
--- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex
+++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex
@@ -95,7 +95,7 @@ defmodule Indexer.Fetcher.Beacon.Deposit do
   def handle_cast({:lost_consensus, block_number}, %__MODULE__{} = state) do
     max_reorg_depth_block_number = block_number + 64
 
-    {_deleted_deposits_count, deleted_deposits} =
+    {_deleted_deposits_count, _deleted_deposits} =
       Repo.delete_all(
         from(
           d in Deposit,
diff --git a/mix.lock b/mix.lock
index c334466988cb..906665bdec51 100644
--- a/mix.lock
+++ b/mix.lock
@@ -140,7 +140,7 @@
   "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
   "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"},
   "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"},
-  "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
+  "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
   "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
   "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"},
   "prometheus": {:hex, :prometheus, "6.0.3", "95302236124c0f919163a7762bf7d2b171b919b6ff6148d26eb38a5d2def7b81", [:rebar3], [{:ddskerl, "0.4.2", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "53554ecadac0354066801d514d1a244dd026175e4ee3a9a30192b71d530c8268"},

From d19a85c0109daa131b8b0f116a4e55627df62a72 Mon Sep 17 00:00:00 2001
From: Fedor Ivanov 
Date: Thu, 25 Sep 2025 14:51:12 +0300
Subject: [PATCH 0367/1006] chore: add PR title conventional commit check
 workflow (#13238)

---
 .github/workflows/pr-title-check.yml | 15 +++++++++++++++
 1 file changed, 15 insertions(+)
 create mode 100644 .github/workflows/pr-title-check.yml

diff --git a/.github/workflows/pr-title-check.yml b/.github/workflows/pr-title-check.yml
new file mode 100644
index 000000000000..06362683d3a7
--- /dev/null
+++ b/.github/workflows/pr-title-check.yml
@@ -0,0 +1,15 @@
+name: PR Conventional Commit Validation
+
+on:
+  pull_request:
+    types: [opened, synchronize, reopened, edited]
+
+jobs:
+  validate-pr-title:
+    runs-on: ubuntu-latest
+    steps:
+      - name: PR Conventional Commit Validation
+        uses:  ytanikin/pr-conventional-commits@b72758283dcbee706975950e96bc4bf323a8d8c0
+        with:
+          task_types: '["feat","fix","chore","perf","refactor","docs","doc"]'
+          add_label: 'false'

From 7a65237efca650d405f8946bd619321349cec617 Mon Sep 17 00:00:00 2001
From: Kirill Fedoseev 
Date: Fri, 26 Sep 2025 09:58:21 +0100
Subject: [PATCH 0368/1006] feat: add ci:core label (#13249)

Co-authored-by: Victor Baranov 
---
 .github/workflows/config.yml           | 24 ++++++++++++++----------
 .github/workflows/generate-swagger.yml | 10 ++--------
 2 files changed, 16 insertions(+), 18 deletions(-)

diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml
index c726d36b7063..8814caeaeb4c 100644
--- a/.github/workflows/config.yml
+++ b/.github/workflows/config.yml
@@ -46,10 +46,11 @@ jobs:
         run: |
           echo "matrix=$(node -e '
 
-            // Add/remove CI matrix chain types here
             const defaultChainTypes = ["default"];
 
+            // Add/remove CI matrix chain types here
             const chainTypes = [
+              "default",
               "arbitrum",
               "blackfort",
               "celo",
@@ -67,17 +68,20 @@ jobs:
               "neon"
             ];
 
-            const extraChainTypes = [];
-
-            // Chain type matrix we use in master branch
-            const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes);
+            // Add/remove CI matrix chain types for "ci:core" label here
+            const coreChainTypes = [
+              "default",
+              "ethereum",
+              "optimism",
+              "celo"
+            ];
 
             const labels = ${{ github.event_name == 'pull_request' && toJson(github.event.pull_request.labels.*.name) || '[]' }};
             const ciLabels = labels.filter(label => label.startsWith("ci:"));
-            const labeledChainTypes = [].concat(
-              defaultChainTypes.filter(chainType => ciLabels.includes("ci:" + chainType)),
-              chainTypes.filter(chainType => ciLabels.includes("ci:all") || ciLabels.includes("ci:" + chainType)),
-              extraChainTypes.filter(chainType => ciLabels.includes("ci:" + chainType))
+            const labeledChainTypes = chainTypes.filter(chainType =>
+              ciLabels.includes("ci:all") ||
+              ciLabels.includes("ci:core") && coreChainTypes.includes(chainType) ||
+              ciLabels.includes("ci:" + chainType)
             );
 
             // Chain type matrix we use in PRs to master branch
@@ -87,7 +91,7 @@ jobs:
             const hasBridgedTokensLabel = ciLabels.includes("ci:bridged-tokens");
 
             // Create matrix combinations
-            const targetChainTypes = ${{ github.event_name == 'pull_request' && 'ciChainTypes' || 'allChainTypes' }};
+            const targetChainTypes = ${{ github.event_name == 'pull_request' && 'ciChainTypes' || 'chainTypes' }};
             const bridgedTokensConfigs = hasBridgedTokensLabel ? [false, true] : [false];
             
             const matrixIncludes = [];
diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml
index 3cc77bf727f0..299a0a6a048c 100644
--- a/.github/workflows/generate-swagger.yml
+++ b/.github/workflows/generate-swagger.yml
@@ -31,9 +31,8 @@ jobs:
           echo "matrix=$(node -e '
 
             // Add/remove CI matrix chain types here
-            const defaultChainTypes = ["default"];
-
             const chainTypes = [
+              "default",
               "arbitrum",
               "blackfort",
               "celo",
@@ -51,12 +50,7 @@ jobs:
               "neon"
             ];
 
-            const extraChainTypes = [];
-
-            // Chain type matrix we use in master branch
-            const allChainTypes = [].concat(defaultChainTypes, chainTypes, extraChainTypes);
-
-            const matrix = { "chain-type": ${{ 'allChainTypes' }} };
+            const matrix = { "chain-type": ${{ 'chainTypes' }} };
             console.log(JSON.stringify(matrix));
           ')" >> $GITHUB_OUTPUT
 

From 7a87af984dc3aefaa5bc4fab3fee4abecafebe22 Mon Sep 17 00:00:00 2001
From: Rim Rakhimov 
Date: Tue, 30 Sep 2025 14:41:10 +0400
Subject: [PATCH 0369/1006] perf: Optimize `EmptyBlocksSanitizer` queries
 (#13132)

---
 .../indexer/fetcher/empty_blocks_sanitizer.ex |  91 +++++---
 .../fetcher/empty_blocks_sanitizer_test.exs   | 201 ++++++++++++++++++
 .../empty_blocks_sanitizer_supervisor_case.ex |   9 +
 config/runtime.exs                            |   3 +-
 docker-compose/envs/common-blockscout.env     |   4 +-
 5 files changed, 273 insertions(+), 35 deletions(-)
 create mode 100644 apps/indexer/test/indexer/fetcher/empty_blocks_sanitizer_test.exs
 create mode 100644 apps/indexer/test/support/indexer/fetcher/empty_blocks_sanitizer_supervisor_case.ex

diff --git a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex
index b53ebfe80722..98fbbbaa29d2 100644
--- a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex
+++ b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex
@@ -21,9 +21,10 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
   @update_timeout 60_000
 
   @interval :timer.seconds(10)
+  @batch_size 10
+  @head_offset 1000
 
-  defstruct interval: @interval,
-            json_rpc_named_arguments: []
+  defstruct json_rpc_named_arguments: []
 
   def child_spec([init_arguments]) do
     child_spec([init_arguments, []])
@@ -44,22 +45,21 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
 
   @impl GenServer
   def init(opts) when is_list(opts) do
-    interval = Application.get_env(:indexer, __MODULE__)[:interval]
+    # For the first call we want it to start immediately
+    # (don't affect implementation in any way, but helps tests not to flake)
+    Kernel.send(self(), :sanitize_empty_blocks)
 
     state = %__MODULE__{
-      json_rpc_named_arguments: Keyword.fetch!(opts, :json_rpc_named_arguments),
-      interval: interval || @interval
+      json_rpc_named_arguments: Keyword.fetch!(opts, :json_rpc_named_arguments)
     }
 
-    Process.send_after(self(), :sanitize_empty_blocks, state.interval)
-
     {:ok, state}
   end
 
   @impl GenServer
   def handle_info(
         :sanitize_empty_blocks,
-        %{interval: interval, json_rpc_named_arguments: json_rpc_named_arguments} = state
+        %{json_rpc_named_arguments: json_rpc_named_arguments} = state
       ) do
     Logger.info("Start sanitizing of empty blocks. Batch size is #{limit()}",
       fetcher: :empty_blocks_to_refetch
@@ -67,7 +67,7 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
 
     sanitize_empty_blocks(json_rpc_named_arguments)
 
-    Process.send_after(self(), :sanitize_empty_blocks, interval)
+    Process.send_after(self(), :sanitize_empty_blocks, interval())
 
     {:noreply, state}
   end
@@ -80,7 +80,8 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
         block in Block,
         where: block.hash in subquery(unprocessed_non_empty_blocks_query)
       ),
-      set: [is_empty: false, updated_at: Timex.now()]
+      [set: [is_empty: false, updated_at: Timex.now()]],
+      timeout: :infinity
     )
 
     unprocessed_empty_blocks_list = unprocessed_empty_blocks_list_query(limit())
@@ -88,7 +89,7 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
     unless Enum.empty?(unprocessed_empty_blocks_list) do
       blocks_response =
         unprocessed_empty_blocks_list
-        |> Enum.map(fn {block_number, _} -> %{number: integer_to_quantity(block_number)} end)
+        |> Enum.map(fn %{number: block_number} -> %{number: integer_to_quantity(block_number)} end)
         |> id_to_params()
         |> Blocks.requests(&ByNumber.request(&1, false, false))
         |> json_rpc(json_rpc_named_arguments)
@@ -208,50 +209,74 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
       {:error, %{exception: postgrex_error}}
   end
 
-  @head_offset 1000
   defp consensus_blocks_with_nil_is_empty_query(limit) do
-    safe_block_number = BlockNumber.get_max() - @head_offset
+    safe_block_number = BlockNumber.get_max() - head_offset()
 
     from(block in Block,
+      as: :block,
+      select: %{hash: block.hash, number: block.number},
       where: is_nil(block.is_empty),
       where: block.number <= ^safe_block_number,
       where: block.consensus == true,
       where: block.refetch_needed == false,
-      order_by: [asc: block.hash],
       limit: ^limit
     )
   end
 
+  defp any_block_transactions_query do
+    # NOTE: relies on parent_as(:block) set by the caller query (see consensus_blocks_with_nil_is_empty_query/1)
+    from(
+      t in Transaction,
+      select: 1,
+      where: parent_as(:block).hash == t.block_hash
+    )
+  end
+
   defp unprocessed_non_empty_blocks_query(limit) do
-    blocks_query = consensus_blocks_with_nil_is_empty_query(limit)
-
-    from(q in subquery(blocks_query),
-      inner_join: transaction in Transaction,
-      on: q.number == transaction.block_number,
-      select: q.hash,
-      order_by: [asc: q.hash],
-      lock: fragment("FOR NO KEY UPDATE OF ?", q)
+    candidate_blocks_query = consensus_blocks_with_nil_is_empty_query(limit)
+
+    non_empty_blocks_query =
+      from(
+        block in candidate_blocks_query,
+        where: exists(any_block_transactions_query())
+      )
+
+    # Inner Join is required in order to lock only `blocks` table.
+    # As `non_empty_blocks_query` has WHERE condition on `transactions` table,
+    # if you apply lock to the query, the `transactions` table is also locked
+    # and that results in obtaining locks before the sort.
+    from(
+      block in Block,
+      inner_join: non_empty_block in subquery(non_empty_blocks_query),
+      on: block.hash == non_empty_block.hash,
+      select: %{hash: block.hash},
+      order_by: [asc: block.hash],
+      lock: fragment("FOR NO KEY UPDATE OF ?", block)
     )
   end
 
   defp unprocessed_empty_blocks_list_query(limit) do
-    blocks_query = consensus_blocks_with_nil_is_empty_query(limit)
+    candidate_blocks_query = consensus_blocks_with_nil_is_empty_query(limit)
 
-    query =
-      from(q in subquery(blocks_query),
-        left_join: transaction in Transaction,
-        on: q.number == transaction.block_number,
-        where: is_nil(transaction.block_number),
-        select: {q.number, q.hash},
-        distinct: q.number,
-        order_by: [asc: q.hash]
+    empty_blocks_query =
+      from(
+        block in candidate_blocks_query,
+        where: not exists(any_block_transactions_query())
       )
 
-    query
+    empty_blocks_query
     |> Repo.all(timeout: :infinity)
   end
 
   defp limit do
-    Application.get_env(:indexer, __MODULE__)[:batch_size]
+    Application.get_env(:indexer, __MODULE__)[:batch_size] || @batch_size
+  end
+
+  defp interval do
+    Application.get_env(:indexer, __MODULE__)[:interval] || @interval
+  end
+
+  defp head_offset do
+    Application.get_env(:indexer, __MODULE__)[:head_offset] || @head_offset
   end
 end
diff --git a/apps/indexer/test/indexer/fetcher/empty_blocks_sanitizer_test.exs b/apps/indexer/test/indexer/fetcher/empty_blocks_sanitizer_test.exs
new file mode 100644
index 000000000000..6b575c2896d2
--- /dev/null
+++ b/apps/indexer/test/indexer/fetcher/empty_blocks_sanitizer_test.exs
@@ -0,0 +1,201 @@
+defmodule Indexer.Fetcher.EmptyBlocksSanitizerTest do
+  # `async: false` due to use of named GenServer
+  use EthereumJSONRPC.Case, async: false
+  use Explorer.DataCase, async: false
+
+  import Mox
+
+  alias Indexer.Fetcher.EmptyBlocksSanitizer
+  alias Explorer.Chain.Block
+
+  @head_offset 1
+
+  # We decrease the number of blocks required to be inserted for the test
+  # in order to make it faster and to prevent filling the database with lots of trash.
+  # Otherwise, with default offset of `1000`, the tests start to flake periodically.
+  setup_all do
+    opts = Application.get_env(:indexer, EmptyBlocksSanitizer)
+    new_opts = Keyword.put(opts, :head_offset, @head_offset)
+    Application.put_env(:indexer, EmptyBlocksSanitizer, new_opts)
+    :ok
+  end
+
+  setup :set_mox_global
+  setup :verify_on_exit!
+
+  # Uncomment if you need to see what queries are sent to the Postgres
+  # (check the database logs)
+  #
+  # setup do
+  #   Repo.query("load 'auto_explain';")
+  #   Repo.query("SET auto_explain.log_min_duration = 0;")
+  #   Repo.query("SET auto_explain.log_analyze = true;")
+  #   :ok
+  # end
+
+  @moduletag [capture_log: true, no_geth: true]
+
+  test "process db-non-empty blocks", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+    # Setup
+    block_to_process = insert(:block, is_empty: nil)
+    _transaction = insert(:transaction) |> with_block(block_to_process)
+    populate_database_with_dummy_blocks()
+    assert Repo.get!(Block, block_to_process.hash).is_empty == nil, "precondition to check setup correctness"
+
+    EmptyBlocksSanitizer.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
+
+    processed_block =
+      wait_for_results(fn ->
+        Repo.one!(
+          from(block in Block,
+            where: block.hash == ^block_to_process.hash and block.updated_at != ^block_to_process.updated_at
+          )
+        )
+      end)
+
+    assert processed_block.is_empty == false, "invalid `is_empty` value set for processed block"
+    assert processed_block.refetch_needed == false, "invalid `refetch_needed` value set for processed block"
+  end
+
+  test "process db-empty blocks without transactions", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+    # Setup
+    block_to_process = insert(:block, is_empty: nil)
+    populate_database_with_dummy_blocks()
+    assert Repo.get!(Block, block_to_process.hash).is_empty == nil, "precondition to check setup correctness"
+
+    # Setup jsonrpc client
+    encoded_expected_block_number = "0x" <> Integer.to_string(block_to_process.number, 16)
+
+    if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
+      EthereumJSONRPC.Mox
+      |> expect(
+        :json_rpc,
+        fn [
+             %{
+               id: id,
+               method: "eth_getBlockByNumber",
+               params: [^encoded_expected_block_number, false]
+             }
+           ],
+           _options ->
+          eth_get_block_by_number_response(id, block_to_process.number, block_to_process.hash, [])
+        end
+      )
+    end
+
+    EmptyBlocksSanitizer.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
+
+    processed_block =
+      wait_for_results(fn ->
+        Repo.one!(
+          from(block in Block,
+            where: block.hash == ^block_to_process.hash and block.updated_at != ^block_to_process.updated_at
+          )
+        )
+      end)
+
+    assert processed_block.is_empty == true, "invalid `is_empty` value set for processed block"
+    assert processed_block.refetch_needed == false, "invalid `refetch_needed` value set for processed block"
+  end
+
+  test "process db-empty blocks with transactions", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+    # Setup
+    block_to_process = insert(:block, is_empty: nil)
+    populate_database_with_dummy_blocks()
+    assert Repo.get!(Block, block_to_process.hash).is_empty == nil, "precondition to check setup correctness"
+
+    # Setup jsonrpc client
+    encoded_expected_block_number = "0x" <> Integer.to_string(block_to_process.number, 16)
+    transaction_hash = "0x0000000000000000000000000000000000000000000000000000000000000001"
+
+    if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
+      EthereumJSONRPC.Mox
+      |> expect(
+        :json_rpc,
+        fn [
+             %{
+               id: id,
+               method: "eth_getBlockByNumber",
+               params: [^encoded_expected_block_number, false]
+             }
+           ],
+           _options ->
+          eth_get_block_by_number_response(id, encoded_expected_block_number, block_to_process.hash, [transaction_hash])
+        end
+      )
+    end
+
+    EmptyBlocksSanitizer.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
+
+    processed_block =
+      wait_for_results(fn ->
+        Repo.one!(
+          from(block in Block,
+            where: block.hash == ^block_to_process.hash and block.updated_at != ^block_to_process.updated_at
+          )
+        )
+      end)
+
+    assert processed_block.is_empty == nil, "invalid `is_empty` value set for processed block"
+    assert processed_block.refetch_needed == true, "invalid `refetch_needed` value set for processed block"
+  end
+
+  test "only old enough blocks are sanitized", %{json_rpc_named_arguments: json_rpc_named_arguments} do
+    # Setup
+    block_to_process = insert(:block, is_empty: nil)
+    insert(:transaction) |> with_block(block_to_process)
+
+    Enum.each(1..@head_offset, fn _index ->
+      insert(:block, is_empty: nil)
+    end)
+
+    assert Repo.one!(from(b in Block, select: count("*"), where: is_nil(b.is_empty))) == @head_offset + 1
+
+    EmptyBlocksSanitizer.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
+
+    wait_for_results(fn ->
+      Repo.one!(
+        from(block in Block,
+          where: block.hash == ^block_to_process.hash and block.updated_at != ^block_to_process.updated_at
+        )
+      )
+    end)
+
+    assert Repo.one!(from(b in Block, select: count("*"), where: is_nil(b.is_empty))) == @head_offset
+  end
+
+  defp populate_database_with_dummy_blocks() do
+    Enum.each(1..@head_offset, fn _index ->
+      insert(:block, is_empty: true)
+    end)
+  end
+
+  defp eth_get_block_by_number_response(id, encoded_block_number, block_hash, transaction_hashes) do
+    {:ok,
+     [
+       %{
+         id: id,
+         result: %{
+           "difficulty" => "0x0",
+           "gasLimit" => "0x0",
+           "gasUsed" => "0x0",
+           "hash" => block_hash,
+           "extraData" => "0x0",
+           "logsBloom" => "0x0",
+           "miner" => "0x0",
+           "number" => encoded_block_number,
+           "parentHash" => "0x0",
+           "receiptsRoot" => "0x0",
+           "size" => "0x0",
+           "sha3Uncles" => "0x0",
+           "stateRoot" => "0x0",
+           "timestamp" => "0x0",
+           "totalDifficulty" => "0x0",
+           "transactions" => transaction_hashes,
+           "transactionsRoot" => "0x0",
+           "uncles" => []
+         }
+       }
+     ]}
+  end
+end
diff --git a/apps/indexer/test/support/indexer/fetcher/empty_blocks_sanitizer_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/empty_blocks_sanitizer_supervisor_case.ex
new file mode 100644
index 000000000000..c0a01ef5e6fd
--- /dev/null
+++ b/apps/indexer/test/support/indexer/fetcher/empty_blocks_sanitizer_supervisor_case.ex
@@ -0,0 +1,9 @@
+defmodule Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor.Case do
+  alias Indexer.Fetcher.EmptyBlocksSanitizer
+
+  def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do
+    [fetcher_arguments]
+    |> EmptyBlocksSanitizer.Supervisor.child_spec()
+    |> ExUnit.Callbacks.start_supervised!()
+  end
+end
diff --git a/config/runtime.exs b/config/runtime.exs
index c8298d50223a..5c950b75e4d3 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -1078,7 +1078,8 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersFetcher.Supervisor,
 
 config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer,
   batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 10),
-  interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s")
+  interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s"),
+  head_offset: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_HEAD_OFFSET", 1000)
 
 config :indexer, Indexer.Block.Realtime.Fetcher,
   max_gap: ConfigHelper.parse_integer_env_var("INDEXER_REALTIME_FETCHER_MAX_GAP", 1_000),
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index 22275bef597f..9d8b2963527d 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -196,7 +196,9 @@ API_V1_WRITE_METHODS_DISABLED=false
 # INDEXER_CATCHUP_BLOCKS_BATCH_SIZE=
 # INDEXER_CATCHUP_BLOCKS_CONCURRENCY=
 # INDEXER_CATCHUP_BLOCK_INTERVAL=
-# INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL=
+# INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE=10
+# INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL=10s
+# INDEXER_EMPTY_BLOCKS_SANITIZER_HEAD_OFFSET=1000
 # INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE=
 # INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY=
 # INDEXER_BLOCK_REWARD_BATCH_SIZE=

From 5689d2edea23f191c4ba0c8acfeaef6d1882057c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 30 Sep 2025 13:41:59 +0300
Subject: [PATCH 0370/1006] chore(deps): bump phoenix_live_view from 1.1.11 to
 1.1.13 (#13302)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index 906665bdec51..5807f4870cfb 100644
--- a/mix.lock
+++ b/mix.lock
@@ -133,7 +133,7 @@
   "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"},
   "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
   "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"},
-  "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.11", "1b4d8fa56898d93b6f528c89227198a3fce7c5b242819b22ed9e92b73c1bb077", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "266823602e11a54e562ac03a25b3d232d79de12514262db7cfcbb83fdfd8fd57"},
+  "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.13", "11f48f8fbe5d7d0731d4e122a692e0f9ae8d5f98c54d573d29046833c34eada3", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9d06e93573b8419ef8ee832b4a9bfe0def10b15f7c129ea477dfa54f0136f7ec"},
   "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
   "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
   "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},

From c04ed0bf41cd45c049140ccda0c85d008d5a6893 Mon Sep 17 00:00:00 2001
From: varasev <33550681+varasev@users.noreply.github.com>
Date: Tue, 30 Sep 2025 15:06:18 +0400
Subject: [PATCH 0371/1006] feat: OP Alt-DA support for batch indexer (#13179)

Co-authored-by: POA <33550681+poa@users.noreply.github.com>
---
 .devcontainer/Dockerfile                      |  2 +-
 .devcontainer/docker-compose.yml              |  2 +-
 .../controllers/api/v2/optimism_controller.ex |  4 +-
 .../api/v2/transaction_controller.ex          |  4 +-
 .../block_scout_web/schemas/api/v2/block.ex   | 23 ++++-
 .../views/api/v2/optimism_view.ex             |  2 +-
 .../explorer/chain/optimism/frame_sequence.ex | 10 +-
 .../chain/optimism/frame_sequence_blob.ex     | 23 ++++-
 .../chain/optimism/transaction_batch.ex       | 17 ++--
 .../20250908073612_add_alt_da_blobs.exs       |  7 ++
 .../fetcher/optimism/transaction_batch.ex     | 94 ++++++++++++++++++-
 apps/indexer/lib/indexer/helper.ex            | 26 +++--
 config/runtime.exs                            |  1 +
 docker-compose/envs/common-blockscout.env     |  3 +
 14 files changed, 179 insertions(+), 39 deletions(-)
 create mode 100644 apps/explorer/priv/optimism/migrations/20250908073612_add_alt_da_blobs.exs

diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 98f0627b5d8f..447440aa245a 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -1,6 +1,6 @@
 # Since this is a copy of https://github.com/blockscout/devcontainer-elixir/blob/main/Dockerfile
 # So after successful testing this file, the original one must be updated as well.
-ARG VARIANT="1.17.3-erlang-27.1-debian-bullseye-20240926"
+ARG VARIANT="1.17.3-erlang-27.1.3-debian-bookworm-20250811"
 FROM hexpm/elixir:${VARIANT}
 
 # ARGs declared before FROM are not persisted beyond the FROM instruction.
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index b02f2981ad5f..7d20dfdabd33 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -1,6 +1,6 @@
 services:
   elixir:
-    image: ghcr.io/blockscout/devcontainer-elixir:1.17.3-erlang-27.1
+    image: ghcr.io/blockscout/devcontainer-elixir:1.17.3-erlang-27.1.3
 
     # Uncomment next lines to use test Dockerfile with new Elixir version
     # build:
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex
index 2faacc0a70ee..7e7e95c9d575 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex
@@ -86,8 +86,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do
       batches
       |> Enum.map(fn fs ->
         Task.async(fn ->
-          l2_block_number_from = TransactionBatch.edge_l2_block_number(fs.id, :min)
-          l2_block_number_to = TransactionBatch.edge_l2_block_number(fs.id, :max)
+          l2_block_number_from = TransactionBatch.edge_l2_block_number(fs.id, :min, @api_true)
+          l2_block_number_to = TransactionBatch.edge_l2_block_number(fs.id, :max, @api_true)
 
           l2_block_range =
             if not is_nil(l2_block_number_from) and not is_nil(l2_block_number_to) do
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
index b9b51f1deccd..4af08fdb693e 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex
@@ -301,8 +301,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do
   def optimism_batch(conn, %{"batch_number" => batch_number_string} = params) do
     {batch_number, ""} = Integer.parse(batch_number_string)
 
-    l2_block_number_from = OptimismTransactionBatch.edge_l2_block_number(batch_number, :min)
-    l2_block_number_to = OptimismTransactionBatch.edge_l2_block_number(batch_number, :max)
+    l2_block_number_from = OptimismTransactionBatch.edge_l2_block_number(batch_number, :min, @api_true)
+    l2_block_number_to = OptimismTransactionBatch.edge_l2_block_number(batch_number, :max, @api_true)
 
     handle_block_range_transactions(conn, params, l2_block_number_from, l2_block_number_to)
   end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
index 1ea8176540d6..069335ff4257 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
@@ -105,6 +105,17 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
     additionalProperties: false
   }
 
+  @alt_da_schema %Schema{
+    type: :object,
+    nullable: false,
+    properties: %{
+      commitment: %Schema{type: :string, nullable: true},
+      l1_transaction_hash: General.FullHashNullable,
+      l1_timestamp: General.TimestampNullable
+    },
+    required: [:commitment, :l1_transaction_hash, :l1_timestamp]
+  }
+
   @optimism_schema %Schema{
     type: :object,
     nullable: false,
@@ -112,8 +123,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
       number: %Schema{type: :integer, nullable: true},
       l1_timestamp: General.TimestampNullable,
       l1_transaction_hashes: %Schema{type: :array, items: General.FullHash, nullable: false},
-      batch_data_container: %Schema{type: :string, nullable: false, enum: ["in_blob4844", "in_celestia", "in_calldata"]},
-      blobs: %Schema{type: :array, items: %Schema{anyOf: [@blob4844_schema, @celestia_schema]}, nullable: false}
+      batch_data_container: %Schema{
+        type: :string,
+        nullable: false,
+        enum: ["in_blob4844", "in_celestia", "in_alt_da", "in_calldata"]
+      },
+      blobs: %Schema{
+        type: :array,
+        items: %Schema{anyOf: [@blob4844_schema, @celestia_schema, @alt_da_schema]},
+        nullable: false
+      }
     },
     required: [:number, :l1_timestamp, :l1_transaction_hashes, :batch_data_container],
     additionalProperties: false
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
index 87c29cc85772..a49d7b1164b0 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
@@ -361,7 +361,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
           :l2_end_block_number => non_neg_integer(),
           :transactions_count => non_neg_integer(),
           :l1_transaction_hashes => list(),
-          :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil
+          :batch_data_container => :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil
         }
   defp render_base_info_for_batch(number, l2_block_number_from, l2_block_number_to, transactions_count, batch) do
     FrameSequence.prepare_base_info_for_batch(
diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex
index fda6f9dbf1e2..9fe100e3937d 100644
--- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex
+++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex
@@ -105,8 +105,8 @@ defmodule Explorer.Chain.Optimism.FrameSequence do
     batch = select_repo(options).one(query)
 
     if not is_nil(batch) do
-      l2_block_number_from = TransactionBatch.edge_l2_block_number(number, :min)
-      l2_block_number_to = TransactionBatch.edge_l2_block_number(number, :max)
+      l2_block_number_from = TransactionBatch.edge_l2_block_number(number, :min, options)
+      l2_block_number_to = TransactionBatch.edge_l2_block_number(number, :max, options)
       transactions_count = Transaction.transaction_count_for_block_range(l2_block_number_from..l2_block_number_to)
 
       {batch_data_container, blobs} =
@@ -145,7 +145,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do
     - `l2_block_number_from`: Start L2 block number of the batch block range.
     - `l2_block_number_to`: End L2 block number of the batch block range.
     - `transactions_count`: The L2 transaction count included into the blocks of the range.
-    - `batch_data_container`: Designates where the batch info is stored: :in_blob4844, :in_celestia, or :in_calldata.
+    - `batch_data_container`: Designates where the batch info is stored: :in_blob4844, :in_celestia, :in_alt_da, or :in_calldata.
                               Can be `nil` if the container is unknown.
     - `batch`: Either an `Explorer.Chain.Optimism.FrameSequence` entry or a map with
                the corresponding fields.
@@ -158,7 +158,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do
           non_neg_integer(),
           non_neg_integer(),
           non_neg_integer(),
-          :in_blob4844 | :in_celestia | :in_calldata | nil,
+          :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil,
           __MODULE__.t()
           | %{:l1_timestamp => DateTime.t(), :l1_transaction_hashes => list(), optional(any()) => any()}
         ) :: %{
@@ -168,7 +168,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do
           :l2_end_block_number => non_neg_integer(),
           :transactions_count => non_neg_integer(),
           :l1_transaction_hashes => list(),
-          :batch_data_container => :in_blob4844 | :in_celestia | :in_calldata | nil
+          :batch_data_container => :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil
         }
   def prepare_base_info_for_batch(
         number,
diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex
index 001c3c2f318d..9ffe6aea3918 100644
--- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex
+++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex
@@ -7,6 +7,7 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
 
     Migrations:
     - Explorer.Repo.Optimism.Migrations.AddCelestiaBlobMetadata
+    - Explorer.Repo.Optimism.Migrations.AddAltDABlobs
   """
 
   use Explorer.Schema
@@ -20,7 +21,7 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
 
   @typedoc """
     * `key` - A unique id (key) of the blob.
-    * `type` - A type of the blob (`celestia` or `eip4844`).
+    * `type` - A type of the blob (`celestia`, `eip4844`, or `alt_da`).
     * `metadata` - A map containing metadata of the blob.
     * `l1_transaction_hash` - The corresponding L1 transaction hash which point to the blob.
     * `l1_timestamp` - The timestamp of the L1 transaction.
@@ -30,7 +31,7 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
   @primary_key {:id, :integer, autogenerate: false}
   typed_schema "op_frame_sequence_blobs" do
     field(:key, :binary)
-    field(:type, Ecto.Enum, values: [:celestia, :eip4844])
+    field(:type, Ecto.Enum, values: [:celestia, :eip4844, :alt_da])
     field(:metadata, :map)
     field(:l1_transaction_hash, Hash.Full)
     field(:l1_timestamp, :utc_datetime_usec)
@@ -59,10 +60,10 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
     - `options`: A keyword list of options that may include whether to use a replica database.
 
     ## Returns
-    - A tuple {type, blobs} where `type` can be one of: `in_blob4844`, `in_celestia`, `in_calldata`.
+    - A tuple {type, blobs} where `type` can be one of: `in_blob4844`, `in_celestia`, `in_alt_da`, `in_calldata`.
       The `blobs` in the list of blobs related to the specified frame sequence id sorted by an entity id.
   """
-  @spec list(non_neg_integer(), list()) :: {:in_blob4844 | :in_celestia | :in_calldata, [map()]}
+  @spec list(non_neg_integer(), list()) :: {:in_blob4844 | :in_celestia | :in_alt_da | :in_calldata, [map()]}
   def list(frame_sequence_id, options \\ []) do
     repo = select_repo(options)
 
@@ -102,6 +103,17 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
         }
       end)
 
+    alt_da_blobs =
+      blobs
+      |> Enum.filter(fn b -> b.type == :alt_da end)
+      |> Enum.map(fn b ->
+        %{
+          "commitment" => b.metadata["commitment"],
+          "l1_transaction_hash" => b.l1_transaction_hash,
+          "l1_timestamp" => b.l1_timestamp
+        }
+      end)
+
     cond do
       not Enum.empty?(eip4844_blobs) ->
         {:in_blob4844, eip4844_blobs}
@@ -109,6 +121,9 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do
       not Enum.empty?(celestia_blobs) ->
         {:in_celestia, celestia_blobs}
 
+      not Enum.empty?(alt_da_blobs) ->
+        {:in_alt_da, alt_da_blobs}
+
       true ->
         {:in_calldata, []}
     end
diff --git a/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex b/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex
index a1853e01169f..3c17388703a7 100644
--- a/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex
+++ b/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex
@@ -20,7 +20,7 @@ defmodule Explorer.Chain.Optimism.TransactionBatch do
 
   alias Explorer.Chain.Block
   alias Explorer.Chain.Optimism.FrameSequence
-  alias Explorer.{PagingOptions, Repo}
+  alias Explorer.PagingOptions
 
   @required_attrs ~w(l2_block_number frame_sequence_id)a
 
@@ -61,30 +61,33 @@ defmodule Explorer.Chain.Optimism.TransactionBatch do
     ## Parameters
     - `id`: The ID of the frame sequence for which the edge block number must be returned.
     - `type`: Can be :min or :max depending on which block number needs to be returned.
+    - `options`: A keyword list of options that may include whether to use a replica database.
 
     ## Returns
     - The min/max block number or `nil` if the block range is not found.
   """
-  @spec edge_l2_block_number(non_neg_integer(), :min | :max) :: non_neg_integer() | nil
-  def edge_l2_block_number(id, type) when type == :min and is_integer(id) and id >= 0 do
+  @spec edge_l2_block_number(non_neg_integer(), :min | :max, list()) :: non_neg_integer() | nil
+  def edge_l2_block_number(id, type, options \\ [])
+
+  def edge_l2_block_number(id, type, options) when type == :min and is_integer(id) and id >= 0 do
     query =
       id
       |> edge_l2_block_number_query()
       |> order_by([tb], asc: tb.l2_block_number)
 
-    Repo.replica().one(query)
+    select_repo(options).one(query)
   end
 
-  def edge_l2_block_number(id, type) when type == :max and is_integer(id) and id >= 0 do
+  def edge_l2_block_number(id, type, options) when type == :max and is_integer(id) and id >= 0 do
     query =
       id
       |> edge_l2_block_number_query()
       |> order_by([tb], desc: tb.l2_block_number)
 
-    Repo.replica().one(query)
+    select_repo(options).one(query)
   end
 
-  def edge_l2_block_number(_id, _type), do: nil
+  def edge_l2_block_number(_id, _type, _options), do: nil
 
   defp edge_l2_block_number_query(id) do
     from(
diff --git a/apps/explorer/priv/optimism/migrations/20250908073612_add_alt_da_blobs.exs b/apps/explorer/priv/optimism/migrations/20250908073612_add_alt_da_blobs.exs
new file mode 100644
index 000000000000..3d5927a4d6b5
--- /dev/null
+++ b/apps/explorer/priv/optimism/migrations/20250908073612_add_alt_da_blobs.exs
@@ -0,0 +1,7 @@
+defmodule Explorer.Repo.Optimism.Migrations.AddAltDABlobs do
+  use Ecto.Migration
+
+  def change do
+    execute("ALTER TYPE op_frame_sequence_blob_type ADD VALUE 'alt_da'")
+  end
+end
diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex
index 86196f5a5d2b..d05a3ec2649e 100644
--- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex
+++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex
@@ -133,6 +133,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
          batch_submitter: batch_submitter,
          eip4844_blobs_api_url: Helper.trim_url(env[:eip4844_blobs_api_url]),
          celestia_blobs_api_url: Helper.trim_url(env[:celestia_blobs_api_url]),
+         alt_da_server_url: Helper.trim_url(env[:alt_da_server_url]),
          block_check_interval: block_check_interval,
          start_block: start_block,
          end_block: last_safe_block,
@@ -216,6 +217,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
   # - `batch_submitter`: L1 address which sends L1 batch transactions to the `batch_inbox`
   # - `eip4844_blobs_api_url`: URL of Blockscout Blobs API to get EIP-4844 blobs
   # - `celestia_blobs_api_url`: URL of the server where Celestia blobs can be read from
+  # - `alt_da_server_url`: URL of Alt-DA server where keccak commitment data can be read from
   # - `block_check_interval`: time interval for checking latest block number
   # - `start_block`: start block number of the block range
   # - `end_block`: end block number of the block range
@@ -234,6 +236,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
           batch_submitter: batch_submitter,
           eip4844_blobs_api_url: eip4844_blobs_api_url,
           celestia_blobs_api_url: celestia_blobs_api_url,
+          alt_da_server_url: alt_da_server_url,
           block_check_interval: block_check_interval,
           start_block: start_block,
           end_block: end_block,
@@ -276,7 +279,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
                 {genesis_block_l2, block_duration},
                 incomplete_channels_acc,
                 {json_rpc_named_arguments, json_rpc_named_arguments_l2},
-                {eip4844_blobs_api_url, celestia_blobs_api_url, chain_id_l1},
+                {eip4844_blobs_api_url, celestia_blobs_api_url, alt_da_server_url, chain_id_l1},
                 Helper.infinite_retries_number()
               )
 
@@ -735,13 +738,76 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
     []
   end
 
+  # Gets Alt-DA data & commitment hash by L1 transaction input (encoding the commitment).
+  # The data is then read from the remote DA server.
+  #
+  # ## Parameters
+  # - `transaction_input`: The contents of transaction `input` field.
+  # - `transaction_hash`: The L1 transaction hash for logging purposes.
+  # - `da_server_url`: The URL to DA server implementing a get request.
+  #
+  # ## Returns
+  # - A list with a map containing commitment data and hash.
+  # - An empty list in case of an error (DA server didn't respond, URL is not defined, or transaction input is incorrect).
+  @spec alt_da_commitment_to_input(binary(), String.t(), String.t()) :: [
+          %{
+            :bytes => binary(),
+            :alt_da_commitment => String.t()
+          }
+        ]
+  defp alt_da_commitment_to_input("0x" <> transaction_input, transaction_hash, da_server_url) do
+    transaction_input
+    |> Base.decode16!(case: :mixed)
+    |> alt_da_commitment_to_input(transaction_hash, da_server_url)
+  end
+
+  defp alt_da_commitment_to_input(transaction_input, _transaction_hash, da_server_url)
+       when byte_size(transaction_input) == 2 + 32 and da_server_url != "" do
+    commitment_for_url = binary_part(transaction_input, 1, 1 + 32)
+    commitment_for_url_string = "0x" <> Base.encode16(commitment_for_url, case: :lower)
+
+    url = da_server_url <> "/" <> commitment_for_url_string
+
+    with {:ok, data} <- Helper.http_get_request(url, :raw),
+         true <- byte_size(data) > 0 do
+      [
+        %{
+          bytes: data,
+          alt_da_commitment: "0x" <> Base.encode16(transaction_input, case: :lower)
+        }
+      ]
+    else
+      false ->
+        Logger.error("DA server response is empty for the request #{url}")
+
+        []
+
+      _ ->
+        Logger.error("Cannot read a response from DA server for the request #{url}")
+        []
+    end
+  end
+
+  defp alt_da_commitment_to_input(_transaction_input, transaction_hash, da_server_url) when da_server_url != "" do
+    Logger.error("L1 transaction with Alt-DA commitment has incorrect input length. Tx hash: #{transaction_hash}")
+    []
+  end
+
+  defp alt_da_commitment_to_input(_transaction_input, _transaction_hash, "") do
+    Logger.error(
+      "Cannot read data from the DA server as its URL is not defined. Please, check INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL env variable."
+    )
+
+    []
+  end
+
   defp get_transaction_batches_inner(
          transactions_filtered,
          blocks_params,
          {genesis_block_l2, block_duration},
          incomplete_channels,
          json_rpc_named_arguments_l2,
-         {eip4844_blobs_api_url, celestia_blobs_api_url, chain_id_l1}
+         {eip4844_blobs_api_url, celestia_blobs_api_url, alt_da_server_url, chain_id_l1}
        ) do
     transactions_filtered
     |> Enum.reduce({:ok, incomplete_channels, [], [], []}, fn transaction,
@@ -750,7 +816,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
       inputs =
         cond do
           transaction.type == 3 ->
-            # this is EIP-4844 transaction, so we get the inputs from the blobs
+            # this is EIP-4844 transaction, so we get the inputs from EIP-4844 blobs
             block_timestamp = get_block_timestamp_by_number(transaction.block_number, blocks_params)
 
             eip4844_blobs_to_inputs(
@@ -765,6 +831,10 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
             # this is Celestia DA transaction, so we get the data from Celestia blob
             celestia_blob_to_input(transaction.input, 3, transaction.hash, celestia_blobs_api_url)
 
+          commitment_alt_da_signature(transaction.input) == 0x0100 ->
+            # this is Alt-DA transaction with a keccak commitment, so we get the data from a DA server
+            alt_da_commitment_to_input(transaction.input, transaction.hash, alt_da_server_url)
+
           first_byte(transaction.input) == 0xCE ->
             # backward compatibility with OP Celestia Raspberry
             celestia_blob_to_input(transaction.input, 1, transaction.hash, celestia_blobs_api_url)
@@ -820,7 +890,8 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
         block_timestamp: block_timestamp,
         transaction_hash: transaction.hash,
         eip4844_blob_hash: Map.get(input, :eip4844_blob_hash),
-        celestia_blob_metadata: Map.get(input, :celestia_blob_metadata)
+        celestia_blob_metadata: Map.get(input, :celestia_blob_metadata),
+        alt_da_commitment: Map.get(input, :alt_da_commitment)
       })
 
     l1_timestamp =
@@ -917,6 +988,21 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do
                   }
                 ]
 
+            !is_nil(Map.get(frame, :alt_da_commitment)) ->
+              # credo:disable-for-next-line /Credo.Check.Refactor.AppendSingleItem/
+              new_blobs_acc ++
+                [
+                  %{
+                    id: next_blob_id,
+                    key: Base.decode16!(String.trim_leading(frame.alt_da_commitment, "0x"), case: :mixed),
+                    type: :alt_da,
+                    metadata: %{commitment: frame.alt_da_commitment},
+                    l1_transaction_hash: frame.transaction_hash,
+                    l1_timestamp: frame.block_timestamp,
+                    frame_sequence_id: frame_sequence_id
+                  }
+                ]
+
             true ->
               new_blobs_acc
           end
diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex
index ec46bf216c2b..159f6aeebf45 100644
--- a/apps/indexer/lib/indexer/helper.ex
+++ b/apps/indexer/lib/indexer/helper.ex
@@ -772,32 +772,37 @@ defmodule Indexer.Helper do
   end
 
   @doc """
-    Sends HTTP GET request to the given URL and returns JSON response. Makes max 10 attempts and then returns an error in case of failure.
+    Sends HTTP GET request to the given URL and returns a response. Makes max 10 attempts and then returns an error in case of failure.
     There is a timeout between attempts (increasing from 3 seconds to 20 minutes max as the number of attempts increases).
 
     ## Parameters
     - `url`: The URL which needs to be requested.
+    - `response_format`: Can be `:json` (by default) or `:raw`. In case of `:json`, the response is decoded with `Jason.decode`.
     - `attempts_done`: The number of attempts done. Incremented by the function itself.
 
     ## Returns
-    - `{:ok, response}` where `response` is a map decoded from a JSON object.
+    - `{:ok, response}` where `response` is a map decoded from a JSON object, or the raw bytes (depending on the `response_format` parameter).
     - `{:error, reason}` in case of failure (after three unsuccessful attempts).
   """
-  @spec http_get_request(String.t(), non_neg_integer()) :: {:ok, map()} | {:error, any()}
-  def http_get_request(url, attempts_done \\ 0) do
+  @spec http_get_request(String.t(), :json | :raw, non_neg_integer()) :: {:ok, map() | binary()} | {:error, any()}
+  def http_get_request(url, response_format \\ :json, attempts_done \\ 0) do
     recv_timeout = 5_000
     connect_timeout = 8_000
     client = Tesla.client([{Tesla.Middleware.Timeout, timeout: recv_timeout}], Tesla.Adapter.Mint)
 
     case Tesla.get(client, url, opts: [adapter: [timeout: recv_timeout, transport_opts: [timeout: connect_timeout]]]) do
       {:ok, %{body: body, status: 200}} ->
-        Jason.decode(body)
+        if response_format == :json do
+          Jason.decode(body)
+        else
+          {:ok, body}
+        end
 
       {:ok, %{body: body, status: _}} ->
-        http_get_request_error(url, body, attempts_done)
+        http_get_request_error(url, response_format, body, attempts_done)
 
       {:error, error} ->
-        http_get_request_error(url, error, attempts_done)
+        http_get_request_error(url, response_format, error, attempts_done)
     end
   end
 
@@ -805,14 +810,15 @@ defmodule Indexer.Helper do
   #
   # ## Parameters
   # - `url`: The URL which needs to be requested.
+  # - `response_format`: Can be `:json` (by default) or `:raw`. In case of `:json`, the response is decoded with `Jason.decode`.
   # - `error`: The error description for logging purposes.
   # - `attempts_done`: The number of attempts done. Incremented by the function itself.
   #
   # ## Returns
   # - `{:ok, response}` tuple if the re-call was successful.
   # - `{:error, reason}` if all attempts were failed.
-  @spec http_get_request_error(String.t(), any(), non_neg_integer()) :: {:ok, map()} | {:error, any()}
-  defp http_get_request_error(url, error, attempts_done) do
+  @spec http_get_request_error(String.t(), :json | :raw, any(), non_neg_integer()) :: {:ok, map()} | {:error, any()}
+  defp http_get_request_error(url, response_format, error, attempts_done) do
     old_truncate = Application.get_env(:logger, :truncate)
     Logger.configure(truncate: :infinity)
 
@@ -832,7 +838,7 @@ defmodule Indexer.Helper do
       # wait up to 20 minutes and then retry
       :timer.sleep(min(3000 * Integer.pow(2, attempts_done - 1), 1_200_000))
       Logger.info("Retry to send the request to #{url} ...")
-      http_get_request(url, attempts_done)
+      http_get_request(url, response_format, attempts_done)
     else
       {:error, "Error while sending request to #{url}"}
     end
diff --git a/config/runtime.exs b/config/runtime.exs
index 5c950b75e4d3..10b05bc6d879 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -1267,6 +1267,7 @@ config :indexer, Indexer.Fetcher.Optimism.TransactionBatch,
   blocks_chunk_size: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE", "4"),
   eip4844_blobs_api_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL", ""),
   celestia_blobs_api_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_CELESTIA_BLOBS_API_URL", ""),
+  alt_da_server_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL", ""),
   genesis_block_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER"),
   inbox: System.get_env("INDEXER_OPTIMISM_L1_BATCH_INBOX"),
   submitter: System.get_env("INDEXER_OPTIMISM_L1_BATCH_SUBMITTER")
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index 9d8b2963527d..fc3195d709b2 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -310,6 +310,9 @@ API_V1_WRITE_METHODS_DISABLED=false
 # INDEXER_OPTIMISM_L1_BATCH_INBOX=
 # INDEXER_OPTIMISM_L1_BATCH_SUBMITTER=
 # INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE=
+# INDEXER_OPTIMISM_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL=
+# INDEXER_OPTIMISM_L1_BATCH_CELESTIA_BLOBS_API_URL=
+# INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL=
 # INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER=
 # INDEXER_OPTIMISM_BLOCK_DURATION=
 # INDEXER_OPTIMISM_L1_OUTPUT_ORACLE_CONTRACT=

From 30c36b3d9b8d7d78137fda635764140dc9a449e1 Mon Sep 17 00:00:00 2001
From: Victor Baranov 
Date: Wed, 1 Oct 2025 12:51:28 +0300
Subject: [PATCH 0372/1006] chore: OpenAPI spec for the REST API block
 controller (#13274)

---
 .../lib/block_scout_web/chain.ex              |   9 +
 .../controllers/api/v2/block_controller.ex    | 252 ++++++++++++++++--
 .../lib/block_scout_web/paging_helper.ex      |  28 +-
 .../lib/block_scout_web/routers/api_router.ex |  18 +-
 .../block_scout_web/schemas/api/v2/address.ex |   5 +-
 .../block_scout_web/schemas/api/v2/block.ex   | 150 +++++++----
 .../schemas/api/v2/block/countdown.ex         |  53 ++++
 .../schemas/api/v2/block/response.ex          |  18 ++
 .../schemas/api/v2/error_responses.ex         |  21 ++
 .../block_scout_web/schemas/api/v2/general.ex | 167 +++++++++++-
 .../api/v2/block_controller_test.exs          | 220 ++++++++++++++-
 .../api/v2/transaction_controller_test.exs    |   2 +
 .../indexer/fetcher/beacon/deposit_test.exs   | 251 ++++++++---------
 13 files changed, 950 insertions(+), 244 deletions(-)
 create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/countdown.ex
 create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex

diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex
index 5ff84100fda0..3c973a265c86 100644
--- a/apps/block_scout_web/lib/block_scout_web/chain.ex
+++ b/apps/block_scout_web/lib/block_scout_web/chain.ex
@@ -627,6 +627,10 @@ defmodule BlockScoutWeb.Chain do
     [paging_options: %{@default_paging_options | key: %{block_index: index}}]
   end
 
+  def paging_options(%{block_index: index}) when is_integer(index) do
+    [paging_options: %{@default_paging_options | key: %{block_index: index}}]
+  end
+
   # Clause for `Explorer.Chain.Blackfort.Validator`,
   #  returned by `BlockScoutWeb.API.V2.ValidatorController.blackfort_validators_list/2` (`/api/v2/validators/blackfort`)
   def paging_options(%{
@@ -1028,6 +1032,11 @@ defmodule BlockScoutWeb.Chain do
     end
   end
 
+  def parse_block_hash_or_number_param(number)
+      when is_integer(number) do
+    {:ok, :number, number}
+  end
+
   @doc """
   Fetches the scam token toggle from conn.cookies["show_scam_tokens"]. And put it to the params keyword.
 
diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
index 7adf7e610d59..6cf8c35dadca 100644
--- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
+++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex
@@ -1,6 +1,7 @@
 defmodule BlockScoutWeb.API.V2.BlockController do
   use BlockScoutWeb, :controller
   use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
+  use OpenApiSpex.ControllerSpecs
 
   import BlockScoutWeb.Chain,
     only: [
@@ -39,6 +40,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
   alias Explorer.Chain.InternalTransaction
   alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch
   alias Explorer.Chain.Scroll.Reader, as: ScrollReader
+  alias Schemas.ErrorResponses.NotFoundResponse
   alias Timex.Duration
 
   case @chain_type do
@@ -133,14 +135,30 @@ defmodule BlockScoutWeb.API.V2.BlockController do
 
   action_fallback(BlockScoutWeb.API.V2.FallbackController)
 
+  plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true)
+
+  tags(["blocks"])
+
+  operation(:block,
+    summary: "Retrieve detailed information about a specific block",
+    description:
+      "Retrieves detailed information for a specific block, including transactions, internal transactions, and metadata.",
+    parameters: [block_hash_or_number_param() | base_params()],
+    responses: [
+      ok: {"Detailed information about the specified block.", "application/json", Schemas.Block.Response},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number` endpoint.
+  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param` endpoint.
   """
   @spec block(Plug.Conn.t(), map()) ::
           {:error, :not_found | {:invalid, :hash | :number}}
           | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
           | Plug.Conn.t()
-  def block(conn, %{"block_hash_or_number" => block_hash_or_number}) do
+  def block(conn, %{block_hash_or_number_param: block_hash_or_number}) do
     with {:ok, block} <- block_param_to_block(block_hash_or_number, @block_params) do
       conn
       |> put_status(200)
@@ -162,6 +180,27 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     end
   end
 
+  operation(:blocks,
+    summary: "List blocks with optional filtering and sorting",
+    description: "Retrieves a paginated list of blocks with optional filtering by block type and sorting options.",
+    parameters:
+      base_params() ++
+        [block_type_param()] ++
+        define_paging_params(["block_number", "items_count"]),
+    responses: [
+      ok:
+        {"List of blocks with pagination.", "application/json",
+         paginated_response(
+           items: Schemas.Block,
+           next_page_params_example: %{
+             "block_number" => 22_566_361,
+             "items_count" => 50
+           },
+           title_prefix: "Blocks"
+         )}
+    ]
+  )
+
   @doc """
   Function to handle GET requests to `/api/v2/blocks` endpoint.
   """
@@ -187,12 +226,35 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     })
   end
 
+  operation(:arbitrum_batch,
+    summary: "List L2 blocks in an Arbitrum batch",
+    description: "Retrieves L2 blocks that are bound to a specific Arbitrum batch number.",
+    parameters:
+      base_params() ++
+        [batch_number_param()] ++
+        define_paging_params(["block_number", "items_count"]),
+    responses: [
+      ok:
+        {"L2 blocks in the specified Arbitrum batch.", "application/json",
+         paginated_response(
+           items: Schemas.Block,
+           next_page_params_example: %{
+             "block_number" => 22_566_361,
+             "items_count" => 50
+           },
+           title_prefix: "ArbitrumBatchBlocks"
+         )},
+      unprocessable_entity: JsonErrorResponse.response()
+    ]
+  )
+
   @doc """
-    Function to handle GET requests to `/api/v2/blocks/arbitrum-batch/:batch_number` endpoint.
+    Function to handle GET requests to `/api/v2/blocks/arbitrum-batch/:batch_number_param` endpoint.
     It renders the list of L2 blocks bound to the specified batch.
   """
   @spec arbitrum_batch(Plug.Conn.t(), any()) :: Plug.Conn.t()
-  def arbitrum_batch(conn, %{"batch_number" => batch_number} = params) do
+  def arbitrum_batch(conn, %{batch_number_param: batch_number} = params) do
+    # todo: remove select_block_type() as it is actually not processed in the endpoint
     full_options =
       params
       |> select_block_type()
@@ -213,12 +275,35 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     })
   end
 
+  operation(:optimism_batch,
+    summary: "List L2 blocks in an Optimism batch",
+    description: "Retrieves L2 blocks that are bound to a specific Optimism batch number.",
+    parameters:
+      base_params() ++
+        [batch_number_param()] ++
+        define_paging_params(["block_number", "items_count"]),
+    responses: [
+      ok:
+        {"L2 blocks in the specified Optimism batch.", "application/json",
+         paginated_response(
+           items: Schemas.Block,
+           next_page_params_example: %{
+             "block_number" => 22_566_361,
+             "items_count" => 50
+           },
+           title_prefix: "OptimismBatchBlocks"
+         )},
+      unprocessable_entity: JsonErrorResponse.response()
+    ]
+  )
+
   @doc """
-    Function to handle GET requests to `/api/v2/blocks/optimism-batch/:batch_number` endpoint.
+    Function to handle GET requests to `/api/v2/blocks/optimism-batch/:batch_number_param` endpoint.
     It renders the list of L2 blocks bound to the specified batch.
   """
   @spec optimism_batch(Plug.Conn.t(), any()) :: Plug.Conn.t()
-  def optimism_batch(conn, %{"batch_number" => batch_number} = params) do
+  def optimism_batch(conn, %{batch_number_param: batch_number} = params) do
+    # todo: remove select_block_type() as it is actually not processed in the endpoint
     full_options =
       params
       |> select_block_type()
@@ -240,12 +325,35 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     })
   end
 
+  operation(:scroll_batch,
+    summary: "List L2 blocks in a Scroll batch",
+    description: "Retrieves L2 blocks that are bound to a specific Scroll batch number.",
+    parameters:
+      base_params() ++
+        [batch_number_param()] ++
+        define_paging_params(["block_number", "items_count"]),
+    responses: [
+      ok:
+        {"L2 blocks in the specified Scroll batch.", "application/json",
+         paginated_response(
+           items: Schemas.Block,
+           next_page_params_example: %{
+             "block_number" => 22_566_361,
+             "items_count" => 50
+           },
+           title_prefix: "ScrollBatchBlocks"
+         )},
+      unprocessable_entity: JsonErrorResponse.response()
+    ]
+  )
+
   @doc """
-    Function to handle GET requests to `/api/v2/blocks/scroll-batch/:batch_number` endpoint.
+    Function to handle GET requests to `/api/v2/blocks/scroll-batch/:batch_number_param` endpoint.
     It renders the list of L2 blocks bound to the specified batch.
   """
   @spec scroll_batch(Plug.Conn.t(), any()) :: Plug.Conn.t()
-  def scroll_batch(conn, %{"batch_number" => batch_number} = params) do
+  def scroll_batch(conn, %{batch_number_param: batch_number} = params) do
+    # todo: remove select_block_type() as it is actually not processed in the endpoint
     full_options =
       params
       |> select_block_type()
@@ -267,14 +375,38 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     })
   end
 
+  operation(:transactions,
+    summary: "List transactions in a specific block",
+    description: "Retrieves transactions included in a specific block with optional filtering and sorting.",
+    parameters:
+      base_params() ++
+        [block_hash_or_number_param(), transaction_type_param()] ++
+        define_paging_params(["block_number", "index", "items_count"]),
+    responses: [
+      ok:
+        {"Transactions in the specified block.", "application/json",
+         paginated_response(
+           items: Schemas.Transaction,
+           next_page_params_example: %{
+             "block_number" => 12_345_678,
+             "index" => 103,
+             "items_count" => 50
+           },
+           title_prefix: "BlockTransactions"
+         )},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number/transactions` endpoint.
+  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/transactions` endpoint.
   """
   @spec transactions(Plug.Conn.t(), map()) ::
           {:error, :not_found | {:invalid, :hash | :number}}
           | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
           | Plug.Conn.t()
-  def transactions(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do
+  def transactions(conn, %{block_hash_or_number_param: block_hash_or_number} = params) do
     with {:ok, block} <- block_param_to_block(block_hash_or_number) do
       full_options =
         @transaction_necessity_by_association
@@ -300,8 +432,32 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     end
   end
 
+  operation(:internal_transactions,
+    summary: "List internal transactions in a specific block",
+    description:
+      "Retrieves internal transactions included in a specific block with optional filtering by type and call type.",
+    parameters:
+      base_params() ++
+        [block_hash_or_number_param(), internal_transaction_type_param(), internal_transaction_call_type_param()] ++
+        define_paging_params(["block_index", "items_count"]),
+    responses: [
+      ok:
+        {"Internal transactions in the specified block.", "application/json",
+         paginated_response(
+           items: Schemas.InternalTransaction,
+           next_page_params_example: %{
+             "block_index" => 8,
+             "items_count" => 50
+           },
+           title_prefix: "BlockInternalTransactions"
+         )},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number/internal-transactions` endpoint.
+  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/internal-transactions` endpoint.
   Query params:
    - `type` - Filters internal transactions by type. Possible values: (#{Explorer.Chain.InternalTransaction.Type.values()})
    - `call_type` - Filters internal transactions by call type. Possible values: (#{Explorer.Chain.InternalTransaction.CallType.values()})
@@ -311,7 +467,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
           {:error, :not_found | {:invalid, :hash | :number}}
           | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
           | Plug.Conn.t()
-  def internal_transactions(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do
+  def internal_transactions(conn, %{block_hash_or_number_param: block_hash_or_number} = params) do
     with {:ok, block} <- block_param_to_block(block_hash_or_number) do
       full_options =
         @internal_transaction_necessity_by_association
@@ -343,14 +499,37 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     end
   end
 
+  operation(:withdrawals,
+    summary: "List withdrawals in a specific block",
+    description: "Retrieves validator withdrawals included in a specific block.",
+    parameters:
+      base_params() ++
+        [block_hash_or_number_param()] ++
+        define_paging_params(["index", "items_count"]),
+    responses: [
+      ok:
+        {"Withdrawals in the specified block.", "application/json",
+         paginated_response(
+           items: Schemas.Withdrawal,
+           next_page_params_example: %{
+             "index" => 88_192_653,
+             "items_count" => 50
+           },
+           title_prefix: "BlockWithdrawals"
+         )},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number/withdrawals` endpoint.
+  Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/withdrawals` endpoint.
   """
   @spec withdrawals(Plug.Conn.t(), map()) ::
           {:error, :not_found | {:invalid, :hash | :number}}
           | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
           | Plug.Conn.t()
-  def withdrawals(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do
+  def withdrawals(conn, %{block_hash_or_number_param: block_hash_or_number} = params) do
     with {:ok, block} <- block_param_to_block(block_hash_or_number) do
       full_options =
         [
@@ -376,8 +555,20 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     end
   end
 
+  operation(:block_countdown,
+    summary: "Get countdown information for a target block number",
+    description:
+      "Calculates the estimated time remaining until a specified block number is reached based on current block and average block time.",
+    parameters: [block_number_param() | base_params()],
+    responses: [
+      ok: {"Block countdown information.", "application/json", Schemas.Block.Countdown},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Function to handle GET requests to `/api/v2/blocks/:block_number/countdown` endpoint.
+  Function to handle GET requests to `/api/v2/blocks/:block_number_param/countdown` endpoint.
   Calculates the estimated time remaining until a specified block number is reached
   based on the current block number and average block time.
 
@@ -395,7 +586,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
           | {:max_block, nil}
           | {:average_block_time, {:error, :disabled}}
           | {:remaining_blocks, 0}
-  def block_countdown(conn, %{"block_number" => block_number}) do
+  def block_countdown(conn, %{block_number_param: block_number}) do
     with {:format, {:ok, target_block_number}} <- {:format, param_to_block_number(block_number)},
          {:max_block, current_block_number} when not is_nil(current_block_number) <-
            {:max_block, BlockNumber.get_max()},
@@ -414,8 +605,31 @@ defmodule BlockScoutWeb.API.V2.BlockController do
     end
   end
 
+  operation(:beacon_deposits,
+    summary: "List beacon deposits in a specific block",
+    description: "Retrieves beacon deposits included in a specific block with pagination support.",
+    parameters:
+      base_params() ++
+        [block_hash_or_number_param()] ++
+        define_paging_params(["index", "items_count"]),
+    responses: [
+      ok:
+        {"Beacon deposits in the specified block.", "application/json",
+         paginated_response(
+           items: Schemas.Beacon.Deposit,
+           next_page_params_example: %{
+             "index" => 123,
+             "items_count" => 50
+           },
+           title_prefix: "BlockBeaconDeposits"
+         )},
+      unprocessable_entity: JsonErrorResponse.response(),
+      not_found: NotFoundResponse.response()
+    ]
+  )
+
   @doc """
-  Handles `api/v2/blocks/:block_hash_or_number/beacon/deposits` endpoint.
+  Handles `api/v2/blocks/:block_hash_or_number_param/beacon/deposits` endpoint.
   Fetches beacon deposits included in a specific block with pagination support.
 
   This endpoint retrieves all beacon deposits that were included in the
@@ -428,7 +642,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
   ## Parameters
   - `conn`: The Plug connection.
   - `params`: A map containing:
-    - `"block_hash_or_number"`: The block identifier (hash or number) to fetch
+    - `"block_hash_or_number_param"`: The block identifier (hash or number) to fetch
       deposits from.
     - Optional pagination parameter:
       - `"index"`: non-negative integer, the starting index for pagination.
@@ -446,7 +660,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do
           {:error, :not_found | {:invalid, :hash | :number}}
           | {:lost_consensus, {:error, :not_found} | {:ok, Explorer.Chain.Block.t()}}
           | Plug.Conn.t()
-  def beacon_deposits(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do
+  def beacon_deposits(conn, %{block_hash_or_number_param: block_hash_or_number} = params) do
     with {:ok, block} <- block_param_to_block(block_hash_or_number) do
       full_options =
         [
diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex
index d6b0eaf79b17..14723f227736 100644
--- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex
+++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex
@@ -7,6 +7,7 @@ defmodule BlockScoutWeb.PagingHelper do
   import Explorer.Chain, only: [string_to_full_hash: 1]
   import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0]
 
+  alias BlockScoutWeb.Schemas.API.V2.General
   alias Explorer.Chain.InternalTransaction.CallType, as: InternalTransactionCallType
   alias Explorer.Chain.InternalTransaction.Type, as: InternalTransactionType
   alias Explorer.Chain.{SmartContract, Transaction}
@@ -16,27 +17,6 @@ defmodule BlockScoutWeb.PagingHelper do
   @default_paging_options %PagingOptions{page_size: @page_size + 1}
   @allowed_filter_labels ["validated", "pending"]
 
-  case @chain_type do
-    :ethereum ->
-      @allowed_type_labels [
-        "coin_transfer",
-        "contract_call",
-        "contract_creation",
-        "token_transfer",
-        "token_creation",
-        "blob_transaction"
-      ]
-
-    _ ->
-      @allowed_type_labels [
-        "coin_transfer",
-        "contract_call",
-        "contract_creation",
-        "token_transfer",
-        "token_creation"
-      ]
-  end
-
   @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"]
   @allowed_nft_type_labels ["ERC-721", "ERC-1155", "ERC-404"]
   @allowed_chain_id [1, 56, 99]
@@ -128,7 +108,7 @@ defmodule BlockScoutWeb.PagingHelper do
   def chain_ids_filter_options(_), do: [chain_id: []]
 
   def type_filter_options(%{"type" => type}) do
-    [type: type |> parse_filter(@allowed_type_labels) |> Enum.map(&String.to_existing_atom/1)]
+    [type: type |> parse_filter(General.allowed_transaction_types()) |> Enum.map(&String.to_existing_atom/1)]
   end
 
   def type_filter_options(_params), do: [type: []]
@@ -178,7 +158,7 @@ defmodule BlockScoutWeb.PagingHelper do
     |> Enum.uniq()
   end
 
-  def select_block_type(%{"type" => type}) do
+  def select_block_type(%{type: type}) do
     case String.downcase(type) do
       "uncle" ->
         [
@@ -231,10 +211,12 @@ defmodule BlockScoutWeb.PagingHelper do
     params
     |> Map.drop([
       :address_hash_param,
+      :block_hash_or_number_param,
       :type,
       :apikey,
       "apikey",
       "block_hash_or_number",
+      "block_hash_or_number_param",
       "transaction_hash_param",
       "address_hash_param",
       "type",
diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
index d3c82d181714..73bef13f9b5b 100644
--- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
@@ -206,26 +206,26 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
 
     scope "/blocks" do
       get("/", V2.BlockController, :blocks)
-      get("/:block_hash_or_number", V2.BlockController, :block)
-      get("/:block_hash_or_number/transactions", V2.BlockController, :transactions)
-      get("/:block_hash_or_number/internal-transactions", V2.BlockController, :internal_transactions)
-      get("/:block_hash_or_number/withdrawals", V2.BlockController, :withdrawals)
-      get("/:block_number/countdown", V2.BlockController, :block_countdown)
+      get("/:block_hash_or_number_param", V2.BlockController, :block)
+      get("/:block_hash_or_number_param/transactions", V2.BlockController, :transactions)
+      get("/:block_hash_or_number_param/internal-transactions", V2.BlockController, :internal_transactions)
+      get("/:block_hash_or_number_param/withdrawals", V2.BlockController, :withdrawals)
+      get("/:block_number_param/countdown", V2.BlockController, :block_countdown)
 
       if @chain_type == :arbitrum do
-        get("/arbitrum-batch/:batch_number", V2.BlockController, :arbitrum_batch)
+        get("/arbitrum-batch/:batch_number_param", V2.BlockController, :arbitrum_batch)
       end
 
       chain_scope :optimism do
-        get("/optimism-batch/:batch_number", V2.BlockController, :optimism_batch)
+        get("/optimism-batch/:batch_number_param", V2.BlockController, :optimism_batch)
       end
 
       if @chain_type == :scroll do
-        get("/scroll-batch/:batch_number", V2.BlockController, :scroll_batch)
+        get("/scroll-batch/:batch_number_param", V2.BlockController, :scroll_batch)
       end
 
       chain_scope :ethereum do
-        get("/:block_hash_or_number/beacon/deposits", V2.BlockController, :beacon_deposits)
+        get("/:block_hash_or_number_param/beacon/deposits", V2.BlockController, :beacon_deposits)
       end
     end
 
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex
index 5a1e7e2b946a..0d70cb047139 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex
@@ -116,10 +116,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do
         :implementations,
         :is_verified,
         :ens_domain_name,
-        :metadata,
-        :private_tags,
-        :watchlist_names,
-        :public_tags
+        :metadata
       ],
       additionalProperties: false
     }
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
index 069335ff4257..c62571030822 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex
@@ -91,6 +91,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
     additionalProperties: false
   }
 
+  @doc """
+    Returns the Blob4844 schema.
+  """
+  @spec blob4844_schema() :: Schema.t()
+  def blob4844_schema, do: @blob4844_schema
+
   @celestia_schema %Schema{
     type: :object,
     nullable: false,
@@ -116,6 +122,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
     required: [:commitment, :l1_transaction_hash, :l1_timestamp]
   }
 
+  @doc """
+    Returns the Celestia schema.
+  """
+  @spec celestia_schema() :: Schema.t()
+  def celestia_schema, do: @celestia_schema
+
   @optimism_schema %Schema{
     type: :object,
     nullable: false,
@@ -259,12 +271,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
         schema
         |> Helper.extend_schema(
           properties: %{
-            blob_transactions_count: %Schema{type: :integer, nullable: false},
+            blob_transactions_count: %Schema{type: :integer, minimum: 0, nullable: false},
             blob_gas_used: General.IntegerStringNullable,
             excess_blob_gas: General.IntegerStringNullable,
-            blob_gas_price: General.IntegerString,
+            blob_gas_price: General.IntegerStringNullable,
             burnt_blob_fees: General.IntegerString,
-            beacon_deposits_count: %Schema{type: :integer, nullable: true}
+            beacon_deposits_count: %Schema{type: :integer, minimum: 0, nullable: true}
           },
           required: [:blob_transactions_count, :blob_gas_used, :excess_blob_gas, :beacon_deposits_count]
         )
@@ -281,20 +293,77 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do
   end
 end
 
-defmodule BlockScoutWeb.Schemas.API.V2.Block do
+defmodule BlockScoutWeb.Schemas.API.V2.Block.Common do
   @moduledoc """
-  This module defines the schema for the Block struct.
+  This module defines common schema fields for block.
   """
-  require OpenApiSpex
-
-  alias BlockScoutWeb.Schemas.API.V2.{Address, Block.ChainTypeCustomizations, General}
+  alias BlockScoutWeb.Schemas.API.V2.{Address, General}
   alias OpenApiSpex.Schema
 
-  OpenApiSpex.schema(
+  @required_fields [
+    :height,
+    :timestamp,
+    :transactions_count,
+    :internal_transactions_count,
+    :miner,
+    :size,
+    :hash,
+    :parent_hash,
+    :difficulty,
+    :total_difficulty,
+    :gas_used,
+    :gas_limit,
+    :nonce,
+    :base_fee_per_gas,
+    :burnt_fees,
+    :priority_fee,
+    :uncles_hashes,
+    :rewards,
+    :gas_target_percentage,
+    :gas_used_percentage,
+    :burnt_fees_percentage,
+    :type,
+    :transaction_fees,
+    :withdrawals_count,
+    :is_pending_update
+  ]
+
+  @rewards_schema %Schema{
+    type: :array,
+    items: %Schema{
+      type: :object,
+      properties: %{
+        address_hash: General.AddressHash,
+        reward: General.IntegerString,
+        type: %Schema{type: :string, nullable: false}
+      },
+      required: [:type, :reward],
+      additionalProperties: false
+    },
+    nullable: false
+  }
+
+  @doc """
+    Returns the common rewards schema for block.
+  """
+  @spec rewards_schema() :: Schema.t()
+  def rewards_schema, do: @rewards_schema
+
+  @doc """
+    Returns the list of required fields for the block schema.
+  """
+  @spec required_fields() :: [atom()]
+  def required_fields, do: @required_fields
+
+  @doc """
+    Returns the common schema for block.
+  """
+  @spec schema() :: map()
+  def schema do
     %{
       type: :object,
       properties: %{
-        height: %Schema{type: :integer, nullable: false},
+        height: %Schema{type: :integer, nullable: false, minimum: 0},
         timestamp: General.Timestamp,
         transactions_count: %Schema{type: :integer, nullable: false},
         internal_transactions_count: %Schema{type: :integer, nullable: true},
@@ -302,8 +371,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do
         size: %Schema{type: :integer, nullable: false},
         hash: General.FullHash,
         parent_hash: General.FullHash,
-        difficulty: General.IntegerString,
-        total_difficulty: General.IntegerString,
+        difficulty: General.IntegerStringNullable,
+        total_difficulty: General.IntegerStringNullable,
         gas_used: General.IntegerString,
         gas_limit: General.IntegerString,
         nonce: General.HexStringNullable,
@@ -321,56 +390,31 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block do
           },
           nullable: false
         },
-        rewards: %Schema{
-          type: :array,
-          items: %Schema{
-            type: :object,
-            properties: %{
-              type: %Schema{type: :string, nullable: false},
-              reward: General.IntegerString
-            },
-            required: [:type, :reward],
-            additionalProperties: false
-          },
-          nullable: false
-        },
+        rewards: @rewards_schema,
         gas_target_percentage: %Schema{type: :number, format: :float, nullable: false},
         gas_used_percentage: %Schema{type: :number, format: :float, nullable: false},
         burnt_fees_percentage: %Schema{type: :number, format: :float, nullable: true},
         type: %Schema{type: :string, nullable: false, enum: ["block", "uncle", "reorg"]},
         transaction_fees: General.IntegerString,
-        withdrawals_count: %Schema{type: :integer, nullable: true},
+        withdrawals_count: %Schema{type: :integer, minimum: 0, nullable: true},
         is_pending_update: %Schema{type: :boolean, nullable: false}
       },
-      required: [
-        :height,
-        :timestamp,
-        :transactions_count,
-        :internal_transactions_count,
-        :miner,
-        :size,
-        :hash,
-        :parent_hash,
-        :difficulty,
-        :total_difficulty,
-        :gas_used,
-        :gas_limit,
-        :nonce,
-        :base_fee_per_gas,
-        :burnt_fees,
-        :priority_fee,
-        :uncles_hashes,
-        :rewards,
-        :gas_target_percentage,
-        :gas_used_percentage,
-        :burnt_fees_percentage,
-        :type,
-        :transaction_fees,
-        :withdrawals_count,
-        :is_pending_update
-      ],
+      required: required_fields(),
       additionalProperties: false
     }
+  end
+end
+
+defmodule BlockScoutWeb.Schemas.API.V2.Block do
+  @moduledoc """
+  This module defines the schema for the Block struct.
+  """
+  require OpenApiSpex
+
+  alias BlockScoutWeb.Schemas.API.V2.{Block.ChainTypeCustomizations, Block.Common}
+
+  OpenApiSpex.schema(
+    Common.schema()
     |> ChainTypeCustomizations.chain_type_fields()
   )
 end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/countdown.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/countdown.ex
new file mode 100644
index 000000000000..83639c6797a1
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/countdown.ex
@@ -0,0 +1,53 @@
+defmodule BlockScoutWeb.Schemas.API.V2.Block.Countdown do
+  @moduledoc """
+  This module defines the schema for block countdown response from /api/v2/blocks/:block_number/countdown.
+  """
+  require OpenApiSpex
+
+  alias OpenApiSpex.Schema
+
+  OpenApiSpex.schema(%{
+    title: "BlockCountdown",
+    description: "Block countdown information showing estimated time until a target block is reached",
+    type: :object,
+    properties: %{
+      current_block: %Schema{
+        type: :integer,
+        description: "The current highest block number in the blockchain",
+        minimum: 0,
+        example: 22_566_361
+      },
+      countdown_block: %Schema{
+        type: :integer,
+        description: "The target block number for the countdown",
+        minimum: 0,
+        example: 22_600_000
+      },
+      remaining_blocks: %Schema{
+        type: :integer,
+        description: "Number of blocks remaining until the target block is reached",
+        minimum: 0,
+        example: 33_639
+      },
+      estimated_time_in_sec: %Schema{
+        type: :number,
+        format: :float,
+        description: "Estimated time in seconds until the target block is reached",
+        minimum: 0,
+        example: 404_868.0
+      }
+    },
+    required: [
+      :current_block,
+      :countdown_block,
+      :remaining_blocks,
+      :estimated_time_in_sec
+    ],
+    example: %{
+      current_block: 22_566_361,
+      countdown_block: 22_600_000,
+      remaining_blocks: 33_639,
+      estimated_time_in_sec: 404_868.0
+    }
+  })
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex
new file mode 100644
index 000000000000..8c31054d78d8
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex
@@ -0,0 +1,18 @@
+defmodule BlockScoutWeb.Schemas.API.V2.Block.Response do
+  @moduledoc """
+  This module defines the schema for block response from /api/v2/blocks/:hash_or_number.
+  """
+  require OpenApiSpex
+
+  alias BlockScoutWeb.Schemas.API.V2.Block
+  alias BlockScoutWeb.Schemas.Helper
+
+  OpenApiSpex.schema(
+    Block.schema()
+    |> Helper.extend_schema(
+      title: "BlockResponse",
+      description: "Block response",
+      additionalProperties: false
+    )
+  )
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex
index 919ff39aeaef..3f4eb8fa0f90 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex
@@ -27,4 +27,25 @@ defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do
       {"Forbidden", "application/json", __MODULE__}
     end
   end
+
+  defmodule NotFoundResponse do
+    @moduledoc false
+    OpenApiSpex.schema(%{
+      title: "NotFoundResponse",
+      description: "Response returned when the requested resource is not found",
+      type: :object,
+      properties: %{
+        message: %Schema{
+          type: :string,
+          description: "Error message indicating the requested resource was not found",
+          example: "Resource not found"
+        }
+      }
+    })
+
+    @spec response() :: {String.t(), String.t(), module()}
+    def response do
+      {"Not Found", "application/json", __MODULE__}
+    end
+  end
 end
diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex
index 528977c0b938..567f33c5e766 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex
@@ -3,12 +3,30 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do
   This module defines the schema for general types used in the API.
   """
   require OpenApiSpex
+  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
 
   alias BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type, as: CeloElectionRewardType
   alias BlockScoutWeb.Schemas.API.V2.Token.Type, as: TokenType
   alias BlockScoutWeb.Schemas.Helper
+  alias Explorer.Chain.InternalTransaction.CallType
   alias OpenApiSpex.{Parameter, Schema}
 
+  @base_transaction_types [
+    "coin_transfer",
+    "contract_call",
+    "contract_creation",
+    "token_transfer",
+    "token_creation"
+  ]
+
+  case @chain_type do
+    :ethereum ->
+      @allowed_transaction_types ["blob_transaction" | @base_transaction_types]
+
+    _ ->
+      @allowed_transaction_types @base_transaction_types
+  end
+
   defmodule AddressHash do
     @moduledoc false
     OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{40})$", nullable: false})
@@ -280,6 +298,140 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do
     }
   end
 
+  @doc """
+  Returns a parameter definition for a block hash or number in the path.
+  """
+  @spec block_hash_or_number_param() :: Parameter.t()
+  def block_hash_or_number_param do
+    %Parameter{
+      name: :block_hash_or_number_param,
+      in: :path,
+      schema: %Schema{anyOf: [%Schema{type: :integer}, FullHash]},
+      required: true
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for a block number in the path.
+  """
+  @spec block_number_param() :: Parameter.t()
+  def block_number_param do
+    %Parameter{
+      name: :block_number_param,
+      in: :path,
+      schema: %Schema{type: :integer, minimum: 0},
+      required: true
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for filtering blocks by type (uncle or reorg).
+  """
+  @spec block_type_param() :: Parameter.t()
+  def block_type_param do
+    %Parameter{
+      name: :type,
+      in: :query,
+      schema: %Schema{type: :string, enum: ["uncle", "reorg", "block"]},
+      required: false,
+      description: """
+      Filter by block type:
+      * reorg - Only show reorgs
+      * uncle - Only show uncle blocks
+      * block - Only show main blocks
+      If omitted, default value "block" is used.
+      """
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for a batch number in the path.
+  """
+  @spec batch_number_param() :: Parameter.t()
+  def batch_number_param do
+    %Parameter{
+      name: :batch_number,
+      in: :path,
+      schema: %Schema{type: :integer, minimum: 0},
+      required: true,
+      description: "Batch number"
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for filtering transactions by type.
+  """
+  @spec transaction_type_param() :: Parameter.t()
+  def transaction_type_param do
+    %Parameter{
+      name: :type,
+      in: :query,
+      schema: %Schema{type: :string, enum: @allowed_transaction_types},
+      required: false,
+      description: """
+      Filter transactions by type:
+      * coin_transfer - Only show coin transfer transactions
+      * contract_call - Only show contract call transactions
+      * contract_creation - Only show contract creation transactions
+      * token_transfer - Only show token transfer transactions
+      * token_creation - Only show token creation transactions
+      * blob_transaction - Only show blob transactions (Ethereum only)
+      """
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for filtering internal transactions by type.
+  """
+  @spec internal_transaction_type_param() :: Parameter.t()
+  def internal_transaction_type_param do
+    %Parameter{
+      name: :internal_type,
+      in: :query,
+      schema: %Schema{
+        type: :string,
+        enum: Explorer.Chain.InternalTransaction.Type.values()
+      },
+      required: false,
+      description: """
+      Filter internal transactions by type:
+      * all - Show all internal transactions (default)
+      * call - Only show call internal transactions
+      * create - Only show create internal transactions
+      * create2 - Only show create2 internal transactions
+      * reward - Only show reward internal transactions
+      * selfdestruct - Only show selfdestruct internal transactions
+      * stop - Only show stop internal transactions
+      * invalid - Only show invalid internal transactions (Arbitrum only)
+      """
+    }
+  end
+
+  @doc """
+  Returns a parameter definition for filtering internal transactions by call type.
+  """
+  @spec internal_transaction_call_type_param() :: Parameter.t()
+  def internal_transaction_call_type_param do
+    %Parameter{
+      name: :call_type,
+      in: :query,
+      schema: %Schema{
+        type: :string,
+        enum: CallType.values()
+      },
+      required: false,
+      description: """
+      Filter internal transactions by call type:
+      * all - Show all internal transactions (default)
+      * call - Only show call internal transactions
+      * callcode - Only show callcode internal transactions
+      * delegatecall - Only show delegatecall internal transactions
+      * staticcall - Only show staticcall internal transactions
+      * invalid - Only show invalid internal transactions (Arbitrum only)
+      """
+    }
+  end
+
   @doc """
   Returns a parameter definition for filtering transactions by direction (to/from).
   """
@@ -499,7 +651,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do
   @paging_params %{
     "block_number" => %Parameter{
       in: :query,
-      schema: %Schema{type: :integer},
+      schema: %Schema{type: :integer, minimum: 0},
       required: false,
       description: "Block number for paging",
       name: :block_number
@@ -539,6 +691,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do
       description: "Transaction index for paging",
       name: :index
     },
+    "block_index" => %Parameter{
+      in: :query,
+      schema: %Schema{type: :integer},
+      required: false,
+      description: "Block index for paging",
+      name: :block_index
+    },
     "inserted_at" => %Parameter{
       in: :query,
       schema: %Schema{type: :string, format: :"date-time"},
@@ -705,4 +864,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do
       Map.get(@paging_params, field) || raise "Unknown paging param: #{field}"
     end)
   end
+
+  @doc """
+  Returns the list of allowed transaction type labels based on the configured chain type.
+  """
+  @spec allowed_transaction_types() :: [String.t()]
+  def allowed_transaction_types, do: @allowed_transaction_types
 end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
index 39126b14dbc7..c0edc4fcfea3 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
@@ -213,10 +213,50 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
   describe "/blocks/{block_hash_or_number}" do
     test "return 422 on invalid parameter", %{conn: conn} do
       request_1 = get(conn, "/api/v2/blocks/0x123123")
-      assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_1, 422)
 
       request_2 = get(conn, "/api/v2/blocks/123qwe")
-      assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_2, 422)
     end
 
     test "return 404 on non existing block", %{conn: conn} do
@@ -511,10 +551,50 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
   describe "/blocks/{block_hash_or_number}/transactions" do
     test "return 422 on invalid parameter", %{conn: conn} do
       request_1 = get(conn, "/api/v2/blocks/0x123123/transactions")
-      assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_1, 422)
 
       request_2 = get(conn, "/api/v2/blocks/123qwe/transactions")
-      assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_2, 422)
     end
 
     test "return 404 on non existing block", %{conn: conn} do
@@ -599,10 +679,50 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
   describe "/blocks/{block_hash_or_number}/withdrawals" do
     test "return 422 on invalid parameter", %{conn: conn} do
       request_1 = get(conn, "/api/v2/blocks/0x123123/withdrawals")
-      assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_1, 422)
 
       request_2 = get(conn, "/api/v2/blocks/123qwe/withdrawals")
-      assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_2, 422)
     end
 
     test "return 404 on non existing block", %{conn: conn} do
@@ -670,10 +790,50 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
   describe "/blocks/{block_hash_or_number}/internal-transactions" do
     test "returns 422 on invalid parameter", %{conn: conn} do
       request_1 = get(conn, "/api/v2/blocks/0x123123/internal-transactions")
-      assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_1, 422)
 
       request_2 = get(conn, "/api/v2/blocks/123qwe/internal-transactions")
-      assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+
+      assert %{
+               "errors" => [
+                 %{
+                   "detail" =>
+                     "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid integer. Got: string",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 },
+                 %{
+                   "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                   "source" => %{"pointer" => "/block_hash_or_number_param"},
+                   "title" => "Invalid value"
+                 }
+               ]
+             } = json_response(request_2, 422)
     end
 
     test "returns 404 on non existing block", %{conn: conn} do
@@ -756,10 +916,50 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
 
       test "get 422 on invalid block", %{conn: conn} do
         request_1 = get(conn, "/api/v2/blocks/0x123123/beacon/deposits")
-        assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+        assert %{
+                 "errors" => [
+                   %{
+                     "detail" =>
+                       "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   },
+                   %{
+                     "detail" => "Invalid integer. Got: string",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   },
+                   %{
+                     "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   }
+                 ]
+               } = json_response(request_1, 422)
 
         request_2 = get(conn, "/api/v2/blocks/123qwe/beacon/deposits")
-        assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+
+        assert %{
+                 "errors" => [
+                   %{
+                     "detail" =>
+                       "Failed to cast value using any of: Schema(title: \"FullHash\", type: :string), Schema(type: :integer)",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   },
+                   %{
+                     "detail" => "Invalid integer. Got: string",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   },
+                   %{
+                     "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/",
+                     "source" => %{"pointer" => "/block_hash_or_number_param"},
+                     "title" => "Invalid value"
+                   }
+                 ]
+               } = json_response(request_2, 422)
       end
 
       test "get deposits", %{conn: conn} do
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
index 17e085f34593..795cdfcc184f 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
@@ -2929,6 +2929,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
   end
 
   if @chain_type == :neon do
+    import Ecto.Query, only: [from: 2]
+
     describe "neon linked transactions service" do
       test "fetches data from the node and caches in the db", %{conn: conn} do
         transaction = insert(:transaction)
diff --git a/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs
index 96cf17e22a48..73b941b2d60e 100644
--- a/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs
+++ b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs
@@ -335,172 +335,173 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do
                  Repo.all(from(d in Deposit, order_by: [asc: :index]))
       end
 
-      test "fails to process non-sequential logs (logs starts not from 0, between batches)" do
-        deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
+      # todo: return these tests when sequentiality requirement is added back
+      # test "fails to process non-sequential logs (logs starts not from 0, between batches)" do
+      #   deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
 
-        transaction_a_to_be_ignored = insert(:transaction) |> with_block()
+      #   transaction_a_to_be_ignored = insert(:transaction) |> with_block()
 
-        _log_to_be_ignored_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 1,
-            transaction: transaction_a_to_be_ignored,
-            block: transaction_a_to_be_ignored.block
-          )
+      #   _log_to_be_ignored_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 1,
+      #       transaction: transaction_a_to_be_ignored,
+      #       block: transaction_a_to_be_ignored.block
+      #     )
 
-        transaction_a = insert(:transaction) |> with_block()
+      #   transaction_a = insert(:transaction) |> with_block()
+
+      #   _log_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 2,
+      #       transaction: transaction_a,
+      #       block: transaction_a.block
+      #     )
 
-        _log_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 2,
-            transaction: transaction_a,
-            block: transaction_a.block
-          )
+      #   transaction_b = insert(:transaction) |> with_block()
 
-        transaction_b = insert(:transaction) |> with_block()
+      #   _log_b =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 3,
+      #       transaction: transaction_b,
+      #       block: transaction_b.block
+      #     )
 
-        _log_b =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 3,
-            transaction: transaction_b,
-            block: transaction_b.block
-          )
+      #   log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, @state) end)
 
-        log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, @state) end)
+      #   assert log =~ "Non-sequential deposits detected:"
 
-        assert log =~ "Non-sequential deposits detected:"
+      #   assert [] == Repo.all(Deposit)
+      # end
 
-        assert [] == Repo.all(Deposit)
-      end
+      # test "fails to process non-sequential logs (non-sequential between, between batches)" do
+      #   deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
 
-      test "fails to process non-sequential logs (non-sequential between, between batches)" do
-        deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
+      #   transaction_a = insert(:transaction) |> with_block()
 
-        transaction_a = insert(:transaction) |> with_block()
+      #   log_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 0,
+      #       transaction: transaction_a,
+      #       block: transaction_a.block
+      #     )
 
-        log_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 0,
-            transaction: transaction_a,
-            block: transaction_a.block
-          )
+      #   log_a_transaction_hash = log_a.transaction_hash
+      #   transaction_a_to_be_ignored = insert(:transaction) |> with_block()
 
-        log_a_transaction_hash = log_a.transaction_hash
-        transaction_a_to_be_ignored = insert(:transaction) |> with_block()
+      #   _log_to_be_ignored_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 2,
+      #       transaction: transaction_a_to_be_ignored,
+      #       block: transaction_a_to_be_ignored.block
+      #     )
 
-        _log_to_be_ignored_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 2,
-            transaction: transaction_a_to_be_ignored,
-            block: transaction_a_to_be_ignored.block
-          )
+      #   transaction_b = insert(:transaction) |> with_block()
 
-        transaction_b = insert(:transaction) |> with_block()
+      #   _log_b =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 3,
+      #       transaction: transaction_b,
+      #       block: transaction_b.block
+      #     )
 
-        _log_b =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 3,
-            transaction: transaction_b,
-            block: transaction_b.block
-          )
-
-        {:noreply, new_state} = DepositFetcher.handle_info(:process_logs, @state)
+      #   {:noreply, new_state} = DepositFetcher.handle_info(:process_logs, @state)
 
-        log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, new_state) end)
+      #   log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, new_state) end)
 
-        assert log =~ "Non-sequential deposits detected:"
+      #   assert log =~ "Non-sequential deposits detected:"
 
-        assert [%Deposit{transaction_hash: ^log_a_transaction_hash}] = Repo.all(Deposit)
-      end
+      #   assert [%Deposit{transaction_hash: ^log_a_transaction_hash}] = Repo.all(Deposit)
+      # end
 
-      test "fails to process non-sequential logs (logs starts not from 0, inside batch)" do
-        state = Map.put(@state, :batch_size, 5)
+      # test "fails to process non-sequential logs (logs starts not from 0, inside batch)" do
+      #   state = Map.put(@state, :batch_size, 5)
 
-        deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
+      #   deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
 
-        transaction_a_to_be_ignored = insert(:transaction) |> with_block()
+      #   transaction_a_to_be_ignored = insert(:transaction) |> with_block()
 
-        _log_to_be_ignored_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 1,
-            transaction: transaction_a_to_be_ignored,
-            block: transaction_a_to_be_ignored.block
-          )
+      #   _log_to_be_ignored_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 1,
+      #       transaction: transaction_a_to_be_ignored,
+      #       block: transaction_a_to_be_ignored.block
+      #     )
 
-        transaction_a = insert(:transaction) |> with_block()
+      #   transaction_a = insert(:transaction) |> with_block()
 
-        _log_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 2,
-            transaction: transaction_a,
-            block: transaction_a.block
-          )
+      #   _log_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 2,
+      #       transaction: transaction_a,
+      #       block: transaction_a.block
+      #     )
 
-        transaction_b = insert(:transaction) |> with_block()
+      #   transaction_b = insert(:transaction) |> with_block()
 
-        _log_b =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 3,
-            transaction: transaction_b,
-            block: transaction_b.block
-          )
+      #   _log_b =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 3,
+      #       transaction: transaction_b,
+      #       block: transaction_b.block
+      #     )
 
-        log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end)
+      #   log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end)
 
-        assert log =~ "Non-sequential deposits detected:"
+      #   assert log =~ "Non-sequential deposits detected:"
 
-        assert [] == Repo.all(Deposit)
-      end
+      #   assert [] == Repo.all(Deposit)
+      # end
 
-      test "fails to process non-sequential logs (non-sequential between, inside batch)" do
-        state = Map.put(@state, :batch_size, 5)
+      # test "fails to process non-sequential logs (non-sequential between, inside batch)" do
+      #   state = Map.put(@state, :batch_size, 5)
 
-        deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
+      #   deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa")
 
-        transaction_a = insert(:transaction) |> with_block()
+      #   transaction_a = insert(:transaction) |> with_block()
 
-        _log_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 0,
-            transaction: transaction_a,
-            block: transaction_a.block
-          )
+      #   _log_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 0,
+      #       transaction: transaction_a,
+      #       block: transaction_a.block
+      #     )
 
-        transaction_a_to_be_ignored = insert(:transaction) |> with_block()
+      #   transaction_a_to_be_ignored = insert(:transaction) |> with_block()
 
-        _log_to_be_ignored_a =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 2,
-            transaction: transaction_a_to_be_ignored,
-            block: transaction_a_to_be_ignored.block
-          )
+      #   _log_to_be_ignored_a =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 2,
+      #       transaction: transaction_a_to_be_ignored,
+      #       block: transaction_a_to_be_ignored.block
+      #     )
 
-        transaction_b = insert(:transaction) |> with_block()
+      #   transaction_b = insert(:transaction) |> with_block()
 
-        _log_b =
-          insert(:beacon_deposit_log,
-            address: deposit_contract_address,
-            deposit_index: 3,
-            transaction: transaction_b,
-            block: transaction_b.block
-          )
+      #   _log_b =
+      #     insert(:beacon_deposit_log,
+      #       address: deposit_contract_address,
+      #       deposit_index: 3,
+      #       transaction: transaction_b,
+      #       block: transaction_b.block
+      #     )
 
-        log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end)
+      #   log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end)
 
-        assert log =~ "Non-sequential deposits detected:"
+      #   assert log =~ "Non-sequential deposits detected:"
 
-        assert [] = Repo.all(Deposit)
-      end
+      #   assert [] = Repo.all(Deposit)
+      # end
 
       test "signature verification" do
         state = Map.put(@state, :batch_size, 5)

From 0478f9a59e112fc666565b951883a825b5616473 Mon Sep 17 00:00:00 2001
From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>
Date: Wed, 1 Oct 2025 20:17:48 +0400
Subject: [PATCH 0373/1006] fix: Fix SanitizeDuplicatedLogIndexLogs migration
 completion check (#13308)

---
 .../sanitize_duplicated_log_index_logs.ex     | 29 +++++++++----------
 1 file changed, 14 insertions(+), 15 deletions(-)

diff --git a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex
index 3e5c11fd7975..5f68da48e067 100644
--- a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex
+++ b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex
@@ -8,7 +8,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do
 
   import Ecto.Query
 
-  alias Explorer.Chain.Cache.BackgroundMigrations
+  alias Explorer.Chain.Cache.{BackgroundMigrations, BlockNumber}
   alias Explorer.Chain.{Log, TokenTransfer}
   alias Explorer.Chain.Token.Instance
   alias Explorer.Migrator.FillingMigration
@@ -22,19 +22,16 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do
   def migration_name, do: @migration_name
 
   @impl FillingMigration
+  def last_unprocessed_identifiers(%{"block_number_to_process" => -1} = state), do: {[], state}
+
   def last_unprocessed_identifiers(state) do
-    block_number = state[:block_number_to_process] || 0
+    block_number = state["block_number_to_process"] || BlockNumber.get_max()
 
     limit = batch_size() * concurrency()
 
-    ids =
-      block_number
-      |> unprocessed_data_query(block_number + limit)
-      |> Repo.all(timeout: :infinity)
-      |> Enum.group_by(& &1.block_hash)
-      |> Map.to_list()
+    from_block_number = max(block_number - limit, 0)
 
-    {ids, Map.put(state, :block_number_to_process, block_number + limit)}
+    {Enum.to_list(from_block_number..block_number), Map.put(state, "block_number_to_process", from_block_number - 1)}
   end
 
   @doc """
@@ -46,11 +43,6 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do
     nil
   end
 
-  def unprocessed_data_query(block_number_start, block_number_end) do
-    Log
-    |> where([l], l.block_number >= ^block_number_start and l.block_number < ^block_number_end)
-  end
-
   @impl FillingMigration
   @doc """
   Updates a batch of logs grouped by block.
@@ -63,7 +55,14 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do
 
     :ok
   """
-  def update_batch(logs_by_block) do
+  def update_batch(block_numbers) do
+    logs_by_block =
+      Log
+      |> where([l], l.block_number in ^block_numbers)
+      |> Repo.all(timeout: :infinity)
+      |> Enum.group_by(& &1.block_hash)
+      |> Map.to_list()
+
     logs_to_update =
       logs_by_block
       |> Enum.map(&process_block/1)

From 1ffbf69c51a08688b4fa6e793c558fe219275dd7 Mon Sep 17 00:00:00 2001
From: varasev <33550681+varasev@users.noreply.github.com>
Date: Wed, 1 Oct 2025 22:23:20 +0400
Subject: [PATCH 0374/1006] feat: Fields for OP Withdrawal Claim button
 (#13272)

Co-authored-by: POA <33550681+poa@users.noreply.github.com>
Co-authored-by: Victor Baranov 
---
 .../views/api/v2/optimism_view.ex             | 87 +++++++++++++++++--
 .../lib/explorer/chain/optimism/withdrawal.ex | 73 ++++++++++++++--
 apps/explorer/lib/explorer/helper.ex          | 13 ++-
 ...50919073011_op_withdrawal_claim_button.exs | 29 +++++++
 apps/indexer/lib/indexer/fetcher/optimism.ex  |  7 ++
 .../indexer/fetcher/optimism/withdrawal.ex    |  9 +-
 .../indexer/transform/optimism/withdrawals.ex |  7 +-
 7 files changed, 200 insertions(+), 25 deletions(-)
 create mode 100644 apps/explorer/priv/optimism/migrations/20250919073011_op_withdrawal_claim_button.exs

diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
index a49d7b1164b0..ba5211036e05 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
@@ -2,10 +2,11 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
   use BlockScoutWeb, :view
 
   import Ecto.Query, only: [from: 2]
+  import Explorer.Helper, only: [truncate_address_hash: 1, decode_data: 2]
 
   alias BlockScoutWeb.API.V2.Helper
   alias Explorer.{Chain, Repo}
-  alias Explorer.Chain.{Block, Transaction}
+  alias Explorer.Chain.{Block, Data, Hash, Transaction}
   alias Explorer.Chain.Optimism.{DisputeGame, FrameSequence, FrameSequenceBlob, InteropMessage, Withdrawal}
 
   @api_true [api?: true]
@@ -182,6 +183,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
         conn: conn
       }) do
     respected_games = Withdrawal.respected_games(@api_true)
+    portal_contract_address_hash = Withdrawal.portal_contract_address()
 
     %{
       items:
@@ -213,6 +215,9 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
 
           {status, challenge_period_end} = Withdrawal.status(w, respected_games, @api_true)
 
+          {sender_address_hash, target_address_hash, msg_value, msg_gas_limit, msg_data} =
+            withdrawal_msg_transaction_fields(w)
+
           %{
             "msg_nonce_raw" => Decimal.to_string(w.msg_nonce, :normal),
             "msg_nonce" => msg_nonce,
@@ -222,7 +227,13 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
             "l2_timestamp" => w.l2_timestamp,
             "status" => status,
             "l1_transaction_hash" => w.l1_transaction_hash,
-            "challenge_period_end" => challenge_period_end
+            "challenge_period_end" => challenge_period_end,
+            "portal_contract_address_hash" => portal_contract_address_hash,
+            "msg_sender_address_hash" => sender_address_hash,
+            "msg_target_address_hash" => target_address_hash,
+            "msg_value" => msg_value,
+            "msg_gas_limit" => msg_gas_limit,
+            "msg_data" => msg_data
           }
         end),
       next_page_params: next_page_params
@@ -440,7 +451,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
     |> add_optional_transaction_field(transaction, :l1_fee_scalar)
     |> add_optional_transaction_field(transaction, :l1_gas_price)
     |> add_optional_transaction_field(transaction, :l1_gas_used)
-    |> add_optimism_fields(transaction.hash)
+    |> add_optimism_fields(transaction)
   end
 
   defp add_optional_transaction_field(out_json, transaction, field) do
@@ -450,20 +461,41 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
     end
   end
 
-  defp add_optimism_fields(out_json, transaction_hash) do
+  # Extends the json output for a transaction adding Optimism-related info to the output
+  # (such as related withdrawals, interop messages).
+  #
+  # ## Parameters
+  # - `out_json`: A map defining output json which will be extended.
+  # - `transaction`: transaction structure containing necessary data for the OP fields.
+  #
+  # ## Returns
+  # - An extended map containing `op_withdrawals`, `op_interop_messages` (optional).
+  @spec add_optimism_fields(map(), Transaction.t()) :: map()
+  defp add_optimism_fields(out_json, transaction) do
+    portal_contract_address_hash = Withdrawal.portal_contract_address()
+
     withdrawals =
-      transaction_hash
+      transaction.hash
       |> Withdrawal.transaction_statuses()
-      |> Enum.map(fn {nonce, status, l1_transaction_hash} ->
+      |> Enum.map(fn {nonce, status, w} ->
+        {sender_address_hash, target_address_hash, value, gas_limit, data} = withdrawal_msg_transaction_fields(w)
+
         %{
           "nonce" => nonce,
           "status" => status,
-          "l1_transaction_hash" => l1_transaction_hash
+          "l1_transaction_hash" => w.l1_transaction_hash,
+          "portal_contract_address_hash" => portal_contract_address_hash,
+          "msg_nonce_raw" => w.msg_nonce,
+          "msg_sender_address_hash" => sender_address_hash,
+          "msg_target_address_hash" => target_address_hash,
+          "msg_value" => value,
+          "msg_gas_limit" => gas_limit,
+          "msg_data" => data
         }
       end)
 
     interop_messages =
-      transaction_hash
+      transaction.hash
       |> InteropMessage.messages_by_transaction()
 
     out_json = Map.put(out_json, "op_withdrawals", withdrawals)
@@ -484,4 +516,43 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
       _ -> msg
     end
   end
+
+  # Retrieves withdrawal message transaction fields from the `MessagePassed` event emitted by `L2ToL1MessagePasser` contract.
+  #
+  # The event looks as follows:
+  # MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash)
+  #
+  # ## Parameters
+  # - `w`: A map containing `msg_log_sender_address_hash`, `msg_log_target_address_hash`, and `msg_log_data` components
+  #        of the `MessagePassed` event.
+  #
+  # ## Returns
+  # - A tuple containing the following fields in form of string (each one can be `nil`):
+  #   {sender address, target address, value, gas limit, data}
+  @spec withdrawal_msg_transaction_fields(%{
+          msg_log_sender_address_hash: Hash.t(),
+          msg_log_target_address_hash: Hash.t(),
+          msg_log_data: Data.t()
+        }) :: {String.t() | nil, String.t() | nil, String.t() | nil, String.t() | nil, String.t() | nil}
+  defp withdrawal_msg_transaction_fields(w) do
+    sender_address_hash =
+      if not is_nil(w.msg_log_sender_address_hash) do
+        truncate_address_hash(w.msg_log_sender_address_hash)
+      end
+
+    target_address_hash =
+      if not is_nil(w.msg_log_target_address_hash) do
+        truncate_address_hash(w.msg_log_target_address_hash)
+      end
+
+    if is_nil(w.msg_log_data) do
+      {sender_address_hash, target_address_hash, nil, nil, nil}
+    else
+      [msg_value, msg_gas_limit, msg_data, _withdrawal_hash] =
+        decode_data(w.msg_log_data, [{:uint, 256}, {:uint, 256}, :bytes, {:bytes, 32}])
+
+      {sender_address_hash, target_address_hash, to_string(msg_value), to_string(msg_gas_limit),
+       "0x" <> Base.encode16(msg_data, case: :lower)}
+    end
+  end
 end
diff --git a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex
index c2eff65dfdad..af3d083a1f72 100644
--- a/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex
+++ b/apps/explorer/lib/explorer/chain/optimism/withdrawal.ex
@@ -7,7 +7,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
 
   alias Explorer.Application.Constants
   alias Explorer.Chain
-  alias Explorer.Chain.{Block, Hash, Transaction}
+  alias Explorer.Chain.{Block, Hash, Log, Transaction}
   alias Explorer.Chain.Cache.OptimismFinalizationPeriod
   alias Explorer.Chain.Optimism.{DisputeGame, OutputRoot, WithdrawalEvent}
   alias Explorer.{Helper, PagingOptions, Repo}
@@ -25,6 +25,9 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
   @dispute_game_finality_delay_seconds "optimism_dispute_game_finality_delay_seconds"
   @proof_maturity_delay_seconds "optimism_proof_maturity_delay_seconds"
 
+  # 32-byte signature of the event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash)
+  @message_passed_event "0x02a52367d10742d8032712c1bb8e0144ff1ec5ffda1ed7d70bb05a2744955054"
+
   @required_attrs ~w(msg_nonce hash l2_transaction_hash l2_block_number)a
   @game_fields ~w(created_at resolved_at status)a
 
@@ -74,6 +77,10 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
             on: w.l2_block_number == l2_block.number,
             left_join: we in WithdrawalEvent,
             on: we.withdrawal_hash == w.hash and we.l1_event_type == :WithdrawalFinalized,
+            left_join: log in Log,
+            on:
+              log.transaction_hash == w.l2_transaction_hash and log.first_topic == ^@message_passed_event and
+                log.second_topic == fragment("numeric_to_bytea32(msg_nonce)"),
             select: %{
               msg_nonce: w.msg_nonce,
               hash: w.hash,
@@ -81,7 +88,10 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
               l2_timestamp: l2_block.timestamp,
               l2_transaction_hash: w.l2_transaction_hash,
               l1_transaction_hash: we.l1_transaction_hash,
-              from: l2_transaction.from_address_hash
+              from: l2_transaction.from_address_hash,
+              msg_log_sender_address_hash: log.third_topic,
+              msg_log_target_address_hash: log.fourth_topic,
+              msg_log_data: log.data
             }
           )
 
@@ -133,9 +143,16 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
   @doc """
     Gets withdrawal statuses for Optimism Withdrawal transaction.
     For each withdrawal associated with this transaction,
-    returns the status and the corresponding L1 transaction hash if the status is `Relayed`.
+    returns the status, the corresponding L1 transaction hash if the status is `Relayed`,
+    and withdrawal message's data (such as nonce, sender, target, etc.).
+
+    ## Parameters
+    - `l2_transaction_hash`: The transaction hash associated with the withdrawal.
+
+    ## Returns
+    - A tuple containing the withdrawal message nonce, the withdrawal status, and a map with other message's data.
   """
-  @spec transaction_statuses(Hash.t()) :: [{non_neg_integer(), String.t(), Hash.t() | nil}]
+  @spec transaction_statuses(Hash.t()) :: [{non_neg_integer(), String.t(), map()}]
   def transaction_statuses(l2_transaction_hash) do
     query =
       from(w in __MODULE__,
@@ -144,11 +161,18 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
         on: w.l2_block_number == l2_block.number and l2_block.consensus == true,
         left_join: we in WithdrawalEvent,
         on: we.withdrawal_hash == w.hash and we.l1_event_type == :WithdrawalFinalized,
+        left_join: log in Log,
+        on:
+          log.transaction_hash == w.l2_transaction_hash and log.first_topic == ^@message_passed_event and
+            log.second_topic == fragment("numeric_to_bytea32(msg_nonce)"),
         select: %{
           hash: w.hash,
           l2_block_number: w.l2_block_number,
           l1_transaction_hash: we.l1_transaction_hash,
-          msg_nonce: w.msg_nonce
+          msg_nonce: w.msg_nonce,
+          msg_log_sender_address_hash: log.third_topic,
+          msg_log_target_address_hash: log.fourth_topic,
+          msg_log_data: log.data
         }
       )
 
@@ -162,7 +186,7 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
         )
 
       {status, _} = status(w, nil, @api_true)
-      {msg_nonce, status, w.l1_transaction_hash}
+      {msg_nonce, status, w}
     end)
   end
 
@@ -435,4 +459,41 @@ defmodule Explorer.Chain.Optimism.Withdrawal do
       end
     end
   end
+
+  @doc """
+    Returns a signature of the `MessagePassed` event emitted by `L2ToL1MessagePasser` contract
+    in form of 0x-prefixed string.
+
+    ## Returns
+    - A 0x-prefixed string representing the signature.
+  """
+  @spec message_passed_event() :: String.t()
+  def message_passed_event, do: @message_passed_event
+
+  @doc """
+    Returns `OptimismPortal` contract address. First, the function tries to get the address from the
+    `INDEXER_OPTIMISM_L1_PORTAL_CONTRACT` env variable. If that's not defined, the address is retrieved
+    from the database (constants table) where it was saved by other modules. If the address is unknown,
+    the function returns `nil`.
+
+    ## Returns
+    - The OptimismPortal contract address in form of 0x-prefixed string.
+    - `nil` if the address cannot be determined.
+  """
+  @spec portal_contract_address() :: String.t() | nil
+  def portal_contract_address do
+    portal_address = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism][:portal]
+
+    if Indexer.Helper.address_correct?(portal_address) do
+      portal_address
+    else
+      Constants.get_constant_value(portal_contract_address_constant(), @api_true)
+    end
+  end
+
+  @doc """
+  Returns the name of the optimism_portal_contract_address constant.
+  """
+  @spec portal_contract_address_constant() :: binary()
+  def portal_contract_address_constant, do: "optimism_portal_contract_address"
 end
diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex
index c6d6a327c9b8..4f581e618e43 100644
--- a/apps/explorer/lib/explorer/helper.ex
+++ b/apps/explorer/lib/explorer/helper.ex
@@ -39,7 +39,7 @@ defmodule Explorer.Helper do
   address.
 
   ## Parameters
-  - `address_hash` (`EthereumJSONRPC.hash()` | `nil`): The full address hash to
+  - `address_hash` (`EthereumJSONRPC.hash()` | `Hash.t()` | `nil`): The full address hash to
     be truncated, or `nil`.
 
   ## Returns
@@ -51,12 +51,21 @@ defmodule Explorer.Helper do
       iex> truncate_address_hash("0x000000000000000000000000abcdef1234567890abcdef1234567890abcdef")
       "0xabcdef1234567890abcdef1234567890abcdef"
 
+      iex> truncate_address_hash(%Explorer.Chain.Hash{byte_count: 32, bytes: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7>>})
+      "0x4200000000000000000000000000000000000007"
+
       iex> truncate_address_hash(nil)
       "0x0000000000000000000000000000000000000000"
   """
-  @spec truncate_address_hash(EthereumJSONRPC.hash() | nil) :: EthereumJSONRPC.address()
+  @spec truncate_address_hash(EthereumJSONRPC.hash() | Hash.t() | nil) :: EthereumJSONRPC.address()
   def truncate_address_hash(address_hash)
 
+  def truncate_address_hash(%Hash{} = address_hash) do
+    address_hash
+    |> Hash.to_string()
+    |> truncate_address_hash()
+  end
+
   def truncate_address_hash(nil), do: burn_address_hash_string()
 
   def truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do
diff --git a/apps/explorer/priv/optimism/migrations/20250919073011_op_withdrawal_claim_button.exs b/apps/explorer/priv/optimism/migrations/20250919073011_op_withdrawal_claim_button.exs
new file mode 100644
index 000000000000..23d4b01a107e
--- /dev/null
+++ b/apps/explorer/priv/optimism/migrations/20250919073011_op_withdrawal_claim_button.exs
@@ -0,0 +1,29 @@
+defmodule Explorer.Repo.Optimism.Migrations.OPWithdrawalClaimButton do
+  use Ecto.Migration
+
+  def up do
+    execute("""
+    CREATE OR REPLACE FUNCTION numeric_to_bytea32(n NUMERIC)
+    RETURNS BYTEA AS $$
+    DECLARE
+        zero_left_pad INT := 32;
+        bytes BYTEA := repeat(E'\\\\000', zero_left_pad)::bytea; -- preallocate zero bytes
+        v INT;
+        pos INT := zero_left_pad - 1; -- index from rightmost byte
+    BEGIN
+        WHILE n > 0 AND pos >= 0 LOOP
+            v := n % 256;
+            bytes := set_byte(bytes, pos, v);
+            n := (n - v) / 256;
+            pos := pos - 1;
+        END LOOP;
+        RETURN bytes;
+    END;
+    $$ LANGUAGE plpgsql IMMUTABLE STRICT;
+    """)
+  end
+
+  def down do
+    execute("DROP FUNCTION IF EXISTS numeric_to_bytea32(n NUMERIC);")
+  end
+end
diff --git a/apps/indexer/lib/indexer/fetcher/optimism.ex b/apps/indexer/lib/indexer/fetcher/optimism.ex
index 16d64816a0de..265008759e90 100644
--- a/apps/indexer/lib/indexer/fetcher/optimism.ex
+++ b/apps/indexer/lib/indexer/fetcher/optimism.ex
@@ -18,8 +18,10 @@ defmodule Indexer.Fetcher.Optimism do
 
   alias EthereumJSONRPC.Block.{ByHash, ByNumber}
   alias EthereumJSONRPC.{Blocks, Contract}
+  alias Explorer.Application.Constants
   alias Explorer.Chain.Cache.ChainId
   alias Explorer.Chain.Cache.Counters.LastFetchedCounter
+  alias Explorer.Chain.Optimism.Withdrawal
   alias Explorer.Chain.RollupReorgMonitorQueue
   alias Explorer.Repo
   alias Indexer.Fetcher.RollupL1ReorgMonitor
@@ -246,6 +248,11 @@ defmodule Indexer.Fetcher.Optimism do
             |> Map.get(:result, fallback_start_block)
             |> quantity_to_integer()
 
+          if not is_nil(optimism_portal) do
+            # we save the OptimismPortal contract address to use it in other modules (e.g. by `BlockScoutWeb.API.V2.OptimismView`)
+            Constants.set_constant_value(Withdrawal.portal_contract_address_constant(), optimism_portal)
+          end
+
           {optimism_portal, start_block}
 
         _ ->
diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex
index c995be37bced..1cf23a844206 100644
--- a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex
+++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal.ex
@@ -22,9 +22,6 @@ defmodule Indexer.Fetcher.Optimism.Withdrawal do
   @fetcher_name :optimism_withdrawals
   @counter_type "optimism_withdrawals_fetcher_last_l2_block_hash"
 
-  # 32-byte signature of the event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash)
-  @message_passed_event "0x02a52367d10742d8032712c1bb8e0144ff1ec5ffda1ed7d70bb05a2744955054"
-
   def child_spec(start_link_arguments) do
     spec = %{
       id: __MODULE__,
@@ -217,13 +214,15 @@ defmodule Indexer.Fetcher.Optimism.Withdrawal do
          block_end,
          json_rpc_named_arguments
        ) do
+    message_passed_event = OptimismWithdrawal.message_passed_event()
+
     withdrawals =
       if scan_db do
         query =
           from(log in Log,
             select: {log.second_topic, log.data, log.transaction_hash, log.block_number},
             where:
-              log.first_topic == ^@message_passed_event and log.address_hash == ^message_passer and
+              log.first_topic == ^message_passed_event and log.address_hash == ^message_passer and
                 log.block_number >= ^block_start and log.block_number <= ^block_end
           )
 
@@ -238,7 +237,7 @@ defmodule Indexer.Fetcher.Optimism.Withdrawal do
             block_start,
             block_end,
             message_passer,
-            [@message_passed_event],
+            [message_passed_event],
             json_rpc_named_arguments,
             0,
             3
diff --git a/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex b/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex
index eca8d19e7a87..df023a86f9a6 100644
--- a/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex
+++ b/apps/indexer/lib/indexer/transform/optimism/withdrawals.ex
@@ -5,12 +5,10 @@ defmodule Indexer.Transform.Optimism.Withdrawals do
 
   require Logger
 
+  alias Explorer.Chain.Optimism.Withdrawal
   alias Indexer.Fetcher.Optimism.Withdrawal, as: OptimismWithdrawal
   alias Indexer.Helper
 
-  # 32-byte signature of the event MessagePassed(uint256 indexed nonce, address indexed sender, address indexed target, uint256 value, uint256 gasLimit, bytes data, bytes32 withdrawalHash)
-  @message_passed_event "0x02a52367d10742d8032712c1bb8e0144ff1ec5ffda1ed7d70bb05a2744955054"
-
   @doc """
   Returns a list of withdrawals given a list of logs.
   """
@@ -23,10 +21,11 @@ defmodule Indexer.Transform.Optimism.Withdrawals do
            message_passer = Application.get_env(:indexer, OptimismWithdrawal)[:message_passer],
            true <- Helper.address_correct?(message_passer) do
         message_passer = String.downcase(message_passer)
+        message_passed_event = Withdrawal.message_passed_event()
 
         logs
         |> Enum.filter(fn log ->
-          !is_nil(log.first_topic) && String.downcase(log.first_topic) == @message_passed_event &&
+          !is_nil(log.first_topic) && String.downcase(log.first_topic) == message_passed_event &&
             String.downcase(Helper.address_hash_to_string(log.address_hash)) == message_passer
         end)
         |> Enum.map(fn log ->

From e9c8821810b67716237d1dbf08ad87b4602e5a4f Mon Sep 17 00:00:00 2001
From: Victor Baranov 
Date: Fri, 3 Oct 2025 12:51:39 +0300
Subject: [PATCH 0375/1006] fix: Hide compile-time chain type API routes in
 other chain type swaggers (#13309)

---
 .../lib/block_scout_web/routers/api_router.ex | 24 +++++++++----------
 1 file changed, 12 insertions(+), 12 deletions(-)

diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
index 73bef13f9b5b..42a8e8047aa5 100644
--- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
+++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex
@@ -121,7 +121,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
 
     get("/smart-contracts/:address_hash_param", V2.ImportController, :try_to_search_contract)
 
-    chain_scope :optimism do
+    if @chain_type == :optimism do
       post("/optimism/interop/", V2.OptimismController, :interop_import)
     end
   end
@@ -141,7 +141,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       get("/csv-export", V2.ConfigController, :csv_export)
       get("/public-metrics", V2.ConfigController, :public_metrics)
 
-      chain_scope :celo do
+      if @chain_type == :celo do
         get("/celo", V2.ConfigController, :celo)
       end
     end
@@ -163,7 +163,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
         get("/arbitrum-batch/:batch_number", V2.TransactionController, :arbitrum_batch)
       end
 
-      chain_scope :optimism do
+      if @chain_type == :optimism do
         get("/optimism-batch/:batch_number", V2.TransactionController, :optimism_batch)
       end
 
@@ -191,7 +191,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
         get("/:transaction_hash_param/blobs", V2.TransactionController, :blobs)
       end
 
-      chain_scope :ethereum do
+      if @chain_type == :ethereum do
         get("/:transaction_hash_param/beacon/deposits", V2.TransactionController, :beacon_deposits)
       end
     end
@@ -216,7 +216,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
         get("/arbitrum-batch/:batch_number_param", V2.BlockController, :arbitrum_batch)
       end
 
-      chain_scope :optimism do
+      if @chain_type == :optimism do
         get("/optimism-batch/:batch_number_param", V2.BlockController, :optimism_batch)
       end
 
@@ -224,7 +224,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
         get("/scroll-batch/:batch_number_param", V2.BlockController, :scroll_batch)
       end
 
-      chain_scope :ethereum do
+      if @chain_type == :ethereum do
         get("/:block_hash_or_number_param/beacon/deposits", V2.BlockController, :beacon_deposits)
       end
     end
@@ -251,12 +251,12 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       get("/:address_hash_param/nft", V2.AddressController, :nft_list)
       get("/:address_hash_param/nft/collections", V2.AddressController, :nft_collections)
 
-      chain_scope :celo do
+      if @chain_type == :celo do
         get("/:address_hash_param/celo/election-rewards", V2.AddressController, :celo_election_rewards)
         get("/:address_hash_param/celo/election-rewards/csv", V2.CsvExportController, :celo_election_rewards_csv)
       end
 
-      chain_scope :ethereum do
+      if @chain_type == :ethereum do
         get("/:address_hash_param/beacon/deposits", V2.AddressController, :beacon_deposits)
       end
     end
@@ -267,7 +267,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       get("/transactions/watchlist", V2.MainPageController, :watchlist_transactions)
       get("/indexing-status", V2.MainPageController, :indexing_status)
 
-      chain_scope :optimism do
+      if @chain_type == :optimism do
         get("/optimism-deposits", V2.MainPageController, :optimism_deposits)
       end
 
@@ -298,7 +298,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       end
     end
 
-    chain_scope :optimism do
+    if @chain_type == :optimism do
       scope "/optimism" do
         get("/txn-batches", V2.OptimismController, :transaction_batches)
         get("/txn-batches/count", V2.OptimismController, :transaction_batches_count)
@@ -322,7 +322,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       end
     end
 
-    chain_scope :celo do
+    if @chain_type == :celo do
       scope "/celo/epochs" do
         get("/", V2.CeloController, :epochs)
         get("/:number", V2.CeloController, :epoch)
@@ -431,7 +431,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do
       end
     end
 
-    chain_scope :ethereum do
+    if @chain_type == :ethereum do
       scope "/beacon" do
         get("/deposits", V2.Ethereum.DepositController, :list)
         get("/deposits/count", V2.Ethereum.DepositController, :count)

From da41b83c57afd8b2056f4da15e512c26156455d3 Mon Sep 17 00:00:00 2001
From: Victor Baranov 
Date: Fri, 3 Oct 2025 17:40:10 +0300
Subject: [PATCH 0376/1006] fix: Fix web tests after hiding compile-time chain
 types routes (#13324)

---
 .../api/v2/address_controller_test.exs        | 14 +++++---------
 .../api/v2/block_controller_test.exs          | 11 ++---------
 .../v2/ethereum/deposit_controller_test.exs   | 19 ++-----------------
 .../api/v2/optimism_controller_test.exs       |  5 -----
 .../api/v2/transaction_controller_test.exs    | 11 ++---------
 5 files changed, 11 insertions(+), 49 deletions(-)

diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
index 1351698d0d6b..98f1ef0f2d1d 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
@@ -5496,8 +5496,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
     end
   end
 
-  describe "/addresses/{address_hash}/beacon/deposits" do
-    if Application.compile_env(:explorer, :chain_type) == :ethereum do
+  if @chain_type == :ethereum do
+    describe "/addresses/{address_hash}/beacon/deposits" do
       test "get empty list on non-existing address", %{conn: conn} do
         address = build(:address)
 
@@ -5530,17 +5530,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
         request = get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits")
         assert response = json_response(request, 200)
 
-        request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits", response["next_page_params"])
+        request_2nd_page =
+          get(conn, "/api/v2/addresses/#{address.hash}/beacon/deposits", response["next_page_params"])
+
         assert response_2nd_page = json_response(request_2nd_page, 200)
 
         check_paginated_response(response, response_2nd_page, deposits)
       end
-    else
-      test "returns an error about chain type", %{conn: conn} do
-        request = get(conn, "/api/v2/addresses/address/beacon/deposits")
-        assert response = json_response(request, 404)
-        assert %{"message" => "Endpoint not available for current chain type"} = response
-      end
     end
   end
 
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
index c0edc4fcfea3..1f91a1164ca2 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
@@ -905,8 +905,8 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
     end
   end
 
-  describe "blocks/{block_hash_or_number}/beacon/deposits" do
-    if Application.compile_env(:explorer, :chain_type) == :ethereum do
+  if @chain_type == :ethereum do
+    describe "blocks/{block_hash_or_number}/beacon/deposits" do
       test "get 404 on non-existing block", %{conn: conn} do
         block = build(:block)
 
@@ -977,13 +977,6 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do
 
         check_paginated_response(response, response_2nd_page, deposits)
       end
-    else
-      test "returns an error about chain type", %{conn: conn} do
-        block = insert(:block)
-        request = get(conn, "/api/v2/blocks/#{block.hash}/beacon/deposits")
-        assert response = json_response(request, 404)
-        assert %{"message" => "Endpoint not available for current chain type"} = response
-      end
     end
   end
 
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs
index 62b6a509afbb..b29935eea3bf 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/ethereum/deposit_controller_test.exs
@@ -1,9 +1,10 @@
 defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do
   use BlockScoutWeb.ConnCase
+  use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type]
 
   alias Explorer.Repo
 
-  if Application.compile_env(:explorer, :chain_type) == :ethereum do
+  if @chain_type == :ethereum do
     describe "/beacon/deposits" do
       test "get empty list when no deposits exist", %{conn: conn} do
         request = get(conn, "/api/v2/beacon/deposits")
@@ -50,21 +51,5 @@ defmodule BlockScoutWeb.Api.V2.Ethereum.DepositControllerTest do
         assert %{"deposits_count" => ^deposits_count} = response
       end
     end
-  else
-    describe "/beacon/deposits" do
-      test "returns an error about chain type", %{conn: conn} do
-        request = get(conn, "/api/v2/beacon/deposits/count")
-        assert response = json_response(request, 404)
-        assert %{"message" => "Endpoint not available for current chain type"} = response
-      end
-    end
-
-    describe "/beacon/deposits/count" do
-      test "returns an error about chain type", %{conn: conn} do
-        request = get(conn, "/api/v2/beacon/deposits/count")
-        assert response = json_response(request, 404)
-        assert %{"message" => "Endpoint not available for current chain type"} = response
-      end
-    end
   end
 end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs
index 2e2400ca5057..8929cc44df22 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs
@@ -36,11 +36,6 @@ defmodule BlockScoutWeb.API.V2.OptimismControllerTest do
                  "next_page_params" => nil
                } = json_response(conn, 200)
       end
-    else
-      test "returns 404 in non optimism chain type", %{conn: conn} do
-        conn = get(conn, "/api/v2/optimism/interop/messages")
-        assert json_response(conn, 404) == %{"message" => "Endpoint not available for current chain type"}
-      end
     end
   end
 end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
index 795cdfcc184f..c11c5dba1754 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
@@ -2085,8 +2085,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
     end
   end
 
-  describe "transactions/{transaction_hash}/beacon/deposits" do
-    if Application.compile_env(:explorer, :chain_type) == :ethereum do
+  if @chain_type == :ethereum do
+    describe "transactions/{transaction_hash}/beacon/deposits" do
       test "get 404 on non-existing transaction", %{conn: conn} do
         transaction = build(:transaction)
 
@@ -2111,13 +2111,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
 
         check_paginated_response(response, response_2nd_page, deposits)
       end
-    else
-      test "returns an error about chain type", %{conn: conn} do
-        transaction = insert(:transaction)
-        request = get(conn, "/api/v2/transactions/#{transaction.hash}/beacon/deposits")
-        assert response = json_response(request, 404)
-        assert %{"message" => "Endpoint not available for current chain type"} = response
-      end
     end
   end
 

From 39510fce9355e11caa843ae3ace5d117e66278fe Mon Sep 17 00:00:00 2001
From: Fedor Ivanov 
Date: Tue, 7 Oct 2025 11:07:08 +0300
Subject: [PATCH 0377/1006] fix: enforce legacy query usage when sort by id
 (#13323)

---
 apps/explorer/lib/explorer/chain/smart_contract.ex | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex
index 9b9407285e27..2aee6a0f54c6 100644
--- a/apps/explorer/lib/explorer/chain/smart_contract.ex
+++ b/apps/explorer/lib/explorer/chain/smart_contract.ex
@@ -1455,19 +1455,23 @@ defmodule Explorer.Chain.SmartContract do
     # If no sorting options are provided, we sort by `:id` descending only. If
     # there are some sorting options supplied, we sort by `:hash` ascending as a
     # secondary key.
-    {sorting_options, default_sorting_options} =
+    {sorting_options, default_sorting_options, use_legacy_query?} =
       options
       |> Keyword.get(:sorting)
       |> case do
         nil ->
-          {[], [{:desc, :id, :smart_contract}]}
+          {[], [{:desc, :id, :smart_contract}], true}
 
         options ->
-          {options, [asc: :hash]}
+          {options, [asc: :hash], false}
       end
 
+    # We don't use alias so the mock of background_migrations_finished?/0 in
+    # tests is working properly
+    #
+    # credo:disable-for-lines:2 Credo.Check.Design.AliasUsage
     addresses_query =
-      if background_migrations_finished?() do
+      if Explorer.Chain.SmartContract.background_migrations_finished?() and not use_legacy_query? do
         verified_addresses_query(options)
       else
         # Legacy query approach - will be removed in future releases

From 1372e269c12e9033171361b99f885f73febd06dc Mon Sep 17 00:00:00 2001
From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com>
Date: Tue, 7 Oct 2025 12:12:36 +0400
Subject: [PATCH 0378/1006] fix: Improve timeout exception definition (#13286)

---
 apps/indexer/lib/indexer/block/catchup/fetcher.ex | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex
index a0bb96d027ac..d3c78a689c19 100644
--- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex
+++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex
@@ -254,11 +254,19 @@ defmodule Indexer.Block.Catchup.Fetcher do
   end
 
   defp timeout_exception?(%{message: message}) when is_binary(message) do
-    String.match?(message, ~r/due to a timeout/) or String.match?(message, ~r/due to user request/)
+    match_timeout_exception?(message)
+  end
+
+  defp timeout_exception?(%{postgres: %{message: message}}) when is_binary(message) do
+    match_timeout_exception?(message)
   end
 
   defp timeout_exception?(_exception), do: false
 
+  defp match_timeout_exception?(error_message) do
+    String.match?(error_message, ~r/due to a timeout/) or String.match?(error_message, ~r/due to user request/)
+  end
+
   defp add_range_to_massive_blocks(range) do
     clear_missing_ranges(range)
 

From 8e64e9065de203bd053479083bde82f8e378fed7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 11:19:04 +0300
Subject: [PATCH 0379/1006] chore(deps-dev): bump cowboy from 2.14.0 to 2.14.1
 (#13353)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index 5807f4870cfb..746246c453cb 100644
--- a/mix.lock
+++ b/mix.lock
@@ -29,7 +29,7 @@
   "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"},
   "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
   "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"},
-  "cowboy": {:hex, :cowboy, "2.14.0", "565dcf221ba99b1255b0adcec24d2d8dbe79e46ec79f30f8373cceadc6a41e2a", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "ea99769574550fe8a83225c752e8a62780a586770ef408816b82b6fe6d46476b"},
+  "cowboy": {:hex, :cowboy, "2.14.1", "031d338393e5a128a7de9613b4a0558aabc31b07082004abecb27cac790f5cd6", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e5310d5afd478ba90b1fed4fcdbc0230082b4510009505c586725c30b44e356f"},
   "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
   "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
   "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},

From 323c76ca77abe316ada112dd6c01e6b264fa9e34 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 7 Oct 2025 11:19:47 +0300
Subject: [PATCH 0380/1006] chore(deps): bump ex_aws from 2.5.11 to 2.6.0
 (#13352)

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 mix.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mix.lock b/mix.lock
index 746246c453cb..dd35b1daf77d 100644
--- a/mix.lock
+++ b/mix.lock
@@ -52,7 +52,7 @@
   "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"},
   "evision": {:hex, :evision, "0.2.14", "6005e44b38ea61a33109694c5b097d42e81c767723c55d0c230d9662d6c87987", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "630714395f2c242e488944b4c9c75a5de96cbb95d3cbb69f0525bdb3e0a88775"},
   "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"},
-  "ex_aws": {:hex, :ex_aws, "2.5.11", "5646eaad701485505b78246b0cd406fde9b1619459a86e85b53398810d3d0bd3", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7e16100ff93a118ef01c916d945969535cbe8d4ab6593fcf01d1cf854eb75345"},
+  "ex_aws": {:hex, :ex_aws, "2.6.0", "346e87e35e5df0b3c016a96fb30adf6001de102981a71648dfc3ce3ad04765af", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "30729ee9cbaacda674a4e4260d74206fa89bcd712267c4eaf42a0fc34592c0b3"},
   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"},
   "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"},
   "ex_cldr": {:hex, :ex_cldr, "2.43.2", "92370062f72d8efbeba3a7dc60a331fbb70f061cbc87041cb9f3c6701c6e2ddc", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "095137a7bd081166f77d23291b0649db2136ca013245cb73955fb0515031272a"},

From c8eb92f14bae8d7321d2bdb65ddbe5522043d99a Mon Sep 17 00:00:00 2001
From: GimluCom <79271880+GimluCom@users.noreply.github.com>
Date: Tue, 7 Oct 2025 10:33:16 +0200
Subject: [PATCH 0381/1006] fix: Check if CoinBalance Realtime fetcher is
 disabled (#13223)

---
 .../indexer/lib/indexer/fetcher/coin_balance/realtime.ex | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
index ebd055e2f150..2aa4677f8058 100644
--- a/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
+++ b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
@@ -8,6 +8,7 @@ defmodule Indexer.Fetcher.CoinBalance.Realtime do
 
   alias Explorer.Chain.{Block, Hash}
   alias Indexer.{BufferedTask, Tracer}
+  alias Indexer.Fetcher.CoinBalance.Realtime.Supervisor, as: CoinBalanceSupervisor
   alias Indexer.Fetcher.CoinBalance.Helper
 
   @behaviour BufferedTask
@@ -22,9 +23,13 @@ defmodule Indexer.Fetcher.CoinBalance.Realtime do
           %{required(:address_hash) => Hash.Address.t(), required(:block_number) => Block.block_number()}
         ]) :: :ok
   def async_fetch_balances(balance_fields) when is_list(balance_fields) do
-    entries = Enum.map(balance_fields, &Helper.entry/1)
+    if CoinBalanceSupervisor.disabled?() do
+      :ok
+    else
+      entries = Enum.map(balance_fields, &Helper.entry/1)
 
-    BufferedTask.buffer(__MODULE__, entries, true)
+      BufferedTask.buffer(__MODULE__, entries, true)
+    end
   end
 
   def child_spec(params) do

From f350feea4f68e0ea727634f61a21ab2ee0043d88 Mon Sep 17 00:00:00 2001
From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com>
Date: Tue, 7 Oct 2025 12:04:59 +0300
Subject: [PATCH 0382/1006] fix: set skip metadata only on contract errors
 (#12858)

---
 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex |  24 ++++
 .../lib/explorer/token/metadata_retriever.ex  |  37 ++---
 ...50725162308_reset_tokens_skip_metadata.exs |   9 ++
 .../token/metadata_retriever_test.exs         | 130 ++++++++++++------
 apps/indexer/lib/indexer/fetcher/token.ex     |  24 ++--
 .../lib/indexer/fetcher/token_balance.ex      |   6 +-
 6 files changed, 154 insertions(+), 76 deletions(-)
 create mode 100644 apps/explorer/priv/repo/migrations/20250725162308_reset_tokens_skip_metadata.exs

diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
index 53e0c29c9ef5..75db46fb9f8f 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
@@ -959,4 +959,28 @@ defmodule EthereumJSONRPC do
   defp address_correct?(_address) do
     false
   end
+
+  @doc """
+  Determines whether a given error represents a contract execution failure.
+
+  This function checks if an error indicates a contract failure by examining
+  specific error patterns. It supports both map-based and tuple-based error
+  formats. The function identifies two types of contract failures:
+  1. Errors with the atom `:unable_to_decode`
+  2. Binary errors containing the pattern "execution" followed by "revert"
+
+  ## Parameters
+  - `error`: The error to check. Can be a map with an `:error` key, a tuple
+    `{:error, reason}`, or any other value.
+
+  ## Returns
+  - `true` if the error represents a contract failure
+  - `false` otherwise
+  """
+  @spec contract_failure?(any()) :: boolean()
+  def contract_failure?(%{error: :unable_to_decode}), do: true
+  def contract_failure?(%{error: error}) when is_binary(error), do: String.match?(error, ~r/execution.*revert/)
+  def contract_failure?({:error, :unable_to_decode}), do: true
+  def contract_failure?({:error, error}) when is_binary(error), do: String.match?(error, ~r/execution.*revert/)
+  def contract_failure?(_), do: false
 end
diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex
index 98aa18aba1d3..13604eb6e26c 100644
--- a/apps/explorer/lib/explorer/token/metadata_retriever.ex
+++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex
@@ -5,7 +5,7 @@ defmodule Explorer.Token.MetadataRetriever do
 
   require Logger
 
-  alias Explorer.{HttpClient, MetadataURIValidator, Repo}
+  alias Explorer.{HttpClient, MetadataURIValidator}
   alias Explorer.Chain.{Hash, Token}
   alias Explorer.Helper, as: ExplorerHelper
   alias Explorer.SmartContract.Reader
@@ -170,8 +170,10 @@ defmodule Explorer.Token.MetadataRetriever do
   It will retry to fetch each function in the Smart Contract according to :token_functions_reader_max_retries
   configured in the application env case one of them raised error.
   """
-  @spec get_functions_of([Token.t()] | Token.t()) :: map() | {:ok, [map()]}
-  def get_functions_of(tokens) when is_list(tokens) do
+  @spec get_functions_of([Token.t()] | Token.t(), Keyword.t()) :: map() | {:ok, [map()]}
+  def get_functions_of(tokens, opts \\ [])
+
+  def get_functions_of(tokens, _opts) when is_list(tokens) do
     requests =
       tokens
       |> Enum.flat_map(fn token ->
@@ -225,23 +227,28 @@ defmodule Explorer.Token.MetadataRetriever do
     {:ok, processed_result}
   end
 
-  def get_functions_of(%Token{contract_address_hash: contract_address_hash, type: type}) do
-    base_metadata =
+  def get_functions_of(%Token{contract_address_hash: contract_address_hash, type: type}, opts) do
+    set_skip_metadata = Keyword.get(opts, :set_skip_metadata, false)
+
+    raw_metadata =
       contract_address_hash
       |> fetch_functions_from_contract(@contract_functions)
+
+    base_metadata =
+      raw_metadata
       |> format_contract_functions_result(contract_address_hash)
 
     metadata = try_to_fetch_erc_1155_name(base_metadata, contract_address_hash, type)
 
-    if metadata == %{} do
-      token_to_update =
-        Token
-        |> Repo.get_by(contract_address_hash: contract_address_hash)
-
-      set_skip_metadata(token_to_update)
+    if Enum.empty?(metadata) && set_skip_metadata do
+      Map.put(
+        metadata,
+        :skip_metadata,
+        Enum.all?(raw_metadata, fn {_key, value} -> EthereumJSONRPC.contract_failure?(value) end)
+      )
+    else
+      metadata
     end
-
-    metadata
   end
 
   defp try_to_fetch_erc_1155_name(base_metadata, contract_address_hash, token_type) do
@@ -307,10 +314,6 @@ defmodule Explorer.Token.MetadataRetriever do
     other
   end
 
-  defp set_skip_metadata(token_to_update) do
-    Token.update(token_to_update, %{skip_metadata: true})
-  end
-
   def get_total_supply_of(contract_address_hash) when is_binary(contract_address_hash) do
     contract_address_hash
     |> fetch_functions_from_contract(@total_supply_function)
diff --git a/apps/explorer/priv/repo/migrations/20250725162308_reset_tokens_skip_metadata.exs b/apps/explorer/priv/repo/migrations/20250725162308_reset_tokens_skip_metadata.exs
new file mode 100644
index 000000000000..456638dd0c98
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20250725162308_reset_tokens_skip_metadata.exs
@@ -0,0 +1,9 @@
+defmodule Explorer.Repo.Migrations.ResetTokensSkipMetadata do
+  use Ecto.Migration
+
+  def change do
+    execute("""
+    UPDATE tokens SET skip_metadata = null WHERE skip_metadata IS TRUE AND decimals IS NULL AND name IS NULL AND symbol IS NULL
+    """)
+  end
+end
diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs
index ce8300e85529..bedb9acc8942 100644
--- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs
+++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs
@@ -534,53 +534,99 @@ defmodule Explorer.Token.MetadataRetrieverTest do
 
       on_exit(fn -> Application.put_env(:explorer, :token_functions_reader_max_retries, original) end)
     end
-  end
 
-  test "returns name and symbol when they are bytes32" do
-    token = insert(:token, contract_address: build(:contract_address))
+    test "returns name and symbol when they are bytes32" do
+      token = insert(:token, contract_address: build(:contract_address))
 
-    expect(
-      EthereumJSONRPC.Mox,
-      :json_rpc,
-      1,
-      fn requests, _opts ->
-        {:ok,
-         Enum.map(requests, fn
-           %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
-             %{
-               id: id,
-               result: "0x0000000000000000000000000000000000000000000000000000000000000012"
-             }
+      expect(
+        EthereumJSONRPC.Mox,
+        :json_rpc,
+        1,
+        fn requests, _opts ->
+          {:ok,
+           Enum.map(requests, fn
+             %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 result: "0x0000000000000000000000000000000000000000000000000000000000000012"
+               }
 
-           %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
-             %{
-               id: id,
-               result: "0x4d616b6572000000000000000000000000000000000000000000000000000000"
-             }
+             %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 result: "0x4d616b6572000000000000000000000000000000000000000000000000000000"
+               }
 
-           %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
-             %{
-               id: id,
-               result: "0x4d4b520000000000000000000000000000000000000000000000000000000000"
-             }
+             %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 result: "0x4d4b520000000000000000000000000000000000000000000000000000000000"
+               }
 
-           %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
-             %{
-               id: id,
-               result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
-             }
-         end)}
-      end
-    )
-
-    expected = %{
-      decimals: 18,
-      name: "Maker",
-      symbol: "MKR",
-      total_supply: 1_000_000_000_000_000_000_000_000
-    }
-
-    assert MetadataRetriever.get_functions_of(token) == expected
+             %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000"
+               }
+           end)}
+        end
+      )
+
+      expected = %{
+        decimals: 18,
+        name: "Maker",
+        symbol: "MKR",
+        total_supply: 1_000_000_000_000_000_000_000_000
+      }
+
+      assert MetadataRetriever.get_functions_of(token) == expected
+    end
+
+    test "sets skip_metadata if contract does not have metadata" do
+      token = insert(:token, contract_address: build(:contract_address))
+
+      expect(
+        EthereumJSONRPC.Mox,
+        :json_rpc,
+        3,
+        fn requests, _opts ->
+          {:ok,
+           Enum.map(requests, fn
+             %{id: id, method: "eth_call", params: [%{data: _, to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 error: %{code: -32015, data: "something", message: "execution reverted"},
+                 jsonrpc: "2.0"
+               }
+           end)}
+        end
+      )
+
+      assert MetadataRetriever.get_functions_of(token, set_skip_metadata: true) == %{skip_metadata: true}
+    end
+
+    test "does not set skip_metadata if errors are not contract related" do
+      token = insert(:token, contract_address: build(:contract_address))
+
+      expect(
+        EthereumJSONRPC.Mox,
+        :json_rpc,
+        3,
+        fn requests, _opts ->
+          {:ok,
+           Enum.map(requests, fn
+             %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
+               %{
+                 id: id,
+                 error: %{code: -32015, data: "something", message: "network error"},
+                 jsonrpc: "2.0"
+               }
+           end)}
+        end
+      )
+
+      assert MetadataRetriever.get_functions_of(token, set_skip_metadata: true) == %{skip_metadata: false}
+    end
   end
 
   describe "fetch_json/4" do
diff --git a/apps/indexer/lib/indexer/fetcher/token.ex b/apps/indexer/lib/indexer/fetcher/token.ex
index bcae7ca34218..63c618ca37a4 100644
--- a/apps/indexer/lib/indexer/fetcher/token.ex
+++ b/apps/indexer/lib/indexer/fetcher/token.ex
@@ -67,22 +67,22 @@ defmodule Indexer.Fetcher.Token do
   end
 
   defp catalog_token(token) do
-    token_params =
-      token
-      |> MetadataRetriever.get_functions_of()
-      |> (&if(&1 == %{}, do: &1, else: Map.put(&1, :cataloged, true))).()
+    token
+    |> MetadataRetriever.get_functions_of(set_skip_metadata: true)
+    |> case do
+      %{skip_metadata: false} ->
+        :ok
 
-    {:ok, _} = Token.update(token, token_params)
+      token_params ->
+        data_for_multichain = MultichainSearch.prepare_token_metadata_for_queue(token, token_params)
 
-    if Map.get(token_params, :cataloged) do
-      data_for_multichain = MultichainSearch.prepare_token_metadata_for_queue(token, token_params)
+        %{}
+        |> Map.put(token.contract_address_hash.bytes, data_for_multichain)
+        |> MultichainSearch.send_token_info_to_queue(:metadata)
 
-      %{}
-      |> Map.put(token.contract_address_hash.bytes, data_for_multichain)
-      |> MultichainSearch.send_token_info_to_queue(:metadata)
+        {:ok, _} = Token.update(token, Map.put(token_params, :cataloged, true))
+        :ok
     end
-
-    :ok
   end
 
   defp defaults do
diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex
index a6f291c7d5a5..ad45a62a8170 100644
--- a/apps/indexer/lib/indexer/fetcher/token_balance.ex
+++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex
@@ -163,11 +163,7 @@ defmodule Indexer.Fetcher.TokenBalance do
 
   defp handle_missing_balance_of_tokens(failed_token_balances) do
     {missing_balance_of_balances, other_failed_balances} =
-      Enum.split_with(failed_token_balances, fn
-        %{error: :unable_to_decode} -> true
-        %{error: error} when is_binary(error) -> String.match?(error, ~r/execution.*revert/)
-        _ -> false
-      end)
+      Enum.split_with(failed_token_balances, &EthereumJSONRPC.contract_failure?/1)
 
     MissingBalanceOfToken.insert_from_params(missing_balance_of_balances)
 

From 4ad5d56365a39479ac5f31dbf56ea1001acf86e2 Mon Sep 17 00:00:00 2001
From: Rim Rakhimov 
Date: Tue, 7 Oct 2025 13:40:31 +0400
Subject: [PATCH 0383/1006] perf: optimize token balance synchronous import
 steps (#13217)

---
 .../explorer/chain/import/runner/blocks.ex    | 111 +++++++----------
 apps/explorer/lib/explorer/query_helper.ex    |  59 +++++++++
 .../chain/import/runner/blocks_test.exs       | 113 ++++++++++++++++++
 3 files changed, 213 insertions(+), 70 deletions(-)

diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex
index e107a2453507..c3ab221cd421 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex
@@ -7,6 +7,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
 
   import Ecto.Query, only: [dynamic: 1, dynamic: 2, from: 2, where: 3, subquery: 1]
   import Explorer.Chain.Import.Runner.Helper, only: [chain_type_dependent_import: 3]
+  import Explorer.QueryHelper, only: [select_ctid: 1, join_on_ctid: 2]
 
   alias Ecto.{Changeset, Multi, Repo}
 
@@ -720,7 +721,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
     ordered_query =
       from(tb in Address.TokenBalance,
         where: tb.block_number in ^non_consensus_block_numbers,
-        select: map(tb, [:address_hash, :token_contract_address_hash, :token_id, :block_number]),
+        select: select_ctid(tb),
         # Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
         order_by: [
           tb.token_contract_address_hash,
@@ -735,14 +736,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
       from(tb in Address.TokenBalance,
         select: map(tb, [:address_hash, :token_contract_address_hash, :block_number]),
         inner_join: ordered_address_token_balance in subquery(ordered_query),
-        on:
-          ordered_address_token_balance.address_hash == tb.address_hash and
-            ordered_address_token_balance.token_contract_address_hash ==
-              tb.token_contract_address_hash and
-            ((is_nil(ordered_address_token_balance.token_id) and is_nil(tb.token_id)) or
-               (ordered_address_token_balance.token_id == tb.token_id and
-                  not is_nil(ordered_address_token_balance.token_id) and not is_nil(tb.token_id))) and
-            ordered_address_token_balance.block_number == tb.block_number
+        on: join_on_ctid(tb, ordered_address_token_balance)
       )
 
     try do
@@ -763,7 +757,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
     ordered_query =
       from(ctb in Address.CurrentTokenBalance,
         where: ctb.block_number in ^non_consensus_block_numbers,
-        select: map(ctb, [:address_hash, :token_contract_address_hash, :token_id]),
+        select: select_ctid(ctb),
         # Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
         order_by: [
           ctb.token_contract_address_hash,
@@ -786,12 +780,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
             :value
           ]),
         inner_join: ordered_address_current_token_balance in subquery(ordered_query),
-        on:
-          ordered_address_current_token_balance.address_hash == ctb.address_hash and
-            ordered_address_current_token_balance.token_contract_address_hash == ctb.token_contract_address_hash and
-            ((is_nil(ordered_address_current_token_balance.token_id) and is_nil(ctb.token_id)) or
-               (ordered_address_current_token_balance.token_id == ctb.token_id and
-                  not is_nil(ordered_address_current_token_balance.token_id) and not is_nil(ctb.token_id)))
+        on: join_on_ctid(ctb, ordered_address_current_token_balance)
       )
 
     try do
@@ -812,43 +801,37 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
          %{timeout: timeout} = options
        )
        when is_list(deleted_address_current_token_balances) do
-    final_query = derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances)
-
-    new_current_token_balance_query =
-      from(new_current_token_balance in subquery(final_query),
-        inner_join: tb in Address.TokenBalance,
-        on:
-          tb.address_hash == new_current_token_balance.address_hash and
-            tb.token_contract_address_hash == new_current_token_balance.token_contract_address_hash and
-            ((is_nil(tb.token_id) and is_nil(new_current_token_balance.token_id)) or
-               (tb.token_id == new_current_token_balance.token_id and
-                  not is_nil(tb.token_id) and not is_nil(new_current_token_balance.token_id))) and
-            tb.block_number == new_current_token_balance.block_number,
+    base_query =
+      from(
+        tb in Address.TokenBalance,
         select: %{
-          address_hash: new_current_token_balance.address_hash,
-          token_contract_address_hash: new_current_token_balance.token_contract_address_hash,
-          token_id: new_current_token_balance.token_id,
+          address_hash: tb.address_hash,
+          token_contract_address_hash: tb.token_contract_address_hash,
+          token_id: tb.token_id,
+          block_number: tb.block_number,
           token_type: tb.token_type,
-          block_number: new_current_token_balance.block_number,
           value: tb.value,
-          value_fetched_at: tb.value_fetched_at,
-          inserted_at: over(min(tb.inserted_at), :w),
-          updated_at: over(max(tb.updated_at), :w)
+          value_fetched_at: tb.value_fetched_at
         },
-        windows: [
-          w: [partition_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]]
-        ]
+        distinct: [tb.address_hash, tb.token_contract_address_hash, fragment("COALESCE(?, -1::numeric)", tb.token_id)],
+        order_by: [desc: tb.block_number]
       )
 
-    current_token_balance =
-      new_current_token_balance_query
+    query =
+      derive_address_current_token_balances_to_deleted_entries_only_query(
+        base_query,
+        deleted_address_current_token_balances
+      )
+
+    current_token_balances =
+      query
       |> repo.all()
 
     timestamps = Import.timestamps()
 
     result =
       CurrentTokenBalances.insert_changes_list_with_and_without_token_id(
-        current_token_balance,
+        current_token_balances,
         repo,
         timestamps,
         timeout,
@@ -1039,41 +1022,29 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
     )
   end
 
-  defp derive_address_current_token_balances_grouped_query(deleted_address_current_token_balances) do
-    initial_query =
-      from(tb in Address.TokenBalance,
-        select: %{
-          address_hash: tb.address_hash,
-          token_contract_address_hash: tb.token_contract_address_hash,
-          token_id: tb.token_id,
-          block_number: max(tb.block_number)
-        },
-        group_by: [tb.address_hash, tb.token_contract_address_hash, tb.token_id]
-      )
+  defp derive_address_current_token_balances_to_deleted_entries_only_query(
+         base_query,
+         deleted_address_current_token_balances
+       ) do
+    Enum.reduce(
+      deleted_address_current_token_balances,
+      base_query,
+      fn address_current_token_balance, accumulated_query ->
+        %{
+          address_hash: address_hash,
+          token_contract_address_hash: token_contract_address_hash,
+          token_id: token_id
+        } = address_current_token_balance
 
-    Enum.reduce(deleted_address_current_token_balances, initial_query, fn %{
-                                                                            address_hash: address_hash,
-                                                                            token_contract_address_hash:
-                                                                              token_contract_address_hash,
-                                                                            token_id: token_id
-                                                                          },
-                                                                          acc_query ->
-      if token_id do
-        from(tb in acc_query,
-          or_where:
-            tb.address_hash == ^address_hash and
-              tb.token_contract_address_hash == ^token_contract_address_hash and
-              tb.token_id == ^token_id
-        )
-      else
-        from(tb in acc_query,
+        from(
+          tb in accumulated_query,
           or_where:
             tb.address_hash == ^address_hash and
               tb.token_contract_address_hash == ^token_contract_address_hash and
-              is_nil(tb.token_id)
+              fragment("coalesce(?.\"token_id\", -1::numeric) = coalesce(?::numeric, -1::numeric)", tb, ^token_id)
         )
       end
-    end)
+    )
   end
 
   # `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is
diff --git a/apps/explorer/lib/explorer/query_helper.ex b/apps/explorer/lib/explorer/query_helper.ex
index d4c5e96fe8f6..1a320b8d62ff 100644
--- a/apps/explorer/lib/explorer/query_helper.ex
+++ b/apps/explorer/lib/explorer/query_helper.ex
@@ -38,4 +38,63 @@ defmodule Explorer.QueryHelper do
     |> Code.eval_quoted()
     |> elem(0)
   end
+
+  @doc """
+  A macro generating a fragment that selects CTID column.
+
+  CTID - is a system column representing the physical location
+  of the row version within its table.
+
+  The macro is supposed to be used in `SELECT FOR UPDATE` part of update and delete statements
+  where corresponding rows are locked before modification in order to prevent deadlocks
+  (see docs: sharelock.md). Should be used along with `join_on_ctid/2`.
+
+  ## Example
+
+  ```
+    ordered_query =
+      from(table in Table,
+        select: select_ctid(table),
+        # Enforce Table ShareLocks order
+        order_by: [
+          table.column_1,
+          table.column_2,
+        ],
+        lock: "FOR UPDATE"
+      )
+
+    query =
+      from(table in Table,
+        inner_join: ordered_table in subquery(ordered_query),
+        on: join_on_ctid(table, ordered_table)
+      )
+    Repo.delete_all(query)
+  ```
+
+  Will be transformed to such SQL:
+  ```
+    DELETE
+    FROM "table" AS t0 USING (SELECT st0."ctid" AS "ctid"
+                              FROM "table" AS st0
+                              ORDER BY st0."column_1", st0."column_2" FOR UPDATE) AS s1
+    WHERE (t0."ctid" = s1."ctid");
+  ```
+  """
+  defmacro select_ctid(table_binding) do
+    quote do
+      %{ctid: fragment(~s(?."ctid"), unquote(table_binding))}
+    end
+  end
+
+  @doc """
+  A macro generating a fragment that joins 2 tables on CTID column.
+
+  It is supposed to be used as an `:on` option for joins.
+  See `select_ctid/1` for more details on usage.
+  """
+  defmacro join_on_ctid(first_table_binding, second_table_binding) do
+    quote do
+      fragment(~s(?."ctid" = ?."ctid"), unquote(first_table_binding), unquote(second_table_binding))
+    end
+  end
 end
diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
index b24ea5623968..60251bda3e78 100644
--- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
+++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
@@ -234,6 +234,119 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
                )
     end
 
+    # The test checks that `derive_address_current_token_balances` function derives balances only for removed block numbers,
+    # and not for all possible (address_hash, token_contract_address_hash) pairs, which would be very inefficient.
+    #
+    # Here, we intentionally insert an Address.TokenBalance entry but omit corresponding Address.CurrentTokenBalance
+    # for the `_to_not_derive_address_hash`, as otherwise even if that Address.CurrentTokenBalance is derived it
+    # still won't be inserted and returned due to on_conflict statement. In production it should be impossible
+    # for the Address.CurrentTokenBalance to be missing if corresponding Address.TokenBalance exists.
+    test "derive_address_current_token_balances only updates rows removed by delete_address_current_token_balances",
+         %{consensus_block: %{number: block_number} = block, options: options} do
+      # Setup
+
+      token = insert(:token)
+      token_contract_address_hash = token.contract_address_hash
+
+      %Address.TokenBalance{
+        address_hash: _to_not_derive_address_hash
+      } =
+        insert(:token_balance,
+          token_contract_address_hash: token_contract_address_hash,
+          block_number: block_number - 1,
+          value: 1
+        )
+
+      %Address{hash: to_derive_address_hash} =
+        insert_address_with_token_balances(%{
+          previous: %{value: 1},
+          current: %{block_number: block_number, value: 2},
+          token_contract_address_hash: token_contract_address_hash
+        })
+
+      assert count(Address.TokenBalance) == 3
+      assert count(Address.CurrentTokenBalance) == 1
+
+      # Test and assert
+
+      insert(:block, number: block_number, consensus: true)
+      previous_block_number = block_number - 1
+
+      assert {:ok,
+              %{
+                delete_address_current_token_balances: [
+                  %{
+                    address_hash: ^to_derive_address_hash,
+                    token_contract_address_hash: ^token_contract_address_hash
+                  }
+                ],
+                delete_address_token_balances: [
+                  %{
+                    address_hash: ^to_derive_address_hash,
+                    token_contract_address_hash: ^token_contract_address_hash,
+                    block_number: ^block_number
+                  }
+                ],
+                # the main assertion: should not be any other derived values
+                derive_address_current_token_balances: [
+                  %{
+                    address_hash: ^to_derive_address_hash,
+                    token_contract_address_hash: ^token_contract_address_hash,
+                    block_number: ^previous_block_number
+                  }
+                ],
+                blocks_update_token_holder_counts: []
+              }} = run_block_consensus_change(block, true, options)
+
+      assert count(Address.TokenBalance) == 2
+      assert count(Address.CurrentTokenBalance) == 1
+    end
+
+    test "derive_address_current_token_balances derives balances from latest token balances",
+         %{consensus_block: %{number: block_number} = block, options: options} do
+      # Setup
+
+      token = insert(:token)
+      token_contract_address_hash = token.contract_address_hash
+
+      address =
+        insert_address_with_token_balances(%{
+          previous: %{value: 2},
+          current: %{block_number: block_number, value: 3},
+          token_contract_address_hash: token_contract_address_hash
+        })
+
+      insert(:token_balance,
+        address: address,
+        token_contract_address_hash: token_contract_address_hash,
+        block_number: block_number - 2,
+        value: 1
+      )
+
+      assert count(Address.TokenBalance) == 3
+      assert count(Address.CurrentTokenBalance) == 1
+
+      initial_value = Decimal.new("3")
+
+      assert %Address.CurrentTokenBalance{block_number: ^block_number, value: ^initial_value} =
+               Repo.one(Address.CurrentTokenBalance)
+
+      # Test and assert
+
+      insert(:block, number: block_number, consensus: true)
+
+      run_block_consensus_change(block, true, options)
+
+      assert count(Address.TokenBalance) == 2
+      assert count(Address.CurrentTokenBalance) == 1
+
+      previous_block_number = block_number - 1
+      expected_value = Decimal.new("2")
+
+      assert %Address.CurrentTokenBalance{block_number: ^previous_block_number, value: ^expected_value} =
+               Repo.one(Address.CurrentTokenBalance)
+    end
+
     test "a non-holder reverting to a holder increases the holder_count",
          %{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do
       token = insert(:token)

From 84865221327e5a8285e94c4beff4e59ce8df3eff Mon Sep 17 00:00:00 2001
From: Victor Baranov 
Date: Tue, 7 Oct 2025 13:27:38 +0300
Subject: [PATCH 0384/1006] Process credo report

---
 apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
index 2aa4677f8058..91c6611d4eac 100644
--- a/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
+++ b/apps/indexer/lib/indexer/fetcher/coin_balance/realtime.ex
@@ -8,8 +8,8 @@ defmodule Indexer.Fetcher.CoinBalance.Realtime do
 
   alias Explorer.Chain.{Block, Hash}
   alias Indexer.{BufferedTask, Tracer}
-  alias Indexer.Fetcher.CoinBalance.Realtime.Supervisor, as: CoinBalanceSupervisor
   alias Indexer.Fetcher.CoinBalance.Helper
+  alias Indexer.Fetcher.CoinBalance.Realtime.Supervisor, as: CoinBalanceSupervisor
 
   @behaviour BufferedTask
 

From 89d10e0c4a2b6fad71655612e618fe42750e34b7 Mon Sep 17 00:00:00 2001
From: varasev <33550681+varasev@users.noreply.github.com>
Date: Tue, 7 Oct 2025 19:19:13 +0400
Subject: [PATCH 0385/1006] feat: OP operator fee indexing (#13139)

Co-authored-by: POA <33550681+poa@users.noreply.github.com>
Co-authored-by: Victor Baranov 
---
 .../schemas/api/v2/transaction.ex             |   3 +-
 .../views/api/v2/optimism_view.ex             |  17 ++-
 .../lib/ethereum_jsonrpc/receipt.ex           |  27 +++-
 .../lib/ethereum_jsonrpc/receipts.ex          |   4 +-
 apps/explorer/lib/explorer/application.ex     |   4 +
 .../chain/import/runner/transactions.ex       |   8 +-
 .../lib/explorer/chain/transaction.ex         |  98 +++++++++++-
 ...ransactions_operator_fee_constant_index.ex |  66 ++++++++
 .../20250901063610_op_operator_fee.exs        |  17 +++
 apps/indexer/lib/indexer/buffered_task.ex     |   7 +
 .../indexer/fetcher/optimism/operator_fee.ex  | 144 ++++++++++++++++++
 apps/indexer/lib/indexer/supervisor.ex        |   2 +
 config/runtime.exs                            |  23 ++-
 docker-compose/envs/common-blockscout.env     |   6 +
 14 files changed, 406 insertions(+), 20 deletions(-)
 create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_transactions_operator_fee_constant_index.ex
 create mode 100644 apps/explorer/priv/optimism/migrations/20250901063610_op_operator_fee.exs
 create mode 100644 apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex

diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
index c017bda3eddf..f34b9ff5e8f2 100644
--- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
+++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex
@@ -200,7 +200,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do
               },
               required: [:nonce, :status, :sender_address_hash, :target_address_hash, :payload],
               additionalProperties: false
-            }
+            },
+            operator_fee: General.IntegerString
           }
         )
 
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
index ba5211036e05..4e83afcaa4fe 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex
@@ -439,7 +439,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
     - `transaction`: transaction structure containing extra Optimism-related info.
 
     ## Returns
-    An extended map containing `l1_*` and `op_withdrawals` items related to Optimism.
+    An extended map containing `l1_*`, `op_withdrawals`, `op_interop_messages`, and other items related to Optimism.
   """
   @spec extend_transaction_json_response(map(), %{
           :__struct__ => Explorer.Chain.Transaction,
@@ -462,14 +462,15 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
   end
 
   # Extends the json output for a transaction adding Optimism-related info to the output
-  # (such as related withdrawals, interop messages).
+  # (such as related withdrawals, operator fee, interop messages).
   #
   # ## Parameters
   # - `out_json`: A map defining output json which will be extended.
   # - `transaction`: transaction structure containing necessary data for the OP fields.
   #
   # ## Returns
-  # - An extended map containing `op_withdrawals`, `op_interop_messages` (optional).
+  # - An extended map containing `op_withdrawals`, `operator_fee` (optional), `op_interop_messages` (optional).
+  #   If the operator fee is zero, it's not presented in the resulting map.
   @spec add_optimism_fields(map(), Transaction.t()) :: map()
   defp add_optimism_fields(out_json, transaction) do
     portal_contract_address_hash = Withdrawal.portal_contract_address()
@@ -500,6 +501,16 @@ defmodule BlockScoutWeb.API.V2.OptimismView do
 
     out_json = Map.put(out_json, "op_withdrawals", withdrawals)
 
+    operator_fee = Transaction.operator_fee(transaction)
+
+    # credo:disable-for-next-line
+    out_json =
+      if Decimal.gt?(operator_fee, Decimal.new(0)) do
+        Map.put(out_json, "operator_fee", operator_fee)
+      else
+        out_json
+      end
+
     if interop_messages == [] do
       out_json
     else
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
index 6a2298478e46..23f0bbd0f010 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex
@@ -24,7 +24,9 @@ defmodule EthereumJSONRPC.Receipt do
                              l1_fee: non_neg_integer(),
                              l1_fee_scalar: non_neg_integer(),
                              l1_gas_price: non_neg_integer(),
-                             l1_gas_used: non_neg_integer()
+                             l1_gas_used: non_neg_integer(),
+                             operator_fee_scalar: non_neg_integer() | nil,
+                             operator_fee_constant: non_neg_integer() | nil
                            ]
                          )
 
@@ -127,7 +129,9 @@ defmodule EthereumJSONRPC.Receipt do
           l1_fee: 0,\
           l1_fee_scalar: 0,\
           l1_gas_price: 0,\
-          l1_gas_used: 0\
+          l1_gas_used: 0,\
+          operator_fee_scalar: nil,\
+          operator_fee_constant: nil\
       """
     :scroll -> """
           l1_fee: 0\
@@ -179,7 +183,9 @@ defmodule EthereumJSONRPC.Receipt do
           l1_fee: 0,\
           l1_fee_scalar: 0,\
           l1_gas_price: 0,\
-          l1_gas_used: 0\
+          l1_gas_used: 0,\
+          operator_fee_scalar: nil,\
+          operator_fee_constant: nil\
       """
     :scroll -> """
           l1_fee: 0\
@@ -243,12 +249,23 @@ defmodule EthereumJSONRPC.Receipt do
 
     :optimism ->
       defp chain_type_fields(params, elixir) do
+        isthmus_timestamp_l2 = Application.get_env(:indexer, Indexer.Fetcher.Optimism)[:isthmus_timestamp_l2]
+
+        {operator_fee_scalar_default, operator_fee_constant_default} =
+          if is_nil(isthmus_timestamp_l2) do
+            {nil, nil}
+          else
+            {0, 0}
+          end
+
         params
         |> Map.merge(%{
           l1_fee: Map.get(elixir, "l1Fee", 0),
           l1_fee_scalar: Map.get(elixir, "l1FeeScalar", 0),
           l1_gas_price: Map.get(elixir, "l1GasPrice", 0),
-          l1_gas_used: Map.get(elixir, "l1GasUsed", 0)
+          l1_gas_used: Map.get(elixir, "l1GasUsed", 0),
+          operator_fee_scalar: Map.get(elixir, "operatorFeeScalar", operator_fee_scalar_default),
+          operator_fee_constant: Map.get(elixir, "operatorFeeConstant", operator_fee_constant_default)
         })
       end
 
@@ -392,7 +409,7 @@ defmodule EthereumJSONRPC.Receipt do
   defp entry_to_elixir({key, quantity})
        when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex blobGasUsed
                       blobGasPrice l1Fee l1GasPrice l1GasUsed effectiveGasPrice gasUsedForL1
-                      l1BlobBaseFeeScalar l1BlobBaseFee l1BaseFeeScalar) do
+                      l1BlobBaseFeeScalar l1BlobBaseFee l1BaseFeeScalar operatorFeeScalar operatorFeeConstant) do
     result =
       if is_nil(quantity) do
         nil
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
index 1f104c181458..79f8509bb23c 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
@@ -111,7 +111,9 @@ defmodule EthereumJSONRPC.Receipts do
         l1_fee: 0,\
         l1_fee_scalar: 0,\
         l1_gas_price: 0,\
-        l1_gas_used: 0\
+        l1_gas_used: 0,\
+        operator_fee_scalar: nil,\
+        operator_fee_constant: nil\
       """
     :scroll -> """
         l1_fee: 0\
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index 757c61c389ae..7c7c921cb176 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -308,6 +308,10 @@ defmodule Explorer.Application do
           Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex,
           :indexer
         ),
+        configure_mode_dependent_process(
+          Explorer.Migrator.HeavyDbIndexOperation.CreateTransactionsOperatorFeeConstantIndex,
+          :indexer
+        ),
         Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync),
         configure(Explorer.Chain.Fetcher.AddressesBlacklist),
         Explorer.Migrator.SwitchPendingOperations,
diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
index 7e9f4d2c34f3..0eb4dda8cdc6 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex
@@ -284,6 +284,8 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
               l1_gas_used: fragment("EXCLUDED.l1_gas_used"),
               l1_transaction_origin: fragment("EXCLUDED.l1_transaction_origin"),
               l1_block_number: fragment("EXCLUDED.l1_block_number"),
+              operator_fee_scalar: fragment("EXCLUDED.operator_fee_scalar"),
+              operator_fee_constant: fragment("EXCLUDED.operator_fee_constant"),
               # Don't update `hash` as it is part of the primary key and used for the conflict target
               inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at),
               updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at)
@@ -291,7 +293,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
           ],
           where:
             fragment(
-              "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_transaction_origin, EXCLUDED.l1_block_number) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+              "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_transaction_origin, EXCLUDED.l1_block_number, EXCLUDED.operator_fee_scalar, EXCLUDED.operator_fee_constant) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
               transaction.block_hash,
               transaction.block_number,
               transaction.block_consensus,
@@ -322,7 +324,9 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
               transaction.l1_gas_price,
               transaction.l1_gas_used,
               transaction.l1_transaction_origin,
-              transaction.l1_block_number
+              transaction.l1_block_number,
+              transaction.operator_fee_scalar,
+              transaction.operator_fee_constant
             )
         )
       end
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index fa227232c07e..c4e57f136f61 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -49,6 +49,8 @@ defmodule Explorer.Chain.Transaction.Schema do
                               field(:l1_gas_used, :decimal)
                               field(:l1_transaction_origin, Hash.Full)
                               field(:l1_block_number, :integer)
+                              field(:operator_fee_scalar, :decimal)
+                              field(:operator_fee_constant, :decimal)
                             end,
                             2
                           )
@@ -341,7 +343,7 @@ defmodule Explorer.Chain.Transaction do
 
   @chain_type_optional_attrs (case @chain_type do
                                 :optimism ->
-                                  ~w(l1_fee l1_fee_scalar l1_gas_price l1_gas_used l1_transaction_origin l1_block_number)a
+                                  ~w(l1_fee l1_fee_scalar l1_gas_price l1_gas_used l1_transaction_origin l1_block_number operator_fee_scalar operator_fee_constant)a
 
                                 :scroll ->
                                   ~w(l1_fee queue_index)a
@@ -522,6 +524,8 @@ defmodule Explorer.Chain.Transaction do
    * `wrapped_r` - R field of the signature from the `wrapped` field (used by Suave)
    * `wrapped_s` - S field of the signature from the `wrapped` field (used by Suave)
    * `wrapped_hash` - hash from the `wrapped` field (used by Suave)
+   * `operator_fee_scalar` - operatorFeeScalar is a uint32 scalar set by a chain operator (used by some OP chains)
+   * `operator_fee_constant` - operatorFeeConstant is a uint64 constant set by a chain operator (used by some OP chains)
   """
   Explorer.Chain.Transaction.Schema.generate()
 
@@ -1429,6 +1433,39 @@ defmodule Explorer.Chain.Transaction do
     )
   end
 
+  @doc """
+  Streams a batch of transactions without OP operator fee for which the fee needs to be defined.
+
+  This function selects specific fields from the transaction records and applies a reducer function to each entry in the stream, accumulating the result.
+
+  ## Parameters
+  - `initial`: The initial accumulator value.
+  - `reducer`: A function that takes an entry and the current accumulator, returning the updated accumulator.
+  - `start_timestamp`: A timestamp starting from which the transactions should be scanned.
+
+  ## Returns
+  - `{:ok, accumulator}`: A tuple containing `:ok` and the final accumulator after processing the stream.
+  """
+  @spec stream_transactions_without_operator_fee(
+          initial :: accumulator,
+          reducer :: (entry :: Hash.t(), accumulator -> accumulator),
+          start_timestamp :: non_neg_integer()
+        ) :: {:ok, accumulator}
+        when accumulator: term()
+  def stream_transactions_without_operator_fee(initial, reducer, start_timestamp) when is_function(reducer, 2) do
+    limit = Application.get_env(:indexer, Indexer.Fetcher.Optimism.OperatorFee)[:init_limit]
+    start_datetime = DateTime.from_unix!(start_timestamp)
+
+    __MODULE__
+    |> select([t], t.hash)
+    |> where(
+      [t],
+      t.block_timestamp >= ^start_datetime and t.block_consensus == true and is_nil(t.operator_fee_constant)
+    )
+    |> limit(^limit)
+    |> Repo.stream_reduce(initial, reducer)
+  end
+
   @doc """
   Returns true if the transaction is a Rootstock REMASC transaction.
   """
@@ -1985,8 +2022,8 @@ defmodule Explorer.Chain.Transaction do
   @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil}
   def fee(%Transaction{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil}
 
-  def fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil} = transaction, unit) do
-    {:maximum, fee_calc(transaction, gas_price, gas, unit)}
+  def fee(%Transaction{gas: gas, gas_price: _gas_price, gas_used: nil} = transaction, unit) do
+    {:maximum, fee_calc(transaction, gas, unit)}
   end
 
   if @chain_type == :optimism do
@@ -2000,24 +2037,71 @@ defmodule Explorer.Chain.Transaction do
     end
   end
 
-  def fee(%Transaction{gas_price: gas_price, gas_used: gas_used} = transaction, unit) do
-    {:actual, fee_calc(transaction, gas_price, gas_used, unit)}
+  def fee(%Transaction{gas_price: _gas_price, gas_used: gas_used} = transaction, unit) do
+    {:actual, fee_calc(transaction, gas_used, unit)}
   end
 
-  defp fee_calc(transaction, gas_price, gas_used, unit) do
+  # Internal function calculating a total fee of the transaction as follows:
+  #   total_fee = l2_fee + l1_fee + operator_fee
+  # The `operator_fee` is only calculated for OP chains (for others it's zero) starting from the Isthmus upgrade.
+  #
+  # ## Parameters
+  # - `transaction`: The transaction entity.
+  # - `gas_used`: The amount of gas used in the transaction. Equals to gas limit for pending transactions.
+  # - `unit`: Which unit the result should be presented in. One of [:ether, :gwei, :wei].
+  #
+  # ## Returns
+  # - The calculated total fee.
+  @spec fee_calc(Transaction.t(), Decimal.t(), :ether | :gwei | :wei) :: Decimal.t()
+  defp fee_calc(transaction, gas_used, unit) do
     l1_fee =
       case Map.get(transaction, :l1_fee) do
         nil -> Wei.from(Decimal.new(0), :wei)
         value -> value
       end
 
-    gas_price
+    {:ok, operator_fee} =
+      transaction
+      |> operator_fee()
+      |> Wei.cast()
+
+    transaction.gas_price
     |> l2_fee_calc(gas_used, unit)
     |> Wei.from(unit)
     |> Wei.sum(l1_fee)
+    |> Wei.sum(operator_fee)
     |> Wei.to(unit)
   end
 
+  @doc """
+    The operator fee is calculated for OP chains starting from the Isthmus upgrade
+    as described in https://specs.optimism.io/protocol/isthmus/exec-engine.html#operator-fee
+
+    If the `operatorFeeScalar` or `operatorFeeConstant` is `nil`, it's treated as zero.
+
+    ## Parameters
+    - `transaction`: The transaction entity.
+
+    ## Returns
+    - The calculated operator fee for the given transaction.
+  """
+  @spec operator_fee(Transaction.t()) :: Decimal.t()
+  def operator_fee(
+        %Transaction{
+          gas: gas,
+          gas_used: gas_used
+        } = transaction
+      ) do
+    gas_used = gas_used || gas
+    operator_fee_scalar = Map.get(transaction, :operator_fee_scalar) || Decimal.new(0)
+    operator_fee_constant = Map.get(transaction, :operator_fee_constant) || Decimal.new(0)
+
+    gas_used
+    |> Decimal.mult(operator_fee_scalar)
+    |> Decimal.div_int(1_000_000)
+    |> Decimal.add(operator_fee_constant)
+  end
+
   @doc """
     The execution fee a `transaction` paid for the `t:Explorer.Chain.Transaction.t/0` `gas`.
     Doesn't include L1 fee. See the description for the `fee` function for parameters and return values.
diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_transactions_operator_fee_constant_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_transactions_operator_fee_constant_index.ex
new file mode 100644
index 000000000000..74db18a252d1
--- /dev/null
+++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_transactions_operator_fee_constant_index.ex
@@ -0,0 +1,66 @@
+defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateTransactionsOperatorFeeConstantIndex do
+  @moduledoc """
+  Creates partial index on the `transactions` table filtering by `operator_fee_constant IS NULL`.
+  """
+
+  use Explorer.Migrator.HeavyDbIndexOperation
+
+  require Logger
+
+  alias Explorer.Migrator.{
+    HeavyDbIndexOperation,
+    MigrationStatus
+  }
+
+  alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
+
+  @table_name :transactions
+  @index_name "transactions_operator_fee_constant_index"
+  @operation_type :create
+
+  @impl HeavyDbIndexOperation
+  def table_name, do: @table_name
+
+  @impl HeavyDbIndexOperation
+  def operation_type, do: @operation_type
+
+  @impl HeavyDbIndexOperation
+  def index_name, do: @index_name
+
+  @impl HeavyDbIndexOperation
+  def dependent_from_migrations, do: []
+
+  @query_string """
+  CREATE INDEX #{HeavyDbIndexOperationHelper.add_concurrently_flag?()} IF NOT EXISTS "#{@index_name}"
+  ON #{@table_name} (operator_fee_constant)
+  WHERE operator_fee_constant IS NULL;
+  """
+
+  @impl HeavyDbIndexOperation
+  def db_index_operation do
+    HeavyDbIndexOperationHelper.create_db_index(@query_string)
+  end
+
+  @impl HeavyDbIndexOperation
+  def check_db_index_operation_progress do
+    HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, @query_string)
+  end
+
+  @impl HeavyDbIndexOperation
+  def db_index_operation_status do
+    HeavyDbIndexOperationHelper.db_index_creation_status(@index_name)
+  end
+
+  @impl HeavyDbIndexOperation
+  def restart_db_index_operation do
+    HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
+  end
+
+  @impl HeavyDbIndexOperation
+  def running_other_heavy_migration_exists?(migration_name) do
+    MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
+  end
+
+  @impl HeavyDbIndexOperation
+  def update_cache, do: :ok
+end
diff --git a/apps/explorer/priv/optimism/migrations/20250901063610_op_operator_fee.exs b/apps/explorer/priv/optimism/migrations/20250901063610_op_operator_fee.exs
new file mode 100644
index 000000000000..d1cbf4ddbac3
--- /dev/null
+++ b/apps/explorer/priv/optimism/migrations/20250901063610_op_operator_fee.exs
@@ -0,0 +1,17 @@
+defmodule Explorer.Repo.Optimism.Migrations.OPOperatorFee do
+  use Ecto.Migration
+
+  def up do
+    alter table(:transactions) do
+      add(:operator_fee_scalar, :decimal, null: true, default: nil)
+      add(:operator_fee_constant, :decimal, null: true, default: nil)
+    end
+  end
+
+  def down do
+    alter table(:transactions) do
+      remove(:operator_fee_constant)
+      remove(:operator_fee_scalar)
+    end
+  end
+end
diff --git a/apps/indexer/lib/indexer/buffered_task.ex b/apps/indexer/lib/indexer/buffered_task.ex
index a8d055de0265..0db811c01bb0 100644
--- a/apps/indexer/lib/indexer/buffered_task.ex
+++ b/apps/indexer/lib/indexer/buffered_task.ex
@@ -693,6 +693,13 @@ defmodule Indexer.BufferedTask do
     {:noreply, flush(state)}
   end
 
+  # Handles graceful shutdown. A fetcher implementing BufferedTask behaviour
+  # can invoke `Process.send(__MODULE__, :shutdown, [])` to shutdown itself.
+  # Its `restart` configuration must be set to `:transient`.
+  def handle_info(:shutdown, state) do
+    {:stop, :shutdown, state}
+  end
+
   # Handles the successful completion of the initial streaming task.
   def handle_info({ref, :ok}, %__MODULE__{init_task: ref} = state) do
     {:noreply, state}
diff --git a/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex b/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex
new file mode 100644
index 000000000000..c50f36fc71c6
--- /dev/null
+++ b/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex
@@ -0,0 +1,144 @@
+defmodule Indexer.Fetcher.Optimism.OperatorFee do
+  @moduledoc """
+  Retrieves and saves operator fee for historic transactions from their receipts using RPC calls
+  starting from the timestamp defined in INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP env variable.
+
+  If the env variable is not defined or the chain type is not :optimism, the module doesn't start.
+
+  Once the historic transactions are handled, the module stops working and doesn't start again
+  after instance restarts. If there is a need to make it work again, the corresponding constant
+  should be manually removed from the `constants` database table.
+
+  The transaction queue handling is adjusted by `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_*` env variables.
+  """
+
+  require Logger
+
+  use Indexer.Fetcher, restart: :transient
+  use Spandex.Decorators
+
+  import Ecto.Query
+
+  import EthereumJSONRPC,
+    only: [
+      json_rpc: 2
+    ]
+
+  alias EthereumJSONRPC.Receipt
+  alias EthereumJSONRPC.Receipts.ByTransactionHash
+  alias Explorer.Application.Constants
+  alias Explorer.Chain.{Hash, Transaction}
+  alias Explorer.Repo
+
+  alias Indexer.{BufferedTask, Helper}
+
+  @behaviour BufferedTask
+
+  @default_max_batch_size 100
+  @default_max_concurrency 3
+  @fetcher_name :optimism_operator_fee
+  @fetcher_finished_constant_key "optimism_operator_fee_fetcher_finished"
+
+  @doc false
+  def child_spec([init_options, gen_server_options]) do
+    {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
+
+    unless state do
+      raise ArgumentError,
+            ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <>
+              "to allow for json_rpc calls when running."
+    end
+
+    merged_init_opts =
+      defaults()
+      |> Keyword.merge(mergeable_init_options)
+      |> Keyword.put(:state, state)
+
+    Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__)
+  end
+
+  @impl BufferedTask
+  def init(initial_acc, reducer, _) do
+    if Constants.get_constant_value(@fetcher_finished_constant_key) == "true" do
+      Logger.info("All known transactions are previously handled by #{__MODULE__} module so it won't be started.",
+        fetcher: @fetcher_name
+      )
+
+      Process.send(__MODULE__, :shutdown, [])
+      initial_acc
+    else
+      isthmus_timestamp_l2 = Application.get_env(:indexer, Indexer.Fetcher.Optimism)[:isthmus_timestamp_l2]
+
+      {:ok, acc} =
+        Transaction.stream_transactions_without_operator_fee(
+          initial_acc,
+          fn data, acc ->
+            Helper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__)
+          end,
+          isthmus_timestamp_l2
+        )
+
+      {transactions_count, _} = acc
+
+      if transactions_count == 0 do
+        Logger.info("All known transactions are handled by #{__MODULE__} module so it will be stopped.",
+          fetcher: @fetcher_name
+        )
+
+        Constants.set_constant_value(@fetcher_finished_constant_key, "true")
+        Process.send(__MODULE__, :shutdown, [])
+      end
+
+      acc
+    end
+  end
+
+  @impl BufferedTask
+  def run(hashes, json_rpc_named_arguments) when is_list(hashes) do
+    Logger.metadata(fetcher: @fetcher_name)
+
+    requests =
+      hashes
+      |> Enum.with_index()
+      |> Enum.map(fn {hash, id} ->
+        ByTransactionHash.request(id, Hash.to_string(hash))
+      end)
+
+    error_message = &"eth_getTransactionReceipt failed. Error: #{inspect(&1)}"
+
+    {:ok, receipts} =
+      Helper.repeated_call(
+        &json_rpc/2,
+        [requests, json_rpc_named_arguments],
+        error_message,
+        Helper.infinite_retries_number()
+      )
+
+    receipts
+    |> Enum.map(& &1.result)
+    |> Enum.reject(&is_nil/1)
+    |> Enum.map(&Receipt.to_elixir/1)
+    |> Enum.map(&Receipt.elixir_to_params/1)
+    |> Enum.each(fn receipt ->
+      now = DateTime.utc_now()
+
+      Repo.update_all(
+        from(t in Transaction, where: t.hash == ^receipt.transaction_hash),
+        set: [
+          operator_fee_scalar: receipt.operator_fee_scalar,
+          operator_fee_constant: receipt.operator_fee_constant,
+          updated_at: now
+        ]
+      )
+    end)
+  end
+
+  defp defaults do
+    [
+      flush_interval: :timer.seconds(10),
+      max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency,
+      max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size,
+      task_supervisor: __MODULE__.TaskSupervisor
+    ]
+  end
+end
diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex
index 0d41e0f46293..15a606afcb0c 100644
--- a/apps/indexer/lib/indexer/supervisor.ex
+++ b/apps/indexer/lib/indexer/supervisor.ex
@@ -195,6 +195,8 @@ defmodule Indexer.Supervisor do
           Indexer.Fetcher.Optimism.Interop.MultichainExport.Supervisor,
           [[memory_monitor: memory_monitor]]
         },
+        {Indexer.Fetcher.Optimism.OperatorFee.Supervisor,
+         [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
         configure(Indexer.Fetcher.Shibarium.L2.Supervisor, [
           [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]
         ]),
diff --git a/config/runtime.exs b/config/runtime.exs
index 10b05bc6d879..f42613cb3531 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -797,6 +797,9 @@ config :explorer, Explorer.Migrator.ArbitrumDaRecordsNormalization,
 config :explorer, Explorer.Migrator.HeavyDbIndexOperation.CreateArbitrumBatchL2BlocksUnconfirmedBlocksIndex,
   enabled: ConfigHelper.chain_type() == :arbitrum
 
+config :explorer, Explorer.Migrator.HeavyDbIndexOperation.CreateTransactionsOperatorFeeConstantIndex,
+  enabled: ConfigHelper.chain_type() == :optimism
+
 config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations,
   enabled: ConfigHelper.chain_type() == :filecoin,
   batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_BATCH_SIZE", 100),
@@ -914,6 +917,9 @@ trace_block_ranges =
 disable_multichain_search_db_export_counters_queue_fetcher =
   ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_FETCHER")
 
+optimism_l2_isthmus_timestamp =
+  ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP")
+
 config :indexer,
   block_transformer: ConfigHelper.block_transformer(),
   chain_id: System.get_env("CHAIN_ID"),
@@ -1250,7 +1256,8 @@ config :indexer, Indexer.Fetcher.Optimism,
   l2_eth_get_logs_range_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_L2_ETH_GET_LOGS_RANGE_SIZE", 250),
   block_duration: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_BLOCK_DURATION", 2),
   start_block_l1: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L1_START_BLOCK"),
-  portal: System.get_env("INDEXER_OPTIMISM_L1_PORTAL_CONTRACT")
+  portal: System.get_env("INDEXER_OPTIMISM_L1_PORTAL_CONTRACT"),
+  isthmus_timestamp_l2: optimism_l2_isthmus_timestamp
 
 config :indexer, Indexer.Fetcher.Optimism.Deposit,
   transaction_type: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_L1_DEPOSITS_TRANSACTION_TYPE", 126)
@@ -1291,6 +1298,20 @@ config :indexer, Indexer.Fetcher.Optimism.Interop.MessageQueue,
 config :indexer, Indexer.Fetcher.Optimism.Interop.MultichainExport,
   batch_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE", 100)
 
+config :indexer, Indexer.Fetcher.Optimism.OperatorFee,
+  concurrency: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_CONCURRENCY", 3),
+  batch_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_BATCH_SIZE", 100),
+  enqueue_busy_waiting_timeout:
+    ConfigHelper.parse_time_env_var(
+      "INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT",
+      "1s"
+    ),
+  max_queue_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_MAX_QUEUE_SIZE", 1_000),
+  init_limit: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_INIT_QUERY_LIMIT", 1_000)
+
+config :indexer, Indexer.Fetcher.Optimism.OperatorFee.Supervisor,
+  disabled?: is_nil(optimism_l2_isthmus_timestamp) or ConfigHelper.chain_type() != :optimism
+
 config :indexer, Indexer.Fetcher.Withdrawal.Supervisor,
   disabled?: System.get_env("INDEXER_DISABLE_WITHDRAWALS_FETCHER", "true") == "true"
 
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index fc3195d709b2..08f7749b6e06 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -323,6 +323,7 @@ API_V1_WRITE_METHODS_DISABLED=false
 # INDEXER_OPTIMISM_L2_ETH_GET_LOGS_RANGE_SIZE=
 # INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP=
 # INDEXER_OPTIMISM_L2_HOLOCENE_BLOCKS_CHUNK_SIZE=
+# INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP=
 # INDEXER_OPTIMISM_L2_INTEROP_START_BLOCK=
 # INDEXER_OPTIMISM_L2_INTEROP_BLOCKS_CHUNK_SIZE=
 # INDEXER_OPTIMISM_CHAINSCOUT_API_URL=
@@ -332,6 +333,11 @@ API_V1_WRITE_METHODS_DISABLED=false
 # INDEXER_OPTIMISM_INTEROP_RECV_TIMEOUT=
 # INDEXER_OPTIMISM_INTEROP_EXPORT_EXPIRATION_DAYS=
 # INDEXER_OPTIMISM_MULTICHAIN_BATCH_SIZE=
+# INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_CONCURRENCY=
+# INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_BATCH_SIZE=
+# INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT=
+# INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_MAX_QUEUE_SIZE=
+# INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_INIT_QUERY_LIMIT=
 # INDEXER_DISABLE_OPTIMISM_INTEROP_MULTICHAIN_EXPORT=
 # INDEXER_SCROLL_L1_RPC=
 # INDEXER_SCROLL_L1_MESSENGER_CONTRACT=

From 6e116ef23ab8c81d2a332374a8bf86e9a2b599c5 Mon Sep 17 00:00:00 2001
From: Olexandr88 
Date: Thu, 9 Oct 2025 19:54:24 +0300
Subject: [PATCH 0386/1006] chore: edited the broken Discord badge (#13115)

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index 5ab52a8e6af3..54f4063679eb 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
 
[![Blockscout](https://github.com/blockscout/blockscout/actions/workflows/config.yml/badge.svg)](https://github.com/blockscout/blockscout/actions) -[![Discord](https://dcbadge.vercel.app/api/server/blockscout?style=flat)](https://discord.gg/blockscout) +[![Discord](https://img.shields.io/badge/chat-Blockscout-green.svg)](https://discord.gg/blockscout)
From a1f925e5522c2a587fe934fc6be6ad604aed9b49 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 10 Oct 2025 12:00:00 +0300 Subject: [PATCH 0387/1006] feat: celo accounts indexing (#12893) --- apps/explorer/lib/explorer/application.ex | 1 + .../lib/explorer/chain/celo/account.ex | 105 +++++++ .../lib/explorer/chain/celo/epoch_reward.ex | 8 - .../explorer/chain/celo/legacy/accounts.ex | 210 ++++++++++++++ .../lib/explorer/chain/celo/legacy/events.ex | 161 +++++++++++ .../chain/celo/pending_account_operation.ex | 70 +++++ .../chain/import/runner/celo/accounts.ex | 82 ++++++ .../runner/celo/pending_account_operations.ex | 82 ++++++ .../chain/import/stage/chain_type_specific.ex | 2 + .../lib/explorer/migrator/celo_accounts.ex | 68 +++++ .../20250731143827_create_account.exs | 54 ++++ ...2346_create_pending_account_operations.exs | 20 ++ .../explorer/migrator/celo_accounts_test.exs | 73 +++++ .../lib/indexer/block/catchup/fetcher.ex | 2 + apps/indexer/lib/indexer/block/fetcher.ex | 40 ++- .../lib/indexer/block/realtime/fetcher.ex | 2 + .../epoch_block_operations/distributions.ex | 2 +- .../indexer/fetcher/celo/legacy/account.ex | 152 ++++++++++ .../fetcher/celo/legacy/account/reader.ex | 267 ++++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 3 + .../lib/indexer/transform/addresses.ex | 28 ++ .../test/indexer/block/fetcher_test.exs | 75 ++++- config/runtime.exs | 16 +- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 4 + 25 files changed, 1509 insertions(+), 19 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/celo/account.ex create mode 100644 apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex create mode 100644 apps/explorer/lib/explorer/chain/celo/legacy/events.ex create mode 100644 apps/explorer/lib/explorer/chain/celo/pending_account_operation.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/celo/accounts.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/celo/pending_account_operations.ex create mode 100644 apps/explorer/lib/explorer/migrator/celo_accounts.ex create mode 100644 apps/explorer/priv/celo/migrations/20250731143827_create_account.exs create mode 100644 apps/explorer/priv/celo/migrations/20250801112346_create_pending_account_operations.exs create mode 100644 apps/explorer/test/explorer/migrator/celo_accounts_test.exs create mode 100644 apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex create mode 100644 apps/indexer/lib/indexer/fetcher/celo/legacy/account/reader.ex diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 7c7c921cb176..b37ae35d5eac 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -162,6 +162,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.FilecoinPendingAddressOperations, :indexer), configure_mode_dependent_process(Explorer.Migrator.SmartContractLanguage, :indexer), configure_mode_dependent_process(Explorer.Migrator.CeloL2Epochs, :indexer), + configure_mode_dependent_process(Explorer.Migrator.CeloAccounts, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, :indexer), Explorer.Migrator.BackfillMultichainSearchDB |> configure_mode_dependent_process(:indexer) diff --git a/apps/explorer/lib/explorer/chain/celo/account.ex b/apps/explorer/lib/explorer/chain/celo/account.ex new file mode 100644 index 000000000000..4f918904f496 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/account.ex @@ -0,0 +1,105 @@ +defmodule Explorer.Chain.Celo.Account do + @moduledoc """ + Represents a Celo blockchain account with associated metadata, and locked gold + amounts. + + Celo accounts can be regular accounts, validators, or validator groups, each + with different roles in the network governance and consensus mechanisms. + """ + + require Logger + + use Explorer.Schema + + alias Explorer.Chain.{Address, Hash, Wei} + + @required_attrs ~w(address_hash )a + @optional_attrs [ + :type, + :name, + :metadata_url, + :nonvoting_locked_celo, + :locked_celo, + :vote_signer_address_hash, + :validator_signer_address_hash, + :attestation_signer_address_hash + ] + @allowed_attrs @required_attrs ++ @optional_attrs + + @typedoc """ + * `address_hash` - the hash of the account address + * `type` - account type: regular, validator, or validator group + * `name` - human-readable name of the account + * `metadata_url` - URL to additional account metadata + * `locked_celo` - total amount of CELO locked by this account + * `nonvoting_locked_celo` - amount of locked CELO that is not used for voting + """ + @primary_key false + typed_schema "celo_accounts" do + field(:type, Ecto.Enum, + values: [:regular, :validator, :group], + default: :regular + ) + + field(:name, :string) + field(:metadata_url, :string) + field(:nonvoting_locked_celo, Wei) + field(:locked_celo, Wei) + + belongs_to( + :address, + Address, + primary_key: true, + foreign_key: :address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + belongs_to( + :vote_signer_address, + Address, + foreign_key: :vote_signer_address_hash, + references: :hash, + type: Hash.Address, + null: true + ) + + belongs_to( + :validator_signer_address, + Address, + foreign_key: :validator_signer_address_hash, + references: :hash, + type: Hash.Address, + null: true + ) + + belongs_to( + :attestation_signer_address, + Address, + foreign_key: :attestation_signer_address_hash, + references: :hash, + type: Hash.Address, + null: true + ) + + timestamps() + end + + @doc """ + Creates a changeset for a Celo account with the given attributes. + + ## Parameters + - `celo_account`: The Celo account struct to update + - `attrs`: A map of attributes to cast and validate + + ## Returns + - An Ecto changeset with validation results + """ + @spec changeset(t(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = celo_account, attrs) do + celo_account + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + end +end diff --git a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex index 3b3a8b4f0275..ba0d4567d4f5 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex @@ -10,14 +10,6 @@ defmodule Explorer.Chain.Celo.EpochReward do @required_attrs ~w(epoch_number)a @optional_attrs ~w(reserve_bolster_transfer_log_index community_transfer_log_index carbon_offsetting_transfer_log_index)a - # @optional_attrs [ - # :reserve_bolster_transfer_log_index, - # :reserve_bolster_transfer_value, - # :community_transfer_log_index, - # :community_transfer_value, - # :carbon_offsetting_transfer_log_index, - # :carbon_offsetting_transfer_value - # ] @allowed_attrs @required_attrs ++ @optional_attrs @primary_key false diff --git a/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex b/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex new file mode 100644 index 000000000000..ffc855746a4f --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex @@ -0,0 +1,210 @@ +defmodule Explorer.Chain.Celo.Legacy.Accounts do + @moduledoc """ + Helper functions for transforming data for Celo accounts. + + TODO: this implementation is ported from the celo's fork of blockscout and + could be improved in the future. + """ + + require Logger + + alias ABI.TypeDecoder + alias Explorer.Chain.Celo.Legacy.Events + + @max_name_length 30 + + @doc """ + Returns a list of account addresses given a list of logs. + """ + def parse(logs) do + logs = + logs + |> Enum.map( + &Map.merge(&1, %{ + first_topic: to_string(&1.first_topic), + second_topic: to_string(&1.second_topic), + third_topic: to_string(&1.third_topic), + fourth_topic: to_string(&1.fourth_topic), + data: to_string(&1.data) + }) + ) + + %{ + # Add special items for voter epoch rewards + accounts: get_addresses(logs, Events.account_events()), + # Adding a group to updated validators means to update all members of the group + validators: + get_addresses(logs, Events.validator_events()) ++ + get_addresses(logs, Events.membership_events()) ++ + get_addresses(logs, Events.membership_events(), fn a -> a.third_topic end), + account_names: get_names(logs), + validator_groups: + get_addresses(logs, Events.validator_group_events()) ++ + get_addresses(logs, Events.vote_events(), fn a -> a.third_topic end), + withdrawals: [get_withdrawal_events(logs, Events.gold_withdrawn())], + unlocked: [get_withdrawal_events(logs, Events.gold_unlocked())], + signers: get_signers(logs, Events.signer_events()), + voters: get_voters(logs, Events.voter_events()), + attestations_fulfilled: get_addresses(logs, [Events.attestation_completed_event()], fn a -> a.fourth_topic end), + attestations_requested: + get_addresses(logs, [Events.attestation_issuer_selected_event()], fn a -> a.fourth_topic end), + wallets: get_wallets(logs) + } + end + + def get_names(logs) do + logs + |> Enum.filter(fn log -> log.first_topic == Events.account_name_event() end) + |> Enum.reduce([], fn log, names -> do_parse_name(log, names) end) + |> Enum.filter(fn %{name: name} -> String.length(name) > 0 end) + end + + defp get_addresses(logs, topics, get_topic \\ fn a -> a.second_topic end) do + logs + |> Enum.filter(fn log -> Enum.member?(topics, log.first_topic) end) + |> Enum.reduce([], fn log, accounts -> do_parse(log, accounts, get_topic) end) + |> Enum.map(fn address -> %{address_hash: address} end) + end + + defp get_withdrawal_events(logs, topics) do + logs + |> Enum.filter(fn log -> Enum.member?(topics, log.first_topic) end) + |> Enum.reduce([], fn log, accounts -> do_parse_withdrawal_events(log, accounts, fn a -> a.second_topic end) end) + end + + defp get_signers(logs, topics) do + logs + |> Enum.filter(fn log -> Enum.member?(topics, log.first_topic) end) + |> Enum.reduce([], fn log, accounts -> do_parse_signers(log, accounts) end) + end + + def get_wallets(logs) do + logs + |> Enum.filter(fn log -> log.first_topic == Events.account_wallet_address_set_event() end) + |> Enum.reduce([], fn log, wallets -> do_parse_wallets(log, wallets) end) + end + + def get_voters(logs, topics) do + logs + |> Enum.filter(fn log -> Enum.member?(topics, log.first_topic) end) + |> Enum.reduce([], fn log, accounts -> do_parse_voters(log, accounts) end) + end + + defp do_parse(log, accounts, get_topic) do + account_address = parse_params(log, get_topic) + + if Enum.member?(accounts, account_address) do + accounts + else + [account_address | accounts] + end + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown account event format: #{inspect(log)}" end) + accounts + end + + defp do_parse_signers(log, accounts) do + signer_pair = parse_signer_params(log) + [signer_pair | accounts] + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown signer authorization event format: #{inspect(log)}" end) + accounts + end + + defp do_parse_wallets(log, wallets) do + wallet = parse_wallet_params(log) + [wallet | wallets] + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown account wallet address set event format: #{inspect(log)}" end) + wallets + end + + defp do_parse_voters(log, accounts) do + pair = parse_voter_params(log) + [pair | accounts] + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown voting event format: #{inspect(log)}" end) + accounts + end + + defp do_parse_name(log, names) do + [name] = decode_data(log.data, [:string]) + + entry = %{ + name: String.slice(name, 0, @max_name_length), + address_hash: truncate_address_hash(log.second_topic), + primary: true + } + + [entry | names] + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown account name event format: #{inspect(log)}" end) + names + end + + defp do_parse_withdrawal_events(log, accounts, get_topic) do + account_address = parse_params(log, get_topic) + + # GoldUnlocked has 2 unindexed parameters which end up in the data field, while the rest of the withdrawal events + # only 1. Each of these parameters are of length 64 plus 2 for the 0x. + if String.length(log.data) > 66 do + [amount, available] = decode_data(log.data, [{:uint, 256}, {:uint, 256}]) + %{address: account_address, amount: amount, available: available} + else + [amount] = decode_data(log.data, [{:uint, 256}]) + %{address: account_address, amount: amount} + end + rescue + _ in [FunctionClauseError, MatchError] -> + Logger.error(fn -> "Unknown account event format: #{inspect(log)}" end) + accounts + end + + defp parse_params(log, get_topic) do + truncate_address_hash(get_topic.(log)) + end + + defp parse_signer_params(log) do + address = truncate_address_hash(log.second_topic) + [signer] = decode_data(log.data, [:address]) + %{address: address, signer: signer} + end + + defp parse_wallet_params(log) do + account = truncate_address_hash(log.second_topic) + wallet = truncate_address_hash(log.data) + + %{ + account_address_hash: account, + wallet_address_hash: wallet, + block_number: log.block_number + } + end + + defp parse_voter_params(log) do + voter_address = truncate_address_hash(log.second_topic) + group_address = truncate_address_hash(log.third_topic) + %{group_address: group_address, voter_address: voter_address} + end + + defp truncate_address_hash(nil), do: "0x0000000000000000000000000000000000000000" + + defp truncate_address_hash("0x000000000000000000000000" <> truncated_hash) do + "0x#{truncated_hash}" + end + + defp decode_data("0x", types) do + for _ <- types, do: nil + end + + defp decode_data("0x" <> encoded_data, types) do + encoded_data + |> Base.decode16!(case: :mixed) + |> TypeDecoder.decode_raw(types) + end +end diff --git a/apps/explorer/lib/explorer/chain/celo/legacy/events.ex b/apps/explorer/lib/explorer/chain/celo/legacy/events.ex new file mode 100644 index 000000000000..cbceff5409d0 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/legacy/events.ex @@ -0,0 +1,161 @@ +defmodule Explorer.Chain.Celo.Legacy.Events do + @moduledoc """ + Helper methods and event groups which define Celo contract event topics + """ + + @account_created "0x805996f252884581e2f74cf3d2b03564d5ec26ccc90850ae12653dc1b72d1fa2" + @account_data_encryption_key_set "0x43fdefe0a824cb0e3bbaf9c4bc97669187996136fe9282382baf10787f0d808d" + @account_name_set "0xa6e2c5a23bb917ba0a584c4b250257ddad698685829b66a8813c004b39934fe4" + @account_url_set "0x0b5629fec5b6b5a1c2cfe0de7495111627a8cf297dced72e0669527425d3f01b" + @account_wallet_address_set "0xf81d74398fd47e35c36b714019df15f200f623dde569b5b531d6a0b4da5c5f26" + @attestation_completed "0x414ff2c18c092697c4b8de49f515ac44f8bebc19b24553cf58ace913a6ac639d" + @attestation_issuer_selected "0xaf7f470b643316cf44c1f2898328a075e7602945b4f8584f48ba4ad2d8a2ea9d" + @attestation_signer_authorized "0x9dfbc5a621c3e2d0d83beee687a17dfc796bbce2118793e5e254409bb265ca0b" + @gold_locked "0x0f0f2fc5b4c987a49e1663ce2c2d65de12f3b701ff02b4d09461421e63e609e7" + @gold_unlocked "0xb1a3aef2a332070da206ad1868a5e327f5aa5144e00e9a7b40717c153158a588" + @gold_withdrawn "0x292d39ba701489b7f640c83806d3eeabe0a32c9f0a61b49e95612ebad42211cd" + @oracle_reported "0x7cebb17173a9ed273d2b7538f64395c0ebf352ff743f1cf8ce66b437a6144213" + @validator_affiliated "0x91ef92227057e201e406c3451698dd780fe7672ad74328591c88d281af31581d" + @validator_deaffiliated "0x71815121f0622b31a3e7270eb28acb9fd10825ff418c9a18591f617bb8a31a6c" + @validator_epoch_payment_distributed "0x6f5937add2ec38a0fa4959bccd86e3fcc2aafb706cd3e6c0565f87a7b36b9975" + @validator_group_active_vote_revoked "0xae7458f8697a680da6be36406ea0b8f40164915ac9cc40c0dad05a2ff6e8c6a8" + @validator_group_commission_updated "0x815d292dbc1a08dfb3103aabb6611233dd2393903e57bdf4c5b3db91198a826c" + @validator_group_deregistered "0xae7e034b0748a10a219b46074b20977a9170bf4027b156c797093773619a8669" + @validator_group_epoch_rewards_distributed "0x91ba34d62474c14d6c623cd322f4256666c7a45b7fdaa3378e009d39dfcec2a7" + @validator_group_member_added "0xbdf7e616a6943f81e07a7984c9d4c00197dc2f481486ce4ffa6af52a113974ad" + @validator_group_member_removed "0xc7666a52a66ff601ff7c0d4d6efddc9ac20a34792f6aa003d1804c9d4d5baa57" + @validator_group_member_reordered "0x38819cc49a343985b478d72f531a35b15384c398dd80fd191a14662170f895c6" + @validator_group_pending_vote_revoked "0x148075455e24d5cf538793db3e917a157cbadac69dd6a304186daf11b23f76fe" + @validator_group_registered "0xbf4b45570f1907a94775f8449817051a492a676918e38108bb762e991e6b58dc" + @validator_group_vote_activated "0x45aac85f38083b18efe2d441a65b9c1ae177c78307cb5a5d4aec8f7dbcaeabfe" + @validator_group_vote_cast "0xd3532f70444893db82221041edb4dc26c94593aeb364b0b14dfc77d5ee905152" + @validator_registered "0xd09501348473474a20c772c79c653e1fd7e8b437e418fe235d277d2c88853251" + @validator_score_updated "0xedf9f87e50e10c533bf3ae7f5a7894ae66c23e6cbbe8773d7765d20ad6f995e9" + @validator_signer_authorized "0x16e382723fb40543364faf68863212ba253a099607bf6d3a5b47e50a8bf94943" + @vote_signer_authorized "0xaab5f8a189373aaa290f42ae65ea5d7971b732366ca5bf66556e76263944af28" + @voter_rewards "0x91ba34d62474c14d6c623cd322f4256666c7a45b7fdaa3378e009d39dfcec2a7" + + @doc """ + Events for updating account + """ + def account_events, + do: [ + @gold_withdrawn, + @gold_unlocked, + @gold_locked, + @account_created, + @account_wallet_address_set, + @account_url_set, + @account_name_set, + @account_data_encryption_key_set, + @validator_group_pending_vote_revoked, + @validator_group_active_vote_revoked, + @validator_group_vote_activated, + @validator_group_vote_cast, + @validator_group_epoch_rewards_distributed, + @validator_epoch_payment_distributed, + @validator_registered, + @validator_group_registered + ] + + @doc """ + Events for updating validator + """ + def validator_events, + do: [ + @validator_registered, + @validator_affiliated, + @validator_deaffiliated, + @validator_score_updated, + @validator_signer_authorized, + @validator_epoch_payment_distributed + ] + + def membership_events, + do: [ + @validator_group_member_added, + @validator_group_member_removed, + @validator_group_member_reordered + ] + + @doc """ + Events for updating validator group + """ + def validator_group_events, + do: [ + @validator_group_epoch_rewards_distributed, + @validator_group_commission_updated, + @validator_group_registered, + @validator_group_deregistered + ] + + def validator_group_voter_reward_events, + do: [ + @validator_group_epoch_rewards_distributed + ] + + def vote_events, + do: [ + @validator_group_pending_vote_revoked, + @validator_group_active_vote_revoked, + @validator_group_vote_cast + ] + + @doc """ + Events for notifications + """ + def withdrawal_events, + do: [ + @gold_withdrawn, + @gold_unlocked, + @gold_locked + ] + + def gold_unlocked, + do: [ + @gold_unlocked + ] + + def gold_withdrawn, + do: [ + @gold_withdrawn + ] + + def signer_events, + do: [ + @validator_signer_authorized, + @vote_signer_authorized, + @attestation_signer_authorized + ] + + @doc """ + Events for updating voter + """ + def voter_events, + do: [ + @validator_group_active_vote_revoked, + @validator_group_pending_vote_revoked, + @validator_group_vote_activated, + @validator_group_vote_cast + ] + + def distributed_events, + do: [ + @voter_rewards + ] + + def attestation_issuer_selected_event, + do: @attestation_issuer_selected + + def attestation_completed_event, + do: @attestation_completed + + def oracle_reported_event, + do: @oracle_reported + + def account_name_event, + do: @account_name_set + + def account_wallet_address_set_event, + do: @account_wallet_address_set +end diff --git a/apps/explorer/lib/explorer/chain/celo/pending_account_operation.ex b/apps/explorer/lib/explorer/chain/celo/pending_account_operation.ex new file mode 100644 index 000000000000..aca3bcaaa5b3 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/pending_account_operation.ex @@ -0,0 +1,70 @@ +defmodule Explorer.Chain.Celo.PendingAccountOperation do + @moduledoc """ + Tracks an address that is pending for fetching of celo account info. + """ + + use Explorer.Schema + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash} + + @required_attrs ~w(address_hash)a + @allowed_attrs @required_attrs + + @typedoc """ + * `address_hash` - the hash of the address that is pending to be fetched. + """ + @primary_key false + typed_schema "celo_pending_account_operations" do + belongs_to(:address, Address, + foreign_key: :address_hash, + references: :hash, + type: Hash.Address, + primary_key: true + ) + + timestamps() + end + + @spec changeset( + t(), + :invalid | %{optional(:__struct__) => none(), optional(atom() | binary()) => any()} + ) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = pending_ops, attrs) do + pending_ops + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:address_hash, name: :celo_pending_account_operations_address_hash_fkey) + |> unique_constraint(:address_hash, name: :celo_pending_account_operations_pkey) + end + + @doc """ + Returns a stream of pending operations. + """ + @spec stream( + initial :: accumulator, + reducer :: (entry :: term(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream(initial, reducer, limited? \\ false) + when is_function(reducer, 2) do + __MODULE__ + |> order_by([op], desc: op.address_hash) + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Deletes pending operations by address hashes. + """ + @spec delete_by_address_hashes([Hash.Address.t()]) :: {integer, nil | [term()]} + def delete_by_address_hashes(address_hashes) do + query = + from(p in __MODULE__, + where: p.address_hash in ^address_hashes + ) + + Repo.delete_all(query) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/accounts.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/accounts.ex new file mode 100644 index 000000000000..52ed902ff579 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/accounts.ex @@ -0,0 +1,82 @@ +defmodule Explorer.Chain.Import.Runner.Celo.Accounts do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Celo.Account.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Celo.Account + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Account.t()] + + @impl Import.Runner + def ecto_schema_module, do: Account + + @impl Import.Runner + def option_key, do: :celo_accounts + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_celo_accounts, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :address_referencing, + :celo_accounts, + :celo_accounts + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [Account.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce Celo.Epoch.Account ShareLocks order (see docs: sharelock.md) + ordered_changes_list = + changes_list + |> Enum.uniq_by(& &1.address_hash) + |> Enum.sort_by(& &1.address_hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: Account, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :address_hash, + on_conflict: :replace_all + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/pending_account_operations.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/pending_account_operations.ex new file mode 100644 index 000000000000..71cd9cac9de9 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/pending_account_operations.ex @@ -0,0 +1,82 @@ +defmodule Explorer.Chain.Import.Runner.Celo.PendingAccountOperations do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Celo.PendingAccountOperation.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Celo.PendingAccountOperation + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [PendingAccountOperation.t()] + + @impl Import.Runner + def ecto_schema_module, do: PendingAccountOperation + + @impl Import.Runner + def option_key, do: :celo_pending_account_operations + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :celo_pending_account_operations, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :address_referencing, + :celo_pending_account_operations, + :celo_pending_account_operations + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [PendingAccountOperation.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce Celo.Epoch.Account ShareLocks order (see docs: sharelock.md) + ordered_changes_list = + changes_list + |> Enum.uniq_by(& &1.address_hash) + |> Enum.sort_by(& &1.address_hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: PendingAccountOperation, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :address_hash, + on_conflict: :nothing + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 035f573801d8..8586b9b365bf 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -56,6 +56,8 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Runner.Scroll.L1FeeParams ], celo: [ + Runner.Celo.PendingAccountOperations, + Runner.Celo.Accounts, Runner.Celo.ValidatorGroupVotes, Runner.Celo.Epochs, Runner.Celo.ElectionRewards, diff --git a/apps/explorer/lib/explorer/migrator/celo_accounts.ex b/apps/explorer/lib/explorer/migrator/celo_accounts.ex new file mode 100644 index 000000000000..dabd43691c1f --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/celo_accounts.ex @@ -0,0 +1,68 @@ +defmodule Explorer.Migrator.CeloAccounts do + @moduledoc """ + Backfills pending account operations table for each address that has + Celo-specific events, indicating that their Celo account information needs to + be fetched and updated. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Celo.Legacy.{Accounts, Events} + alias Explorer.Chain.Celo.{Account, PendingAccountOperation} + alias Explorer.Chain.Log + alias Explorer.Migrator.FillingMigration + + @migration_name "celo_accounts" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + ids = + unprocessed_data_query() + |> select([log], log) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {ids, state} + end + + @impl FillingMigration + def unprocessed_data_query do + pending_op_hashes_query = from(op in PendingAccountOperation, select: op.address_hash) + existing_account_hashes_query = from(a in Account, select: a.address_hash) + excluded_hashes_query = pending_op_hashes_query |> union(^existing_account_hashes_query) + + from( + log in Log, + where: + log.first_topic in ^Events.account_events() and + fragment("SUBSTRING(? from 13)", log.second_topic) not in subquery(excluded_hashes_query), + order_by: [asc: log.block_number, asc: log.index] + ) + end + + @impl FillingMigration + def update_batch(logs) do + %{accounts: pending_operation_params} = Accounts.parse(logs) + + unique_pending_operation_params = Enum.uniq_by(pending_operation_params, & &1.address_hash) + address_params = Enum.map(unique_pending_operation_params, &%{hash: &1.address_hash}) + + Chain.import(%{ + addresses: %{params: address_params}, + celo_pending_account_operations: %{ + params: unique_pending_operation_params + } + }) + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/priv/celo/migrations/20250731143827_create_account.exs b/apps/explorer/priv/celo/migrations/20250731143827_create_account.exs new file mode 100644 index 000000000000..90d9b56a7d4b --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250731143827_create_account.exs @@ -0,0 +1,54 @@ +defmodule Explorer.Repo.Celo.Migrations.CreateAccount do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE celo_account_type AS ENUM ('regular', 'validator', 'group')", + "DROP TYPE celo_account_type" + ) + + create table(:celo_accounts, primary_key: false) do + add( + :address_hash, + references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), + null: false, + primary_key: true + ) + + add( + :type, + :celo_account_type, + null: false, + default: "regular" + ) + + add(:name, :string) + add(:metadata_url, :string) + add(:nonvoting_locked_celo, :numeric, precision: 100, null: false) + add(:locked_celo, :numeric, precision: 100, null: false) + + add( + :vote_signer_address_hash, + references(:addresses, column: :hash, type: :bytea), + null: true + ) + + add( + :validator_signer_address_hash, + references(:addresses, column: :hash, type: :bytea), + null: true + ) + + add( + :attestation_signer_address_hash, + references(:addresses, column: :hash, type: :bytea), + null: true + ) + + timestamps() + end + + create(constraint(:celo_accounts, :nonvoting_locked_celo_nonnegative, check: "nonvoting_locked_celo >= 0")) + create(constraint(:celo_accounts, :locked_celo_nonnegative, check: "locked_celo >= 0")) + end +end diff --git a/apps/explorer/priv/celo/migrations/20250801112346_create_pending_account_operations.exs b/apps/explorer/priv/celo/migrations/20250801112346_create_pending_account_operations.exs new file mode 100644 index 000000000000..2fbc3c0335cd --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20250801112346_create_pending_account_operations.exs @@ -0,0 +1,20 @@ +defmodule Explorer.Repo.Celo.Migrations.CreatePendingAccountOperations do + use Ecto.Migration + + def change do + create table(:celo_pending_account_operations, primary_key: false) do + add( + :address_hash, + references( + :addresses, + column: :hash, + type: :bytea, + on_delete: :delete_all + ), + primary_key: true + ) + + timestamps() + end + end +end diff --git a/apps/explorer/test/explorer/migrator/celo_accounts_test.exs b/apps/explorer/test/explorer/migrator/celo_accounts_test.exs new file mode 100644 index 000000000000..5218a773835f --- /dev/null +++ b/apps/explorer/test/explorer/migrator/celo_accounts_test.exs @@ -0,0 +1,73 @@ +defmodule Explorer.Migrator.CeloAccountsTest do + use Explorer.DataCase, async: false + + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type] + + if @chain_type == :celo do + alias Explorer.Factory + alias Explorer.Migrator.{CeloAccounts, MigrationStatus} + alias Explorer.Chain.{Address, Hash} + alias Explorer.Chain.Celo.{Account, PendingAccountOperation} + alias Explorer.Chain.Celo.Legacy.Events + alias Explorer.Repo + + describe "celo_accounts migration" do + test "enqueues pending operations for new celo addresses" do + new_address_hash = Factory.address_hash() + existing_pending_address = insert(:address) + existing_account_address = insert(:address) + + %PendingAccountOperation{} + |> PendingAccountOperation.changeset(%{address_hash: existing_pending_address.hash}) + |> Repo.insert!() + + %Account{} + |> Account.changeset(%{ + address_hash: existing_account_address.hash, + type: :regular, + locked_celo: 0, + nonvoting_locked_celo: 0 + }) + |> Repo.insert!() + + # take AccountCreated topic + event_topic = Events.account_events() |> Enum.at(3) + + insert(:log, first_topic: event_topic, second_topic: zero_pad(new_address_hash)) + insert(:log, first_topic: event_topic, second_topic: zero_pad(new_address_hash)) + insert(:log, first_topic: event_topic, second_topic: zero_pad(existing_pending_address.hash)) + insert(:log, first_topic: event_topic, second_topic: zero_pad(existing_account_address.hash)) + + assert MigrationStatus.set_status("celo_accounts", "started") + {:ok, pid} = CeloAccounts.start_link([]) + assert Repo.get(Address, new_address_hash) == nil + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, :process, ^pid, :normal}, 1_000 + + pending_addresses = + PendingAccountOperation + |> Repo.all() + |> Enum.map(&Hash.to_string(&1.address_hash)) + |> Enum.sort() + + expected_addresses = + [existing_pending_address.hash, new_address_hash] + |> Enum.map(&Hash.to_string/1) + |> Enum.sort() + + assert pending_addresses == expected_addresses + + assert Repo.get(Address, new_address_hash) + assert Repo.get(PendingAccountOperation, existing_account_address.hash) == nil + assert Repo.aggregate(PendingAccountOperation, :count, :address_hash) == 2 + assert MigrationStatus.get_status("celo_accounts") == "completed" + end + end + + defp zero_pad(address_hash) do + <<"0x", rest::binary>> = Hash.to_string(address_hash) + "0x" <> String.duplicate("0", 24) <> rest + end + end +end diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index d3c78a689c19..a6b0752522b4 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -12,6 +12,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_blobs: 2, async_import_block_rewards: 2, async_import_celo_epoch_block_operations: 2, + async_import_celo_accounts: 2, async_import_coin_balances: 2, async_import_created_contract_codes: 2, async_import_filecoin_addresses_info: 2, @@ -146,6 +147,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_token_instances(imported) async_import_blobs(imported, realtime?) async_import_celo_epoch_block_operations(imported, realtime?) + async_import_celo_accounts(imported, realtime?) async_import_filecoin_addresses_info(imported, realtime?) async_import_signed_authorizations_statuses(imported, realtime?) end diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index e051787bcb86..f80740fb98f8 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -3,6 +3,8 @@ defmodule Indexer.Block.Fetcher do Fetches and indexes block ranges. """ + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + use Spandex.Decorators require Logger @@ -11,15 +13,17 @@ defmodule Indexer.Block.Fetcher do alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries} alias Explorer.Chain + alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei} alias Explorer.Chain.Block.Reward - alias Explorer.Chain.Cache.Blocks, as: BlocksCache alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions, Uncles} + alias Explorer.Chain.Cache.Blocks, as: BlocksCache + alias Explorer.Chain.Celo.Legacy.Accounts, as: CeloAccountsTransform alias Explorer.Chain.Filecoin.PendingAddressOperation, as: FilecoinPendingAddressOperation - alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei} alias Indexer.Block.Fetcher.Receipts alias Indexer.Fetcher.Arbitrum.MessagesToL2Matcher, as: ArbitrumMessagesToL2Matcher alias Indexer.Fetcher.Celo.EpochBlockOperations, as: CeloEpochBlockOperations alias Indexer.Fetcher.Celo.EpochLogs, as: CeloEpochLogs + alias Indexer.Fetcher.Celo.Legacy.Account, as: CeloAccount alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime alias Indexer.Fetcher.Filecoin.AddressInfo, as: FilecoinAddressInfo @@ -174,6 +178,7 @@ defmodule Indexer.Block.Fetcher do token_transfers = token_transfers ++ celo_native_token_transfers, celo_l1_epochs = CeloL1Epochs.parse(blocks), celo_l2_epochs = CeloL2Epochs.parse(logs), + celo_pending_account_operations = parse_celo_pending_account_operations(logs), tokens = Enum.uniq(tokens ++ celo_tokens), %{transaction_actions: transaction_actions} = TransactionActions.parse(logs), %{mint_transfers: mint_transfers} = MintTransfers.parse(logs), @@ -209,7 +214,8 @@ defmodule Indexer.Block.Fetcher do transactions: transactions_with_receipts, transaction_actions: transaction_actions, withdrawals: withdrawals_params, - polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations + polygon_zkevm_bridge_operations: polygon_zkevm_bridge_operations, + celo_pending_account_operations: celo_pending_account_operations }), coin_balances_params_set = %{ @@ -257,6 +263,7 @@ defmodule Indexer.Block.Fetcher do shibarium_bridge_operations: shibarium_bridge_operations, celo_gas_tokens: celo_gas_tokens, celo_epochs: celo_l1_epochs ++ celo_l2_epochs, + celo_pending_account_operations: celo_pending_account_operations, arbitrum_messages: arbitrum_xlevel_messages, stability_validators: stability_validators } @@ -324,13 +331,18 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:shibarium_bridge_operations, %{params: shibarium_bridge_operations}) end - defp do_import_options(:celo, basic_import_options, %{celo_gas_tokens: celo_gas_tokens, celo_epochs: celo_epochs}) do + defp do_import_options(:celo, basic_import_options, %{ + celo_gas_tokens: celo_gas_tokens, + celo_epochs: celo_epochs, + celo_pending_account_operations: celo_pending_account_operations + }) do tokens = basic_import_options |> Map.get(:tokens, %{}) |> Map.get(:params, []) basic_import_options + |> Map.put_new(:celo_pending_account_operations, %{params: celo_pending_account_operations}) |> Map.put_new(:celo_epochs, %{params: celo_epochs}) |> Map.put( :tokens, @@ -543,6 +555,13 @@ defmodule Indexer.Block.Fetcher do def async_import_celo_epoch_block_operations(_, _), do: :ok + @spec async_import_celo_accounts(map(), boolean()) :: :ok + def async_import_celo_accounts(%{celo_pending_account_operations: celo_pending_account_operations}, realtime?) do + CeloAccount.async_fetch(celo_pending_account_operations, realtime?) + end + + def async_import_celo_accounts(_, _), do: :ok + def async_import_filecoin_addresses_info(%{addresses: addresses}, realtime?) do addresses |> Enum.map(&%FilecoinPendingAddressOperation{address_hash: &1.hash}) @@ -823,4 +842,17 @@ defmodule Indexer.Block.Fetcher do end) |> List.flatten() end + + defp parse_celo_pending_account_operations(logs) do + if chain_type() == :celo do + logs + |> CeloAccountsTransform.parse() + |> Map.take([:accounts, :attestations_fulfilled, :attestations_requested]) + |> Map.values() + |> Enum.concat() + |> Enum.uniq_by(& &1.address_hash) + else + [] + end + end end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 38b80cb147a7..5e64fca5c5e3 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -16,6 +16,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_blobs: 2, async_import_block_rewards: 2, async_import_celo_epoch_block_operations: 2, + async_import_celo_accounts: 2, async_import_created_contract_codes: 2, async_import_filecoin_addresses_info: 2, async_import_internal_transactions: 2, @@ -551,6 +552,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_blobs(imported, realtime?) async_import_polygon_zkevm_bridge_l1_tokens(imported) async_import_celo_epoch_block_operations(imported, realtime?) + async_import_celo_accounts(imported, realtime?) async_import_filecoin_addresses_info(imported, realtime?) async_import_signed_authorizations_statuses(imported, realtime?) end diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex index 9ce3cc842b67..1ddef78beb34 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/distributions.ex @@ -6,7 +6,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.Distributions do use Utils.RuntimeEnvHelper, celo_unreleased_treasury_contract_address: [ :explorer, - [:celo, :celo_unreleased_treasury_contract_address] + [:celo, :unreleased_treasury_contract_address] ] import Ecto.Query, only: [from: 2, subquery: 1] diff --git a/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex new file mode 100644 index 000000000000..04732263fdaa --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex @@ -0,0 +1,152 @@ +defmodule Indexer.Fetcher.Celo.Legacy.Account do + @moduledoc """ + Asynchronously fetches Celo blockchain account data from event logs and + imports them into the database. + + This fetcher processes blockchain logs to extract account-related events, + retrieves detailed account information from Celo smart contracts, and performs + bulk imports of account data. It supports batching and concurrency for + efficient processing. + + The fetcher integrates with the BufferedTask system to handle asynchronous + processing and automatic retry logic for failed operations. + + ## Note + + This implementation is ported from Celo's fork of Blockscout and could be + revised in future iterations. + """ + + alias Explorer.Chain + alias Explorer.Chain.Celo.PendingAccountOperation + alias Indexer.BufferedTask + alias Indexer.Fetcher.Celo.Legacy.Account.Reader, as: AccountReader + alias Indexer.Transform.Addresses + + require Logger + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + @behaviour BufferedTask + + @doc false + def child_spec([init_options, gen_server_options]) do + {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) + + unless state do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec` " <> + "to allow for json_rpc calls when running." + end + + merged_init_opts = + defaults() + |> Keyword.merge(mergeable_init_options) + |> Keyword.put(:state, state) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + def defaults do + [ + poll: true, + flush_interval: :timer.seconds(3), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency], + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size], + task_supervisor: __MODULE__.TaskSupervisor, + metadata: [fetcher: :celo_accounts] + ] + end + + @doc """ + Asynchronously enqueues processing of Celo account operations. + + ## Parameters + - `operations`: List of `PendingAccountOperation` structs to process + - `realtime?`: Boolean indicating if this is realtime processing + - `timeout`: Timeout for buffering tasks (default: 5000ms) + + ## Returns + - `:ok` once the operations have been enqueued + """ + @spec async_fetch([PendingAccountOperation.t()], boolean(), timeout()) :: :ok + def async_fetch(operations, realtime?, timeout \\ 5000) when is_list(operations) do + if __MODULE__.Supervisor.disabled?() do + :ok + else + unique_operations = Enum.uniq_by(operations, & &1.address_hash) + BufferedTask.buffer(__MODULE__, unique_operations, realtime?, timeout) + end + end + + @impl BufferedTask + def init(initial, reducer, _) do + {:ok, final} = + PendingAccountOperation.stream( + initial, + reducer, + true + ) + + final + end + + @impl BufferedTask + def run(accounts, _json_rpc_named_arguments) do + accounts + |> fetch_from_blockchain() + |> import_accounts() + |> case do + :ok -> :ok + :error -> {:retry, accounts} + end + end + + defp fetch_from_blockchain(operations) do + operations + |> Enum.map(fn + %{voter: _} = account -> + Map.put(account, :error, :unresolved_voter) + + account -> + account.address_hash + |> to_string() + |> AccountReader.fetch() + |> case do + {:ok, data} -> data + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + end + + defp import_accounts(accounts) do + addresses = + Addresses.extract_addresses(%{ + celo_accounts: accounts + }) + + import_params = %{ + addresses: %{params: addresses}, + celo_accounts: %{params: accounts}, + timeout: :infinity + } + + case Chain.import(import_params) do + {:ok, _imported} -> + Logger.info("Imported #{Enum.count(accounts)} Celo accounts.") + + accounts + |> Enum.map(& &1.address_hash) + |> PendingAccountOperation.delete_by_address_hashes() + + :ok + + {:error, reason} -> + Logger.error(fn -> ["failed to import Celo account data: ", inspect(reason)] end) + + :error + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/celo/legacy/account/reader.ex b/apps/indexer/lib/indexer/fetcher/celo/legacy/account/reader.ex new file mode 100644 index 000000000000..a1663b45c061 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/celo/legacy/account/reader.ex @@ -0,0 +1,267 @@ +defmodule Indexer.Fetcher.Celo.Legacy.Account.Reader do + @moduledoc """ + Reads Celo account data from core smart contracts. + + This module provides functionality to fetch account information including + name, metadata URL, locked gold amounts, and validator status from the + Celo blockchain's core contracts. + """ + require Logger + + use Utils.RuntimeEnvHelper, + accounts_contract_address_hash: [:explorer, [:celo, :accounts_contract_address]], + locked_gold_contract_address_hash: [:explorer, [:celo, :locked_gold_contract_address]], + validators_contract_address_hash: [:explorer, [:celo, :validators_contract_address]], + json_rpc_named_arguments: [:indexer, :json_rpc_named_arguments] + + import Explorer.Helper, only: [abi_to_method_id: 1] + + import Indexer.Helper, + only: [read_contracts_with_retries: 4] + + @repeated_request_max_retries 3 + + @abi %{ + accounts: %{ + get_name: %{ + "constant" => true, + "inputs" => [ + %{"internalType" => "address", "name" => "account", "type" => "address"} + ], + "name" => "getName", + "outputs" => [ + %{"internalType" => "string", "name" => "", "type" => "string"} + ], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + get_metadata_url: %{ + "constant" => true, + "inputs" => [ + %{"internalType" => "address", "name" => "account", "type" => "address"} + ], + "name" => "getMetadataURL", + "outputs" => [ + %{"internalType" => "string", "name" => "", "type" => "string"} + ], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + get_vote_signer: %{ + "constant" => true, + "inputs" => [%{"name" => "account", "type" => "address"}], + "name" => "getVoteSigner", + "outputs" => [%{"name" => "", "type" => "address"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + get_validator_signer: %{ + "constant" => true, + "inputs" => [%{"name" => "account", "type" => "address"}], + "name" => "getValidatorSigner", + "outputs" => [%{"name" => "", "type" => "address"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + get_attestation_signer: %{ + "constant" => true, + "inputs" => [%{"name" => "account", "type" => "address"}], + "name" => "getAttestationSigner", + "outputs" => [%{"name" => "", "type" => "address"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + }, + locked_gold: %{ + get_account_total_locked_gold: %{ + "constant" => true, + "inputs" => [ + %{"internalType" => "address", "name" => "account", "type" => "address"} + ], + "name" => "getAccountTotalLockedGold", + "outputs" => [ + %{"internalType" => "uint256", "name" => "", "type" => "uint256"} + ], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + get_account_nonvoting_locked_gold: %{ + "constant" => true, + "inputs" => [ + %{"internalType" => "address", "name" => "account", "type" => "address"} + ], + "name" => "getAccountNonvotingLockedGold", + "outputs" => [ + %{"internalType" => "uint256", "name" => "", "type" => "uint256"} + ], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + }, + validators: %{ + is_validator: %{ + "constant" => true, + "inputs" => [%{"name" => "_someone", "type" => "address"}], + "name" => "isValidator", + "outputs" => [%{"name" => "", "type" => "bool"}], + "payable" => false, + "signature" => "0xfacd743b", + "stateMutability" => "view", + "type" => "function" + }, + is_validator_group: %{ + "constant" => true, + "inputs" => [ + %{"internalType" => "address", "name" => "account", "type" => "address"} + ], + "name" => "isValidatorGroup", + "outputs" => [%{"internalType" => "bool", "name" => "", "type" => "bool"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + } + } + + @abis @abi |> Map.values() |> Enum.flat_map(&Map.values/1) + + @doc """ + Read Celo account data from core smart contracts. + """ + @spec fetch(String.t()) :: {:ok, map()} | :error + def fetch(account_address) do + account_address + |> do_fetch() + |> case do + {:ok, + [ + {:ok, [name]}, + {:ok, [url]}, + {:ok, [locked_gold]}, + {:ok, [nonvoting_locked_gold]}, + {:ok, [is_validator]}, + {:ok, [is_validator_group]}, + {:ok, [vote_signer]}, + {:ok, [validator_signer]}, + {:ok, [attestation_signer]} + ]} -> + type = + cond do + is_validator -> + :validator + + is_validator_group -> + :group + + true -> + :regular + end + + {:ok, + %{ + address_hash: account_address, + name: truncate(name), + metadata_url: truncate(url), + locked_celo: locked_gold, + nonvoting_locked_celo: nonvoting_locked_gold, + type: type, + vote_signer_address_hash: normalize_signer(account_address, vote_signer), + validator_signer_address_hash: normalize_signer(account_address, validator_signer), + attestation_signer_address_hash: normalize_signer(account_address, attestation_signer) + }} + + {:error, errors} -> + Logger.error(fn -> + ["Failed to fetch Celo account data for ", account_address, ": ", inspect(errors)] + end) + + :error + end + end + + @spec truncate(binary()) :: binary() + defp truncate(binary) when is_binary(binary) do + String.slice(binary, 0, 255) + end + + @spec normalize_signer(String.t(), String.t()) :: String.t() | nil + defp normalize_signer(address_hash, signer_address_hash) do + if address_hash == signer_address_hash do + nil + else + signer_address_hash + end + end + + @spec do_fetch(String.t()) :: {:ok, keyword()} | {:error, any()} + defp do_fetch(account_address) do + requests = [ + %{ + contract_address: accounts_contract_address_hash(), + method_id: abi_to_method_id(@abi.accounts.get_name), + args: [account_address] + }, + %{ + contract_address: accounts_contract_address_hash(), + method_id: abi_to_method_id(@abi.accounts.get_metadata_url), + args: [account_address] + }, + %{ + contract_address: locked_gold_contract_address_hash(), + method_id: abi_to_method_id(@abi.locked_gold.get_account_total_locked_gold), + args: [account_address] + }, + %{ + contract_address: locked_gold_contract_address_hash(), + method_id: abi_to_method_id(@abi.locked_gold.get_account_nonvoting_locked_gold), + args: [account_address] + }, + %{ + contract_address: validators_contract_address_hash(), + method_id: abi_to_method_id(@abi.validators.is_validator), + args: [account_address] + }, + %{ + contract_address: validators_contract_address_hash(), + method_id: abi_to_method_id(@abi.validators.is_validator_group), + args: [account_address] + }, + %{ + contract_address: accounts_contract_address_hash(), + method_id: abi_to_method_id(@abi.accounts.get_vote_signer), + args: [account_address] + }, + %{ + contract_address: accounts_contract_address_hash(), + method_id: abi_to_method_id(@abi.accounts.get_validator_signer), + args: [account_address] + }, + %{ + contract_address: accounts_contract_address_hash(), + method_id: abi_to_method_id(@abi.accounts.get_attestation_signer), + args: [account_address] + } + ] + + requests + |> read_contracts_with_retries( + @abis, + json_rpc_named_arguments(), + @repeated_request_max_retries + ) + |> case do + {responses, []} -> + {:ok, responses} + + {_responses, errors} -> + {:error, errors} + end + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 15a606afcb0c..5a7136aecaff 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -249,6 +249,9 @@ defmodule Indexer.Supervisor do configure(Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, [ [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] ]), + configure(Indexer.Fetcher.Celo.Legacy.Account.Supervisor, [ + [json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor] + ]), {Indexer.Fetcher.Filecoin.AddressInfo.Supervisor, [ [ diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index ca4e4939ebff..92b43cf8e537 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -176,6 +176,22 @@ defmodule Indexer.Transform.Addresses do [ %{from: :group_address_hash, to: :hash} ] + ], + celo_accounts: [ + [ + %{from: :vote_signer_address_hash, to: :hash} + ], + [ + %{from: :validator_signer_address_hash, to: :hash} + ], + [ + %{from: :attestation_signer_address_hash, to: :hash} + ] + ], + celo_pending_account_operations: [ + [ + %{from: :address_hash, to: :hash} + ] ] } @@ -499,6 +515,18 @@ defmodule Indexer.Transform.Addresses do required(:account_address_hash) => String.t(), required(:group_address_hash) => String.t() } + ], + optional(:celo_accounts) => [ + %{ + optional(:vote_signer_address_hash) => String.t() | nil, + optional(:validator_signer_address_hash) => String.t() | nil, + optional(:attestation_signer_address_hash) => String.t() | nil + } + ], + optional(:celo_pending_account_operations) => [ + %{ + required(:address_hash) => String.t() + } ] }) :: [params] def extract_addresses(fetched_data, options \\ []) when is_map(fetched_data) and is_list(options) do diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 65a1592aa7d5..7df78c3fb1c2 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -7,6 +7,8 @@ defmodule Indexer.Block.FetcherTest do import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain + alias Explorer.Chain.Celo.Legacy.Events + alias Explorer.Chain.Celo.PendingAccountOperation alias Explorer.Chain.{Address, Log, Transaction, Wei} alias Indexer.Block.Fetcher alias Indexer.BufferedTask @@ -837,6 +839,7 @@ defmodule Indexer.Block.FetcherTest do block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher } do block_number = @first_full_block_number + accounts_contract_address = "0x000000000000000000000000000000000000ce10" if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do case Keyword.fetch!(json_rpc_named_arguments, :variant) do @@ -847,6 +850,23 @@ defmodule Indexer.Block.FetcherTest do transaction_hash = "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" gas_token_contract_address_hash = "0x471ece3750da237f93b8e339c536989b8978a438" + account_event_topic = hd(Events.account_events()) + pending_address = to_address_hash + pending_suffix = String.slice(pending_address, 2, 40) + pending_topic = "0x000000000000000000000000" <> pending_suffix + + account_event_log = %{ + "address" => accounts_contract_address, + "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", + "blockNumber" => "0x25", + "data" => "0x", + "logIndex" => "0x1", + "topics" => [account_event_topic, pending_topic], + "transactionHash" => transaction_hash, + "transactionIndex" => "0x0", + "transactionLogIndex" => "0x1" + } + transaction = %{ "blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", "blockNumber" => "0x25", @@ -949,7 +969,8 @@ defmodule Indexer.Block.FetcherTest do "transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", "transactionIndex" => "0x0", "transactionLogIndex" => "0x0" - } + }, + account_event_log ], "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", @@ -966,7 +987,7 @@ defmodule Indexer.Block.FetcherTest do end) # async requests need to be grouped in one expect because the order is non-deterministic while multiple expect # calls on the same name/arity are used in order - |> expect(:json_rpc, 5, fn json, _options -> + |> expect(:json_rpc, 7, fn json, _options -> case json do [ %{ @@ -1019,6 +1040,12 @@ defmodule Indexer.Block.FetcherTest do [%{id: id, method: "eth_getBalance", params: [^from_address_hash, ^block_quantity]}] -> {:ok, [%{id: id, jsonrpc: "2.0", result: "0xd0d4a965ab52d8cd740000"}]} + [%{id: id, method: "eth_getBalance", params: [^accounts_contract_address, ^block_quantity]}] -> + {:ok, [%{id: id, jsonrpc: "2.0", result: "0xd0d4a965ab52d8cd740000"}]} + + [%{id: id, method: "eth_getBalance", params: [_, ^block_quantity]}] -> + {:ok, [%{id: id, jsonrpc: "2.0", result: "0x0"}]} + [%{id: id, method: "trace_replayBlockTransactions", params: [^block_quantity, ["trace"]]}] -> {:ok, [ @@ -1090,14 +1117,34 @@ defmodule Indexer.Block.FetcherTest do end end + account_supervisor_configuration = + Application.get_env(:indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor) + + Application.put_env(:indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor, disabled?: true) + + on_exit(fn -> + if account_supervisor_configuration do + Application.put_env( + :indexer, + Indexer.Fetcher.Celo.Legacy.Account.Supervisor, + account_supervisor_configuration + ) + else + Application.delete_env(:indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor) + end + end) + case Keyword.fetch!(json_rpc_named_arguments, :variant) do EthereumJSONRPC.Nethermind -> gateway_fee_value = Decimal.new(0) + {:ok, accounts_contract_address} = Explorer.Chain.Hash.Address.cast(accounts_contract_address) + assert {:ok, %{ inserted: %{ addresses: [ + %Address{hash: ^accounts_contract_address}, %Address{ hash: %Explorer.Chain.Hash{ @@ -1136,6 +1183,15 @@ defmodule Indexer.Block.FetcherTest do <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> } + }, + %Log{ + index: 1, + transaction_hash: %Explorer.Chain.Hash{ + byte_count: 32, + bytes: + <<83, 189, 136, 72, 114, 222, 62, 72, 134, 146, 136, 27, 174, 236, 38, 46, 123, 149, 35, + 77, 57, 101, 36, 140, 57, 254, 153, 47, 255, 212, 51, 229>> + } } ], transactions: [ @@ -1157,6 +1213,16 @@ defmodule Indexer.Block.FetcherTest do gas_fee_recipient_address_hash: nil, gateway_fee: %Explorer.Chain.Wei{value: ^gateway_fee_value} } + ], + celo_pending_account_operations: [ + %PendingAccountOperation{ + address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: + <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, + 223, 65, 91>> + } + } ] }, errors: [] @@ -1172,9 +1238,10 @@ defmodule Indexer.Block.FetcherTest do wait_for_tasks(CoinBalanceCatchup) assert Repo.aggregate(Chain.Block, :count, :hash) == 1 - assert Repo.aggregate(Address, :count, :hash) == 2 - assert Repo.aggregate(Log, :count) == 1 + assert Repo.aggregate(Address, :count, :hash) == 3 + assert Repo.aggregate(Log, :count) == 2 assert Repo.aggregate(Transaction, :count, :hash) == 1 + assert Repo.aggregate(PendingAccountOperation, :count, :address_hash) == 1 first_address = Repo.get!(Address, first_address_hash) diff --git a/config/runtime.exs b/config/runtime.exs index f42613cb3531..ece8d4015957 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -595,8 +595,10 @@ celo_epoch_manager_contract_address = System.get_env("CELO_EPOCH_MANAGER_CONTRAC config :explorer, :celo, l2_migration_block: celo_l2_migration_block, epoch_manager_contract_address: celo_epoch_manager_contract_address, - celo_unreleased_treasury_contract_address: System.get_env("CELO_UNRELEASED_TREASURY_CONTRACT"), - validators_contract_address: System.get_env("CELO_VALIDATORS_CONTRACT") + unreleased_treasury_contract_address: System.get_env("CELO_UNRELEASED_TREASURY_CONTRACT"), + validators_contract_address: System.get_env("CELO_VALIDATORS_CONTRACT"), + locked_gold_contract_address: System.get_env("CELO_LOCKED_GOLD_CONTRACT"), + accounts_contract_address: System.get_env("CELO_ACCOUNTS_CONTRACT") config :explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: ConfigHelper.parse_json_env_var("CELO_CORE_CONTRACTS") @@ -805,6 +807,8 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_CONCURRENCY", 1) +config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_type() == :celo + config :explorer, Explorer.Migrator.CeloL2Epochs, enabled: ConfigHelper.chain_type() == :celo && @@ -1502,6 +1506,14 @@ config :indexer, Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, enabled: celo_epoch_fetchers_enabled?, disabled?: not celo_epoch_fetchers_enabled? +config :indexer, Indexer.Fetcher.Celo.Legacy.Account, + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_CELO_ACCOUNTS_CONCURRENCY", 1), + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_CELO_ACCOUNTS_BATCH_SIZE", 100) + +config :indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor, + enabled: ConfigHelper.chain_type() == :celo, + disabled?: not (ConfigHelper.chain_type() == :celo) + config :indexer, Indexer.Fetcher.Filecoin.BeryxAPI, base_url: ConfigHelper.safe_get_env("BERYX_API_BASE_URL", "https://api.zondax.ch/fil/data/v3/mainnet"), api_token: System.get_env("BERYX_API_TOKEN") diff --git a/cspell.json b/cspell.json index 72a75824c719..3418514dcbdb 100644 --- a/cspell.json +++ b/cspell.json @@ -178,6 +178,7 @@ "datepicker", "DATETIME", "deae", + "deaffiliated", "decamelize", "decompiled", "decompiler", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 08f7749b6e06..852bda3c5497 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -362,9 +362,13 @@ API_V1_WRITE_METHODS_DISABLED=false # CELO_EPOCH_MANAGER_CONTRACT= # CELO_UNRELEASED_TREASURY_CONTRACT= # CELO_VALIDATORS_CONTRACT= +# CELO_LOCKED_GOLD_CONTRACT= +# CELO_ACCOUNTS_CONTRACT= # INDEXER_CELO_VALIDATOR_GROUP_VOTES_BATCH_SIZE=200000 # INDEXER_DISABLE_CELO_EPOCH_FETCHER=false # INDEXER_DISABLE_CELO_VALIDATOR_GROUP_VOTES_FETCHER=false +# INDEXER_CELO_ACCOUNTS_CONCURRENCY=1 +# INDEXER_CELO_ACCOUNTS_BATCH_SIZE=100 # BERYX_API_TOKEN= # BERYX_API_BASE_URL= # FILECOIN_NETWORK_PREFIX=f From a0f25b8906975d268cf8ede327dfad6360c27590 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 13 Oct 2025 13:19:27 +0300 Subject: [PATCH 0388/1006] feat: celo accounts api (#12982) --- .../controllers/api/v2/address_controller.ex | 33 ++++++++--- .../schemas/api/v2/address/response.ex | 55 ++++++++++++++++++- .../views/api/v2/address_view.ex | 6 ++ .../block_scout_web/views/api/v2/celo_view.ex | 42 +++++++++++++- apps/explorer/lib/explorer/chain/address.ex | 12 ++++ .../lib/explorer/chain/celo/account.ex | 9 ++- .../indexer/fetcher/celo/legacy/account.ex | 11 +++- .../lib/indexer/transform/addresses.ex | 4 ++ 8 files changed, 158 insertions(+), 14 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index cbeeb69e4f4b..1bef35ef89a1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -89,14 +89,32 @@ defmodule BlockScoutWeb.API.V2.AddressController do api?: true ] + case @chain_type do + :celo -> + @chain_type_address_necessity_by_association %{ + [ + celo_account: [ + :vote_signer_address, + :validator_signer_address, + :attestation_signer_address + ] + ] => :optional + } + + _ -> + @chain_type_address_necessity_by_association %{} + end + @address_options [ - necessity_by_association: %{ - :names => :optional, - :scam_badge => :optional, - :signed_authorization => :optional, - :smart_contract => :optional, - [token: reputation_association()] => :optional - }, + necessity_by_association: + %{ + :names => :optional, + :scam_badge => :optional, + :signed_authorization => :optional, + :smart_contract => :optional, + [token: reputation_association()] => :optional + } + |> Map.merge(@chain_type_address_necessity_by_association), api?: true ] @@ -238,6 +256,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do @spec counters(Plug.Conn.t(), map()) :: {:format, :error} | {:restricted_access, true} | Plug.Conn.t() def counters(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do + # TODO: check if @address_options is needed here case Chain.hash_to_address(address_hash, @address_options) do {:ok, address} -> {validation_count} = Counters.address_counters(address, @api_true) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index 36815e342598..1fe47a70219c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations @moduledoc false require OpenApiSpex - alias BlockScoutWeb.Schemas.Helper + alias BlockScoutWeb.Schemas.{API.V2.Address, Helper} alias OpenApiSpex.Schema @filecoin_robust_address_schema %Schema{ @@ -21,6 +21,52 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations additionalProperties: false } + @celo_schema %Schema{ + type: :object, + properties: %{ + account: %Schema{ + type: :object, + nullable: true, + properties: %{ + type: %Schema{ + type: :string, + enum: [:regular, :validator, :group], + nullable: false + }, + name: %Schema{type: :string, nullable: true}, + metadata_url: %Schema{type: :string, nullable: true}, + nonvoting_locked_celo: %Schema{type: :string, nullable: false}, + locked_celo: %Schema{type: :string, nullable: false}, + vote_signer_address: %Schema{allOf: [Address], nullable: true}, + validator_signer_address: %Schema{allOf: [Address], nullable: true}, + attestation_signer_address: %Schema{allOf: [Address], nullable: true} + }, + required: [ + :type, + :name, + :metadata_url, + :nonvoting_locked_celo, + :locked_celo, + :vote_signer_address, + :validator_signer_address, + :attestation_signer_address + ], + example: %{ + type: "validator", + name: "Celo Validator", + metadata_url: "https://example.com/metadata", + nonvoting_locked_celo: "1000000000000000000", + locked_celo: "2000000000000000000", + vote_signer_address: nil, + validator_signer_address: nil, + attestation_signer_address: nil + } + } + }, + required: [:account], + additionalProperties: false + } + @doc """ Applies chain-specific field customizations to the given schema based on the configured chain type. @@ -47,6 +93,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations required: [:zilliqa] ) + :celo -> + schema + |> Helper.extend_schema( + properties: %{celo: @celo_schema}, + required: [:celo] + ) + _ -> schema end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index a2da2fe63718..228e5a9bca30 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -282,6 +282,12 @@ defmodule BlockScoutWeb.API.V2.AddressView do }) end + :celo -> + defp chain_type_fields(result, %{address: address}) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.CeloView.extend_address_json_response(result, address) + end + :zilliqa -> defp chain_type_fields(result, %{address: address}) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index d3c123a823a3..2a427947386e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -12,8 +12,8 @@ defmodule BlockScoutWeb.API.V2.CeloView do alias Explorer.Chain alias Explorer.Chain.Cache.{CeloCoreContracts, CeloEpochs} alias Explorer.Chain.Celo.Helper, as: CeloHelper - alias Explorer.Chain.Celo.{Epoch, EpochReward} - alias Explorer.Chain.{Address.Reputation, Block, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.Celo.{Account, Epoch, EpochReward} + alias Explorer.Chain.{Address, Address.Reputation, Block, Token, TokenTransfer, Transaction, Wei} @address_params [ necessity_by_association: %{ @@ -234,6 +234,44 @@ defmodule BlockScoutWeb.API.V2.CeloView do Map.put(out_json, :celo, celo_json) end + def extend_address_json_response(out_json, %Address{} = address) do + account_json = + address + |> Map.get(:celo_account) + |> case do + %Account{} = account -> + %{ + type: account.type, + name: account.name, + metadata_url: account.metadata_url, + nonvoting_locked_celo: account.nonvoting_locked_celo, + locked_celo: account.locked_celo, + vote_signer_address: + Helper.address_with_info( + account.vote_signer_address, + account.vote_signer_address_hash + ), + validator_signer_address: + Helper.address_with_info( + account.validator_signer_address, + account.validator_signer_address_hash + ), + attestation_signer_address: + Helper.address_with_info( + account.attestation_signer_address, + account.attestation_signer_address_hash + ) + } + + _ -> + nil + end + + Map.put(out_json, :celo, %{ + account: account_json + }) + end + @doc """ Extends the JSON output with a sub-map containing information about the gas token used to pay for the transaction fees. diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index b2d248b3b998..542977167d2c 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -67,6 +67,18 @@ defmodule Explorer.Chain.Address.Schema do ] end + :celo -> + quote do + [ + has_one( + :celo_account, + Explorer.Chain.Celo.Account, + foreign_key: :address_hash, + references: :hash + ) + ] + end + _ -> [] end) diff --git a/apps/explorer/lib/explorer/chain/celo/account.ex b/apps/explorer/lib/explorer/chain/celo/account.ex index 4f918904f496..ff1ed918c603 100644 --- a/apps/explorer/lib/explorer/chain/celo/account.ex +++ b/apps/explorer/lib/explorer/chain/celo/account.ex @@ -13,7 +13,7 @@ defmodule Explorer.Chain.Celo.Account do alias Explorer.Chain.{Address, Hash, Wei} - @required_attrs ~w(address_hash )a + @required_attrs ~w(address_hash)a @optional_attrs [ :type, :name, @@ -24,6 +24,7 @@ defmodule Explorer.Chain.Celo.Account do :validator_signer_address_hash, :attestation_signer_address_hash ] + @allowed_attrs @required_attrs ++ @optional_attrs @typedoc """ @@ -33,6 +34,12 @@ defmodule Explorer.Chain.Celo.Account do * `metadata_url` - URL to additional account metadata * `locked_celo` - total amount of CELO locked by this account * `nonvoting_locked_celo` - amount of locked CELO that is not used for voting + * `vote_signer_address_hash` – Address authorized to vote in governance and + validator elections on behalf of this account. + * `validator_signer_address_hash` – Address authorized to manage a validator + or validator group and sign consensus messages for this account. + * `attestation_signer_address_hash` – Address whose key this account uses to + sign attestations on the Attestations contract. """ @primary_key false typed_schema "celo_accounts" do diff --git a/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex index 04732263fdaa..b03e17aa4622 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex @@ -128,9 +128,14 @@ defmodule Indexer.Fetcher.Celo.Legacy.Account do }) import_params = %{ - addresses: %{params: addresses}, - celo_accounts: %{params: accounts}, - timeout: :infinity + addresses: %{ + params: addresses, + timeout: :infinity + }, + celo_accounts: %{ + params: accounts, + timeout: :infinity + } } case Chain.import(import_params) do diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index 92b43cf8e537..8ab87631ad0f 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -178,6 +178,9 @@ defmodule Indexer.Transform.Addresses do ] ], celo_accounts: [ + [ + %{from: :address_hash, to: :hash} + ], [ %{from: :vote_signer_address_hash, to: :hash} ], @@ -518,6 +521,7 @@ defmodule Indexer.Transform.Addresses do ], optional(:celo_accounts) => [ %{ + optional(:address_hash) => String.t() | nil, optional(:vote_signer_address_hash) => String.t() | nil, optional(:validator_signer_address_hash) => String.t() | nil, optional(:attestation_signer_address_hash) => String.t() | nil From 2ac3dc8e1b354c771ed9f77d5905280c26745b22 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 13 Oct 2025 15:22:12 +0300 Subject: [PATCH 0389/1006] feat: Implement websocket endpoints support in the Universal Proxy config (#13167) Signed-off-by: dependabot[bot] Co-authored-by: Alexander Kolotov Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../api/v2/proxy/noves_fi_controller.ex | 6 +- .../api/v2/proxy/xname_controller.ex | 2 +- .../lib/block_scout_web/endpoint.ex | 29 +++- .../lib/block_scout_web/routers/api_router.ex | 21 --- .../universal_proxy.ex | 158 ++++++++++++++---- .../universal_proxy/socket_handler.ex | 69 ++++++++ .../universal_proxy/target_client.ex | 55 ++++++ 7 files changed, 276 insertions(+), 64 deletions(-) create mode 100644 apps/explorer/lib/explorer/third_party_integrations/universal_proxy/socket_handler.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/universal_proxy/target_client.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex index 11a309af7429..39a0cf3fc4fd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/noves_fi_controller.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.NovesFiController do action_fallback(BlockScoutWeb.API.V2.FallbackController) @doc """ - Function to handle GET requests to `/api/v2/proxy/noves-fi/transactions/:transaction_hash_param` endpoint. + Function to handle GET requests to `/api/v2/proxy/3rdparty/noves-fi/transactions/:transaction_hash_param` endpoint. """ @spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do @@ -27,7 +27,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.NovesFiController do end @doc """ - Function to handle GET requests to `/api/v2/proxy/noves-fi/addresses/:address_hash_param/transactions` endpoint. + Function to handle GET requests to `/api/v2/proxy/3rdparty/noves-fi/addresses/:address_hash_param/transactions` endpoint. """ @spec address_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def address_transactions(conn, %{"address_hash_param" => address_hash_string} = params) do @@ -42,7 +42,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.NovesFiController do end @doc """ - Function to handle GET requests to `/api/v2/proxy/noves-fi/transaction-descriptions` endpoint. + Function to handle GET requests to `/api/v2/proxy/3rdparty/noves-fi/transaction-descriptions` endpoint. """ @spec describe_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def describe_transactions(conn, _) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/xname_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/xname_controller.ex index aae469fd60b5..c308b63a7d86 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/xname_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/xname_controller.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.XnameController do action_fallback(BlockScoutWeb.API.V2.FallbackController) @doc """ - Function to handle GET requests to `/api/v2/proxy/xname/address/:address_hash_param` endpoint. + Function to handle GET requests to `/api/v2/proxy/3rdparty/xname/address/:address_hash_param` endpoint. """ @spec address(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def address(conn, %{"address_hash_param" => address_hash_string} = params) do diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index 6edc1dd63e33..22f88cab8307 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -8,6 +8,8 @@ defmodule BlockScoutWeb.Endpoint do cookie_domain: [:block_scout_web, :cookie_domain], session_cookie_ttl: [:block_scout_web, :session_cookie_ttl] + alias Explorer.ThirdPartyIntegrations.UniversalProxy + if @sql_sandbox do plug(Phoenix.Ecto.SQL.Sandbox, repo: Explorer.Repo) end @@ -96,9 +98,32 @@ defmodule BlockScoutWeb.Endpoint do def init(_key, config) do if config[:load_from_system_env] do port = System.get_env("PORT") || raise "expected the PORT environment variable to be set" - {:ok, Keyword.put(config, :http, [:inet6, port: port])} + {:ok, Keyword.put(config, :http, [:inet6, port: port, dispatch: dispatch()])} else - {:ok, config} + {:ok, + config + |> Keyword.put(:http, Keyword.put_new(Keyword.get(config, :http), :dispatch, dispatch()))} end end + + defp dispatch do + websocket_proxies = UniversalProxy.websocket_proxies() + + universal_proxy_routes = + websocket_proxies + |> Enum.map(fn {platform_id, url} -> + {"/api/v2/proxy/3rdparty/#{platform_id}", Explorer.ThirdPartyIntegrations.UniversalProxy.SocketHandler, + [url: url]} + end) + + all_routes = + [ + {:_, Phoenix.Endpoint.Cowboy2Handler, {BlockScoutWeb.Endpoint, []}} | universal_proxy_routes + ] + |> Enum.reverse() + + [ + {:_, all_routes} + ] + end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 42a8e8047aa5..cc6d11305b06 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -389,27 +389,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - # todo: deprecate in the next major/minor release in 10.0.0. Related frontend task https://github.com/blockscout/frontend/issues/2945. - scope "/3dparty" do - get("/:platform_id", V2.Proxy.UniversalProxyController, :index) - - scope "/noves-fi" do - get("/transactions/:transaction_hash_param", V2.Proxy.NovesFiController, :transaction) - - get("/addresses/:address_hash_param/transactions", V2.Proxy.NovesFiController, :address_transactions) - - get("/transaction-descriptions", V2.Proxy.NovesFiController, :describe_transactions) - end - - scope "/xname" do - get("/addresses/:address_hash_param", V2.Proxy.XnameController, :address) - end - - scope "/solidityscan" do - get("/smart-contracts/:address_hash/report", V2.SmartContractController, :solidityscan_report) - end - end - scope "/account-abstraction" do get("/operations/:operation_hash_param", V2.Proxy.AccountAbstractionController, :operation) get("/operations/:operation_hash_param/summary", V2.Proxy.AccountAbstractionController, :summary) diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex index d4c7b5cc6eb4..dde95bfde855 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex @@ -1,6 +1,6 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do @moduledoc """ - Module for universal proxying 3rd party API endpoints + A single point of entry for proxying third-party HTTP API endpoints """ require Logger alias Explorer.{Helper, HttpClient} @@ -12,6 +12,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do body: String.t(), headers: [{String.t(), String.t()}], method: atom() | nil, + protocol: atom() | nil, platform_config: map() | nil, platform_id: String.t() | nil } @@ -28,6 +29,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do } @allowed_methods [:get, :post, :put, :patch, :delete] + @allowed_protocols [:http, :https, :ws, :wss] @reserved_param_types ~w(address chain_id chain_id_dependent) @unexpected_error "Unexpected error when calling proxied endpoint" @@ -77,47 +79,59 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do - The function uses the `Explorer.HttpClient` with pre-configured adapter. - A timeout is applied to the request using the `@recv_timeout` module attribute. """ - @spec api_request(map()) :: {any(), integer()} + @spec api_request(map()) :: + {any(), integer()} | {:ok, pid()} | :ignore | {:error, {:already_started, pid()} | (reason :: term())} def api_request(proxy_params) do - %{ - url: url, - body: body, - headers: headers, - method: method, - platform_config: platform_config, - platform_id: platform_id - } = parse_proxy_params(proxy_params) - - error_message = - cond do - !platform_config -> - "Platform '#{platform_id}' not found in config or 'platforms' property doesn't exist at all" - - is_nil(url) -> - "'base_url' is not defined for platform_id '#{platform_id}' or 'base' endpoint is not defined or 'base' endpoint path is not defined" - - is_nil(method) -> - "Invalid HTTP request method for platform '#{platform_id}'" - - true -> - "Unexpected error" - end + proxy_params + |> parse_proxy_params() + |> api_request_inner() + end - with {:invalid_config, false} <- {:invalid_config, is_nil(url)}, - {:invalid_config, false} <- {:invalid_config, is_nil(method)}, + defp api_request_inner(%{ + url: url, + body: body, + headers: headers, + method: method, + protocol: protocol + }) + when protocol in [:http, :https] and not is_nil(url) and not is_nil(method) do + with {:invalid_config, false} <- {:invalid_config, is_nil(method)}, {:ok, %{status_code: status, body: body}} <- HttpClient.request(method, url, headers, body, recv_timeout: @recv_timeout) do {Helper.decode_json(body), status} else - {:invalid_config, true} -> - {"Invalid config: #{error_message}", 422} - err -> Logger.error(fn -> ["#{@unexpected_error}: ", inspect(err)] end) {@unexpected_error, 500} end end + defp api_request_inner(%{ + url: url, + method: method, + platform_config: platform_config, + platform_id: platform_id + }) do + error_message = construct_error_message(platform_config, platform_id, url, method) + {"Invalid config: #{error_message}", 422} + end + + defp construct_error_message(platform_config, platform_id, url, method) do + cond do + !platform_config -> + "Platform '#{platform_id}' not found in config or 'platforms' property doesn't exist at all" + + is_nil(url) -> + "'base_url' is not defined for platform_id '#{platform_id}' or 'base' endpoint is not defined or 'base' endpoint path is not defined" + + is_nil(method) -> + "Invalid HTTP request method for platform '#{platform_id}'" + + true -> + "Unexpected error" + end + end + @doc """ Parses the proxy parameters and constructs an API request. @@ -158,6 +172,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do endpoint_api_key = platform_config && platform_config["api_key"] method = parse_method(platform_config) + protocol = parse_protocol(platform_config) raw_url = with true <- not is_nil(platform_config), @@ -178,6 +193,7 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do body: "", headers: [], method: method, + protocol: protocol, platform_config: platform_config, platform_id: platform_id } @@ -189,21 +205,32 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do defp parse_method(nil), do: nil # sobelow_skip ["DOS.StringToAtom"] - defp parse_method(platform_config) do + defp parse_method(%{ + "endpoints" => %{ + "base" => %{"method" => method} + } + }) do raw_method = - platform_config["endpoints"]["base"]["method"] + method # limit size of the input to prevent memory leak |> String.slice(0..10) |> String.downcase() |> String.to_atom() - if raw_method in @allowed_methods do - raw_method - else - nil - end + if raw_method in @allowed_methods, do: raw_method, else: nil + end + + defp parse_method(_platform_config), do: nil + + # sobelow_skip ["DOS.StringToAtom"] + defp parse_protocol(%{"base_url" => base_url}) do + protocol = base_url |> URI.parse() |> Map.get(:scheme) |> String.to_atom() + + if protocol in @allowed_protocols, do: protocol, else: nil end + defp parse_protocol(_platform_config), do: nil + @spec parse_endpoint_api_key(api_request_params(), api_key() | nil, String.t() | nil) :: api_request_params() defp parse_endpoint_api_key(%{url: _url, headers: _headers} = map, nil, _platform_id), do: map @@ -216,6 +243,13 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do endpoint_api_key_value = System.get_env(api_key_env_name) case location do + "path" -> + if endpoint_api_key_value do + Map.put(map, :url, String.replace(url, ":#{param_name}", endpoint_api_key_value)) + else + map + end + "header" -> if endpoint_api_key["prefix"] do Map.put(map, :headers, [ @@ -352,4 +386,54 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do defp config_url do Application.get_env(:explorer, __MODULE__)[:config_url] end + + @doc """ + Retrieves a list of all WebSocket proxies that have been configured for use within the application. + + Returns: + - A list of tuples {platform_id, url}. + + ## Examples + + iex> Explorer.ThirdPartyIntegrations.UniversalProxy.websocket_proxies() + [ + {"ws_1", "wss://proxy1.example.com"}, + {"ws_2", "wss://proxy2.example.com"} + ] + """ + @spec websocket_proxies() :: [{String.t(), String.t()}] + def websocket_proxies do + if Mix.env() == :test do + [] + else + config = config() + + if is_nil(config["platforms"]) do + [] + else + config["platforms"] + |> Map.keys() + |> Enum.reduce([], fn platform_id, acc -> + proxy_params = parse_proxy_params(%{"platform_id" => platform_id}) + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if valid_websocket_url?(proxy_params.url) do + [{platform_id, proxy_params.url} | acc] + else + acc + end + end) + |> Enum.reverse() + end + end + end + + defp valid_websocket_url?(nil), do: false + + defp valid_websocket_url?(url) do + case URI.parse(url) do + %URI{scheme: scheme, host: host} when scheme in ["ws", "wss"] and not is_nil(host) -> true + _ -> false + end + end end diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/socket_handler.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/socket_handler.ex new file mode 100644 index 000000000000..809345551344 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/socket_handler.ex @@ -0,0 +1,69 @@ +defmodule Explorer.ThirdPartyIntegrations.UniversalProxy.SocketHandler do + @moduledoc """ + A simple WebSocket proxy handler that relays messages between a client and a target WebSocket server. + """ + @behaviour :cowboy_websocket + alias Explorer.ThirdPartyIntegrations.UniversalProxy.TargetClient + + # called after HTTP handshake + @spec init(req :: :cowboy_req.req(), opts :: keyword()) :: + {:cowboy_websocket, :cowboy_req.req(), map(), map()} + def init(req, opts) do + {:cowboy_websocket, req, %{target: nil, opts: opts}, %{idle_timeout: :infinity}} + end + + # called once WebSocket upgraded + @spec websocket_init(state :: map()) :: {:ok, map()} + def websocket_init(state) do + case TargetClient.start_link(self(), state.opts[:url]) do + {:ok, target_pid} -> + Process.monitor(target_pid) + {:ok, %{state | target: target_pid}} + + {:error, reason} -> + # Send error to client and close connection + {:reply, {:close, 1011, "Failed to connect to target: #{inspect(reason)}"}, state} + end + end + + @spec websocket_handle(frame :: tuple(), state :: map()) :: {:ok, map()} | {:reply, tuple(), map()} + def websocket_handle({:text, msg}, %{target: target_pid} = state) when not is_nil(target_pid) do + case TargetClient.forward(target_pid, msg) do + :ok -> + {:ok, state} + + {:error, _reason} -> + {:reply, {:close, 1011, "Target connection error"}, state} + end + end + + def websocket_handle({:text, _msg}, state) do + {:reply, {:close, 1011, "No target connection"}, state} + end + + @spec websocket_handle(other :: any(), state :: map()) :: {:ok, map()} + def websocket_handle(_other, state), do: {:ok, state} + + def websocket_info({:DOWN, _ref, :process, pid, _reason}, %{target: pid} = state) do + {:reply, {:close, 1011, "Target connection lost"}, %{state | target: nil}} + end + + def websocket_info({:EXIT, pid, _reason}, %{target: pid} = state) do + {:reply, {:close, 1011, "Target connection lost"}, %{state | target: nil}} + end + + @spec websocket_info(msg :: tuple(), state :: map()) :: {:ok, map()} | {:reply, tuple(), map()} + def websocket_info({:from_target, msg, :type, type}, state) do + {:reply, {type, msg}, state} + end + + def websocket_info(_, state), do: {:ok, state} + + @spec websocket_terminate(reason :: any(), state :: map()) :: :ok + def websocket_terminate(_reason, %{target: target_pid}) when not is_nil(target_pid) do + Process.exit(target_pid, :shutdown) + :ok + end + + def websocket_terminate(_reason, _state), do: :ok +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/target_client.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/target_client.ex new file mode 100644 index 000000000000..16a7a1a07fbc --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy/target_client.ex @@ -0,0 +1,55 @@ +defmodule Explorer.ThirdPartyIntegrations.UniversalProxy.TargetClient do + @moduledoc """ + A WebSocket client that connects to a target WebSocket server and forwards messages to/from a + parent process. + """ + use WebSockex + + @doc """ + Starts a WebSocket client that connects to the target URL and forwards messages to the parent process. + + ## Parameters + - `parent_pid`: The PID of the parent process that will receive forwarded messages. + - `url`: The WebSocket URL to connect to. + + ## Returns + - `{:ok, pid}` on successful connection + - `{:error, reason}` if the connection fails + """ + @spec start_link(parent_pid :: pid(), url :: String.t()) :: {:ok, pid()} | {:error, term()} + def start_link(parent_pid, url) do + WebSockex.start_link(url, __MODULE__, %{ + parent: parent_pid + }) + end + + ## Callbacks + + @impl true + def handle_frame({:text, msg}, %{parent: parent} = state) do + send(parent, {:from_target, msg, :type, :text}) + {:ok, state} + end + + @impl true + def handle_frame({:binary, msg}, %{parent: parent} = state) do + send(parent, {:from_target, msg, :type, :binary}) + {:ok, state} + end + + @doc """ + Forwards a text message to the target WebSocket server. + + ## Parameters + - `pid`: The PID of the TargetClient process. + - `msg`: The text message to send. + + ## Returns + - `:ok` on success + - `{:error, reason}` if the message cannot be sent + """ + @spec forward(pid :: pid(), msg :: String.t()) :: :ok | {:error, term()} + def forward(pid, msg) do + WebSockex.send_frame(pid, {:text, msg}) + end +end From 4f68cf22e0058a7733caee21ea982dea487ef94d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 13 Oct 2025 19:37:46 +0300 Subject: [PATCH 0390/1006] chore: OpenAPI spec for the REST API endpoints in token and CSV export controllers (#13311) --- apps/block_scout_web/.sobelow-conf | 2 +- .../lib/block_scout_web/api_spec.ex | 12 +- .../lib/block_scout_web/chain.ex | 55 +- .../controllers/api/v2/block_controller.ex | 32 +- .../api/v2/csv_export_controller.ex | 204 ++++++- .../controllers/api/v2/token_controller.ex | 375 ++++++++++++- .../lib/block_scout_web/paging_helper.ex | 18 + .../routers/tokens_api_v2_router.ex | 17 +- .../block_scout_web/schemas/api/v2/address.ex | 2 +- .../schemas/api/v2/beacon/deposit.ex | 2 +- .../block_scout_web/schemas/api/v2/general.ex | 523 +++++++++--------- .../schemas/api/v2/general/address_hash.ex | 6 + .../api/v2/general/address_hash_nullable.ex | 6 + .../schemas/api/v2/general/decoded_input.ex | 32 ++ .../api/v2/general/decoded_log_input.ex | 35 ++ .../schemas/api/v2/general/empty_string.ex | 5 + .../schemas/api/v2/general/float_string.ex | 6 + .../api/v2/general/float_string_nullable.ex | 6 + .../schemas/api/v2/general/full_hash.ex | 6 + .../api/v2/general/full_hash_nullable.ex | 6 + .../schemas/api/v2/general/hex_string.ex | 6 + .../api/v2/general/hex_string_nullable.ex | 6 + .../schemas/api/v2/general/implementation.ex | 22 + .../chain_type_customizations.ex | 37 ++ .../schemas/api/v2/general/integer_string.ex | 6 + .../api/v2/general/integer_string_nullable.ex | 6 + .../api/v2/general/method_name_nullable.ex | 11 + .../schemas/api/v2/general/null_string.ex | 5 + .../schemas/api/v2/general/proxy_type.ex | 12 + .../schemas/api/v2/general/tag.ex | 19 + .../schemas/api/v2/general/timestamp.ex | 10 + .../api/v2/general/timestamp_nullable.ex | 10 + .../schemas/api/v2/general/url_nullable.ex | 11 + .../schemas/api/v2/general/watchlist_name.ex | 16 + .../block_scout_web/schemas/api/v2/token.ex | 10 +- .../schemas/api/v2/token/counters.ex | 23 + .../schemas/api/v2/token/holder.ex | 85 +++ .../schemas/api/v2/token/response.ex | 18 + .../schemas/api/v2/token_instance.ex | 2 + .../api/v2/csv_export_controller_test.exs | 15 - .../api/v2/token_controller_test.exs | 126 +++-- .../test/support/api_schema_assertions.ex | 2 +- .../lib/explorer/chain/csv_export/helper.ex | 1 + cspell.json | 1 + 44 files changed, 1436 insertions(+), 374 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/empty_string.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/method_name_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/null_string.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/proxy_type.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/tag.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/url_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/watchlist_name.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/counters.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/holder.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex diff --git a/apps/block_scout_web/.sobelow-conf b/apps/block_scout_web/.sobelow-conf index 7c1fc3762411..64b8c550a18f 100644 --- a/apps/block_scout_web/.sobelow-conf +++ b/apps/block_scout_web/.sobelow-conf @@ -7,8 +7,8 @@ format: "compact", ignore: ["Config.Headers", "Config.CSWH", "XSS.SendResp", "XSS.Raw"], ignore_files: [ - "apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/utility/rate_limit_config_helper.ex", diff --git a/apps/block_scout_web/lib/block_scout_web/api_spec.ex b/apps/block_scout_web/lib/block_scout_web/api_spec.ex index 09e09051781a..91ec0265d4e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_spec.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_spec.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.ApiSpec do @moduledoc """ This module defines the API specification for the BlockScoutWeb application. """ - alias BlockScoutWeb.{Routers.ApiRouter} + alias BlockScoutWeb.Routers.{ApiRouter, TokensApiV2Router} alias OpenApiSpex.{Contact, Info, OpenApi, Paths, Server} alias Utils.Helper @@ -21,8 +21,16 @@ defmodule BlockScoutWeb.ApiSpec do email: "info@blockscout.com" } }, - paths: Paths.from_router(ApiRouter) + paths: + ApiRouter + |> Paths.from_router() + |> Map.merge(Paths.from_routes(routes_with_prefix(TokensApiV2Router, "/v2/tokens"))) } |> OpenApiSpex.resolve_schema_modules() end + + defp routes_with_prefix(router, prefix) do + router.__routes__() + |> Enum.map(fn %{path: path} = route -> %{route | path: prefix <> path} end) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 3c973a265c86..141df3fb7a64 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -226,17 +226,55 @@ defmodule BlockScoutWeb.Chain do "holders_count" => holders_count_string, "name" => name_string, "contract_address_hash" => contract_address_hash_string, - "is_name_null" => is_name_null_string + "is_name_null" => is_name_null } = params ) when is_binary(market_cap_string) and is_binary(holders_count_string) and is_binary(name_string) and - is_binary(contract_address_hash_string) and is_binary(is_name_null_string) do + is_binary(contract_address_hash_string) do market_cap_decimal = decimal_parse(market_cap_string) fiat_value_decimal = decimal_parse(params["fiat_value"]) holders_count = parse_integer(holders_count_string) - token_name = if is_name_null_string == "true", do: nil, else: name_string + token_name = if is_name_null, do: nil, else: name_string + + case Hash.Address.cast(contract_address_hash_string) do + {:ok, contract_address_hash} -> + [ + paging_options: %{ + @default_paging_options + | key: %{ + fiat_value: fiat_value_decimal, + circulating_market_cap: market_cap_decimal, + holder_count: holders_count, + name: token_name, + contract_address_hash: contract_address_hash + } + } + ] + + _ -> + [paging_options: @default_paging_options] + end + end + + def paging_options( + %{ + market_cap: market_cap_string, + holders_count: holders_count_string, + name: name_string, + contract_address_hash: contract_address_hash_string, + is_name_null: is_name_null + } = params + ) + when is_binary(market_cap_string) and is_binary(holders_count_string) and is_binary(name_string) and + is_binary(contract_address_hash_string) do + market_cap_decimal = decimal_parse(market_cap_string) + + fiat_value_decimal = decimal_parse(params[:fiat_value]) + + holders_count = parse_integer(holders_count_string) + token_name = if is_name_null, do: nil, else: name_string case Hash.Address.cast(contract_address_hash_string) do {:ok, contract_address_hash} -> @@ -485,6 +523,10 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {value, address_hash}}] end + def paging_options(%{value: value, address_hash: address_hash}) do + [paging_options: %{@default_paging_options | key: {value, address_hash}}] + end + def paging_options(%{"fiat_value" => fiat_value_string, "value" => value_string, "id" => id_string}) when is_binary(fiat_value_string) and is_binary(value_string) and is_binary(id_string) do with {id, ""} <- Integer.parse(id_string), @@ -954,12 +996,17 @@ defmodule BlockScoutWeb.Chain do def unique_tokens_paging_options(%{"unique_token" => token_id}), do: [paging_options: %{default_paging_options() | key: {token_id}}] + def unique_tokens_paging_options(%{unique_token: token_id}), + do: [paging_options: %{default_paging_options() | key: {token_id}}] + def unique_tokens_paging_options(_params), do: [paging_options: default_paging_options()] def unique_tokens_next_page([], _list, _params), do: nil def unique_tokens_next_page(_, list, params) do - Map.merge(params, paging_params(List.last(list))) + params + |> Map.merge(paging_params(List.last(list))) + |> delete_parameters_from_next_page_params() end def token_transfers_next_page_params([], _list, _params), do: nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 6cf8c35dadca..93ab33ec9c5d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -33,6 +33,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do WithdrawalView } + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.Chain alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader alias Explorer.Chain.Beacon.Deposit @@ -40,7 +41,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do alias Explorer.Chain.InternalTransaction alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Scroll.Reader, as: ScrollReader - alias Schemas.ErrorResponses.NotFoundResponse alias Timex.Duration case @chain_type do @@ -139,7 +139,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do tags(["blocks"]) - operation(:block, + operation :block, summary: "Retrieve detailed information about a specific block", description: "Retrieves detailed information for a specific block, including transactions, internal transactions, and metadata.", @@ -149,7 +149,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param` endpoint. @@ -180,7 +179,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - operation(:blocks, + operation :blocks, summary: "List blocks with optional filtering and sorting", description: "Retrieves a paginated list of blocks with optional filtering by block type and sorting options.", parameters: @@ -199,7 +198,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do title_prefix: "Blocks" )} ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks` endpoint. @@ -226,7 +224,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do }) end - operation(:arbitrum_batch, + operation :arbitrum_batch, summary: "List L2 blocks in an Arbitrum batch", description: "Retrieves L2 blocks that are bound to a specific Arbitrum batch number.", parameters: @@ -246,7 +244,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do )}, unprocessable_entity: JsonErrorResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/arbitrum-batch/:batch_number_param` endpoint. @@ -275,7 +272,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do }) end - operation(:optimism_batch, + operation :optimism_batch, summary: "List L2 blocks in an Optimism batch", description: "Retrieves L2 blocks that are bound to a specific Optimism batch number.", parameters: @@ -295,7 +292,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do )}, unprocessable_entity: JsonErrorResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/optimism-batch/:batch_number_param` endpoint. @@ -325,7 +321,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do }) end - operation(:scroll_batch, + operation :scroll_batch, summary: "List L2 blocks in a Scroll batch", description: "Retrieves L2 blocks that are bound to a specific Scroll batch number.", parameters: @@ -345,7 +341,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do )}, unprocessable_entity: JsonErrorResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/scroll-batch/:batch_number_param` endpoint. @@ -375,7 +370,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do }) end - operation(:transactions, + operation :transactions, summary: "List transactions in a specific block", description: "Retrieves transactions included in a specific block with optional filtering and sorting.", parameters: @@ -397,7 +392,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/transactions` endpoint. @@ -432,7 +426,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - operation(:internal_transactions, + operation :internal_transactions, summary: "List internal transactions in a specific block", description: "Retrieves internal transactions included in a specific block with optional filtering by type and call type.", @@ -454,7 +448,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/internal-transactions` endpoint. @@ -499,7 +492,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - operation(:withdrawals, + operation :withdrawals, summary: "List withdrawals in a specific block", description: "Retrieves validator withdrawals included in a specific block.", parameters: @@ -520,7 +513,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/:block_hash_or_number_param/withdrawals` endpoint. @@ -555,7 +547,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - operation(:block_countdown, + operation :block_countdown, summary: "Get countdown information for a target block number", description: "Calculates the estimated time remaining until a specified block number is reached based on current block and average block time.", @@ -565,7 +557,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Function to handle GET requests to `/api/v2/blocks/:block_number_param/countdown` endpoint. @@ -605,7 +596,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do end end - operation(:beacon_deposits, + operation :beacon_deposits, summary: "List beacon deposits in a specific block", description: "Retrieves beacon deposits included in a specific block with pagination support.", parameters: @@ -626,7 +617,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] - ) @doc """ Handles `api/v2/blocks/:block_hash_or_number_param/beacon/deposits` endpoint. diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index dff5132cc795..3700c27de5e8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -1,7 +1,9 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.Chain alias Explorer.Chain.Address alias Explorer.Chain.Address.CurrentTokenBalance @@ -20,14 +22,27 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + @api_true [api?: true] + operation :export_token_holders, + summary: "Export token holders as CSV", + description: "Exports the holders of a specific token as a CSV file.", + parameters: base_params() ++ [address_hash_param(), address_id_param()], + responses: [ + ok: {"CSV file of token holders.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["tokens"] + @doc """ Performs CSV export of token holders for a given address Endpoint: `/api/v2/tokens/:address_hash_param/holders/csv` """ @spec export_token_holders(Conn.t(), map()) :: Conn.t() - def export_token_holders(conn, %{"address_hash_param" => address_hash_string} = params) do + def export_token_holders(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do @@ -62,18 +77,17 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do defp items_csv( conn, %{ - # todo: eliminate this parameter in favour address_hash_param - "address_id" => address_hash_string, - "from_period" => from_period, - "to_period" => to_period + address_hash_param: address_hash_string, + from_period: from_period, + to_period: to_period } = params, csv_export_module ) when is_binary(address_hash_string) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:address_exists, true} <- {:address_exists, Address.address_exists?(address_hash)} do - filter_type = Map.get(params, "filter_type") - filter_value = Map.get(params, "filter_value") + filter_type = Map.get(params, :filter_type) + filter_value = Map.get(params, :filter_value) address_hash |> csv_export_module.export(from_period, to_period, fetch_scam_token_toggle([], conn), filter_type, filter_value) @@ -97,22 +111,198 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do defp items_csv(conn, _, _), do: not_found(conn) + operation :token_transfers_csv, + summary: "Export token transfers as CSV", + description: "Exports token transfers for a specific address as a CSV file.", + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], + responses: [ + ok: {"CSV file of token transfers.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["addresses"] + + @doc """ + Handles CSV export of token transfers for a given address. + + ## Parameters + + - `conn`: The Plug connection. + - `params`: A map of request parameters. + + ## Returns + + - The updated Plug connection with the CSV response. + + Delegates the CSV generation to `AddressTokenTransfersCsvExporter`. + """ + @spec token_transfers_csv(Conn.t(), map()) :: Conn.t() def token_transfers_csv(conn, params) do items_csv(conn, params, AddressTokenTransfersCsvExporter) end + operation :transactions_csv, + summary: "Export transactions as CSV", + description: "Exports transactions for a specific address as a CSV file.", + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], + responses: [ + ok: {"CSV file of transactions.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["addresses"] + + @doc """ + Exports transactions related to a specific address in CSV format. + + ## Parameters + + - `conn`: The Plug connection. + - `params`: A map containing request parameters. + + ## Returns + + - The updated Plug connection with the CSV response. + + This endpoint delegates CSV generation to `AddressTransactionsCsvExporter`. + """ + @spec transactions_csv(Conn.t(), map()) :: Conn.t() def transactions_csv(conn, params) do items_csv(conn, params, AddressTransactionsCsvExporter) end + operation :internal_transactions_csv, + summary: "Export internal transactions as CSV", + description: "Exports internal transactions for a specific address as a CSV file.", + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], + responses: [ + ok: {"CSV file of internal transactions.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["addresses"] + + @doc """ + Exports internal transactions as a CSV file. + + ## Parameters + + - `conn`: The Plug connection. + - `params`: A map of request parameters. + + ## Returns + + - The updated Plug connection with the CSV response. + + This function delegates the CSV export logic to `AddressInternalTransactionsCsvExporter`. + """ + @spec internal_transactions_csv(Conn.t(), map()) :: Conn.t() def internal_transactions_csv(conn, params) do items_csv(conn, params, AddressInternalTransactionsCsvExporter) end + operation :logs_csv, + summary: "Export logs as CSV", + description: "Exports logs for a specific address as a CSV file.", + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], + responses: [ + ok: {"CSV file of logs.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["addresses"] + + @doc """ + Exports logs as a CSV file. + + This controller action receives a connection and parameters, then delegates + the CSV export functionality to the `AddressLogsCsvExporter` module. + + ## Parameters + + - `conn`: The Plug connection. + - `params`: A map of request parameters. + + ## Returns + + - The updated Plug connection with the CSV response. + """ + @spec logs_csv(Conn.t(), map()) :: Conn.t() def logs_csv(conn, params) do items_csv(conn, params, AddressLogsCsvExporter) end + operation :celo_election_rewards_csv, + summary: "Export Celo election rewards as CSV", + description: "Exports Celo election rewards for a specific address as a CSV file.", + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], + responses: [ + ok: {"CSV file of Celo election rewards.", "application/csv", nil}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ], + tags: ["addresses"] + + @doc """ + Handles the CSV export of Celo election rewards. + + Receives a connection and parameters, and delegates the CSV generation + to the `AddressCeloElectionRewardsCsvExporter` module. + + ## Parameters + + - `conn`: The Plug connection. + - `params`: A map of request parameters. + + ## Returns + + - The updated Plug connection with the CSV response. + """ @spec celo_election_rewards_csv(Conn.t(), map()) :: Conn.t() def celo_election_rewards_csv(conn, params) do items_csv(conn, params, AddressCeloElectionRewardsCsvExporter) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 384ce30b88f5..2ef12e998179 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -1,11 +1,13 @@ defmodule BlockScoutWeb.API.V2.TokenController do use BlockScoutWeb, :controller use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] + use OpenApiSpex.ControllerSpecs alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{AddressView, TransactionView} - alias Explorer.{Chain, Helper, PagingOptions} + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} + alias Explorer.{Chain, Helper, PagingOptions} alias Explorer.Migrator.BackfillMetadataURL alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand @@ -37,11 +39,29 @@ defmodule BlockScoutWeb.API.V2.TokenController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["tokens"]) + @api_true [api?: true] @token_options [api?: true, necessity_by_association: %{reputation_association() => :optional}] - def token(conn, %{"address_hash_param" => address_hash_string} = params) do + operation :token, + summary: "Retrieve detailed information about a specific token", + description: "Retrieves detailed information for a specific token, including metadata and statistics.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Detailed information about the specified token.", "application/json", Schemas.Token.Response}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param` endpoint. + """ + @spec token(Plug.Conn.t(), map()) :: Plug.Conn.t() + def token(conn, %{address_hash_param: address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, @@ -77,7 +97,21 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def counters(conn, %{"address_hash_param" => address_hash_string} = params) do + operation :counters, + summary: "Get activity count stats for a specific token", + description: "Retrieves count statistics for a token, including transfers, holders, and other metrics.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Count statistics for the specified token", "application/json", Schemas.Token.Counters}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param/counters` endpoint. + """ + @spec counters(Plug.Conn.t(), map()) :: Plug.Conn.t() + def counters(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do @@ -87,7 +121,44 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def transfers(conn, %{"address_hash_param" => address_hash_string} = params) do + operation :transfers, + summary: "List token transfers for a specific token", + description: "Retrieves token transfers for a specific token, with optional filtering and pagination.", + parameters: + base_params() ++ + [address_hash_param()] ++ + define_paging_params([ + "index", + "block_number", + "batch_log_index", + "batch_block_hash", + "batch_transaction_hash", + "index_in_batch" + ]), + responses: [ + ok: + {"List of token transfers.", "application/json", + paginated_response( + items: Schemas.TokenTransfer, + next_page_params_example: %{ + "index" => 259, + "block_number" => 23_484_141, + "batch_log_index" => 3, + "batch_block_hash" => "0x789", + "batch_transaction_hash" => "0xabc", + "index_in_batch" => 2 + }, + title_prefix: "TokenTransfers" + )}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param/transfers` endpoint. + """ + @spec transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() + def transfers(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do @@ -116,7 +187,34 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def holders(conn, %{"address_hash_param" => address_hash_string} = params) do + operation :holders, + summary: "List holders for a specific token instance", + description: "Retrieves holders for a specific token instance, with optional filtering and pagination.", + parameters: + base_params() ++ + [address_hash_param()] ++ + define_paging_params(["address_hash_2", "value", "items_count"]), + responses: [ + ok: + {"List of token holders.", "application/json", + paginated_response( + items: Schemas.Token.Holder, + next_page_params_example: %{ + "address_hash_2" => "0x48bb9b14483e43c7726df702b271d410e7460656", + "value" => 200_000_000_000_000, + "items_count" => 50 + }, + title_prefix: "TokenHolders" + )}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param/holders` endpoint. + """ + @spec holders(Plug.Conn.t(), map()) :: Plug.Conn.t() + def holders(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do @@ -136,9 +234,35 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + operation :instances, + summary: "List all instances of a specific token", + description: "Retrieves all instances of a specific token, including holders and balances.", + parameters: + base_params() ++ + [address_hash_param(), holder_address_hash_param()] ++ + define_paging_params(["unique_token"]), + responses: [ + ok: + {"List of token instances.", "application/json", + paginated_response( + items: Schemas.TokenInstance, + next_page_params_example: %{ + "unique_token" => 782_098 + }, + title_prefix: "TokenInstances" + )}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param/instances` endpoint. + """ + @spec instances(Plug.Conn.t(), map()) :: Plug.Conn.t() + def instances( conn, - %{"address_hash_param" => address_hash_string, "holder_address_hash" => holder_address_hash_string} = params + %{address_hash_param: address_hash_string, holder_address_hash: holder_address_hash_string} = params ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), @@ -163,7 +287,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do {token_instances, next_page} = split_list_by_page(results_plus_one) next_page_params = - next_page |> unique_tokens_next_page(token_instances, params) + next_page + |> unique_tokens_next_page(token_instances, params) conn |> put_status(200) @@ -180,7 +305,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def instances(conn, %{"address_hash_param" => address_hash_string} = params) do + def instances(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)} do @@ -206,7 +331,26 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params) do + operation :instance, + summary: "Get details for a specific token instance", + description: "Retrieves details for a specific token instance by token address and token ID.", + parameters: + base_params() ++ + [ + address_hash_param(), + token_id_param() + ], + responses: [ + ok: {"Details for the token instance.", "application/json", Schemas.TokenInstance}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + @doc """ + Handles GET requests to `/api/v2/tokens/:token_address_param/instances/:token_id_param` endpoint. + """ + @spec instance(Plug.Conn.t(), map()) :: Plug.Conn.t() + def instance(conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)}, @@ -239,21 +383,32 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - defp maybe_run_fill_metadata_url_task(token_instance, token) do - if not is_nil(token_instance.metadata) && is_nil(token_instance.skip_metadata_url) do - Task.async(fn -> - BackfillMetadataURL.update_batch([ - {token_instance.token_contract_address_hash, token_instance.token_id, token.type} - ]) - end) - else - nil - end - end + operation :transfers_by_instance, + summary: "List token transfers for a specific token instance", + description: "Retrieves token transfers for a specific token instance (by token address and token ID).", + parameters: + base_params() ++ + [address_hash_param(), token_id_param()] ++ + define_paging_params(["index", "block_number", "token_id"]), + responses: [ + ok: + {"List of token transfers for the instance.", "application/json", + paginated_response( + items: Schemas.TokenTransfer, + next_page_params_example: %{ + "index" => 920, + "block_number" => 23_489_243, + "token_id" => "4" + }, + title_prefix: "TokenInstanceTransfers" + )}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] def transfers_by_instance( conn, - %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params + %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), @@ -284,7 +439,31 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end - def holders_by_instance(conn, %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params) do + operation :holders_by_instance, + summary: "List holders for a specific token instance", + description: "Retrieves holders for a specific token instance (by token address and token ID).", + parameters: + base_params() ++ + [address_hash_param(), token_id_param()] ++ + define_paging_params(["address_hash_2", "items_count", "token_id", "value"]), + responses: [ + ok: + {"List of token holders for the instance.", "application/json", + paginated_response( + items: Schemas.Token.Holder, + next_page_params_example: %{ + "address_hash_2" => "0x1d2c163fbda9486c3a384b6fa5e34c96fe948e9a", + "items_count" => 50, + "token_id" => "0", + "value" => 4_217_417_051_704_137_590_935 + }, + title_prefix: "TokenInstanceHolders" + )}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + + def holders_by_instance(conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, @@ -314,9 +493,26 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + operation :transfers_count_by_instance, + summary: "Get transfer count for a specific token instance", + description: "Retrieves the number of transfers for a specific token instance (by token address and token ID).", + parameters: + base_params() ++ + [ + address_hash_param(), + token_id_param() + ], + responses: [ + ok: + {"Transfer count for the instance.", "application/json", + %OpenApiSpex.Schema{type: :object, properties: %{transfers_count: %Schema{type: :integer}}}}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + def transfers_count_by_instance( conn, - %{"address_hash_param" => address_hash_string, "token_id" => token_id_string} = params + %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params ) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), @@ -331,8 +527,47 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + operation :tokens_list, + summary: "List tokens with optional filtering and sorting", + description: "Retrieves a paginated list of tokens with optional filtering and sorting.", + parameters: + base_params() ++ + [ + token_type_param(), + q_param(), + sort_param(["fiat_value", "holders_count", "circulating_market_cap"]), + order_param() + ] ++ + define_paging_params([ + "contract_address_hash", + "fiat_value", + "holders_count", + "is_name_null", + "market_cap", + "name", + "items_count" + ]), + responses: [ + ok: + {"List of tokens.", "application/json", + paginated_response( + items: Schemas.Token, + next_page_params_example: %{ + "contract_address_hash" => "0xbe9895146f7af43049ca1c1ae358b0541ea49704", + "fiat_value" => "4724.32", + "holders_count" => 59_731, + "is_name_null" => false, + "market_cap" => "570958125.135513", + "name" => "Wrapped Staked ETH", + "items_count" => 50 + }, + title_prefix: "Tokens" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + def tokens_list(conn, params) do - filter = params["q"] + filter = params[:q] options = params @@ -340,7 +575,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - maybe_parsed_limit = Helper.parse_integer(params["limit"]) + maybe_parsed_limit = Helper.parse_integer(params[:limit]) %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(token_transfers_types_options(params)) @@ -357,8 +592,42 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> render(:tokens, %{tokens: tokens, next_page_params: next_page_params}) end + operation :bridged_tokens_list, + summary: "List bridged tokens with optional filtering and sorting", + description: "Retrieves a paginated list of bridged tokens with optional filtering and sorting.", + parameters: + base_params() ++ + [chain_ids_param(), q_param()] ++ + define_paging_params([ + "contract_address_hash", + "fiat_value", + "holders_count", + "is_name_null", + "market_cap", + "name", + "items_count" + ]), + responses: [ + ok: + {"List of bridged tokens.", "application/json", + paginated_response( + items: Schemas.Token, + next_page_params_example: %{ + "contract_address_hash" => "0xbe9895146f7af43049ca1c1ae358b0541ea49704", + "fiat_value" => "4724.32", + "holders_count" => 59_731, + "is_name_null" => false, + "market_cap" => "570958125.135513", + "name" => "Wrapped Staked ETH", + "items_count" => 50 + }, + title_prefix: "BridgedTokens" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + def bridged_tokens_list(conn, params) do - filter = params["q"] + filter = params[:q] options = params @@ -376,12 +645,30 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> render(:bridged_tokens, %{tokens: tokens, next_page_params: next_page_params}) end + operation :refetch_metadata, + summary: "Trigger metadata refetch for a specific token instance", + description: "Triggers a metadata refetch for a specific token instance (by token address and token ID).", + parameters: + base_params() ++ + [ + address_hash_param(), + token_id_param(), + recaptcha_response_param() + ], + responses: [ + ok: + {"Metadata refetch triggered.", "application/json", + %OpenApiSpex.Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + def refetch_metadata( conn, params ) do - address_hash_string = params["address_hash_param"] - token_id_string = params["token_id"] + address_hash_string = params[:address_hash_param] + token_id_string = params[:token_id_param] with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), @@ -404,16 +691,32 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + operation :trigger_nft_collection_metadata_refetch, + summary: "Trigger metadata refetch for a token's NFT collection", + description: "Triggers a metadata refetch for a token's NFT collection (by token address). Requires API key.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: + {"NFT collection metadata refetch triggered.", "application/json", + %OpenApiSpex.Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + def trigger_nft_collection_metadata_refetch( conn, params ) do - address_hash_string = params["address_hash_param"] + address_hash_string = params[:address_hash_param] ip = AccessHelper.conn_to_ip_string(conn) with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, params["api_key"]}, + {:api_key, ^api_key} <- + {:api_key, + conn.req_headers + |> List.keyfind("x-api-key", 0) + |> (&if(is_tuple(&1), do: elem(&1, 1), else: nil)).()}, {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, @@ -426,6 +729,18 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + defp maybe_run_fill_metadata_url_task(token_instance, token) do + if not is_nil(token_instance.metadata) && is_nil(token_instance.skip_metadata_url) do + Task.async(fn -> + BackfillMetadataURL.update_batch([ + {token_instance.token_contract_address_hash, token_instance.token_id, token.type} + ]) + end) + else + nil + end + end + defp put_owner(token_instances, holder_address, holder_address_hash), do: Enum.map(token_instances, fn token_instance -> diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 14723f227736..31b9ce2813ff 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -105,6 +105,17 @@ defmodule BlockScoutWeb.PagingHelper do ] end + def chain_ids_filter_options(%{chain_ids: chain_id}) do + [ + chain_ids: + chain_id + |> String.split(",") + |> Enum.uniq() + |> Enum.map(&Helper.parse_integer/1) + |> Enum.filter(&Enum.member?(@allowed_chain_id, &1)) + ] + end + def chain_ids_filter_options(_), do: [chain_id: []] def type_filter_options(%{"type" => type}) do @@ -212,11 +223,14 @@ defmodule BlockScoutWeb.PagingHelper do |> Map.drop([ :address_hash_param, :block_hash_or_number_param, + :token_id_param, + :token_id, :type, :apikey, "apikey", "block_hash_or_number", "block_hash_or_number_param", + "token_id_param", "transaction_hash_param", "address_hash_param", "type", @@ -264,6 +278,10 @@ defmodule BlockScoutWeb.PagingHelper do [sorting: do_tokens_sorting(sort_field, order)] end + def tokens_sorting(%{sort: sort_field, order: order}) do + [sorting: do_tokens_sorting(sort_field, order)] + end + def tokens_sorting(_), do: [] defp do_tokens_sorting("fiat_value", "asc"), do: [asc_nulls_first: :fiat_value] diff --git a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex index ad161bc73084..42c06b9a0bfe 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex @@ -25,6 +25,7 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end pipeline :api_v2_no_forgery_protect do @@ -41,12 +42,13 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(:accepts, ["json"]) plug(CheckApiV2) plug(:fetch_session) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end scope "/", as: :api_v2 do pipe_through(:api_v2_no_forgery_protect) - patch("/:address_hash_param/instances/:token_id/refetch-metadata", V2.TokenController, :refetch_metadata) + patch("/:address_hash_param/instances/:token_id_param/refetch-metadata", V2.TokenController, :refetch_metadata) patch( "/:address_hash_param/instances/refetch-metadata", @@ -69,9 +71,14 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do get("/:address_hash_param/holders", V2.TokenController, :holders) get("/:address_hash_param/holders/csv", V2.CsvExportController, :export_token_holders) get("/:address_hash_param/instances", V2.TokenController, :instances) - get("/:address_hash_param/instances/:token_id", V2.TokenController, :instance) - get("/:address_hash_param/instances/:token_id/transfers", V2.TokenController, :transfers_by_instance) - get("/:address_hash_param/instances/:token_id/holders", V2.TokenController, :holders_by_instance) - get("/:address_hash_param/instances/:token_id/transfers-count", V2.TokenController, :transfers_count_by_instance) + get("/:address_hash_param/instances/:token_id_param", V2.TokenController, :instance) + get("/:address_hash_param/instances/:token_id_param/transfers", V2.TokenController, :transfers_by_instance) + get("/:address_hash_param/instances/:token_id_param/holders", V2.TokenController, :holders_by_instance) + + get( + "/:address_hash_param/instances/:token_id_param/transfers-count", + V2.TokenController, + :transfers_count_by_instance + ) end end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index 0d70cb047139..dcc50d4ac6d4 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -96,7 +96,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address do items: General.Tag }, watchlist_names: %Schema{ - description: "Watch list name associated with the address", + description: "Watchlist name associated with the address", type: :array, items: General.WatchlistName }, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex index a86acc70cbf3..cc5645bb3efe 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit do block_number: %Schema{type: :integer, nullable: false}, block_timestamp: General.Timestamp, pubkey: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{96})$", nullable: false}, - withdrawal_credentials: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: false}, + withdrawal_credentials: %Schema{type: :string, pattern: General.full_hash_pattern(), nullable: false}, withdrawal_address: %Schema{allOf: [Address], nullable: true}, amount: General.IntegerString, signature: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{192})$", nullable: false}, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 567f33c5e766..2969b4fa82a7 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -6,10 +6,36 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias BlockScoutWeb.Schemas.API.V2.Celo.ElectionReward.Type, as: CeloElectionRewardType + + alias BlockScoutWeb.Schemas.API.V2.General.{ + AddressHash, + AddressHashNullable, + EmptyString, + FloatString, + FullHash, + HexString, + IntegerString, + IntegerStringNullable, + NullString + } + alias BlockScoutWeb.Schemas.API.V2.Token.Type, as: TokenType - alias BlockScoutWeb.Schemas.Helper alias Explorer.Chain.InternalTransaction.CallType alias OpenApiSpex.{Parameter, Schema} + @integer_pattern ~r"^-?([1-9][0-9]*|0)$" + @float_pattern ~r"^([1-9][0-9]*|0)(\.[0-9]+)?$" + @address_hash_pattern ~r"^0x([A-Fa-f0-9]{40})$" + @full_hash_pattern ~r"^0x([A-Fa-f0-9]{64})$" + @hex_string_pattern ~r"^0x([A-Fa-f0-9]*)$" + @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*\]?$/i + # Matches ISO-like datetime strings where separators between time fields can be ':' or percent-encoded '%3A'. + # Accepts examples like: + # - "2025-10-12T09" + # - "2025-10-12T09:51" + # - "2025-10-12T09:51:00.000Z" + # - "2025-10-12T09%3A51%3A00.000Z" + # - With timezone offsets: "2025-10-12T09:51:00+02:00" or encoded as "%2B02%3A00" + @iso_date_or_datetime_pattern ~r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(?:T(?:[01]\d|2[0-3])(?:(?::|%3A)[0-5]\d(?:(?::|%3A)[0-5]\d(?:\.\d{1,9})?)?)?(?:Z|(?:\+|%2B|-)(?:[01]\d|2[0-3])(?:(?::|%3A)[0-5]\d)?)?)?$"i @base_transaction_types [ "coin_transfer", @@ -27,274 +53,127 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @allowed_transaction_types @base_transaction_types end - defmodule AddressHash do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{40})$", nullable: false}) - end - - defmodule AddressHashNullable do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{40})$", nullable: true}) - end - - defmodule FullHash do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: false}) - end - - defmodule FullHashNullable do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{64})$", nullable: true}) - end - - defmodule HexString do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]*)$", nullable: false}) - end - - defmodule HexStringNullable do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^0x([A-Fa-f0-9]*)$", nullable: true}) - end - - defmodule ProxyType do - @moduledoc false - alias Ecto.Enum - alias Explorer.Chain.SmartContract.Proxy.Models.Implementation - - OpenApiSpex.schema(%{ - type: :string, - enum: Enum.values(Implementation, :proxy_type), - nullable: true - }) - end - - defmodule Implementation.ChainTypeCustomizations do - @moduledoc false - alias OpenApiSpex.Schema - - @doc """ - Applies chain-specific field customizations to the given schema based on the configured chain type. - - ## Parameters - - `schema`: The base schema map to be customized - - ## Returns - - The schema map with chain-specific properties added based on the current chain type configuration - """ - @spec chain_type_fields(map()) :: map() - def chain_type_fields(schema) do - case Application.get_env(:explorer, :chain_type) do - :filecoin -> - schema - |> Helper.extend_schema( - properties: %{ - filecoin_robust_address: %Schema{ - type: :string, - example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", - nullable: true - } - }, - required: [:filecoin_robust_address] - ) - - _ -> - schema - end - end - end - - defmodule Implementation do - @moduledoc false - require OpenApiSpex - - alias Implementation.ChainTypeCustomizations - - OpenApiSpex.schema( - %{ - description: "Proxy smart contract implementation", - type: :object, - properties: %{ - address_hash: AddressHash, - name: %Schema{type: :string, nullable: true} - }, - required: [:address_hash, :name], - additionalProperties: false - } - |> ChainTypeCustomizations.chain_type_fields() - ) - end - - defmodule Tag do - @moduledoc false - OpenApiSpex.schema(%{ - description: "Address tag struct", - type: :object, - properties: %{ - address_hash: AddressHash, - display_name: %Schema{type: :string, nullable: false}, - label: %Schema{type: :string, nullable: false} - }, - required: [:address_hash, :display_name, :label], - additionalProperties: false - }) - end - - defmodule WatchlistName do - @moduledoc false - OpenApiSpex.schema(%{ - description: "Watch list name struct", - type: :object, - properties: %{ - display_name: %Schema{type: :string, nullable: false}, - label: %Schema{type: :string, nullable: false} - }, - required: [:display_name, :label], - additionalProperties: false - }) - end - - defmodule FloatString do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^([1-9][0-9]*|0)(\.[0-9]+)?$"}) - end - - defmodule FloatStringNullable do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^([1-9][0-9]*|0)(\.[0-9]+)?$", nullable: true}) - end - - defmodule IntegerStringNullable do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^-?([1-9][0-9]*|0)$", nullable: true}) - end - - defmodule IntegerString do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^-?([1-9][0-9]*|0)$", nullable: false}) - end - - defmodule URLNullable do - @moduledoc false - OpenApiSpex.schema(%{ - type: :string, - format: :uri, - example: "https://example.com", - nullable: true - }) + @doc """ + Returns a parameter definition for an address hash in the path. + """ + @spec address_hash_param() :: Parameter.t() + def address_hash_param do + %Parameter{ + name: :address_hash_param, + in: :path, + schema: AddressHash, + required: true + } end - defmodule Timestamp do - @moduledoc false - OpenApiSpex.schema(%{ - type: :string, - format: :"date-time", - nullable: false - }) + # todo: It should be removed when the frontend stops sending the address_id parameter with the request + @doc """ + Returns a parameter definition for an address hash in the path. + """ + @spec address_id_param() :: Parameter.t() + def address_id_param do + %Parameter{ + name: :address_id, + in: :query, + schema: AddressHash, + required: false + } end - defmodule TimestampNullable do - @moduledoc false - OpenApiSpex.schema(%{ - type: :string, - format: :"date-time", - nullable: true - }) + @doc """ + Returns a parameter definition for the start of the time period. + """ + @spec from_period_param() :: Parameter.t() + def from_period_param do + %Parameter{ + name: :from_period, + in: :query, + schema: %Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, + required: true, + description: "Start of the time period (ISO 8601 format) in CSV export" + } end - defmodule DecodedInput do - @moduledoc false - OpenApiSpex.schema(%{ - type: :object, - properties: %{ - method_id: %Schema{type: :string, nullable: true}, - method_call: %Schema{type: :string, nullable: true}, - parameters: %Schema{ - type: :array, - items: %Schema{ - type: :object, - properties: %{ - name: %Schema{type: :string, nullable: false}, - type: %Schema{type: :string, nullable: false}, - value: %Schema{ - anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], - nullable: false - } - }, - nullable: false, - additionalProperties: false - } - } - }, - required: [:method_id, :method_call, :parameters], - nullable: false, - additionalProperties: false - }) + @doc """ + Returns a parameter definition for the end of the time period. + """ + @spec to_period_param() :: Parameter.t() + def to_period_param do + %Parameter{ + name: :to_period, + in: :query, + schema: %Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, + required: true, + description: "End of the time period (ISO 8601 format) In CSV export" + } end - defmodule MethodNameNullable do - @moduledoc false - OpenApiSpex.schema(%{ - type: :string, - nullable: true, - example: "transfer", - description: "Method name or hex method id" - }) + @doc """ + Returns a parameter definition for chain IDs in the query. + """ + @spec chain_ids_param() :: Parameter.t() + def chain_ids_param do + %Parameter{ + name: :chain_ids, + in: :query, + schema: %Schema{type: :string, nullable: true}, + required: false, + description: "Chain IDs filter in Bridged tokens" + } end - defmodule DecodedLogInput do - @moduledoc false - OpenApiSpex.schema(%{ - type: :object, - properties: %{ - method_id: %Schema{type: :string, nullable: true}, - method_call: %Schema{type: :string, nullable: true}, - parameters: %Schema{ - type: :array, - items: %Schema{ - type: :object, - properties: %{ - name: %Schema{type: :string, nullable: false}, - type: %Schema{type: :string, nullable: false}, - indexed: %Schema{type: :boolean, nullable: false}, - value: %Schema{ - anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], - nullable: false - } - }, - required: [:name, :type, :indexed, :value], - nullable: false, - additionalProperties: false - }, - nullable: false - } - }, - required: [:method_id, :method_call, :parameters], - nullable: false, - additionalProperties: false - }) + @doc """ + Returns a parameter definition for a search query in the query. + """ + @spec q_param() :: Parameter.t() + def q_param do + %Parameter{ + name: :q, + in: :query, + schema: %Schema{type: :string, nullable: true}, + required: false, + description: "Search query filter" + } end - defmodule EmptyString do - @moduledoc false - OpenApiSpex.schema(%{type: :string, minLength: 0, maxLength: 0}) + @doc """ + Returns a parameter definition for a filter type in the query. + """ + @spec filter_type_param() :: Parameter.t() + def filter_type_param do + %Parameter{ + name: :filter_type, + in: :query, + schema: %Schema{anyOf: [%Schema{type: :string, enum: ["address"], nullable: true}, NullString]}, + required: false, + description: "Filter type in CSV export" + } end - defmodule NullString do - @moduledoc false - OpenApiSpex.schema(%{type: :string, pattern: ~r"^null$"}) + @doc """ + Returns a parameter definition for a filter value in the query. + """ + @spec filter_value_param() :: Parameter.t() + def filter_value_param do + %Parameter{ + name: :filter_value, + in: :query, + schema: %Schema{anyOf: [%Schema{type: :string, enum: ["to", "from"], nullable: true}, NullString]}, + required: false, + description: "Filter value in CSV export" + } end @doc """ - Returns a parameter definition for an address hash in the path. + Returns a parameter definition for a holder address hash in the query. """ - @spec address_hash_param() :: Parameter.t() - def address_hash_param do + @spec holder_address_hash_param() :: Parameter.t() + def holder_address_hash_param do %Parameter{ - name: :address_hash_param, - in: :path, + name: :holder_address_hash, + in: :query, schema: AddressHash, - required: true + required: false } end @@ -469,6 +348,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do * block_number - Sort by block number * value - Sort by transaction value * fee - Sort by transaction fee + * balance - Sort by account balance + * transactions_count - Sort by number of transactions on address + * fiat_value - Sort by fiat value of the token transfer + * holders_count - Sort by number of token holders + * circulating_market_cap - Sort by circulating market cap of the token Should be used together with `order` parameter. """ } @@ -508,7 +392,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do EmptyString, %Schema{ type: :string, - pattern: ~r"^(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*$" + pattern: @token_type_pattern } ] }, @@ -538,7 +422,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do EmptyString, %Schema{ type: :string, - pattern: ~r"^(ERC-721|ERC-1155|ERC-404)(,(ERC-721|ERC-1155|ERC-404))*$" + pattern: @token_type_pattern } ] }, @@ -583,6 +467,48 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for a token ID in the path. + """ + @spec token_id_param() :: Parameter.t() + def token_id_param do + %Parameter{ + name: :token_id_param, + in: :path, + schema: IntegerStringNullable, + required: true, + description: "Token ID for ERC-721/1155/404 tokens" + } + end + + @doc """ + Returns a parameter definition for API key used in rate limiting. + """ + @spec admin_api_key_param() :: Parameter.t() + def admin_api_key_param do + %Parameter{ + name: :x_api_key, + in: :header, + schema: %Schema{type: :string}, + required: true, + description: "API key required for sensitive endpoints" + } + end + + @doc """ + Returns a parameter definition for reCAPTCHA response token. + """ + @spec recaptcha_response_param() :: Parameter.t() + def recaptcha_response_param do + %Parameter{ + name: :recaptcha_response, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "reCAPTCHA response token" + } + end + @doc """ Returns a parameter definition for API key used in rate limiting. """ @@ -720,6 +646,28 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Address hash for paging", name: :hash }, + # todo: consider refactoring to eliminate _2 suffix + "address_hash_2" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Address hash for paging", + name: :address_hash + }, + "address_hash_param" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Address hash for paging", + name: :hash + }, + "contract_address_hash" => %Parameter{ + in: :query, + schema: AddressHashNullable, + required: false, + description: "Contract address hash for paging", + name: :contract_address_hash + }, "value" => %Parameter{ in: :query, schema: IntegerString, @@ -727,6 +675,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Transaction value for paging", name: :value }, + "fiat_value" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, + required: false, + description: "Fiat value for paging", + name: :fiat_value + }, "fee" => %Parameter{ in: :query, schema: IntegerString, @@ -741,6 +696,34 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Number of items returned per page", name: :items_count }, + "holders_count" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [IntegerString, EmptyString, NullString]}, + required: false, + description: "Number of holders returned per page", + name: :holders_count + }, + "is_name_null" => %Parameter{ + in: :query, + schema: %Schema{type: :boolean}, + required: false, + description: "Is name null for paging", + name: :is_name_null + }, + "market_cap" => %Parameter{ + in: :query, + schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, + required: false, + description: "Market cap for paging", + name: :market_cap + }, + "name" => %Parameter{ + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Name for paging", + name: :name + }, "batch_log_index" => %Parameter{ in: :query, schema: %Schema{type: :integer}, @@ -818,6 +801,14 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Token ID for paging", name: :token_id }, + # todo: eliminate in favour token_id + "unique_token" => %Parameter{ + in: :query, + schema: IntegerStringNullable, + required: false, + description: "Token ID for paging", + name: :unique_token + }, "token_type" => %Parameter{ in: :query, schema: TokenType, @@ -870,4 +861,34 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do """ @spec allowed_transaction_types() :: [String.t()] def allowed_transaction_types, do: @allowed_transaction_types + + @doc """ + Returns the integer pattern. + """ + @spec integer_pattern() :: Regex.t() + def integer_pattern, do: @integer_pattern + + @doc """ + Returns the float pattern. + """ + @spec float_pattern() :: Regex.t() + def float_pattern, do: @float_pattern + + @doc """ + Returns the regex pattern for validating address hashes. + """ + @spec address_hash_pattern() :: Regex.t() + def address_hash_pattern, do: @address_hash_pattern + + @doc """ + Returns the regex pattern for validating full hashes. + """ + @spec full_hash_pattern() :: Regex.t() + def full_hash_pattern, do: @full_hash_pattern + + @doc """ + Returns the regex pattern for validating hex strings. + """ + @spec hex_string_pattern() :: Regex.t() + def hex_string_pattern, do: @hex_string_pattern end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash.ex new file mode 100644 index 000000000000..0d6a233da2b3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.AddressHash do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.address_hash_pattern(), nullable: false}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash_nullable.ex new file mode 100644 index 000000000000..341bae3a07a8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/address_hash_nullable.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.AddressHashNullable do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.address_hash_pattern(), nullable: true}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex new file mode 100644 index 000000000000..56726e08b077 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex @@ -0,0 +1,32 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.DecodedInput do + @moduledoc false + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + method_id: %Schema{type: :string, nullable: true}, + method_call: %Schema{type: :string, nullable: true}, + parameters: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + type: %Schema{type: :string, nullable: false}, + value: %Schema{ + anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + nullable: false + } + }, + nullable: false, + additionalProperties: false + } + } + }, + required: [:method_id, :method_call, :parameters], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex new file mode 100644 index 000000000000..445cd02d2d1d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex @@ -0,0 +1,35 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.DecodedLogInput do + @moduledoc false + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + method_id: %Schema{type: :string, nullable: true}, + method_call: %Schema{type: :string, nullable: true}, + parameters: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + type: %Schema{type: :string, nullable: false}, + indexed: %Schema{type: :boolean, nullable: false}, + value: %Schema{ + anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + nullable: false + } + }, + required: [:name, :type, :indexed, :value], + nullable: false, + additionalProperties: false + }, + nullable: false + } + }, + required: [:method_id, :method_call, :parameters], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/empty_string.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/empty_string.ex new file mode 100644 index 000000000000..9bdcd984748c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/empty_string.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.EmptyString do + @moduledoc false + require OpenApiSpex + OpenApiSpex.schema(%{type: :string, minLength: 0, maxLength: 0}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string.ex new file mode 100644 index 000000000000..b4f934048422 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.FloatString do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.float_pattern()}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string_nullable.ex new file mode 100644 index 000000000000..0d06eb26302a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/float_string_nullable.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.FloatStringNullable do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.float_pattern(), nullable: true}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash.ex new file mode 100644 index 000000000000..b736f70110c3 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.FullHash do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.full_hash_pattern(), nullable: false}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash_nullable.ex new file mode 100644 index 000000000000..d56dfa767e70 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/full_hash_nullable.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.FullHashNullable do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.full_hash_pattern(), nullable: true}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string.ex new file mode 100644 index 000000000000..f9c5f92c7cfb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.HexString do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.hex_string_pattern(), nullable: false}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string_nullable.ex new file mode 100644 index 000000000000..00f014c3fa7e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/hex_string_nullable.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.HexStringNullable do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.hex_string_pattern(), nullable: true}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation.ex new file mode 100644 index 000000000000..3283a3be9d6c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation.ex @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.Implementation do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.API.V2.General.Implementation.ChainTypeCustomizations + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + description: "Proxy smart contract implementation", + type: :object, + properties: %{ + address_hash: General.AddressHash, + name: %Schema{type: :string, nullable: true} + }, + required: [:address_hash, :name], + additionalProperties: false + } + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex new file mode 100644 index 000000000000..8e0db25315d6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex @@ -0,0 +1,37 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.Implementation.ChainTypeCustomizations do + @moduledoc false + alias OpenApiSpex.Schema + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + alias BlockScoutWeb.Schemas.Helper + alias OpenApiSpex.Schema + + case Application.get_env(:explorer, :chain_type) do + :filecoin -> + schema + |> Helper.extend_schema( + properties: %{ + filecoin_robust_address: %Schema{ + type: :string, + example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", + nullable: true + } + }, + required: [:filecoin_robust_address] + ) + + _ -> + schema + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string.ex new file mode 100644 index 000000000000..7fc745512610 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.IntegerString do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.integer_pattern(), nullable: false}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string_nullable.ex new file mode 100644 index 000000000000..de6fdb30495f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/integer_string_nullable.ex @@ -0,0 +1,6 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.IntegerStringNullable do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + OpenApiSpex.schema(%{type: :string, pattern: General.integer_pattern(), nullable: true}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/method_name_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/method_name_nullable.ex new file mode 100644 index 000000000000..9acecdd713f5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/method_name_nullable.ex @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.MethodNameNullable do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + type: :string, + nullable: true, + example: "transfer", + description: "Method name or hex method id" + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/null_string.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/null_string.ex new file mode 100644 index 000000000000..e55211bb033b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/null_string.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.NullString do + @moduledoc false + require OpenApiSpex + OpenApiSpex.schema(%{type: :string, pattern: ~r"^null$"}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/proxy_type.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/proxy_type.ex new file mode 100644 index 000000000000..4345a11bda74 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/proxy_type.ex @@ -0,0 +1,12 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.ProxyType do + @moduledoc false + require OpenApiSpex + alias Ecto.Enum + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + + OpenApiSpex.schema(%{ + type: :string, + enum: Enum.values(Implementation, :proxy_type), + nullable: true + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/tag.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/tag.ex new file mode 100644 index 000000000000..2e692d12ac86 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/tag.ex @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.Tag do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Address tag struct", + type: :object, + properties: %{ + address_hash: General.AddressHash, + display_name: %Schema{type: :string, nullable: false}, + label: %Schema{type: :string, nullable: false} + }, + required: [:address_hash, :display_name, :label], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp.ex new file mode 100644 index 000000000000..478218553352 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp.ex @@ -0,0 +1,10 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.Timestamp do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + type: :string, + format: :"date-time", + nullable: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp_nullable.ex new file mode 100644 index 000000000000..2d5969c3b852 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/timestamp_nullable.ex @@ -0,0 +1,10 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.TimestampNullable do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + type: :string, + format: :"date-time", + nullable: true + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/url_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/url_nullable.ex new file mode 100644 index 000000000000..ab1a8ca2f645 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/url_nullable.ex @@ -0,0 +1,11 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.URLNullable do + @moduledoc false + require OpenApiSpex + + OpenApiSpex.schema(%{ + type: :string, + format: :uri, + example: "https://example.com", + nullable: true + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/watchlist_name.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/watchlist_name.ex new file mode 100644 index 000000000000..53821e8dfca8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/watchlist_name.ex @@ -0,0 +1,16 @@ +defmodule BlockScoutWeb.Schemas.API.V2.General.WatchlistName do + @moduledoc false + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Watchlist name struct", + type: :object, + properties: %{ + display_name: %Schema{type: :string, nullable: false}, + label: %Schema{type: :string, nullable: false} + }, + required: [:display_name, :label], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index d744c42b1ed9..227ce67ca5fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -70,7 +70,15 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token do enum: Reputation.enum_values(), description: "Reputation of the token", nullable: true - } + }, + bridge_type: %Schema{ + type: :string, + enum: ["omni", "amb"], + description: "Type of bridge used for this bridged token", + nullable: true + }, + foreign_address: %Schema{type: :string, pattern: General.address_hash_pattern(), nullable: true}, + origin_chain_id: General.IntegerStringNullable }, required: [ :address_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/counters.ex new file mode 100644 index 000000000000..bd4a5328b485 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/counters.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Token.Counters do + @moduledoc """ + This module defines the schema for the response from /api/v2/tokens/:address_hash_param/counters. + Example response: {"token_holders_count":"0","transfers_count":"0"} + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%Schema{ + title: "TokenCountersResponse", + description: "Token counters response", + type: :object, + properties: %{ + token_holders_count: General.IntegerString, + transfers_count: General.IntegerString + }, + required: [:token_holders_count, :transfers_count], + additionalProperties: false, + example: %{token_holders_count: "0", transfers_count: "0"} + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/holder.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/holder.ex new file mode 100644 index 000000000000..2d66850a581b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/holder.ex @@ -0,0 +1,85 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Token.Holder do + @moduledoc """ + This module defines the schema for a token holder response. + Example response: + { + "address": { ... }, + "token_id": null, + "value": "19474530513868000" + } + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%Schema{ + title: "TokenHolderResponse", + description: "Token holder response", + type: :object, + properties: %{ + address: Address.schema(), + token_id: General.IntegerStringNullable, + value: General.IntegerString + }, + required: [:address, :token_id, :value], + additionalProperties: false, + example: %{ + address: %{ + ens_domain_name: nil, + hash: "0xF977814e90dA44bFA03b6295A0616a897441aceC", + implementations: [], + is_contract: false, + is_scam: false, + is_verified: false, + metadata: %{ + tags: [ + %{ + meta: %{main_entity: "Binance", tooltipUrl: "https://www.binance.com/"}, + name: "Binance: Hot Wallet 20", + ordinal: 10, + slug: "binance-hot-wallet-20", + tagType: "name" + }, + %{ + meta: %{tooltipUrl: "https://www.binance.com"}, + name: "Binance 8", + ordinal: 10, + slug: "binance-8", + tagType: "name" + }, + %{ + meta: %{}, + name: "HOT WALLET", + ordinal: 0, + slug: "hot-wallet", + tagType: "generic" + }, + %{ + meta: %{}, + name: "Exchange", + ordinal: 0, + slug: "exchange", + tagType: "generic" + }, + %{ + meta: %{}, + name: "Binance", + ordinal: 0, + slug: "binance", + tagType: "protocol" + } + ] + }, + name: nil, + private_tags: [], + proxy_type: nil, + public_tags: [], + reputation: "ok", + watchlist_names: [] + }, + token_id: nil, + value: "19474530513868000" + } + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex new file mode 100644 index 000000000000..76dcaf6c75cb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Token.Response do + @moduledoc """ + This module defines the schema for token response from /api/v2/tokens/:token_address_param. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Token + alias BlockScoutWeb.Schemas.Helper + + OpenApiSpex.schema( + Token.schema() + |> Helper.extend_schema( + title: "TokenResponse", + description: "Token response", + additionalProperties: false + ) + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex index cd68c80a13ec..f8e02ca3607c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex @@ -18,6 +18,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstance do }, owner: %Schema{allOf: [Address], nullable: true}, token: %Schema{allOf: [Token], nullable: true}, + token_type: %Schema{allOf: [BlockScoutWeb.Schemas.API.V2.Token.Type], nullable: true}, + value: General.IntegerStringNullable, external_app_url: General.URLNullable, animation_url: General.URLNullable, image_url: General.URLNullable, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index 77b938dcb183..4711b0806662 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -32,7 +32,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = conn |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -43,7 +42,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do Phoenix.ConnTest.build_conn() |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -85,7 +83,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -97,7 +94,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do |> put_req_header("recaptcha-v2-response", "123") |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -128,7 +124,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -140,7 +135,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do Phoenix.ConnTest.build_conn() |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -186,7 +180,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -199,7 +192,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do |> put_req_header("recaptcha-v2-response", "123") |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/token-transfers/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -249,7 +241,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -262,7 +253,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do |> put_req_header("recaptcha-v2-response", "123") |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -349,7 +339,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/internal-transactions/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -362,7 +351,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do |> put_req_header("recaptcha-v2-response", "123") |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/internal-transactions/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -443,7 +431,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -456,7 +443,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do |> put_req_header("recaptcha-v2-response", "123") |> put_req_header("user-agent", "test-agent") |> get("/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ - "address_id" => Address.checksum(address.hash), "from_period" => from_period, "to_period" => to_period }) @@ -488,7 +474,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/logs/csv", %{ - "address_id" => Address.checksum(address.hash), "filter_type" => "null", "filter_value" => "null", "from_period" => from_period, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index bf202c19bc96..a5a749797ccd 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -28,7 +28,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get token", %{conn: conn} do @@ -54,7 +62,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/counters") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get counters", %{conn: conn} do @@ -118,7 +134,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/transfers") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get empty list", %{conn: conn} do @@ -377,7 +401,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/holders") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get empty list", %{conn: conn} do @@ -615,23 +647,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) end - test "ignores wrong ordering params", %{conn: conn} do - tokens = - for i <- 0..50 do - insert(:token, fiat_value: i) - end - - request = get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"}) - - assert response = json_response(request, 200) - - request_2nd_page = - get(conn, "/api/v2/tokens", %{"sort" => "foo", "order" => "bar"} |> Map.merge(response["next_page_params"])) - - assert response_2nd_page = json_response(request_2nd_page, 200) - check_paginated_response(response, response_2nd_page, tokens) - end - test "get token with ok reputation", %{conn: conn} do init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) Application.put_env(:block_scout_web, :hide_scam_addresses, true) @@ -950,7 +965,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/instances") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get empty list", %{conn: conn} do @@ -1078,7 +1101,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/instances/12") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get token instance by token id", %{conn: conn} do @@ -1246,7 +1277,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/instances/12/transfers") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get token transfers by instance", %{conn: conn} do @@ -1490,7 +1529,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/instances/12/holders") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get 422 on invalid id", %{conn: conn} do @@ -1498,7 +1545,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do request = get(conn, "/api/v2/tokens/#{token.contract_address_hash}/instances/123ab/holders") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^-?([1-9][0-9]*|0)$/", + "source" => %{"pointer" => "/token_id_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get token transfers by instance", %{conn: conn} do @@ -1550,7 +1605,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/tokens/0x/instances/12/transfers-count") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "receive 0 count", %{conn: conn} do @@ -2333,11 +2396,8 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do request = patch( - conn, - "/api/v2/tokens/#{token.contract_address.hash}/instances/refetch-metadata", - %{ - "api_key" => "abc" - } + conn |> put_req_header("x-api-key", "abc"), + "/api/v2/tokens/#{token.contract_address.hash}/instances/refetch-metadata" ) assert %{"message" => "OK"} = json_response(request, 200) @@ -2419,8 +2479,6 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do is_unique = value == "1" assert %{ - "token_type" => ^token_type, - "value" => ^value, "id" => ^id, "metadata" => ^metadata, "token" => %{"address_hash" => ^token_address_hash, "name" => ^token_name, "type" => ^token_type}, diff --git a/apps/block_scout_web/test/support/api_schema_assertions.ex b/apps/block_scout_web/test/support/api_schema_assertions.ex index 0d4465cc88a6..70125a93bcd0 100644 --- a/apps/block_scout_web/test/support/api_schema_assertions.ex +++ b/apps/block_scout_web/test/support/api_schema_assertions.ex @@ -22,7 +22,7 @@ defmodule BlockScoutWeb.TestApiSchemaAssertions do defp maybe_assert_schema(%Plug.Conn{method: "GET", request_path: request_path} = _conn, status_code, json) when is_integer(status_code) and is_binary(request_path) do - if String.starts_with?(request_path, "/api/") do + if String.starts_with?(request_path, "/api/v2") do spec = BlockScoutWeb.ApiSpec.spec() with {:path_item, {:ok, %PathItem{} = path_item}} <- {:path_item, find_path_item(spec, request_path)}, diff --git a/apps/explorer/lib/explorer/chain/csv_export/helper.ex b/apps/explorer/lib/explorer/chain/csv_export/helper.ex index a9c1af498815..66f59e148d90 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/helper.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/helper.ex @@ -160,6 +160,7 @@ defmodule Explorer.Chain.CsvExport.Helper do "address" -> filter_value in supported_address_filter_values() + # todo: topic filter is unused, consider removing "topic" -> case Hash.cast(filter_value) do {:ok, _} -> true diff --git a/cspell.json b/cspell.json index 3418514dcbdb..42585360f687 100644 --- a/cspell.json +++ b/cspell.json @@ -347,6 +347,7 @@ "Karnaugh", "keccak", "Keepalive", + "keyfind", "keyout", "kittencream", "KnxbUejwY", From bf207298ccb8ddf3c3509a0050c55f5b9c7698ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:02:15 +0300 Subject: [PATCH 0391/1006] chore(deps-dev): bump cowboy from 2.14.1 to 2.14.2 (#13388) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index dd35b1daf77d..5a431bb93232 100644 --- a/mix.lock +++ b/mix.lock @@ -29,7 +29,7 @@ "con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"}, "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, - "cowboy": {:hex, :cowboy, "2.14.1", "031d338393e5a128a7de9613b4a0558aabc31b07082004abecb27cac790f5cd6", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e5310d5afd478ba90b1fed4fcdbc0230082b4510009505c586725c30b44e356f"}, + "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, From f96bc2c04729d5d27fd42af356245e7f7b295747 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 08:03:55 +0300 Subject: [PATCH 0392/1006] chore(deps): bump phoenix_live_view from 1.1.13 to 1.1.14 (#13387) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 5a431bb93232..ded09a78c25b 100644 --- a/mix.lock +++ b/mix.lock @@ -133,7 +133,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.13", "11f48f8fbe5d7d0731d4e122a692e0f9ae8d5f98c54d573d29046833c34eada3", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9d06e93573b8419ef8ee832b4a9bfe0def10b15f7c129ea477dfa54f0136f7ec"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.14", "cae84abc4cd00dde4bb200b8516db556704c585c267aff9cd4955ff83cceb86c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b827980e2bc00fddd8674e3b567519a4e855b5de04bf8607140414f1101e2627"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, From 233a625448d7784edebfd51c0fce32e93eace8dc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Oct 2025 11:08:26 +0300 Subject: [PATCH 0393/1006] chore(deps): bump ex_cldr_units from 3.19.1 to 3.19.2 (#13389) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index ded09a78c25b..353368c6f39a 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"}, + "cldr_utils": {:hex, :cldr_utils, "2.29.0", "b64e01c8712ed06db4053302e7e3f6d57fd792cb40dd03144978e663ee40b018", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "d21f6a1937461e3b0634e53159d64b07852c54069304fb7aef666aeb46ba5175"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -59,7 +59,7 @@ "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.2", "29c1b17732e089f5cd71623add92d7bbd584b2c4a586e51f782303a592f7eb81", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6db5fc81a7de7efe9e9bd66fb2f436b5f82cdbf88deea38f513e8432533856dd"}, - "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.1", "11a99d0741e8786158a40f9b7a90f7bf055a642d277a4ea1bb2b7e90a108f09f", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "63023b6e5c1ec159d9e50f50c2d271127a1263a2316dff5a08b7b4a0f82bf1f8"}, + "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.2", "a0fd72de6c33e7b836305a27da288404853365538bffb728cbe5055ad92a3ea3", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f3f0c8cd8f192844a447838f33dfb942f54307afb213ad776264fae92d92ed35"}, "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, From 27c3e936096e1559bfde66af03a807015ea8c077 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:13:42 +0400 Subject: [PATCH 0394/1006] fix: Sanitize internal transaction error before insertion (#13362) --- .../chain/import/runner/internal_transactions.ex | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index d562cae32547..cbc0b22c8ac9 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -456,6 +456,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do entry |> Map.put(:block_hash, block_hash) |> Map.put(:block_index, index) + |> sanitize_error() end) else [] @@ -495,6 +496,21 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end + defp sanitize_error(entry) do + error = Map.get(entry, :error) + + sanitized_error = + if is_binary(error) and not String.printable?(error) do + error + |> inspect(binaries: :as_strings) + |> String.trim("\"") + else + error + end + + Map.put(entry, :error, sanitized_error) + end + def defer_internal_transactions_primary_key(repo) do # Allows internal_transactions primary key to not be checked during the # DB transactions and instead be checked only at the end of it. From c31818616fec3457026e09245c7a06020c1b0d59 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 14 Oct 2025 12:41:01 +0400 Subject: [PATCH 0395/1006] feat: Delete internal transactions on reorgs (#13121) --- .../explorer/chain/import/runner/blocks.ex | 30 +++++ .../internal_transaction/delete_queue.ex | 41 ++++++ ...eate_internal_transaction_delete_queue.exs | 11 ++ .../chain/import/runner/blocks_test.exs | 13 ++ apps/explorer/test/support/factory.ex | 7 + .../internal_transaction/delete_queue.ex | 124 ++++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 2 + .../delete_queue_test.exs | 63 +++++++++ ...ransaction_delete_queue_supervisor_case.ex | 19 +++ config/runtime.exs | 5 + docker-compose/envs/common-blockscout.env | 3 + 11 files changed, 318 insertions(+) create mode 100644 apps/explorer/lib/explorer/chain/internal_transaction/delete_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20250901144042_create_internal_transaction_delete_queue.exs create mode 100644 apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex create mode 100644 apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs create mode 100644 apps/indexer/test/support/indexer/fetcher/internal_transaction_delete_queue_supervisor_case.ex diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index c3ab221cd421..861147d6c4ff 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -31,6 +31,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Chain.Import.Runner alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances alias Explorer.Chain.Import.Runner.{Addresses, TokenInstances, Tokens} + alias Explorer.Chain.InternalTransaction.DeleteQueue, as: InternalTransactionDeleteQueue alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator @@ -209,6 +210,14 @@ defmodule Explorer.Chain.Import.Runner.Blocks do :derive_address_current_token_balances ) end) + |> Multi.run(:save_internal_transactions_for_delete, fn repo, %{lose_consensus: non_consensus_blocks} -> + Instrumenter.block_import_stage_runner( + fn -> save_internal_transactions_for_delete(repo, non_consensus_blocks, insert_options) end, + :address_referencing, + :blocks, + :save_internal_transactions_for_delete + ) + end) |> Multi.run(:update_token_instances_owner, fn repo, %{derive_transaction_forks: transactions} -> Instrumenter.block_import_stage_runner( fn -> update_token_instances_owner(repo, transactions, insert_options) end, @@ -844,6 +853,27 @@ defmodule Explorer.Chain.Import.Runner.Blocks do {:ok, derived_address_current_token_balances} end + defp save_internal_transactions_for_delete(_, [], _), do: {:ok, []} + + defp save_internal_transactions_for_delete(repo, non_consensus_blocks, %{timeout: timeout, timestamps: timestamps}) do + insert_params = + Enum.map(non_consensus_blocks, fn {number, _hash} -> + Map.put(timestamps, :block_number, number) + end) + + {_total, result} = + repo.insert_all( + InternalTransactionDeleteQueue, + insert_params, + conflict_target: [:block_number], + on_conflict: {:replace, [:updated_at]}, + returning: [:block_number], + timeout: timeout + ) + + {:ok, Enum.map(result, & &1.block_number)} + end + defp update_token_instances_owner(_, [], _), do: {:ok, []} defp update_token_instances_owner(repo, forked_transaction_hashes, options) do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/delete_queue.ex b/apps/explorer/lib/explorer/chain/internal_transaction/delete_queue.ex new file mode 100644 index 000000000000..9f7ca330634d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/delete_queue.ex @@ -0,0 +1,41 @@ +defmodule Explorer.Chain.InternalTransaction.DeleteQueue do + @moduledoc """ + Stores numbers for blocks, whose internal transactions should be deleted and refetched + """ + + use Explorer.Schema + import Ecto.Query + alias Explorer.Repo + + @primary_key false + typed_schema "internal_transactions_delete_queue" do + field(:block_number, :integer, primary_key: true) + + timestamps() + end + + @doc """ + Streams block numbers from the delete queue that are older than the threshold and reduces them using the provided reducer function. + + ## Parameters + - `initial`: The initial accumulator value + - `reducer`: A 2-arity function that processes each block number and returns an updated accumulator + - `threshold`: Time threshold in milliseconds (default: 600_000 ms / 10 minutes). Only entries older than this are streamed + + ## Returns + - `{:ok, accumulator}`: The final accumulator after processing all entries + """ + @spec stream_data( + initial :: accumulator, + reducer :: (entry :: integer(), accumulator -> accumulator), + threshold :: non_neg_integer() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_data(initial, reducer, threshold \\ 600_000) when is_function(reducer, 2) do + __MODULE__ + |> where([dq], dq.updated_at < ago(^threshold, "millisecond")) + |> order_by([dq], desc: :block_number) + |> select([dq], dq.block_number) + |> Repo.stream_reduce(initial, reducer) + end +end diff --git a/apps/explorer/priv/repo/migrations/20250901144042_create_internal_transaction_delete_queue.exs b/apps/explorer/priv/repo/migrations/20250901144042_create_internal_transaction_delete_queue.exs new file mode 100644 index 000000000000..59f00c6d71d3 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250901144042_create_internal_transaction_delete_queue.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.CreateInternalTransactionDeleteQueue do + use Ecto.Migration + + def change do + create table(:internal_transactions_delete_queue, primary_key: false) do + add(:block_number, :bigint, primary_key: true) + + timestamps() + end + end +end diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 60251bda3e78..226c0c9c8ab4 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -8,6 +8,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do alias Ecto.Multi alias Explorer.Chain.Import.Runner.{Blocks, Transactions} + alias Explorer.Chain.InternalTransaction.DeleteQueue, as: InternalTransactionDeleteQueue alias Explorer.Chain.{Address, Block, Transaction, PendingBlockOperation} alias Explorer.{Chain, Repo} alias Explorer.Utility.MissingBlockRange @@ -440,6 +441,18 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do }} = run_block_consensus_change(block, true, options) end + test "internal transactions are inserted to delete queue if some blocks lost consensus", + %{consensus_block: %{number: block_number} = block, options: options} do + insert(:block, number: block_number, consensus: true) + + assert {:ok, + %{ + save_internal_transactions_for_delete: [^block_number] + }} = run_block_consensus_change(block, true, options) + + assert %{block_number: ^block_number} = Repo.one(InternalTransactionDeleteQueue) + end + # Regression test for https://github.com/poanetwork/blockscout/issues/1644 test "discards neighboring blocks if they aren't related to the current one because of reorg and/or import timeout", %{consensus_block: %{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index c54175ec7ec1..e506a0a82921 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -38,6 +38,7 @@ defmodule Explorer.Factory do Data, Hash, InternalTransaction, + InternalTransaction.DeleteQueue, Log, MultichainSearchDb, PendingBlockOperation, @@ -1479,6 +1480,12 @@ defmodule Explorer.Factory do } end + def internal_transaction_delete_queue_factory do + %DeleteQueue{ + block_number: block_number() + } + end + @beacon_deposit_abi ABI.parse_specification( [ %{ diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex new file mode 100644 index 000000000000..935a530ad889 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex @@ -0,0 +1,124 @@ +defmodule Indexer.Fetcher.InternalTransaction.DeleteQueue do + @moduledoc """ + Deletes internal transactions for block from the queue and inserts new pending operations for them. + """ + + require Logger + + use Indexer.Fetcher, restart: :permanent + + import Ecto.Query + + alias Explorer.{Chain, Repo} + + alias Explorer.Chain.{ + Block, + InternalTransaction, + PendingBlockOperation, + PendingOperationsHelper, + PendingTransactionOperation + } + + alias Explorer.Chain.InternalTransaction.DeleteQueue + alias Explorer.Helper, as: ExplorerHelper + alias Indexer.BufferedTask + alias Indexer.Fetcher.InternalTransaction, as: InternalTransactionFetcher + alias Indexer.Helper, as: IndexerHelper + + @behaviour BufferedTask + + @default_max_batch_size 100 + @default_max_concurrency 1 + @default_threshold :timer.minutes(10) + + @doc false + def child_spec([init_options, gen_server_options]) do + merged_init_opts = + defaults() + |> Keyword.merge(init_options) + |> Keyword.put_new(:state, []) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial_acc, reducer, _) do + {:ok, acc} = + DeleteQueue.stream_data( + initial_acc, + fn data, acc -> + IndexerHelper.reduce_if_queue_is_not_full(data, acc, reducer, __MODULE__) + end, + Application.get_env(:indexer, __MODULE__)[:threshold] || @default_threshold + ) + + acc + end + + @impl BufferedTask + def run(block_numbers, _state) when is_list(block_numbers) do + result = + Repo.transaction(fn -> + DeleteQueue + |> where([dq], dq.block_number in ^block_numbers) + |> Repo.delete_all(timeout: :infinity) + + InternalTransaction + |> where([it], it.block_number in ^block_numbers) + |> Repo.delete_all(timeout: :infinity) + + insert_pending_operations(block_numbers) + end) + + case result do + {:ok, {block_numbers, transactions}} -> + unless is_nil(Process.whereis(InternalTransactionFetcher)) do + InternalTransactionFetcher.async_fetch(block_numbers, transactions, true) + end + + :ok + + {:error, error} -> + Logger.error("Unable to clean internal transactions for reorg: #{inspect(error)}") + {:retry, block_numbers} + end + end + + defp insert_pending_operations(block_numbers) do + case PendingOperationsHelper.pending_operations_type() do + "transactions" -> + transactions = Chain.get_transactions_of_block_numbers(block_numbers) + + pto_params = + transactions + |> Enum.map(&%{transaction_hash: &1.hash}) + |> ExplorerHelper.add_timestamps() + + Repo.insert_all(PendingTransactionOperation, pto_params, on_conflict: :nothing) + {[], transactions} + + "blocks" -> + pbo_params = + Block + |> where([b], b.number in ^block_numbers) + |> where([b], b.consensus == true) + |> select([b], %{block_hash: b.hash, block_number: b.number}) + |> Repo.all() + |> ExplorerHelper.add_timestamps() + + {_total, inserted} = + Repo.insert_all(PendingBlockOperation, pbo_params, on_conflict: :nothing, returning: [:block_number]) + + {Enum.map(inserted, & &1.block_number), []} + end + end + + defp defaults do + [ + flush_interval: :timer.seconds(10), + max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, + max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, + task_supervisor: __MODULE__.TaskSupervisor + ] + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 5a7136aecaff..e19e6d9a2d99 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -23,6 +23,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.Blackfort.Validator, as: ValidatorBlackfort alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime + alias Indexer.Fetcher.InternalTransaction.DeleteQueue, as: InternalTransactionDeleteQueue alias Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue alias Indexer.Fetcher.MultichainSearchDb.CountersExportQueue, as: MultichainSearchDbCountersExportQueue alias Indexer.Fetcher.MultichainSearchDb.CountersFetcher, as: MultichainSearchDbCountersFetcher @@ -157,6 +158,7 @@ defmodule Indexer.Supervisor do {TokenCountersUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {ReplacedTransaction.Supervisor, [[memory_monitor: memory_monitor]]}, + {InternalTransactionDeleteQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbMainExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbBalancesExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, {MultichainSearchDbTokenInfoExportQueue.Supervisor, [[memory_monitor: memory_monitor]]}, diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs new file mode 100644 index 000000000000..b17826dc03fe --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs @@ -0,0 +1,63 @@ +defmodule Indexer.Fetcher.InternalTransaction.DeleteQueueTest do + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.{InternalTransaction, PendingBlockOperation} + alias Explorer.Chain.InternalTransaction.DeleteQueue + alias Indexer.Fetcher.InternalTransaction.DeleteQueue, as: DeleteQueueFetcher + + setup :verify_on_exit! + setup :set_mox_global + + test "deletes internal transactions and inserts pending operations" do + transaction_1 = + :transaction + |> insert() + |> with_block() + + transaction_2 = + :transaction + |> insert() + |> with_block() + + %{block_number: fresh_block_number} = + insert(:internal_transaction_delete_queue, block_number: transaction_1.block_number, updated_at: Timex.now()) + + %{block_number: expired_block_number} = + insert(:internal_transaction_delete_queue, + block_number: transaction_2.block_number, + updated_at: Timex.shift(Timex.now(), minutes: -20) + ) + + insert(:internal_transaction, + transaction: transaction_1, + index: 0, + block_hash: transaction_1.block_hash, + block_index: 0, + block_number: fresh_block_number + ) + + insert(:internal_transaction, + transaction: transaction_2, + index: 0, + block_hash: transaction_2.block_hash, + block_index: 0, + block_number: expired_block_number + ) + + pid = DeleteQueueFetcher.Supervisor.Case.start_supervised!() + + wait_for_results(fn -> + PendingBlockOperation + |> limit(1) + |> Repo.one!() + end) + + assert [%{block_number: ^fresh_block_number}] = Repo.all(DeleteQueue) + assert [%{block_number: ^fresh_block_number}] = Repo.all(InternalTransaction) + assert [%{block_number: ^expired_block_number}] = Repo.all(PendingBlockOperation) + + GenServer.stop(pid) + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/internal_transaction_delete_queue_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/internal_transaction_delete_queue_supervisor_case.ex new file mode 100644 index 000000000000..734cc34d5e6b --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/internal_transaction_delete_queue_supervisor_case.ex @@ -0,0 +1,19 @@ +defmodule Indexer.Fetcher.InternalTransaction.DeleteQueue.Supervisor.Case do + alias Indexer.Fetcher.InternalTransaction.DeleteQueue + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + [ + flush_interval: 50, + max_batch_size: 1, + max_concurrency: 1 + ], + fetcher_arguments + ) + + [merged_fetcher_arguments] + |> DeleteQueue.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/config/runtime.exs b/config/runtime.exs index ece8d4015957..b9bf66a3c941 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1230,6 +1230,11 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersExportQueue, config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) +config :indexer, Indexer.Fetcher.InternalTransaction.DeleteQueue, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_CONCURRENCY", 1), + threshold: ConfigHelper.parse_time_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_THRESHOLD", "10m") + config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.DisputeGame.Supervisor, enabled: ConfigHelper.chain_type() == :optimism diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 852bda3c5497..a5688f3d335c 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -234,6 +234,9 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_MAX_QUEUE_SIZE= # INDEXER_MULTICHAIN_SEARCH_DB_EXPORT_COUNTERS_QUEUE_INIT_QUERY_LIMIT= +# INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_BATCH_SIZE= +# INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_CONCURRENCY= +# INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_THRESHOLD= # INDEXER_TOKEN_INSTANCE_CIDR_BLACKLIST= # INDEXER_TOKEN_INSTANCE_HOST_FILTERING_ENABLED= # INDEXER_TOKEN_INSTANCE_ALLOWED_URI_PROTOCOLS= From f264de7809bf5762b361b2c48fd07aea06cc21d1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 14 Oct 2025 12:10:12 +0300 Subject: [PATCH 0396/1006] Replace INDEXER_INTERNAL_TRANSACTION_ suffix with INDEXER_INTERNAL_TRANSACTIONS_ --- config/runtime.exs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index b9bf66a3c941..4509ad196bde 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1149,6 +1149,11 @@ config :indexer, Indexer.Fetcher.InternalTransaction, indexing_finished_threshold: ConfigHelper.parse_integer_env_var("API_INTERNAL_TRANSACTIONS_INDEXING_FINISHED_THRESHOLD", 1_000) +config :indexer, Indexer.Fetcher.InternalTransaction.DeleteQueue, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_CONCURRENCY", 1), + threshold: ConfigHelper.parse_time_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_THRESHOLD", "10m") + coin_balances_batch_size = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 100) coin_balances_concurrency = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) @@ -1230,11 +1235,6 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersExportQueue, config :indexer, Indexer.Fetcher.SignedAuthorizationStatus, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_SIGNED_AUTHORIZATION_STATUS_BATCH_SIZE", 10) -config :indexer, Indexer.Fetcher.InternalTransaction.DeleteQueue, - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_BATCH_SIZE", 100), - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_CONCURRENCY", 1), - threshold: ConfigHelper.parse_time_env_var("INDEXER_INTERNAL_TRANSACTION_DELETE_QUEUE_THRESHOLD", "10m") - config :indexer, Indexer.Fetcher.Optimism.TransactionBatch.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.OutputRoot.Supervisor, enabled: ConfigHelper.chain_type() == :optimism config :indexer, Indexer.Fetcher.Optimism.DisputeGame.Supervisor, enabled: ConfigHelper.chain_type() == :optimism From 1810ec684285f2752e2efde671f1bbf919f6025b Mon Sep 17 00:00:00 2001 From: Rim Rakhimov Date: Tue, 14 Oct 2025 13:47:05 +0400 Subject: [PATCH 0397/1006] chore(ReindexDuplicatedInternalTransactions): optimize migration performance (#13363) --- ...eindex_duplicated_internal_transactions.ex | 73 +++++++++++++------ ..._duplicated_internal_transactions_test.exs | 16 +++- 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex index ee6ffdc24970..732dd462eea9 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex @@ -9,6 +9,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do require Logger import Ecto.Query + import Explorer.QueryHelper, only: [select_ctid: 1, join_on_ctid: 2] alias Explorer.Repo @@ -28,7 +29,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do def migration_name, do: @migration_name @impl FillingMigration - def last_unprocessed_identifiers(state) do + def last_unprocessed_identifiers(%{"last_processed_block_number" => _} = state) do limit = batch_size() * concurrency() ids = @@ -40,30 +41,44 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do case {ids, state["step"]} do {[], step} when step != "finalize" -> - new_state = %{"step" => "finalize"} + new_state = Map.put(state, "step", "finalize") MigrationStatus.update_meta(migration_name(), new_state) last_unprocessed_identifiers(new_state) - {ids, _field} -> + {ids, step} when step != "finalize" -> + {ids, Map.put(state, "last_processed_block_number", Enum.max(ids))} + + {ids, step} when step == "finalize" -> {ids, state} end end - @impl FillingMigration - def unprocessed_data_query(state) do - field = - case state["step"] do - "finalize" -> :block_hash - _ -> :block_number - end + def last_unprocessed_identifiers(state) do + state + |> Map.put("last_processed_block_number", -1) + |> last_unprocessed_identifiers() + end - from( - it in InternalTransaction, - where: not is_nil(field(it, ^field)), - group_by: [field(it, ^field), it.transaction_index, it.index], - having: count("*") > 1, - select: field(it, ^field) - ) + @impl FillingMigration + def unprocessed_data_query(%{"last_processed_block_number" => last_processed_block_number} = state) do + if state["step"] == "finalize" do + from( + it in InternalTransaction, + select: it.block_hash, + where: not is_nil(it.block_hash), + group_by: [it.block_hash, it.transaction_index, it.index], + having: count("*") > 1 + ) + else + from( + it in InternalTransaction, + select: it.block_number, + where: not is_nil(it.block_number) and it.block_number >= ^last_processed_block_number, + group_by: [it.block_number, it.transaction_index, it.index], + having: count("*") > 1, + order_by: it.block_number + ) + end end @impl FillingMigration @@ -78,9 +93,23 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do result = Repo.transaction(fn -> - InternalTransaction - |> where([it], field(it, ^it_field) in ^block_numbers_or_hashes) - |> Repo.delete_all() + locked_internal_transactions_to_delete_query = + from( + it in InternalTransaction, + select: select_ctid(it), + where: field(it, ^it_field) in ^block_numbers_or_hashes, + order_by: [asc: it.transaction_hash, asc: it.index], + lock: "FOR UPDATE" + ) + + delete_query = + from( + it in InternalTransaction, + inner_join: locked_it in subquery(locked_internal_transactions_to_delete_query), + on: join_on_ctid(it, locked_it) + ) + + Repo.delete_all(delete_query) pbo_params = Block @@ -90,8 +119,10 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do |> Repo.all() |> Enum.map(&Map.merge(&1, %{inserted_at: now, updated_at: now})) + ordered_pbo_params = Enum.sort_by(pbo_params, &{&1.block_hash}) + {_total, inserted} = - Repo.insert_all(PendingBlockOperation, pbo_params, on_conflict: :nothing, returning: [:block_number]) + Repo.insert_all(PendingBlockOperation, ordered_pbo_params, on_conflict: :nothing, returning: [:block_number]) inserted end) diff --git a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs index 86b591e9ecc4..cacb7c825f75 100644 --- a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs +++ b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs @@ -5,6 +5,20 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do alias Explorer.Migrator.{MigrationStatus, ReindexDuplicatedInternalTransactions} alias Explorer.Repo + setup_all do + opts = Application.get_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions) + + Application.put_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, + batch_size: 1, + concurrency: 1, + timeout: 0 + ) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, opts) + end) + end + test "Reindex duplicated internal transactions" do transaction_from_invalid_block_1 = :transaction @@ -57,7 +71,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do :internal_transaction, transaction: transaction_from_invalid_block_2, index: index - 10, - block_number: nil, + block_number: transaction_from_invalid_block_2.block_number, transaction_index: transaction_from_invalid_block_2.index, block_hash: transaction_from_invalid_block_2.block_hash, block_index: index From cb9c66557d5b3efd504d845a554a2b83f7754caa Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 14 Oct 2025 16:16:57 +0400 Subject: [PATCH 0398/1006] chore: Add migration to delete zero-value calls (#13305) --- apps/explorer/config/config.exs | 2 + apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 1 + ...delete_zero_value_internal_transactions.ex | 153 ++++++++++++++++++ ..._zero_value_internal_transactions_test.exs | 109 +++++++++++++ config/runtime.exs | 8 + docker-compose/envs/common-blockscout.env | 4 + 7 files changed, 278 insertions(+) create mode 100644 apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex create mode 100644 apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index b7a2acc14ffa..431c96188ae8 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -110,6 +110,8 @@ config :explorer, Explorer.Chain.Cache.Counters.BlockPriorityFeeCount, config :explorer, Explorer.TokenInstanceOwnerAddressMigration.Supervisor, enabled: true +config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, enabled: false + for migrator <- [ # Background migrations Explorer.Migrator.TransactionsDenormalization, diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 3bfdfb43792d..09dcd525fcd6 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -75,6 +75,7 @@ for migrator <- [ Explorer.Migrator.MergeAdjacentMissingBlockRanges, Explorer.Migrator.UnescapeQuotesInTokens, Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, + Explorer.Migrator.DeleteZeroValueInternalTransactions, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index b37ae35d5eac..c6a160ea0f80 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -195,6 +195,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.UnescapeQuotesInTokens, :indexer), configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, :indexer), + configure_mode_dependent_process(Explorer.Migrator.DeleteZeroValueInternalTransactions, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex new file mode 100644 index 000000000000..b0c4ae169189 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -0,0 +1,153 @@ +defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do + @moduledoc """ + Continuously deletes all zero-value calls older than + `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` from DB. + """ + + use GenServer + + import Ecto.Query + import Explorer.QueryHelper, only: [select_ctid: 1, join_on_ctid: 2] + + alias Explorer.Chain.{Block, InternalTransaction} + alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Explorer.Migrator.MigrationStatus + alias Explorer.Repo + alias Timex.Duration + + @migration_name "delete_zero_value_internal_transactions" + @past_check_interval 10 + @default_future_check_interval :timer.minutes(1) + @default_batch_size 100 + @default_storage_period_days 30 + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_) do + {:ok, %{}, {:continue, :ok}} + end + + @impl true + def handle_continue(:ok, _state) do + state = + case MigrationStatus.fetch(@migration_name) do + nil -> + border_number = get_border_number() + state = %{min_block_number: border_number, max_block_number: border_number} + MigrationStatus.set_status(@migration_name, "started") + MigrationStatus.update_meta(@migration_name, state) + state + + %{meta: meta} -> + meta + end + + schedule_future_check() + schedule_past_check() + + {:noreply, state} + end + + @impl true + def handle_info(:update_future, %{max_block_number: max_number} = state) do + border_number = get_border_number() + clear_internal_transactions(max_number, border_number) + new_state = %{state | max_block_number: border_number + 1} + MigrationStatus.update_meta(@migration_name, new_state) + schedule_future_check() + {:noreply, new_state} + end + + @impl true + def handle_info(:update_past, %{min_block_number: -1} = state) do + new_state = Map.delete(state, :min_block_number) + MigrationStatus.set_status(@migration_name, "completed") + MigrationStatus.update_meta(@migration_name, new_state) + {:noreply, new_state} + end + + def handle_info(:update_past, %{min_block_number: min_number} = state) do + from_number = max(min_number - batch_size(), 0) + clear_internal_transactions(from_number, min_number) + new_state = %{state | min_block_number: from_number - 1} + MigrationStatus.update_meta(@migration_name, new_state) + schedule_past_check() + {:noreply, new_state} + end + + def handle_info(:update_past, state) do + {:noreply, state} + end + + defp clear_internal_transactions(from_number, to_number) when from_number < to_number do + Repo.transaction(fn -> + locked_internal_transactions_to_delete_query = + from( + it in InternalTransaction, + select: select_ctid(it), + where: it.block_number >= ^from_number, + where: it.block_number <= ^to_number, + where: it.type == ^:call, + where: it.value == ^0, + order_by: [asc: it.transaction_hash, asc: it.index], + lock: "FOR UPDATE" + ) + + delete_query = + from( + it in InternalTransaction, + inner_join: locked_it in subquery(locked_internal_transactions_to_delete_query), + on: join_on_ctid(it, locked_it) + ) + + Repo.delete_all(delete_query) + end) + end + + defp clear_internal_transactions(_from, _to), do: :ok + + defp get_border_number do + storage_period = Application.get_env(:explorer, __MODULE__)[:storage_period_days] || @default_storage_period_days + border_timestamp = Timex.shift(Timex.now(), days: -storage_period) + + Block + |> where([b], b.timestamp < ^border_timestamp) + |> order_by([b], desc: b.timestamp) + |> limit(1) + |> select([b], b.number) + |> Repo.one() + |> Kernel.||(0) + end + + defp schedule_past_check do + Process.send_after(self(), :update_past, @past_check_interval) + end + + defp schedule_future_check do + Process.send_after(self(), :update_future, future_check_interval()) + end + + defp future_check_interval do + with nil <- Application.get_env(:explorer, __MODULE__)[:future_check_interval], + nil <- get_average_block_time() do + @default_future_check_interval + else + interval -> interval + end + end + + defp get_average_block_time do + case AverageBlockTime.average_block_time() do + {:error, :disabled} -> nil + average_block_time -> Duration.to_milliseconds(average_block_time) + end + end + + defp batch_size do + max(Application.get_env(:explorer, __MODULE__)[:batch_size] || @default_batch_size, 1) + end +end diff --git a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs new file mode 100644 index 000000000000..a56cd055cd3c --- /dev/null +++ b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs @@ -0,0 +1,109 @@ +defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.InternalTransaction + alias Explorer.Migrator.{DeleteZeroValueInternalTransactions, MigrationStatus} + alias Explorer.Repo + + test "Deletes zero value calls" do + Enum.map(1..3, fn i -> + block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -40)) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_index: i, + type: :call, + value: 0 + ) + end) + + Enum.map(1..4, fn i -> + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_index: i, + type: :call, + value: 1 + ) + end) + + Enum.map(1..5, fn i -> + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction_create, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_index: i, + value: 0 + ) + end) + + Enum.map(1..6, fn i -> + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + block_index: i, + type: :call, + value: 0 + ) + end) + + assert MigrationStatus.get_status("delete_zero_value_internal_transactions") == nil + + DeleteZeroValueInternalTransactions.start_link([]) + + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: ms.migration_name == ^"delete_zero_value_internal_transactions" and ms.status == "completed" + ) + ) + end) + + all_internal_transactions = Repo.all(InternalTransaction) + + assert Enum.count(all_internal_transactions) == 15 + + non_zero_value_calls = + Enum.filter(all_internal_transactions, &(&1.type == :call and not Decimal.eq?(&1.value.value, 0))) + + non_calls = Enum.filter(all_internal_transactions, &(&1.type != :call)) + + recent_zero_value_calls = + Enum.filter(all_internal_transactions, &(&1.type == :call and Decimal.eq?(&1.value.value, 0))) + + assert Enum.count(non_zero_value_calls) == 4 + assert Enum.all?(non_zero_value_calls, &(not Decimal.eq?(&1.value.value, 0))) + + assert Enum.count(non_calls) == 5 + + assert Enum.count(recent_zero_value_calls) == 6 + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 4509ad196bde..8d16333925ff 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -835,6 +835,14 @@ config :explorer, Explorer.Migrator.BackfillMetadataURL, config :explorer, Explorer.Migrator.MergeAdjacentMissingBlockRanges, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE", 100) +config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, + enabled: ConfigHelper.parse_bool_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED", "false"), + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE", 100), + storage_period_days: + ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS", 30), + future_check_interval: + ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL", "1m") + config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index a5688f3d335c..0842266b802d 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -558,6 +558,10 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_BLOCKS_BATCH_SIZE= # MIGRATION_RECOVERY_WETH_TOKEN_TRANSFERS_HIGH_VERBOSITY= # MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL= # HEALTH_MONITOR_CHECK_INTERVAL=1m # HEALTH_MONITOR_BLOCKS_PERIOD=5m # HEALTH_MONITOR_BATCHES_PERIOD=4h From 7f2a75e3c3e81c66b5dd9998839d49c1885e81fc Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 14 Oct 2025 15:27:50 +0300 Subject: [PATCH 0399/1006] fix: Add fallback to cached token counters in corresponding async tasks (#13348) --- .../controllers/api/v2/token_controller.ex | 2 +- apps/explorer/lib/explorer/chain.ex | 33 +++++++++++++++++-- .../cache/counters/token_holders_count.ex | 8 +++-- .../cache/counters/token_transfers_count.ex | 8 +++-- .../smart_contract/solidity/publish_helper.ex | 5 ++- 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 2ef12e998179..d0c7fba07c10 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -115,7 +115,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do - {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 30_000) + {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 5_000) json(conn, %{transfers_count: to_string(transfers_count), token_holders_count: to_string(holders_count)}) end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 7093f253c0ae..7a74817eba93 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3917,23 +3917,50 @@ defmodule Explorer.Chain do [total_token_transfers_task, total_token_holders_task] |> Task.yield_many(timeout) - |> Enum.map(fn {_task, res} -> + |> Enum.map(fn {task, res} -> case res do {:ok, result} -> result {:exit, reason} -> Logger.warning("Query fetching token counters terminated: #{inspect(reason)}") - 0 + + fallback_cached_value_based_on_async_task_pid( + task.pid, + total_token_transfers_task.pid, + total_token_holders_task.pid, + address_hash + ) nil -> Logger.warning("Query fetching token counters timed out.") - 0 + + fallback_cached_value_based_on_async_task_pid( + task.pid, + total_token_transfers_task.pid, + total_token_holders_task.pid, + address_hash + ) end end) |> List.to_tuple() end + defp fallback_cached_value_based_on_async_task_pid( + task_pid, + total_token_transfers_task_pid, + total_token_holders_task_pid, + address_hash + ) do + case task_pid do + ^total_token_transfers_task_pid -> + TokenTransfersCount.fetch_count_from_cache(address_hash) + + ^total_token_holders_task_pid -> + TokenHoldersCount.fetch_count_from_cache(address_hash) + end + end + @spec flat_1155_batch_token_transfers([TokenTransfer.t()], Decimal.t() | nil) :: [TokenTransfer.t()] def flat_1155_batch_token_transfers(token_transfers, token_id \\ nil) when is_list(token_transfers) do token_transfers diff --git a/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex index 188d0fe152f5..68581f04d307 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/token_holders_count.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Cache.Counters.TokenHoldersCount do alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Cache.Counters.Helper - alias Explorer.Chain.Token + alias Explorer.Chain.{Hash, Token} @api_true [api?: true] @cache_name :token_holders_count @@ -69,7 +69,11 @@ defmodule Explorer.Chain.Cache.Counters.TokenHoldersCount do put_into_db_cache(address_hash, new_data) end - defp fetch_count_from_cache(address_hash) do + @doc """ + Fetches the token holders count from the cache or database. + """ + @spec fetch_count_from_cache(Hash.Address.t()) :: integer() + def fetch_count_from_cache(address_hash) do address_hash_string = to_string(address_hash) key = "hash_#{address_hash_string}" diff --git a/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex index 01de27ceba46..736f09f9c56e 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/token_transfers_count.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Cache.Counters.TokenTransfersCount do alias Explorer.Chain alias Explorer.Chain.Cache.Counters.Helper - alias Explorer.Chain.Token + alias Explorer.Chain.{Hash, Token} @api_true [api?: true] @cache_name :token_transfers_counter @@ -69,7 +69,11 @@ defmodule Explorer.Chain.Cache.Counters.TokenTransfersCount do put_into_db_cache(address_hash, new_data) end - defp fetch_count_from_cache(address_hash) do + @doc """ + Fetches the token transfers count from the cache or database. + """ + @spec fetch_count_from_cache(Hash.Address.t()) :: integer() + def fetch_count_from_cache(address_hash) do address_hash_string = to_string(address_hash) key = "hash_#{address_hash_string}" diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publish_helper.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publish_helper.ex index 56597fae83ef..6d8b0cb7a782 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/publish_helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/publish_helper.ex @@ -2,6 +2,7 @@ defmodule Explorer.SmartContract.Solidity.PublishHelper do @moduledoc """ Module responsible for preparing and publishing smart contracts """ + require Logger use Utils.RuntimeEnvHelper, eth_bytecode_db_enabled?: [ @@ -47,7 +48,9 @@ defmodule Explorer.SmartContract.Solidity.PublishHelper do :on_demand ) - _ -> + error -> + Logger.error("Unexpected error during verification: #{inspect(error)}") + EventsPublisher.broadcast( prepare_verification_error("Unexpected error", address_hash_string, conn, api_v2?), :on_demand From 0c41300b6f614eb4ef281c2182fc703db8827905 Mon Sep 17 00:00:00 2001 From: Andrew Gross Date: Tue, 14 Oct 2025 22:50:14 -0600 Subject: [PATCH 0400/1006] Create FUNDING.json (#13393) --- FUNDING.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 FUNDING.json diff --git a/FUNDING.json b/FUNDING.json new file mode 100644 index 000000000000..b5dc16a15a86 --- /dev/null +++ b/FUNDING.json @@ -0,0 +1,7 @@ +{ + "drips": { + "filecoin": { + "ownedBy": "0x5C36Bd76a6c138187C43da92f66f37E23b4017fA" + } + } +} From 9b3040f8aa66f8eaccca20e142875e4a7c673b9c Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Wed, 15 Oct 2025 13:08:49 +0300 Subject: [PATCH 0401/1006] fix: add `log_index` field to celo validator group votes table (#13391) --- .../chain/celo/validator_group_vote.ex | 3 +- .../runner/celo/validator_group_votes.ex | 2 + ...add_log_index_to_validator_group_votes.exs | 46 +++++++++++++++++++ .../fetcher/celo/validator_group_votes.ex | 3 +- 4 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 apps/explorer/priv/celo/migrations/20251014111445_add_log_index_to_validator_group_votes.exs diff --git a/apps/explorer/lib/explorer/chain/celo/validator_group_vote.ex b/apps/explorer/lib/explorer/chain/celo/validator_group_vote.ex index 221b1c62c0b8..e61bf339874b 100644 --- a/apps/explorer/lib/explorer/chain/celo/validator_group_vote.ex +++ b/apps/explorer/lib/explorer/chain/celo/validator_group_vote.ex @@ -9,7 +9,7 @@ defmodule Explorer.Chain.Celo.ValidatorGroupVote do alias Explorer.Chain.{Address, Block, Hash, Transaction} @types_enum ~w(activated revoked)a - @required_attrs ~w(account_address_hash group_address_hash type transaction_hash block_hash block_number)a + @required_attrs ~w(account_address_hash group_address_hash type transaction_hash block_hash block_number log_index)a @typedoc """ * `account_address_hash` - the address of the account that made the vote. @@ -42,6 +42,7 @@ defmodule Explorer.Chain.Celo.ValidatorGroupVote do ) field(:block_number, :integer, null: false) + field(:log_index, :integer, primary_key: true, null: false) belongs_to(:block, Block, foreign_key: :block_hash, diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/validator_group_votes.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/validator_group_votes.ex index 10cd12c5b30d..f196a1fe6724 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/celo/validator_group_votes.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/validator_group_votes.ex @@ -65,6 +65,7 @@ defmodule Explorer.Chain.Import.Runner.Celo.ValidatorGroupVotes do changes_list, &{ &1.transaction_hash, + &1.log_index, &1.account_address_hash, &1.group_address_hash } @@ -80,6 +81,7 @@ defmodule Explorer.Chain.Import.Runner.Celo.ValidatorGroupVotes do timestamps: timestamps, conflict_target: [ :transaction_hash, + :log_index, :account_address_hash, :group_address_hash ], diff --git a/apps/explorer/priv/celo/migrations/20251014111445_add_log_index_to_validator_group_votes.exs b/apps/explorer/priv/celo/migrations/20251014111445_add_log_index_to_validator_group_votes.exs new file mode 100644 index 000000000000..a97666878918 --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20251014111445_add_log_index_to_validator_group_votes.exs @@ -0,0 +1,46 @@ +defmodule Explorer.Repo.Celo.Migrations.AddLogIndexToValidatorGroupVotes do + use Ecto.Migration + + def up do + execute(""" + ALTER TABLE celo_validator_group_votes + DROP CONSTRAINT celo_validator_group_votes_pkey + """) + + alter table(:celo_validator_group_votes) do + add(:log_index, :integer, null: false, default: 0) + end + + execute(""" + ALTER TABLE celo_validator_group_votes + ADD CONSTRAINT celo_validator_group_votes_pkey + PRIMARY KEY ( + transaction_hash, + log_index, + account_address_hash, + group_address_hash + ) + """) + end + + def down do + execute(""" + ALTER TABLE celo_validator_group_votes + DROP CONSTRAINT celo_validator_group_votes_pkey + """) + + alter table(:celo_validator_group_votes) do + remove(:log_index) + end + + execute(""" + ALTER TABLE celo_validator_group_votes + ADD CONSTRAINT celo_validator_group_votes_pkey + PRIMARY KEY ( + transaction_hash, + account_address_hash, + group_address_hash + ) + """) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex index 2c461dc311b6..d45d0e25ff18 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/validator_group_votes.ex @@ -211,7 +211,8 @@ defmodule Indexer.Fetcher.Celo.ValidatorGroupVotes do type: type, block_number: log.block_number, block_hash: log.block_hash, - transaction_hash: log.transaction_hash + transaction_hash: log.transaction_hash, + log_index: log.index } end end From c86dc6c395a70ef7a87b866e1fc217a677f0f816 Mon Sep 17 00:00:00 2001 From: Andrew Gross Date: Wed, 15 Oct 2025 08:53:18 -0600 Subject: [PATCH 0402/1006] fix: update FUNDING.json (#13399) Co-authored-by: Fedor Ivanov --- FUNDING.json | 11 +++++++---- funding.json | 5 ----- 2 files changed, 7 insertions(+), 9 deletions(-) delete mode 100644 funding.json diff --git a/FUNDING.json b/FUNDING.json index b5dc16a15a86..6bc52bce679f 100644 --- a/FUNDING.json +++ b/FUNDING.json @@ -1,7 +1,10 @@ { - "drips": { - "filecoin": { - "ownedBy": "0x5C36Bd76a6c138187C43da92f66f37E23b4017fA" + "drips": { + "filecoin": { + "ownedBy": "0x5C36Bd76a6c138187C43da92f66f37E23b4017fA" + }, + "opRetro": { + "projectId": "0x663e4d25ca3f327365240471b4831ea3c989cb132bbf6ae8f5c1e15268591795" + } } - } } diff --git a/funding.json b/funding.json deleted file mode 100644 index c125d68d15b8..000000000000 --- a/funding.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "opRetro": { - "projectId": "0x663e4d25ca3f327365240471b4831ea3c989cb132bbf6ae8f5c1e15268591795" - } -} From 08c976b7f29b8041f442462ca6536d4d01f5f7e8 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 16 Oct 2025 10:40:37 +0300 Subject: [PATCH 0403/1006] perf: batch preload token transfers in `/api/v2/celo/epochs` (#13398) --- .../explorer/lib/explorer/chain/celo/epoch.ex | 80 ++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex index 05bc2c718eb8..5cbef0c18246 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -12,7 +12,7 @@ defmodule Explorer.Chain.Celo.Epoch do import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] - alias Explorer.{Chain, Repo, SortingHelper} + alias Explorer.{Chain, QueryHelper, Repo, SortingHelper} alias Explorer.Chain.{ Block, @@ -148,9 +148,85 @@ defmodule Explorer.Chain.Celo.Epoch do |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() - |> Enum.map(&with_loaded_distribution_token_transfers(&1, options)) + |> load_all_distribution_token_transfers(options) end + defp load_all_distribution_token_transfers(epochs, options) do + block_hash_log_index_pairs = + epochs + |> Enum.flat_map(fn + %__MODULE__{ + end_processing_block_hash: block_hash, + distribution: %EpochReward{} = distribution + } -> + block_hash = to_string(block_hash) + + [ + {block_hash, distribution.reserve_bolster_transfer_log_index}, + {block_hash, distribution.community_transfer_log_index}, + {block_hash, distribution.carbon_offsetting_transfer_log_index} + ] + |> Enum.reject(fn {_, log_index} -> is_nil(log_index) end) + + _ -> + [] + end) + + token_transfers_query = + from( + tt in TokenTransfer.only_consensus_transfers_query(), + where: ^QueryHelper.tuple_in([:block_hash, :log_index], block_hash_log_index_pairs), + select: {{tt.block_hash, tt.log_index}, tt}, + preload: [token: ^reputation_association()] + ) + + token_transfers_map = + token_transfers_query + |> Chain.select_repo(options).all() + |> Map.new() + + Enum.map(epochs, &populate_epoch_token_transfers(&1, token_transfers_map)) + end + + # Populate a single epoch with its token transfers from the fetched map + defp populate_epoch_token_transfers( + %__MODULE__{ + end_processing_block_hash: block_hash, + distribution: %EpochReward{} = distribution + } = epoch, + token_transfers_map + ) do + updated_distribution = + %EpochReward{ + distribution + | reserve_bolster_transfer: + token_transfers_map[ + { + block_hash, + distribution.reserve_bolster_transfer_log_index + } + ], + community_transfer: + token_transfers_map[ + { + block_hash, + distribution.community_transfer_log_index + } + ], + carbon_offsetting_transfer: + token_transfers_map[ + { + block_hash, + distribution.carbon_offsetting_transfer_log_index + } + ] + } + + %__MODULE__{epoch | distribution: updated_distribution} + end + + defp populate_epoch_token_transfers(epoch, _token_transfers_map), do: epoch + @doc """ Returns a query to find an epoch by its number. From da84ed847798626dff66f5a96da83a1a33e79a74 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 17 Oct 2025 15:16:49 +0300 Subject: [PATCH 0404/1006] fix: ignore old reorgs in beacon deposits fetcher (#13372) --- .../explorer/chain/import/runner/blocks.ex | 12 +++- .../chain/import/runner/blocks_test.exs | 59 +++++++++++++++++++ .../lib/indexer/fetcher/beacon/deposit.ex | 25 ++++---- 3 files changed, 79 insertions(+), 17 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 861147d6c4ff..c7e97e5ddfec 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -28,6 +28,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do } alias Explorer.Chain.Block.Reward + alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Import.Runner alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances alias Explorer.Chain.Import.Runner.{Addresses, TokenInstances, Tokens} @@ -560,8 +561,15 @@ defmodule Explorer.Chain.Import.Runner.Blocks do removed_consensus_blocks |> Enum.map(fn {number, _hash} -> number end) - if not Enum.empty?(removed_consensus_block_numbers) do - GenServer.cast(Indexer.Fetcher.Beacon.Deposit, {:lost_consensus, removed_consensus_block_numbers |> Enum.min()}) + maximum_block_number = BlockNumber.get_max() + + minimum_recent_block_number = + removed_consensus_block_numbers + |> Enum.filter(fn n -> n >= maximum_block_number - 64 end) + |> Enum.min(fn -> nil end) + + if minimum_recent_block_number do + GenServer.cast(Indexer.Fetcher.Beacon.Deposit, {:lost_consensus, minimum_recent_block_number}) end repo.update_all( diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 226c0c9c8ab4..2030578a08ff 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -10,6 +10,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do alias Explorer.Chain.Import.Runner.{Blocks, Transactions} alias Explorer.Chain.InternalTransaction.DeleteQueue, as: InternalTransactionDeleteQueue alias Explorer.Chain.{Address, Block, Transaction, PendingBlockOperation} + alias Explorer.Chain.Cache.BlockNumber alias Explorer.{Chain, Repo} alias Explorer.Utility.MissingBlockRange @@ -909,6 +910,64 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do assert {:ok, [{0, _}, {1, _}]} = Blocks.process_blocks_consensus([new_block1_changes], Repo, opts) end + + test "does not trigger beacon deposit reorg handling on old blocks" do + Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: false) + end) + + BlockNumber.set_max(100) + + Process.register(self(), Indexer.Fetcher.Beacon.Deposit) + + insert(:block, consensus: true, number: 0) + insert(:block, consensus: true, number: 1) + insert(:block, consensus: false, number: 2) + + new_block0 = params_for(:block, miner_hash: insert(:address).hash, number: 0) + new_block1 = params_for(:block, miner_hash: insert(:address).hash, parent_hash: new_block0.hash, number: 1) + + %Ecto.Changeset{valid?: true, changes: new_block1_changes} = Block.changeset(%Block{}, new_block1) + + opts = %{ + timeout: 60_000, + timestamps: %{updated_at: DateTime.utc_now()} + } + + Blocks.process_blocks_consensus([new_block1_changes], Repo, opts) + refute_received {:"$gen_cast", {:lost_consensus, _}} + end + + test "triggers beacon deposit reorg handling on fresh blocks" do + Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.Cache.BlockNumber, enabled: false) + end) + + BlockNumber.set_max(50) + + Process.register(self(), Indexer.Fetcher.Beacon.Deposit) + + insert(:block, consensus: true, number: 0) + insert(:block, consensus: true, number: 1) + insert(:block, consensus: false, number: 2) + + new_block0 = params_for(:block, miner_hash: insert(:address).hash, number: 0) + new_block1 = params_for(:block, miner_hash: insert(:address).hash, parent_hash: new_block0.hash, number: 1) + + %Ecto.Changeset{valid?: true, changes: new_block1_changes} = Block.changeset(%Block{}, new_block1) + + opts = %{ + timeout: 60_000, + timestamps: %{updated_at: DateTime.utc_now()} + } + + Blocks.process_blocks_consensus([new_block1_changes], Repo, opts) + assert_received {:"$gen_cast", {:lost_consensus, _}} + end end defp insert_block(block_params, options) do diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index a006db6a16a6..43d12b5264ca 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -93,30 +93,25 @@ defmodule Indexer.Fetcher.Beacon.Deposit do @impl GenServer def handle_cast({:lost_consensus, block_number}, %__MODULE__{} = state) do - max_reorg_depth_block_number = block_number + 64 - - {_deleted_deposits_count, _deleted_deposits} = + {_deleted_deposits_count, deleted_deposits} = Repo.delete_all( from( d in Deposit, where: d.block_number > ^block_number, - where: d.block_number <= ^max_reorg_depth_block_number, select: d.index ), timeout: :infinity ) - # todo: temporarily do not modify state of the indexer on reorgs. - # It should be handled by a separate process. - # deposit_index = Enum.min(deleted_deposits, fn -> state.deposit_index + 1 end) - # {:noreply, - # %{ - # state - # | deposit_index: deposit_index - 1, - # last_processed_log_block_number: block_number, - # last_processed_log_index: -1 - # }} - {:noreply, state} + deposit_index = Enum.min(deleted_deposits, fn -> state.deposit_index + 1 end) + + {:noreply, + %{ + state + | deposit_index: deposit_index - 1, + last_processed_log_block_number: block_number, + last_processed_log_index: -1 + }} rescue postgrex_error in Postgrex.Error -> Logger.error( From 2f1b0d24f64d8f568fd434b938229b8c1d783bbe Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 17 Oct 2025 15:27:05 +0300 Subject: [PATCH 0405/1006] fix: Advanced filters: ERC-20 value in CSV (#13326) --- .../views/api/v2/advanced_filter_view.ex | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index f8985f4ebd53..1c5a4e5b2d16 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -83,6 +83,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do {opening_price, closing_price} = date_to_prices[DateTime.to_date(advanced_filter.timestamp)] + value = prepare_value(advanced_filter) + [ to_string(advanced_filter.hash), advanced_filter.type, @@ -91,7 +93,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do Address.checksum(advanced_filter.from_address_hash), Address.checksum(advanced_filter.to_address_hash), Address.checksum(advanced_filter.created_contract_address_hash), - decimal_to_string_xsd(advanced_filter.value), + value, if(advanced_filter.type != "coin_transfer", do: Address.checksum(advanced_filter.token_transfer.token.contract_address_hash), else: nil @@ -112,6 +114,38 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do Stream.concat([row_names], af_lists) end + defp prepare_value( + %{ + type: type, + value: value, + token_transfer: + %{ + token: token, + amount: amount + } = token_transfer + } = _advanced_filter + ) do + case type do + "coin_transfer" -> + value + + "ERC-20" -> + if is_nil(token.decimals) or Decimal.equal?(token.decimals, 0) do + token_transfer.amount + else + Decimal.div( + amount, + Integer.pow(10, Decimal.to_integer(token.decimals)) + ) + end + + _ -> + Enum.count(token_transfer.token_ids) + end + end + + defp prepare_value(advanced_filter), do: advanced_filter.value + defp prepare_advanced_filter(advanced_filter, decoded_input) do %{ hash: advanced_filter.hash, From 5f945dda599f5698af90802e99c2e54d4ee45908 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 17 Oct 2025 15:30:11 +0300 Subject: [PATCH 0406/1006] fix: Fix flaky indexer, web tests, refactoring (#13392) --- .../v2/exchange_rate_channel_test.exs | 2 +- .../api/rpc/address_controller_test.exs | 119 ++---- .../api/rpc/eth_controller_test.exs | 1 - .../api/v2/token_controller_test.exs | 196 +++------ .../lib/explorer/etherscan/addresses.ex | 3 + apps/explorer/lib/test_helper.ex | 63 +++ .../chain/import/runner/blocks_test.exs | 2 +- .../chain_spec/geth/importer_test.exs | 83 +--- .../chain_spec/parity/importer_test.exs | 72 +--- ...ze_incorrect_weth_token_transfers_test.exs | 3 +- apps/explorer/test/support/data_case.ex | 3 +- apps/indexer/lib/indexer/buffered_task.ex | 25 +- .../indexer/fetcher/on_demand/coin_balance.ex | 69 +--- .../transform/address_coin_balances_daily.ex | 8 +- .../test/indexer/block/fetcher_test.exs | 42 +- .../indexer/fetcher/block_reward_test.exs | 329 +++------------ .../fetcher/coin_balance/catchup_test.exs | 153 +------ .../fetcher/on_demand/coin_balance_test.exs | 100 +---- .../fetcher/on_demand/contract_code_test.exs | 114 ++---- .../fetcher/on_demand/token_balance_test.exs | 64 +-- .../fetcher/token_instance/helper_test.exs | 378 +++++------------- 21 files changed, 423 insertions(+), 1406 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs index bb4df6801ecd..6e080cad45b6 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs @@ -94,7 +94,7 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do assert payload.exchange_rate == token.fiat_value assert payload.chart_data == records after - :timer.seconds(5) -> + :timer.seconds(10) -> assert false, "Expected message received nothing." end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index d0a85492ad02..e686fd636117 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -4,12 +4,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do import Mox alias BlockScoutWeb.API.RPC.AddressController - alias Explorer.Chain + alias Explorer.{Chain, Repo, TestHelper} alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.{Events.Subscriber, Transaction, Wei, Repo} + alias Explorer.Chain.{Events.Subscriber, Transaction, Wei} alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand - alias Explorer.Repo setup :set_mox_global setup :verify_on_exit! @@ -44,6 +43,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do setup do Subscriber.to(:addresses, :on_demand) Subscriber.to(:address_coin_balances, :on_demand) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) %{params: %{"module" => "account", "action" => "listaccounts"}} end @@ -134,78 +135,41 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do test "with a stale balance", %{conn: conn, params: params} do now = Timex.now() + latest_block_number = 302 mining_address = insert(:address, fetched_coin_balance: 0, - fetched_coin_balance_block_number: 103, + fetched_coin_balance_block_number: latest_block_number, inserted_at: Timex.shift(now, minutes: -10) ) mining_address_hash = to_string(mining_address.hash) - # we space these very far apart so that we know it will consider the 0th block stale (it calculates how far - # back we'd need to go to get 24 hours in the past) - Enum.each(0..101, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, hours: -(103 - i) * 25), miner: mining_address) + + Enum.each(0..301, fn i -> + insert(:block, + number: i, + timestamp: Timex.shift(now, minutes: -25 * 60 - (latest_block_number - i)), + miner: mining_address + ) end) - insert(:block, number: 102, timestamp: Timex.shift(now, hours: -25), miner: mining_address) + insert(:block, number: latest_block_number, timestamp: Timex.shift(now, hours: -25), miner: mining_address) AverageBlockTime.refresh() address = insert(:address, fetched_coin_balance: 100, - fetched_coin_balance_block_number: 101, + # more than 1 hour ago, so should be stale + fetched_coin_balance_block_number: 240, inserted_at: Timex.shift(now, minutes: -5) ) address_hash = to_string(address.hash) - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: id, - method: "eth_getBalance", - params: [^address_hash, "0x66"] - } - ], - _options -> - {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} - end) - - res = eth_block_number_fake_response("0x66") - - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: 0, - method: "eth_getBlockByNumber", - params: ["0x66", true] - } - ], - _ -> - {:ok, [res]} - end) - - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: id, - method: "eth_getBalance", - params: [^mining_address_hash, "0x66"] - } - ], - _options -> - {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} - end) + latest_block_number_hex = "0x" <> Integer.to_string(latest_block_number, 16) - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: 0, - method: "eth_getBlockByNumber", - params: ["0x66", true] - } - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_balance_expectation(address_hash, latest_block_number_hex, "0x02") response = conn @@ -238,7 +202,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert received_address.hash == address.hash assert received_address.fetched_coin_balance == expected_wei - assert received_address.fetched_coin_balance_block_number == 102 + assert received_address.fetched_coin_balance_block_number == latest_block_number end end @@ -5013,39 +4977,16 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> ExJsonSchema.Schema.resolve() end - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } + defp eth_get_block_by_number_expectation(latest_block_number_hex, res) do + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: 0, + method: "eth_getBlockByNumber", + params: [^latest_block_number_hex, true] + } + ], + _ -> + {:ok, [res]} + end) end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs index f8191bac9e95..b54f8aa0772d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs @@ -3,7 +3,6 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} alias Explorer.Repo - alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" @first_topic_hex_string_2 "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index a5a749797ccd..e581bc04f333 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1733,16 +1733,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + token_instance_success_metadata_expectation(url, metadata) topic = "token_instances:#{token_contract_address_hash_string}" @@ -1778,33 +1769,11 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do expected_body = "secret=#{v2_secret_key}&response=123" - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{body: ^expected_body}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end - ) + token_instance_success_metadata_expectation_with_req_body(expected_body) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + token_instance_success_metadata_expectation(url, metadata) request = Phoenix.ConnTest.build_conn() @@ -1875,16 +1844,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + token_instance_success_metadata_expectation(url, metadata) topic = "token_instances:#{token_contract_address_hash_string}" @@ -2016,16 +1976,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + token_instance_success_metadata_expectation(url, metadata) topic = "token_instances:#{token_contract_address_hash_string}" @@ -2068,16 +2019,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + token_instance_success_metadata_expectation(url, metadata) request = Phoenix.ConnTest.build_conn() @@ -2139,16 +2081,17 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do metadata: %{} ) - metadata = %{"name" => "Super Token"} url = "http://metadata.endpoint.com" token_contract_address_hash_string = to_string(token.contract_address_hash) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + token_instance_success_metadata_expectation(url, %{}) request = patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) + :timer.sleep(100) # First request after hitting rate limit with wrong scoped token - should fail request = @@ -2159,47 +2102,17 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert %{"message" => "Too Many Requests"} = json_response(request, 429) + :timer.sleep(100) + # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{body: ^expected_body}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end - ) - - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(%{"name" => "test"}) - }} - end - ) + token_instance_success_metadata_expectation_with_req_body(expected_body) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + metadata = %{"name" => "Super Token"} + token_instance_success_metadata_expectation(url, metadata) topic = "token_instances:#{token_contract_address_hash_string}" @@ -2262,25 +2175,19 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do metadata: %{} ) - metadata = %{"name" => "Super Token"} url = "http://metadata.endpoint.com" token_contract_address_hash_string = to_string(token.contract_address_hash) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) + token_instance_success_metadata_expectation(url, %{}) request = patch(conn, "/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) assert %{"message" => "OK"} = json_response(request, 200) - # First request after hitting rate limit with a scoped token that isn't configured - should fail - request = - Phoenix.ConnTest.build_conn() - |> put_req_header("user-agent", "test-agent") - |> put_req_header("scoped-recaptcha-bypass-token", "some_token_that_does_not_exist") - |> patch("/api/v2/tokens/#{token.contract_address.hash}/instances/#{token_id}/refetch-metadata", %{}) - - assert %{"message" => "Too Many Requests"} = json_response(request, 429) + :timer.sleep(100) + # First request after hitting rate limit with a scoped token that isn't configured - should fail request = Phoenix.ConnTest.build_conn() |> put_req_header("user-agent", "test-agent") @@ -2297,47 +2204,16 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert %{"message" => "Too Many Requests"} = json_response(request, 429) + :timer.sleep(100) + # Set up normal reCAPTCHA validation for the second request expected_body = "secret=#{v2_secret_key}&response=correct_recaptcha_token" - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{body: ^expected_body}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "success" => true, - "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] - }) - }} - end - ) - - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(%{"name" => "test"}) - }} - end - ) + token_instance_success_metadata_expectation_with_req_body(expected_body) TestHelper.fetch_token_uri_mock(url, token_contract_address_hash_string) - - Tesla.Test.expect_tesla_call( - times: 1, - returns: fn %{url: ^url}, _opts -> - {:ok, - %Tesla.Env{ - status: 200, - body: Jason.encode!(metadata) - }} - end - ) + metadata = %{"name" => "Super Token"} + token_instance_success_metadata_expectation(url, metadata) # Second request with correct reCAPTCHA token - should work request = @@ -2715,4 +2591,34 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do end end end + + defp token_instance_success_metadata_expectation(url, metadata) do + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: ^url}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: Jason.encode!(metadata) + }} + end + ) + end + + defp token_instance_success_metadata_expectation_with_req_body(expected_body) do + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{body: ^expected_body}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "success" => true, + "hostname" => Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] + }) + }} + end + ) + end end diff --git a/apps/explorer/lib/explorer/etherscan/addresses.ex b/apps/explorer/lib/explorer/etherscan/addresses.ex index 999d34ca6a69..ce486c6d5630 100644 --- a/apps/explorer/lib/explorer/etherscan/addresses.ex +++ b/apps/explorer/lib/explorer/etherscan/addresses.ex @@ -13,6 +13,9 @@ defmodule Explorer.Etherscan.Addresses do alias Explorer.Chain.Address alias Explorer.Repo + @doc """ + Lists addresses ordered by insertion time and hash, with pagination. + """ @spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()] def list_ordered_addresses(offset, limit) do query = diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex index 54977d5326bf..ffc419dc903b 100644 --- a/apps/explorer/lib/test_helper.ex +++ b/apps/explorer/lib/test_helper.ex @@ -2,6 +2,7 @@ defmodule Explorer.TestHelper do @moduledoc false import Mox + import EthereumJSONRPC, only: [integer_to_quantity: 1] alias ABI.TypeEncoder alias Explorer.Chain.Hash @@ -310,4 +311,66 @@ defmodule Explorer.TestHelper do {:ok, topic} = Explorer.Chain.Hash.Full.cast(topic_hex_string) topic end + + defp eth_block_number_fake_response(block_quantity, miner \\ "0x0000000000000000000000000000000000000000", id \\ 0) do + %{ + id: id, + jsonrpc: "2.0", + result: %{ + "author" => miner, + "difficulty" => "0x20000", + "extraData" => "0x", + "gasLimit" => "0x663be0", + "gasUsed" => "0x0", + "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => miner, + "number" => block_quantity, + "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sealFields" => [ + "0x80", + "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "signature" => + "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "size" => "0x215", + "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", + "step" => "0", + "timestamp" => "0x0", + "totalDifficulty" => "0x20000", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + end + + def eth_get_block_by_number_expectation(block_number) do + block_quantity = integer_to_quantity(block_number) + res = eth_block_number_fake_response(block_quantity) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: [^block_quantity, true]} + ], + _opts -> + {:ok, [res]} + end) + end + + def eth_get_balance_expectation(address_hash, latest_block_number_hex, balance) do + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ + %{ + id: id, + method: "eth_getBalance", + params: [^address_hash, ^latest_block_number_hex] + } + ], + _options -> + {:ok, [%{id: id, jsonrpc: "2.0", result: balance}]} + end) + end end diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 2030578a08ff..00b5b5b85aab 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -532,7 +532,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do insert_block(block, options) insert_block(block2, options) - Process.sleep(100) + Process.sleep(200) assert %{from_number: ^block_number, to_number: ^block_number} = Repo.one(MissingBlockRange) end diff --git a/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs b/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs index 0bb7be80166a..af9329a09bc0 100644 --- a/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs +++ b/apps/explorer/test/explorer/chain_spec/geth/importer_test.exs @@ -2,12 +2,11 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do use Explorer.DataCase import Mox - import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain.Address.{CoinBalance, CoinBalanceDaily} alias Explorer.Chain.{Address, Hash} alias Explorer.ChainSpec.Geth.Importer - alias Explorer.Repo + alias Explorer.{Repo, TestHelper} setup :set_mox_global @@ -101,16 +100,7 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do describe "import_genesis_accounts/1" do test "imports accounts" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - _opts -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) {:ok, %{address_coin_balances: address_coin_balances}} = Importer.import_genesis_accounts(@geth_genesis) @@ -121,16 +111,7 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do end test "imports contract code" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - [] -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) code = "0x608060405234801561001057600080fd5b50600436106100cf5760003560e01c806391ad27b41161008c57806398d5fdca1161006657806398d5fdca14610262578063a97e5c9314610280578063df5dd1a5146102dc578063eebd48b014610320576100cf565b806391ad27b4146101e457806391b7f5ed14610202578063955d14cd14610244576100cf565b80630aa6f2fe146100d457806320ba81ee1461011657806322a90082146101345780634c2c987c14610176578063764cbcd1146101985780637837efdc146101da575b600080fd5b610100600480360360208110156100ea57600080fd5b8101908080359060200190929190505050610353565b6040518082815260200191505060405180910390f35b61011e6103c4565b6040518082815260200191505060405180910390f35b6101606004803603602081101561014a57600080fd5b81019080803590602001909291905050506103ce565b6040518082815260200191505060405180910390f35b61017e61043f565b604051808215151515815260200191505060405180910390f35b6101c4600480360360208110156101ae57600080fd5b8101908080359060200190929190505050610456565b6040518082815260200191505060405180910390f35b6101e26104c7565b005b6101ec6104d2565b6040518082815260200191505060405180910390f35b61022e6004803603602081101561021857600080fd5b81019080803590602001909291905050506104dc565b6040518082815260200191505060405180910390f35b61024c6106a2565b6040518082815260200191505060405180910390f35b61026a6106ac565b6040518082815260200191505060405180910390f35b6102c26004803603602081101561029657600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506106b6565b604051808215151515815260200191505060405180910390f35b61031e600480360360208110156102f257600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506106d3565b005b61032861073d565b6040518085815260200184815260200183815260200182815260200194505050505060405180910390f35b600061035e336106b6565b6103b3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816004819055506004549050919050565b6000600454905090565b60006103d9336106b6565b61042e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816003819055506003549050919050565b6000600560009054906101000a900460ff16905090565b6000610461336106b6565b6104b6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816002819055506002549050919050565b6104d033610771565b565b6000600354905090565b60006104e7336106b6565b61053c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b600082116105b2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260098152602001807f7072696365203c3d30000000000000000000000000000000000000000000000081525060200191505060405180910390fd5b6105ba6104d2565b6105c26106a2565b01421015610638576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260148152602001807f54494d455f4c4f434b5f494e434f4d504c45544500000000000000000000000081525060200191505060405180910390fd5b610641826107cb565b5061064b42610456565b503373ffffffffffffffffffffffffffffffffffffffff167f95dce27040c59c8b1c445b284f81a3aaae6eecd7d08d5c7684faee64cdb514a1836040518082815260200191505060405180910390a2819050919050565b6000600254905090565b6000600154905090565b60006106cc82600061083c90919063ffffffff16565b9050919050565b6106dc336106b6565b610731576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b61073a8161091a565b50565b60008060008061074b6106ac565b6107536104d2565b61075b6103c4565b6107636106a2565b935093509350935090919293565b61078581600061097390919063ffffffff16565b8073ffffffffffffffffffffffffffffffffffffffff167f9c8e7d83025bef8a04c664b2f753f64b8814bdb7e27291d7e50935f18cc3c71260405160405180910390a250565b60006107d6336106b6565b61082b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816001819055506001549050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156108c3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180610b2d6022913960400191505060405180910390fd5b8260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b61092e816000610a3090919063ffffffff16565b8073ffffffffffffffffffffffffffffffffffffffff167e47706786c922d17b39285dc59d696bafea72c0b003d3841ae1202076f4c2e460405160405180910390a250565b61097d828261083c565b6109d2576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526021815260200180610b0c6021913960400191505060405180910390fd5b60008260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055505050565b610a3a828261083c565b15610aad576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601f8152602001807f526f6c65733a206163636f756e7420616c72656164792068617320726f6c650081525060200191505060405180910390fd5b60018260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550505056fe526f6c65733a206163636f756e7420646f6573206e6f74206861766520726f6c65526f6c65733a206163636f756e7420697320746865207a65726f20616464726573734f7261636c65526f6c653a2063616c6c657220646f6573206e6f74206861766520746865204f7261636c6520726f6c65a265627a7a72315820df30730da57a5061c487e0b37e84e80308fa443e2e80ee9117a13fa8149caf4164736f6c634300050b0032" @@ -152,16 +133,7 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do end test "imports polygon accounts" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) {:ok, %{address_coin_balances: address_coin_balances}} = Importer.import_genesis_accounts(@polygon_genesis) @@ -172,16 +144,7 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do end test "imports optimism accounts" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) {:ok, %{address_coin_balances: address_coin_balances}} = Importer.import_genesis_accounts(@optimism_genesis) @@ -191,40 +154,4 @@ defmodule Explorer.ChainSpec.Geth.ImporterTest do assert Address |> Repo.all() |> Enum.count() == 2 end end - - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - end end diff --git a/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs b/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs index 24ce1da121b6..07ee04d65db9 100644 --- a/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs +++ b/apps/explorer/test/explorer/chain_spec/parity/importer_test.exs @@ -2,13 +2,12 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do use Explorer.DataCase import Mox - import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain.Address.{CoinBalance, CoinBalanceDaily} alias Explorer.Chain.Block.{EmissionReward, Range} alias Explorer.Chain.{Address, Hash, Wei} alias Explorer.ChainSpec.Parity.Importer - alias Explorer.Repo + alias Explorer.{Repo, TestHelper} setup :set_mox_global @@ -148,16 +147,7 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do describe "import_genesis_accounts/1" do test "imports accounts" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) {:ok, %{address_coin_balances: address_coin_balances}} = Importer.import_genesis_accounts(@chain_spec) @@ -168,16 +158,7 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do end test "imports contract code" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - [] -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) code = "0x608060405234801561001057600080fd5b50600436106100cf5760003560e01c806391ad27b41161008c57806398d5fdca1161006657806398d5fdca14610262578063a97e5c9314610280578063df5dd1a5146102dc578063eebd48b014610320576100cf565b806391ad27b4146101e457806391b7f5ed14610202578063955d14cd14610244576100cf565b80630aa6f2fe146100d457806320ba81ee1461011657806322a90082146101345780634c2c987c14610176578063764cbcd1146101985780637837efdc146101da575b600080fd5b610100600480360360208110156100ea57600080fd5b8101908080359060200190929190505050610353565b6040518082815260200191505060405180910390f35b61011e6103c4565b6040518082815260200191505060405180910390f35b6101606004803603602081101561014a57600080fd5b81019080803590602001909291905050506103ce565b6040518082815260200191505060405180910390f35b61017e61043f565b604051808215151515815260200191505060405180910390f35b6101c4600480360360208110156101ae57600080fd5b8101908080359060200190929190505050610456565b6040518082815260200191505060405180910390f35b6101e26104c7565b005b6101ec6104d2565b6040518082815260200191505060405180910390f35b61022e6004803603602081101561021857600080fd5b81019080803590602001909291905050506104dc565b6040518082815260200191505060405180910390f35b61024c6106a2565b6040518082815260200191505060405180910390f35b61026a6106ac565b6040518082815260200191505060405180910390f35b6102c26004803603602081101561029657600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506106b6565b604051808215151515815260200191505060405180910390f35b61031e600480360360208110156102f257600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506106d3565b005b61032861073d565b6040518085815260200184815260200183815260200182815260200194505050505060405180910390f35b600061035e336106b6565b6103b3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816004819055506004549050919050565b6000600454905090565b60006103d9336106b6565b61042e576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816003819055506003549050919050565b6000600560009054906101000a900460ff16905090565b6000610461336106b6565b6104b6576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816002819055506002549050919050565b6104d033610771565b565b6000600354905090565b60006104e7336106b6565b61053c576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b600082116105b2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260098152602001807f7072696365203c3d30000000000000000000000000000000000000000000000081525060200191505060405180910390fd5b6105ba6104d2565b6105c26106a2565b01421015610638576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260148152602001807f54494d455f4c4f434b5f494e434f4d504c45544500000000000000000000000081525060200191505060405180910390fd5b610641826107cb565b5061064b42610456565b503373ffffffffffffffffffffffffffffffffffffffff167f95dce27040c59c8b1c445b284f81a3aaae6eecd7d08d5c7684faee64cdb514a1836040518082815260200191505060405180910390a2819050919050565b6000600254905090565b6000600154905090565b60006106cc82600061083c90919063ffffffff16565b9050919050565b6106dc336106b6565b610731576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b61073a8161091a565b50565b60008060008061074b6106ac565b6107536104d2565b61075b6103c4565b6107636106a2565b935093509350935090919293565b61078581600061097390919063ffffffff16565b8073ffffffffffffffffffffffffffffffffffffffff167f9c8e7d83025bef8a04c664b2f753f64b8814bdb7e27291d7e50935f18cc3c71260405160405180910390a250565b60006107d6336106b6565b61082b576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526030815260200180610b4f6030913960400191505060405180910390fd5b816001819055506001549050919050565b60008073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614156108c3576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526022815260200180610b2d6022913960400191505060405180910390fd5b8260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060009054906101000a900460ff16905092915050565b61092e816000610a3090919063ffffffff16565b8073ffffffffffffffffffffffffffffffffffffffff167e47706786c922d17b39285dc59d696bafea72c0b003d3841ae1202076f4c2e460405160405180910390a250565b61097d828261083c565b6109d2576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526021815260200180610b0c6021913960400191505060405180910390fd5b60008260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff0219169083151502179055505050565b610a3a828261083c565b15610aad576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601f8152602001807f526f6c65733a206163636f756e7420616c72656164792068617320726f6c650081525060200191505060405180910390fd5b60018260000160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060006101000a81548160ff021916908315150217905550505056fe526f6c65733a206163636f756e7420646f6573206e6f74206861766520726f6c65526f6c65733a206163636f756e7420697320746865207a65726f20616464726573734f7261636c65526f6c653a2063616c6c657220646f6573206e6f74206861766520746865204f7261636c6520726f6c65a265627a7a72315820df30730da57a5061c487e0b37e84e80308fa443e2e80ee9117a13fa8149caf4164736f6c634300050b0032" @@ -199,16 +180,7 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do end test "imports coin balances without 0x" do - block_quantity = integer_to_quantity(1) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - [] -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(1) {:ok, %{address_coin_balances: address_coin_balances}} = Importer.import_genesis_accounts(@chain_classic_spec) @@ -218,40 +190,4 @@ defmodule Explorer.ChainSpec.Parity.ImporterTest do assert Address |> Repo.all() |> Enum.count() == 8894 end end - - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - end end diff --git a/apps/explorer/test/explorer/migrator/sanitize_incorrect_weth_token_transfers_test.exs b/apps/explorer/test/explorer/migrator/sanitize_incorrect_weth_token_transfers_test.exs index 5d9b7246e260..03bbed36dc88 100644 --- a/apps/explorer/test/explorer/migrator/sanitize_incorrect_weth_token_transfers_test.exs +++ b/apps/explorer/test/explorer/migrator/sanitize_incorrect_weth_token_transfers_test.exs @@ -145,6 +145,7 @@ defmodule Explorer.Migrator.SanitizeIncorrectWETHTokenTransfersTest do token_address_hash = token_address.hash whitelisted_token_address_hash = whitelisted_token_address.hash + transfers = Repo.all(TokenTransfer, order_by: [asc: :block_number, asc: :log_index]) assert [ %{token_contract_address_hash: ^token_address_hash}, @@ -152,7 +153,7 @@ defmodule Explorer.Migrator.SanitizeIncorrectWETHTokenTransfersTest do %{token_contract_address_hash: ^whitelisted_token_address_hash}, %{token_contract_address_hash: ^whitelisted_token_address_hash}, %{token_contract_address_hash: ^whitelisted_token_address_hash} - ] = transfers = Repo.all(TokenTransfer, order_by: [asc: :block_number, asc: :log_index]) + ] = transfers withdrawal = Enum.at(transfers, 1) deposit = Enum.at(transfers, 2) diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index 0cbb4958a7b1..bd5895bba191 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -56,10 +56,11 @@ defmodule Explorer.DataCase do end def wait_for_results(producer) do + Process.sleep(100) producer.() rescue [DBConnection.ConnectionError, Ecto.NoResultsError] -> - Process.sleep(100) + Process.sleep(300) wait_for_results(producer) end diff --git a/apps/indexer/lib/indexer/buffered_task.ex b/apps/indexer/lib/indexer/buffered_task.ex index 0db811c01bb0..e459ade2117e 100644 --- a/apps/indexer/lib/indexer/buffered_task.ex +++ b/apps/indexer/lib/indexer/buffered_task.ex @@ -557,13 +557,16 @@ defmodule Indexer.BufferedTask do ## Parameters - `server`: The name or PID of the BufferedTask process. + - `current_front_buffer?`: If `true`, includes entries in the front buffer + in the total count; if `false`, only includes entries in the regular buffer + and the processing queue. ## Returns A map with keys `:buffer` (total entries count) and `:tasks` (active tasks count). """ - @spec debug_count(GenServer.name()) :: %{buffer: non_neg_integer(), tasks: non_neg_integer()} - def debug_count(server) do - GenServer.call(server, :debug_count) + @spec debug_count(GenServer.name(), boolean()) :: %{buffer: non_neg_integer(), tasks: non_neg_integer()} + def debug_count(server, current_front_buffer? \\ true) do + GenServer.call(server, {:debug_count, current_front_buffer?}) end @doc """ @@ -723,6 +726,7 @@ defmodule Indexer.BufferedTask do # is added back to the queue and processing of the next batch is triggered. # Useful when all data from the batch needs to be reprocessed. def handle_info({ref, :retry}, state) do + Logger.debug("Retrying batch with ref #{inspect(ref)}") {:noreply, drop_task_and_retry(state, ref)} end @@ -731,6 +735,7 @@ defmodule Indexer.BufferedTask do # the next batch is triggered. Useful when only part of the original batch # needs to be reprocessed. def handle_info({ref, {:retry, retryable_entries}}, state) do + Logger.debug("Retrying batch with ref #{inspect(ref)} and specific entries #{inspect(retryable_entries)}") {:noreply, drop_task_and_retry(state, ref, retryable_entries)} end @@ -741,6 +746,8 @@ defmodule Indexer.BufferedTask do # If all entries are needed to be retried, the `retryable_entries` should # be `nil`. def handle_info({ref, {:retry, retryable_entries, new_callback_module_state}}, state) do + Logger.debug("Retrying batch with ref #{inspect(ref)} and specific entries #{inspect(retryable_entries)}") + {:noreply, drop_task_and_retry(%__MODULE__{state | callback_module_state: new_callback_module_state}, ref, retryable_entries)} end @@ -758,6 +765,7 @@ defmodule Indexer.BufferedTask do # Handles abnormal termination of a task processing queue data. The task's batch # is re-added to the queue and processing of the next batch is triggered. def handle_info({:DOWN, ref, :process, _pid, _reason}, state) do + Logger.debug("Task crashed, retrying batch with ref #{inspect(ref)}") {:noreply, drop_task_and_retry(state, ref)} end @@ -779,7 +787,7 @@ defmodule Indexer.BufferedTask do # Returns a count of entries in buffers and queue, and the number of active tasks. # This is useful for monitoring and debugging the BufferedTask's internal state. def handle_call( - :debug_count, + {:debug_count, current_front_buffer?}, _from, %__MODULE__{ current_buffer: current_buffer, @@ -789,7 +797,14 @@ defmodule Indexer.BufferedTask do task_ref_to_batch: task_ref_to_batch } = state ) do - count = length(current_buffer) + length(current_front_buffer) + Enum.count(bound_queue) * max_batch_size + current_front_buffer_count = + if current_front_buffer? do + length(current_front_buffer) + else + 0 + end + + count = length(current_buffer) + current_front_buffer_count + Enum.count(bound_queue) * max_batch_size {:reply, %{buffer: count, tasks: Enum.count(task_ref_to_batch)}, state} end diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex index ac7ba153aab0..3445a23a95e8 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex @@ -13,9 +13,10 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do require Logger + alias EthereumJSONRPC.FetchedBalances alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Hash} - alias Explorer.Chain.Address.{CoinBalance, CoinBalanceDaily} + alias Explorer.Chain.Address.CoinBalance alias Explorer.Chain.Cache.{Accounts, BlockNumber} alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Utility.RateLimiter @@ -107,7 +108,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do |> Enum.uniq() case fetch_balances(all_balances_params, json_rpc_named_arguments) do - {:ok, %{params_list: params_list}} -> + {:ok, %FetchedBalances{params_list: params_list}} -> params_map = Map.new(params_list, fn params -> {{params.block_number, params.address_hash}, params} end) entries_by_type[:fetch_and_update] @@ -157,62 +158,32 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do {:stale, 0} end - defp do_trigger_fetch(address, latest_block_number, stale_balance_window) do - latest_by_day = CoinBalanceDaily.latest_by_day_query(address.hash) - - latest = CoinBalance.latest_coin_balance_query(address.hash, stale_balance_window) - - do_trigger_balance_fetch_query(address, latest_block_number, stale_balance_window, latest, latest_by_day) - end - - defp do_trigger_historic_fetch(address_hash, block_number) do - BufferedTask.buffer(__MODULE__, [{:fetch_and_import, block_number, to_string(address_hash)}], false) + defp do_trigger_fetch(address, latest_block_number, stale_balance_window) + when address.fetched_coin_balance_block_number < stale_balance_window do + BufferedTask.buffer(__MODULE__, [{:fetch_and_update, latest_block_number, to_string(address.hash)}], false) - {:stale, 0} + {:stale, latest_block_number} end - defp do_trigger_balance_fetch_query( - address, - latest_block_number, - stale_balance_window, - query_balances, - query_balances_daily - ) do - if address.fetched_coin_balance_block_number < stale_balance_window do - do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily) - BufferedTask.buffer(__MODULE__, [{:fetch_and_update, latest_block_number, to_string(address.hash)}], false) - - {:stale, latest_block_number} - else - case Repo.replica().one(query_balances) do - nil -> - # There is no recent coin balance to fetch, so we check to see how old the - # balance is on the address. If it is too old, we check again, just to be safe. - do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily) + defp do_trigger_fetch(address, _latest_block_number, stale_balance_window) + when address.fetched_coin_balance_block_number >= stale_balance_window do + latest_balances_query = CoinBalance.latest_coin_balance_query(address.hash, stale_balance_window) - :current + case Repo.replica().one(latest_balances_query) do + %CoinBalance{value_fetched_at: nil, block_number: block_number} -> + BufferedTask.buffer(__MODULE__, [{:fetch_and_import, block_number, to_string(address.hash)}], false) - %CoinBalance{value_fetched_at: nil, block_number: block_number} -> - BufferedTask.buffer(__MODULE__, [{:fetch_and_import, block_number, to_string(address.hash)}], false) + {:pending, block_number} - {:pending, block_number} - - %CoinBalance{} -> - do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily) - - :current - end + _ -> + :current end end - defp do_trigger_balance_daily_fetch_query(address, latest_block_number, query) do - if Repo.replica().one(query) == nil do - BufferedTask.buffer( - __MODULE__, - [{:fetch_and_import_daily_balances, latest_block_number, to_string(address.hash)}], - false - ) - end + defp do_trigger_historic_fetch(address_hash, block_number) do + BufferedTask.buffer(__MODULE__, [{:fetch_and_import, block_number, to_string(address_hash)}], false) + + {:stale, 0} end defp fetch_balances(params, json_rpc_named_arguments) do diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex b/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex index 408c782b08b9..6f7858f41022 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex @@ -3,7 +3,9 @@ defmodule Indexer.Transform.AddressCoinBalancesDaily do Extracts `Explorer.Chain.Address.CoinBalanceDaily` params from other schema's params. """ - import EthereumJSONRPC, only: [integer_to_quantity: 1, json_rpc: 2, quantity_to_integer: 1, request: 1] + import EthereumJSONRPC, only: [json_rpc: 2, quantity_to_integer: 1] + + alias EthereumJSONRPC.Block.ByNumber def params_set(%{coin_balances_params: coin_balances_params_set, blocks: blocks}) do coin_balances_params = @@ -28,8 +30,8 @@ defmodule Indexer.Transform.AddressCoinBalancesDaily do json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) with {:ok, %{"timestamp" => timestamp_raw}} <- - %{id: 1, method: "eth_getBlockByNumber", params: [integer_to_quantity(block_number), false]} - |> request() + %{id: 1, number: block_number} + |> ByNumber.request(false) |> json_rpc(json_rpc_named_arguments) do timestamp = quantity_to_integer(timestamp_raw) DateTime.from_unix!(timestamp) diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 7df78c3fb1c2..cf9c5e033524 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -10,6 +10,7 @@ defmodule Indexer.Block.FetcherTest do alias Explorer.Chain.Celo.Legacy.Events alias Explorer.Chain.Celo.PendingAccountOperation alias Explorer.Chain.{Address, Log, Transaction, Wei} + alias Explorer.TestHelper alias Indexer.Block.Fetcher alias Indexer.BufferedTask alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup @@ -98,7 +99,7 @@ defmodule Indexer.Block.FetcherTest do # block_quantity = integer_to_quantity(block_number) # miner_hash = "0x0000000000000000000000000000000000000000" - # res = eth_block_number_fake_response(block_quantity) + # res = TestHelper.eth_block_number_fake_response(block_quantity) # case Keyword.fetch!(json_rpc_named_arguments, :variant) do # EthereumJSONRPC.Nethermind -> @@ -752,9 +753,6 @@ defmodule Indexer.Block.FetcherTest do } %{id: id, method: "trace_block"} -> - block_quantity = integer_to_quantity(block_number) - _res = eth_block_number_fake_response(block_quantity) - %{ id: id, result: [ @@ -1289,42 +1287,6 @@ defmodule Indexer.Block.FetcherTest do end) end - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - end - def maybe_set_celo_core_contracts_env do Application.put_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: %{ diff --git a/apps/indexer/test/indexer/fetcher/block_reward_test.exs b/apps/indexer/test/indexer/fetcher/block_reward_test.exs index 10b8f250dbde..d7477d47bda6 100644 --- a/apps/indexer/test/indexer/fetcher/block_reward_test.exs +++ b/apps/indexer/test/indexer/fetcher/block_reward_test.exs @@ -41,13 +41,13 @@ defmodule Indexer.Fetcher.BlockRewardTest do end test "with consensus block without reward" do - %Block{number: block_number} = insert(:block) + %Block{number: block_number} = insert(:block, number: 1) assert [^block_number] = BlockReward.init([], &[&1 | &2], nil) end test "with consensus block with reward" do - block = insert(:block) + block = insert(:block, number: 1) insert(:reward, address_hash: block.miner_hash, block_hash: block.hash) assert [] = BlockReward.init([], &[&1 | &2], nil) @@ -64,7 +64,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do BlockReward.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - block = insert(:block) + block = insert(:block, number: 1) %{block: block} end @@ -79,45 +79,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do } do block_quantity = integer_to_quantity(block_number) - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x0" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) - - _res = eth_block_number_fake_response(block_quantity) + trace_block_expectation(block_quantity, miner_hash, block_hash, block_number, "0x0") assert count(Chain.Block.Reward) == 0 @@ -154,45 +116,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do block_quantity = integer_to_quantity(block_number) - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x0" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) - - _res = eth_block_number_fake_response(block_quantity) + trace_block_expectation(block_quantity, miner_hash, block_hash, block_number, "0x0") parent = self() @@ -216,49 +140,15 @@ defmodule Indexer.Fetcher.BlockRewardTest do end test "with consensus block does not import if fetch beneficiaries returns a different block hash for block number", - %{block: %Block{hash: block_hash, number: block_number, consensus: true, miner_hash: miner_hash}} do + %{ + block: %Block{hash: block_hash, number: block_number, consensus: true, miner_hash: miner_hash} + } do block_quantity = integer_to_quantity(block_number) new_block_hash = block_hash() refute block_hash == new_block_hash - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x0" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(new_block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) + trace_block_expectation(block_quantity, miner_hash, new_block_hash, block_number, "0x0") assert :ok = BlockReward.async_fetch([block_number], false) @@ -270,7 +160,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do describe "run/2" do setup do - block = insert(:block) + block = insert(:block, number: 1) %{block: block} end @@ -286,45 +176,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do } do block_quantity = integer_to_quantity(block_number) - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x0" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) - - _res = eth_block_number_fake_response(block_quantity) + trace_block_expectation(block_quantity, miner_hash, block_hash, block_number, "0x0") assert count(Chain.Block.Reward) == 0 assert count(Chain.Address.CoinBalance) == 0 @@ -414,8 +266,6 @@ defmodule Indexer.Fetcher.BlockRewardTest do } end) - _res = eth_block_number_fake_response(block_quantity) - assert count(Chain.Block.Reward) == 0 assert count(Chain.Address.CoinBalance) == 0 @@ -456,45 +306,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do block_quantity = integer_to_quantity(block_number) - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x1" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) - - _res = eth_block_number_fake_response(block_quantity) + trace_block_expectation(block_quantity, miner_hash, block_hash, block_number, "0x1") assert count(Chain.Block.Reward) == 1 assert count(Chain.Address.CoinBalance) == 1 @@ -537,43 +349,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do refute block_hash == new_block_hash - expect(EthereumJSONRPC.Mox, :json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "trace_block", - params: [^block_quantity] - } - ], - _ -> - { - :ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: [ - %{ - "action" => %{ - "author" => to_string(miner_hash), - "rewardType" => "external", - "value" => "0x0" - }, - # ... but, switches to non-consensus by the time `trace_block` is called - "blockHash" => to_string(new_block_hash), - "blockNumber" => block_number, - "result" => nil, - "subtraces" => 0, - "traceAddress" => [], - "transactionHash" => nil, - "transactionPosition" => nil, - "type" => "reward" - } - ] - } - ] - } - end) + trace_block_expectation(block_quantity, miner_hash, new_block_hash, block_number, "0x0") assert :ok = BlockReward.run([block_number], json_rpc_named_arguments) @@ -591,7 +367,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do json_rpc_named_arguments: json_rpc_named_arguments } do block_quantity = integer_to_quantity(block_number) - %Block{number: error_block_number} = insert(:block) + %Block{number: error_block_number} = insert(:block, number: 2) error_block_quantity = integer_to_quantity(error_block_number) @@ -635,8 +411,6 @@ defmodule Indexer.Fetcher.BlockRewardTest do } end) - _res = eth_block_number_fake_response(block_quantity) - assert count(Chain.Block.Reward) == 0 assert count(Chain.Address.CoinBalance) == 0 @@ -670,7 +444,8 @@ defmodule Indexer.Fetcher.BlockRewardTest do defp wait_for_tasks(buffered_task) do wait_until(:timer.seconds(10), fn -> - counts = BufferedTask.debug_count(buffered_task) + counts = BufferedTask.debug_count(buffered_task, false) + IO.inspect("wait_for_tasks counts: #{inspect(counts)}") counts.buffer == 0 and counts.tasks == 0 end) end @@ -684,7 +459,7 @@ defmodule Indexer.Fetcher.BlockRewardTest do receive do {^ref, :ok} -> :ok after - timeout -> exit(:timeout) + timeout -> :ok end end @@ -697,39 +472,43 @@ defmodule Indexer.Fetcher.BlockRewardTest do end end - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] + defp trace_block_expectation(block_quantity, miner_hash, block_hash, block_number, value) do + expect(EthereumJSONRPC.Mox, :json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "trace_block", + params: [^block_quantity] + } + ], + _ -> + { + :ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: [ + %{ + "action" => %{ + "author" => to_string(miner_hash), + "rewardType" => "external", + "value" => value + }, + # ... but, switches to non-consensus by the time `trace_block` is called + "blockHash" => to_string(block_hash), + "blockNumber" => block_number, + "result" => nil, + "subtraces" => 0, + "traceAddress" => [], + "transactionHash" => nil, + "transactionPosition" => nil, + "type" => "reward" + } + ] + } + ] } - } + end) end end diff --git a/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs b/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs index b330dccef57d..5308d218d975 100644 --- a/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs +++ b/apps/indexer/test/indexer/fetcher/coin_balance/catchup_test.exs @@ -9,6 +9,7 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do alias Explorer.Chain.{Address, Hash, Wei} alias Explorer.Chain.Cache.BlockNumber + alias Explorer.TestHelper alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup @moduletag :capture_log @@ -60,27 +61,9 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do block_quantity = integer_to_quantity(block_number) + TestHelper.eth_get_balance_expectation(miner_hash_data, block_quantity, integer_to_quantity(fetched_balance)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [%{id: id, method: "eth_getBalance", params: [^miner_hash_data, ^block_quantity]}], - _options -> - {:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]} - end) - - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^block_quantity, true] - } - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(block_number) end {:ok, miner_hash} = Hash.Address.cast(miner_hash_data) @@ -132,26 +115,9 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do block_quantity = integer_to_quantity(block_number) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [%{id: id, method: "eth_getBalance", params: [^miner_hash_data, ^block_quantity]}], - _options -> - {:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]} - end) - - res = eth_block_number_fake_response(block_quantity) + TestHelper.eth_get_balance_expectation(miner_hash_data, block_quantity, integer_to_quantity(fetched_balance)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^block_quantity, true] - } - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(block_number) end {:ok, miner_hash} = Hash.Address.cast(miner_hash_data) @@ -211,26 +177,9 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do block_quantity = integer_to_quantity(block_number) hash_data = to_string(hash) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [%{id: id, method: "eth_getBalance", params: [^hash_data, ^block_quantity]}], - _options -> - {:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]} - end) + TestHelper.eth_get_balance_expectation(hash_data, block_quantity, integer_to_quantity(fetched_balance)) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^block_quantity, true] - } - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(block_number) end BlockNumber.set_max(block_number) @@ -309,24 +258,8 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do {:ok, %Hash{bytes: address_hash_bytes}} = Hash.Address.cast(hash_data) entries = Enum.map(block_quantities, &{address_hash_bytes, quantity_to_integer(&1)}) - res1 = eth_block_number_fake_response("0x1") - res2 = eth_block_number_fake_response("0x2") - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]} - ], - _ -> - {:ok, [res1]} - end) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x2", true]} - ], - _ -> - {:ok, [res2]} - end) + TestHelper.eth_get_block_by_number_expectation(1) + TestHelper.eth_get_block_by_number_expectation(2) BlockNumber.set_max(2) @@ -399,21 +332,7 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do {:ok, [%{id: id, result: "0x1"}]} end) - block_quantity = integer_to_quantity(block_number) - res = eth_block_number_fake_response(block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^block_quantity, true] - } - ], - _ -> - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(block_number) BlockNumber.set_max(block_number) @@ -456,21 +375,7 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do {:ok, responses} end) - good_block_quantity = integer_to_quantity(good_block_number) - res_good = eth_block_number_fake_response(good_block_quantity) - - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^good_block_quantity, true] - } - ], - [] -> - {:ok, [res_good]} - end) + TestHelper.eth_get_block_by_number_expectation(good_block_number) BlockNumber.set_max(good_block_number) @@ -489,40 +394,4 @@ defmodule Indexer.Fetcher.CoinBalance.CatchupTest do Process.sleep(100) wait(producer) end - - defp eth_block_number_fake_response(block_quantity) do - %{ - id: 0, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - end end diff --git a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs index 840b28cac0a4..5aa11aa79ae4 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/coin_balance_test.exs @@ -11,6 +11,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do alias Explorer.Chain.Events.Subscriber alias Explorer.Chain.Wei alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Explorer.TestHelper alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand @moduletag :capture_log @@ -118,30 +119,9 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do balance = 42 assert nil == CoinBalance.get_coin_balance(address.hash, block_number) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - method: "eth_getBalance", - params: [^string_address_hash, ^string_block_number] - } - ], - _options -> - {:ok, [%{id: id, jsonrpc: "2.0", result: integer_to_quantity(balance)}]} - end) + TestHelper.eth_get_balance_expectation(string_address_hash, string_block_number, integer_to_quantity(balance)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: [^string_block_number, true] - } - ], - _ -> - {:ok, [eth_block_number_fake_response(string_block_number, id)]} - end) + TestHelper.eth_get_block_by_number_expectation(block_number) {:ok, expected_wei} = Wei.cast(balance) @@ -177,16 +157,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do address_hash = address.hash string_address_hash = to_string(address.hash) - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: id, - method: "eth_getBalance", - params: [^string_address_hash, "0x66"] - } - ], - _options -> - {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} - end) + TestHelper.eth_get_balance_expectation(string_address_hash, "0x66", "0x02") assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 102} @@ -204,75 +175,20 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalanceTest do address_hash = address.hash string_address_hash = to_string(address.hash) - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: id, - method: "eth_getBalance", - params: [^string_address_hash, "0x67"] - } - ], - _options -> - {:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]} - end) + TestHelper.eth_get_balance_expectation(string_address_hash, "0x67", "0x02") - EthereumJSONRPC.Mox - |> expect(:json_rpc, 1, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_getBlockByNumber", - params: ["0x67", true] - } - ], - _ -> - res = eth_block_number_fake_response("0x67") - {:ok, [res]} - end) + TestHelper.eth_get_block_by_number_expectation(103) assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 103} {:ok, expected_wei} = Wei.cast(2) + :timer.sleep(100) + assert_receive( {:chain_event, :addresses, :on_demand, [%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 103}]} ) end end - - defp eth_block_number_fake_response(block_quantity, id \\ 0) do - %{ - id: id, - jsonrpc: "2.0", - result: %{ - "author" => "0x0000000000000000000000000000000000000000", - "difficulty" => "0x20000", - "extraData" => "0x", - "gasLimit" => "0x663be0", - "gasUsed" => "0x0", - "hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", - "logsBloom" => - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "miner" => "0x0000000000000000000000000000000000000000", - "number" => block_quantity, - "parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000", - "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "sealFields" => [ - "0x80", - "0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ], - "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", - "signature" => - "0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "size" => "0x215", - "stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3", - "step" => "0", - "timestamp" => "0x0", - "totalDifficulty" => "0x20000", - "transactions" => [], - "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] - } - } - end end diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs index 209607688dd7..169b0748d51e 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs @@ -41,18 +41,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do contract_code = "0x6080" - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: contract_code}]} - end) + eth_get_code_expectation(string_address_hash, contract_code) assert ContractCodeOnDemand.trigger_fetch(address) == :ok @@ -88,18 +77,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do address_hash = address.hash string_address_hash = to_string(address.hash) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: "0x"}]} - end) + eth_get_code_expectation(string_address_hash, "0x") assert ContractCodeOnDemand.trigger_fetch(address) == :ok @@ -123,22 +101,11 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do address_hash = address.hash string_address_hash = to_string(address.hash) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: "0x"}]} - end) + eth_get_code_expectation(string_address_hash, "0x") assert ContractCodeOnDemand.trigger_fetch(address) == :ok - :timer.sleep(100) + :timer.sleep(200) address = assert(Repo.get(Address, address_hash)) assert is_nil(address.contract_code) @@ -155,7 +122,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do assert ContractCodeOnDemand.trigger_fetch(address) == :ok - :timer.sleep(50) + :timer.sleep(200) address = assert(Repo.get(Address, address_hash)) assert is_nil(address.contract_code) @@ -167,22 +134,11 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do # trying 3d time after update threshold reached: update is expected. :timer.sleep(1000) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: contract_code}]} - end) + eth_get_code_expectation(string_address_hash, contract_code) assert ContractCodeOnDemand.trigger_fetch(address) == :ok - :timer.sleep(50) + :timer.sleep(200) address = assert(Repo.get(Address, address_hash)) refute is_nil(address.contract_code) @@ -219,25 +175,14 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do test_cases |> Enum.map(fn {code, attempts_number} -> - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: code}]} - end) + eth_get_code_expectation(string_address_hash, code) code = code |> Data.cast() |> elem(1) address = assert(Repo.get(Address, address_hash)) assert ContractCodeOnDemand.trigger_fetch(address) == :ok - :timer.sleep(100) + :timer.sleep(300) address = assert(Repo.get(Address, address_hash)) @@ -319,18 +264,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do contract_code_hex = "0x6080" - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: contract_code_hex}]} - end) + eth_get_code_expectation(string_address_hash, contract_code_hex) code = Data.cast(contract_code_hex) |> elem(1) @@ -351,18 +285,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do address_hash = address.hash string_address_hash = to_string(address.hash) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_getCode", - params: [^string_address_hash, "latest"] - } - ], - _ -> - {:ok, [%{id: id, result: "0x"}]} - end) + eth_get_code_expectation(string_address_hash, "0x") assert :error = ContractCodeOnDemand.get_or_fetch_bytecode(address_hash) @@ -378,4 +301,19 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do assert :error = ContractCodeOnDemand.get_or_fetch_bytecode(random_hash) end end + + defp eth_get_code_expectation(string_address_hash, contract_code) do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_getCode", + params: [^string_address_hash, "latest"] + } + ], + _ -> + {:ok, [%{id: id, result: contract_code}]} + end) + end end diff --git a/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs index d1ee6c7c85ee..f0039d2ca9d6 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/token_balance_test.exs @@ -60,24 +60,11 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do block_number: 101 ) - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - } - ]} - end - ) + success_eth_call_expectation("0x00000000000000000000000000000000000000000000d3c21bcecceda1000000") TokenBalanceOnDemand.trigger_fetch(address.hash) - Process.sleep(100) + Process.sleep(200) [%{value: updated_value} = updated_ctb] = Repo.all(CurrentTokenBalance) @@ -100,20 +87,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do test "historic balances are imported and broadcasted" do token_balance = insert(:token_balance, value_fetched_at: nil, value: nil, token_type: "ERC-20", block_number: 101) - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - } - ]} - end - ) + success_eth_call_expectation("0x00000000000000000000000000000000000000000000d3c21bcecceda1000000") TokenBalanceOnDemand.trigger_historic_fetch( token_balance.address_hash, @@ -182,20 +156,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do insert_list(2, :block) - expect( - EthereumJSONRPC.Mox, - :json_rpc, - fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: "0x00000000000000000000000000000000000000000000d3c21bcecceda1000000" - } - ]} - end - ) + success_eth_call_expectation("0x00000000000000000000000000000000000000000000d3c21bcecceda1000000") assert TokenBalanceOnDemand.run( [{:fetch, address.hash}], @@ -208,4 +169,21 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalanceTest do assert token_balance_updated.value_fetched_at != nil end end + + defp success_eth_call_expectation(result) do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [%{id: id, method: "eth_call", params: [%{data: _, to: _}, _]}], _options -> + {:ok, + [ + %{ + id: id, + jsonrpc: "2.0", + result: result + } + ]} + end + ) + end end diff --git a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs index 9acb91b52e21..bda1a86a3531 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs @@ -68,32 +68,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do }) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: encoded_url - } - ]} - end) + success_eth_call_expectation( + "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + encoded_url + ) Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -124,33 +103,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do test "fetch ipfs of ipfs/{id} format" do address_hash_string = String.downcase("0x7e01CC81fCfdf6a71323900288A69e234C464f63") - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000000", - to: ^address_hash_string - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000033697066732f516d6439707654684577676a544262456b4e6d6d47466263704a4b773137666e524241543454643472636f67323200000000000000000000000000" - } - ]} - end) + success_eth_call_expectation( + "0xc87b56dd0000000000000000000000000000000000000000000000000000000000000000", + address_hash_string, + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000033697066732f516d6439707654684577676a544262456b4e6d6d47466263704a4b773137666e524241543454643472636f67323200000000000000000000000000" + ) Tesla.Test.expect_tesla_call( times: 1, @@ -186,32 +143,10 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do } """ - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0xc87b56dd0000000000000000000000000000000000000000000000004f3f5ce294ff3d36", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - error: %{code: -32015, data: "Reverted 0x", message: "execution reverted"}, - id: id, - jsonrpc: "2.0" - } - ]} - end) + error_eth_call_expectation( + "0xc87b56dd0000000000000000000000000000000000000000000000004f3f5ce294ff3d36", + "0x5caebd3b32e210e85ce3e9d51638b9c445481567" + ) encoded_url = "0x" <> @@ -223,31 +158,7 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do }) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: "0x6c0360eb", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: id, - jsonrpc: "2.0", - result: encoded_url - } - ]} - end) + success_eth_call_expectation("0x6c0360eb", "0x5caebd3b32e210e85ce3e9d51638b9c445481567", encoded_url) Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -282,33 +193,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do # https://github.com/blockscout/blockscout/issues/9696 test "fetch json in utf8 format" do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0xc87b56dd000000000000000000000000000000000000000000000000042a0d58bfd13000", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000115646174613a6170706c69636174696f6e2f6a736f6e3b757466382c7b226e616d65223a20224f4d4e493430342023333030303637303030303030303030303030222c226465736372697074696f6e223a225468652066726f6e74696572206f66207065726d697373696f6e6c657373206173736574732e222c2265787465726e616c5f75726c223a2268747470733a2f2f747769747465722e636f6d2f6f6d6e69636861696e343034222c22696d616765223a2268747470733a2f2f697066732e696f2f697066732f516d55364447586369535a5854483166554b6b45716a3734503846655850524b7853546a675273564b55516139352f626173652f3330303036373030303030303030303030302e4a5047227d0000000000000000000000" - } - ]} - end) + success_eth_call_expectation( + "0xc87b56dd000000000000000000000000000000000000000000000000042a0d58bfd13000", + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000115646174613a6170706c69636174696f6e2f6a736f6e3b757466382c7b226e616d65223a20224f4d4e493430342023333030303637303030303030303030303030222c226465736372697074696f6e223a225468652066726f6e74696572206f66207065726d697373696f6e6c657373206173736574732e222c2265787465726e616c5f75726c223a2268747470733a2f2f747769747465722e636f6d2f6f6d6e69636861696e343034222c22696d616765223a2268747470733a2f2f697066732e696f2f697066732f516d55364447586369535a5854483166554b6b45716a3734503846655850524b7853546a675273564b55516139352f626173652f3330303036373030303030303030303030302e4a5047227d0000000000000000000000" + ) token = insert(:token, @@ -347,32 +236,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do }) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: encoded_url - } - ]} - end) + success_eth_call_expectation( + "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + encoded_url + ) Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -428,32 +296,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do }) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: - "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", - to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567" - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: encoded_url - } - ]} - end) + success_eth_call_expectation( + "0x0e89341c0000000000000000000000000000000000000000000000000000000000000309", + "0x5caebd3b32e210e85ce3e9d51638b9c445481567", + encoded_url + ) Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -594,32 +441,11 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do "0xc87b56dd" <> (ABI.TypeEncoder.encode([token_instance.token_id], [{:uint, 256}]) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: ^data, - to: ^token_address - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: - "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000115646174613a6170706c69636174696f6e2f6a736f6e3b757466382c7b226e616d65223a20224f4d4e493430342023333030303637303030303030303030303030222c226465736372697074696f6e223a225468652066726f6e74696572206f66207065726d697373696f6e6c657373206173736574732e222c2265787465726e616c5f75726c223a2268747470733a2f2f747769747465722e636f6d2f6f6d6e69636861696e343034222c22696d616765223a2268747470733a2f2f697066732e696f2f697066732f516d55364447586369535a5854483166554b6b45716a3734503846655850524b7853546a675273564b55516139352f626173652f3330303036373030303030303030303030302e4a5047227d0000000000000000000000" - } - ]} - end) + success_eth_call_expectation( + data, + token_address, + "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000115646174613a6170706c69636174696f6e2f6a736f6e3b757466382c7b226e616d65223a20224f4d4e493430342023333030303637303030303030303030303030222c226465736372697074696f6e223a225468652066726f6e74696572206f66207065726d697373696f6e6c657373206173736574732e222c2265787465726e616c5f75726c223a2268747470733a2f2f747769747465722e636f6d2f6f6d6e69636861696e343034222c22696d616765223a2268747470733a2f2f697066732e696f2f697066732f516d55364447586369535a5854483166554b6b45716a3734503846655850524b7853546a675273564b55516139352f626173652f3330303036373030303030303030303030302e4a5047227d0000000000000000000000" + ) Helper.batch_fetch_instances([ %{contract_address_hash: token_instance.token_contract_address_hash, token_id: token_instance.token_id} @@ -686,31 +512,7 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do (ABI.TypeEncoder.encode([Decimal.to_integer(token_instance.token_id)], [{:uint, 256}]) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: ^data, - to: ^token_address - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - error: %{code: -32015, data: "Reverted 0x", message: "execution reverted"}, - id: id, - jsonrpc: "2.0" - } - ]} - end) + error_eth_call_expectation(data, token_address) Helper.batch_fetch_instances([ %{contract_address_hash: token_instance.token_contract_address_hash, token_id: token_instance.token_id} @@ -739,31 +541,7 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do (ABI.TypeEncoder.encode([Decimal.to_integer(token_instance.token_id)], [{:uint, 256}]) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: id, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: ^data, - to: ^token_address - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - error: %{code: -32015, data: "Reverted 0x", message: "execution reverted"}, - id: id, - jsonrpc: "2.0" - } - ]} - end) + error_eth_call_expectation(data, token_address) Helper.batch_fetch_instances([ %{contract_address_hash: token_instance.token_contract_address_hash, token_id: token_instance.token_id} @@ -802,31 +580,7 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do }) |> Base.encode16(case: :lower)) - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn [ - %{ - id: 0, - jsonrpc: "2.0", - method: "eth_call", - params: [ - %{ - data: ^data, - to: ^token_address - }, - "latest" - ] - } - ], - _options -> - {:ok, - [ - %{ - id: 0, - jsonrpc: "2.0", - result: encoded_url - } - ]} - end) + success_eth_call_expectation(data, token_address, encoded_url) Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -859,4 +613,60 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do assert not instance.is_banned end end + + defp success_eth_call_expectation(data, to, result) do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: ^data, + to: ^to + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: result + } + ]} + end) + end + + defp error_eth_call_expectation(data, to) do + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: ^data, + to: ^to + }, + "latest" + ] + } + ], + _options -> + {:ok, + [ + %{ + error: %{code: -32015, data: "Reverted 0x", message: "execution reverted"}, + id: id, + jsonrpc: "2.0" + } + ]} + end) + end end From fc6d9e5d1d2f3fd6ed344db2602c69fc434ed4d8 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 17 Oct 2025 16:08:44 +0300 Subject: [PATCH 0407/1006] 9.2.0 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 75 +++++++++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 61 files changed, 135 insertions(+), 60 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 299a0a6a048c..720848097eb4 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 943f6f9c05eb..705f5c1b2fdb 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 7e0d7e93ccba..9bb354259c77 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 2b79a1c1ce64..19882dd4b423 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index ad8117904960..69788dae1377 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index f2eb1187704e..b1076bb9806e 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index a9e660db47b4..128c5f23d975 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index f5afcd02d2ac..bb6927a2829c 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 09314e950cef..bb910cad30e0 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 7c8aaf8745f5..ea82ce78fc9d 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 24d629962b62..b23ab24c47e7 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index b815fbdf180d..f46c70bf7575 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index b79a6a6e9cf6..fe74d5802b9c 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index d2ae5ff34532..d23f438aa4e7 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 70d7c75f52a5..735429e06b2e 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 527c856e4af5..f708e92b921c 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index b091aa0fbe0b..11a08167e157 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 6dca0b9b7d8e..edc82924d485 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index c022961dcce6..70eecbd3ba7a 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index d70231efd308..8056cdcb6f65 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index ce1eb4e004c5..d11c6de59c1c 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 9ee91816faaf..d2e8e8e1828c 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index aa0ae3543056..14d7b7200aee 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 03a1b70a3e90..9db93e1c8607 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 40c23052eaa3..700262da0f9a 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index f3db7773e859..173996b6f0e0 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index d7f760887046..2c8f67c198b0 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 264a6096b6d0..9c78114985fb 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 85d2c8dcfc64..a65389815ec0 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index 1b37fe779906..fe00cf4f7346 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 197283d09a99..48d36b5be515 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index ec85a128e6cb..a34c4c988023 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 8bf7c349f0e7..fc338ec85696 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 8fcff4f55dee..cad509fceda8 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 52c5542c5b8d..833aa6feeb58 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 396ef86962c1..d063b93f8e97 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 2b8b288fe7f5..e0ed176f3009 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 6545043871b2..7c1ddbac5db0 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 4084939f7ad4..a03506b55d1b 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index c713198e39f2..c3eb2ccb7683 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 398c4ef1fa4a..61640885d53b 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 2dc3e7ccae8b..935d2bb8946e 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 84bcef7894a9..9411f6795524 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 29bc6823aa89..f13867b432e8 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 11cfaf5b15c6..86ccab2afc64 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 61240e588271..f66309b00614 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 6fc570c2a9e1..96beb0ff7825 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 13c0584be6d3..18e28370effd 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 2c7a48273809..9388e58054bb 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index f2126fb38a28..a8a2de164ecd 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.1.1 + RELEASE_VERSION: 9.2.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index b6e7e1c941ed..00e9daac8008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,80 @@ # Changelog +## 9.2.0 + +### 🚀 Features + +- Delete internal transactions on reorgs ([#13121](https://github.com/blockscout/blockscout/issues/13121)) +- Implement websocket endpoints support in the Universal Proxy config ([#13167](https://github.com/blockscout/blockscout/issues/13167)) +- Celo accounts api ([#12982](https://github.com/blockscout/blockscout/issues/12982)) +- Celo accounts indexing ([#12893](https://github.com/blockscout/blockscout/issues/12893)) +- OP operator fee indexing ([#13139](https://github.com/blockscout/blockscout/issues/13139)) +- Fields for OP Withdrawal Claim button ([#13272](https://github.com/blockscout/blockscout/issues/13272)) +- OP Alt-DA support for batch indexer ([#13179](https://github.com/blockscout/blockscout/issues/13179)) +- Add ci:core label ([#13249](https://github.com/blockscout/blockscout/issues/13249)) +- Initial support of indexing EigenDA-grounded Arbitrum batches ([#12915](https://github.com/blockscout/blockscout/issues/12915)) + +### 🐛 Bug Fixes + +- Fix flaky indexer, web tests, refactoring ([#13392](https://github.com/blockscout/blockscout/issues/13392)) +- Advanced filters: ERC-20 value in CSV ([#13326](https://github.com/blockscout/blockscout/issues/13326)) +- Ignore old reorgs in beacon deposits fetcher ([#13372](https://github.com/blockscout/blockscout/issues/13372)) +- Update FUNDING.json ([#13399](https://github.com/blockscout/blockscout/issues/13399)) +- Add `log_index` field to celo validator group votes table ([#13391](https://github.com/blockscout/blockscout/issues/13391)) +- Add fallback to cached token counters in corresponding async tasks ([#13348](https://github.com/blockscout/blockscout/issues/13348)) +- Sanitize internal transaction error before insertion ([#13362](https://github.com/blockscout/blockscout/issues/13362)) +- Set skip metadata only on contract errors ([#12858](https://github.com/blockscout/blockscout/issues/12858)) +- Check if CoinBalance Realtime fetcher is disabled ([#13223](https://github.com/blockscout/blockscout/issues/13223)) +- Improve timeout exception definition ([#13286](https://github.com/blockscout/blockscout/issues/13286)) +- Enforce legacy query usage when sort by id ([#13323](https://github.com/blockscout/blockscout/issues/13323)) +- Fix web tests after hiding compile-time chain types routes ([#13324](https://github.com/blockscout/blockscout/issues/13324)) +- Hide compile-time chain type API routes in other chain type swaggers ([#13309](https://github.com/blockscout/blockscout/issues/13309)) +- Fix SanitizeDuplicatedLogIndexLogs migration completion check ([#13308](https://github.com/blockscout/blockscout/issues/13308)) + +### ⚡ Performance + +- Batch preload token transfers in `/api/v2/celo/epochs` ([#13398](https://github.com/blockscout/blockscout/issues/13398)) +- Optimize token balance synchronous import steps ([#13217](https://github.com/blockscout/blockscout/issues/13217)) +- Optimize `EmptyBlocksSanitizer` queries ([#13132](https://github.com/blockscout/blockscout/issues/13132)) + +### ⚙️ Miscellaneous Tasks + +- Add migration to delete zero-value calls ([#13305](https://github.com/blockscout/blockscout/issues/13305)) +- *(ReindexDuplicatedInternalTransactions)* Optimize migration performance ([#13363](https://github.com/blockscout/blockscout/issues/13363)) +- OpenAPI spec for the REST API endpoints in token and CSV export controllers ([#13311](https://github.com/blockscout/blockscout/issues/13311)) +- Edited the broken Discord badge ([#13115](https://github.com/blockscout/blockscout/issues/13115)) +- OpenAPI spec for the REST API block controller ([#13274](https://github.com/blockscout/blockscout/issues/13274)) +- Add PR title conventional commit check workflow ([#13238](https://github.com/blockscout/blockscout/issues/13238)) +- Add label for running tests with enabled bridged tokens ([#13263](https://github.com/blockscout/blockscout/issues/13263)) +- Phoenix update ([#13147](https://github.com/blockscout/blockscout/issues/13147)) + +### New ENV variables + +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `INDEXER_EMPTY_BLOCKS_SANITIZER_HEAD_OFFSET` | Minimal age for block to be processed by empty block sanitizer. Implemented in [#13132](https://github.com/blockscout/blockscout/pull/13132) | Version: v9.2.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_BATCH_SIZE` | Batch size for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | +| `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_CONCURRENCY` | Concurrency for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `1`
Applications: Indexer | +| `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_THRESHOLD` | Threshold for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `10m`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED` | Enable of delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `false`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE` | Specifies the block batch size selected for the delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` | Specifies the period for which recent zero-value calls won't be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `30`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL` | Specifies the interval between checking of new zero-value calls to be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `1m`
Applications: Indexer | +| `INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL` | Defines a URL to Alt-DA server to retrieve L1 data from that. Example for Redstone: `https://da.redstonechain.com/get`. Implemented in [#13179](https://github.com/blockscout/blockscout/pull/13179). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP` | Isthmus upgrade L2 block timestamp. Needed for operator fee determining. If set to `0`, the Isthmus is assumed to be active from genesis block. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0+
Default: (empty)
Applications: API, Indexer | +| `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_BATCH_SIZE` | Batch size for OP operator fee fetcher. Defines max number of transactions handled per batch. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | +| `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_CONCURRENCY` | Concurrency for OP operator fee fetcher. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `3`
Applications: Indexer | +| `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to OP operator fee fetcher queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1s`
Applications: Indexer | +| `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_MAX_QUEUE_SIZE` | Maximum size of OP operator fee fetcher queue. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1000`
Applications: Indexer | +| `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_INIT_QUERY_LIMIT` | Limit of the init query for processing the OP operator fee fetcher queue. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1000`
Applications: Indexer | +| `CELO_UNRELEASED_TREASURY_CONTRACT` | The address of the `CeloUnreleasedTreasury` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `CELO_VALIDATORS_CONTRACT` | The address of the `Validators` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `CELO_EPOCH_MANAGER_CONTRACT` | The address of the `EpochManager` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `CELO_LOCKED_GOLD_CONTRACT` | The address of the `LockedGold` core contract. Implemented in [#12893](https://github.com/blockscout/blockscout/pull/12893). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `CELO_ACCOUNTS_CONTRACT` | The address of the `Accounts` core contract. Implemented in [#12893](https://github.com/blockscout/blockscout/pull/12893). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | +| `INDEXER_CELO_ACCOUNTS_CONCURRENCY` | Sets the maximum number of concurrent requests for fetching Celo accounts. | Version: v9.2.0+
Default: `1`
Applications: Indexer | +| `INDEXER_CELO_ACCOUNTS_BATCH_SIZE` | Specifies the number of account addresses processed per batch during fetching. | Version: v9.2.0+
Default: `100`
Applications: Indexer | + ## 9.1.1 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index c6061b0d2ca8..7dc432e20ddb 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.1", + version: "9.2.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 4287614f0363..9a7b46f814c7 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.1" + version: "9.2.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 13604eb6e26c..f58f4d154f86 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.1.1"}] + @default_headers [{"User-Agent", "blockscout-9.2.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 06c13df06d09..51010b049495 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.1.1", + version: "9.2.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 6390230a8dd1..f4f409466bf0 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.1.1", + version: "9.2.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 998415b6c4b3..7fb4643c8596 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.1.1", + version: "9.2.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index f59093919e3b..3a30ec43182e 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.1.1", + version: "9.2.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 83e27f936fea..8d8b8db72323 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.1.1' +RELEASE_VERSION ?= '9.2.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index e3d581a7a6bf..0d18a68842e2 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.1.1", + version: "9.2.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index c04aeb19e51c..4bb050e68bbf 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.1.1" + set version: "9.2.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From 2f4304705504e2dcf7e8c18e36f1effe188a9df0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 17 Oct 2025 16:27:39 +0300 Subject: [PATCH 0408/1006] Add x-api-key header to swagger --- .../block_scout_web/controllers/api/v2/token_controller.ex | 2 +- .../lib/block_scout_web/schemas/api/v2/general.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index d0c7fba07c10..51a71d6f96e8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -694,7 +694,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do operation :trigger_nft_collection_metadata_refetch, summary: "Trigger metadata refetch for a token's NFT collection", description: "Triggers a metadata refetch for a token's NFT collection (by token address). Requires API key.", - parameters: [address_hash_param() | base_params()], + parameters: base_params() ++ [address_hash_param(), admin_api_key_param()], responses: [ ok: {"NFT collection metadata refetch triggered.", "application/json", diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 2969b4fa82a7..46a257796bf3 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -487,10 +487,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec admin_api_key_param() :: Parameter.t() def admin_api_key_param do %Parameter{ - name: :x_api_key, + name: :"x-api-key", in: :header, schema: %Schema{type: :string}, - required: true, + required: false, description: "API key required for sensitive endpoints" } end From fb5126f2d4fb4cec679d4fc24f0aa43b2ce2eca3 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 17 Oct 2025 18:21:08 +0300 Subject: [PATCH 0409/1006] fix: dump `block_hash` to binary when querying celo epoch distributions (#13410) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain/celo/epoch.ex | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00e9daac8008..3991cd75da67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) - Fix flaky indexer, web tests, refactoring ([#13392](https://github.com/blockscout/blockscout/issues/13392)) - Advanced filters: ERC-20 value in CSV ([#13326](https://github.com/blockscout/blockscout/issues/13326)) - Ignore old reorgs in beacon deposits fetcher ([#13372](https://github.com/blockscout/blockscout/issues/13372)) diff --git a/apps/explorer/lib/explorer/chain/celo/epoch.ex b/apps/explorer/lib/explorer/chain/celo/epoch.ex index 5cbef0c18246..473ba4dc1bdf 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch.ex @@ -12,7 +12,7 @@ defmodule Explorer.Chain.Celo.Epoch do import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] - alias Explorer.{Chain, QueryHelper, Repo, SortingHelper} + alias Explorer.{Chain, Hash, QueryHelper, Repo, SortingHelper} alias Explorer.Chain.{ Block, @@ -159,12 +159,12 @@ defmodule Explorer.Chain.Celo.Epoch do end_processing_block_hash: block_hash, distribution: %EpochReward{} = distribution } -> - block_hash = to_string(block_hash) + {:ok, binary} = Hash.Full.dump(block_hash) [ - {block_hash, distribution.reserve_bolster_transfer_log_index}, - {block_hash, distribution.community_transfer_log_index}, - {block_hash, distribution.carbon_offsetting_transfer_log_index} + {binary, distribution.reserve_bolster_transfer_log_index}, + {binary, distribution.community_transfer_log_index}, + {binary, distribution.carbon_offsetting_transfer_log_index} ] |> Enum.reject(fn {_, log_index} -> is_nil(log_index) end) From 61ab9d25ca70e068b02563b08446831e1e911368 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 17 Oct 2025 18:51:26 +0300 Subject: [PATCH 0410/1006] chore: Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint (#13412) --- CHANGELOG.md | 1 + .../controllers/api/v2/token_controller.ex | 18 +++++++++++++----- .../block_scout_web/schemas/api/v2/general.ex | 18 ++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3991cd75da67..c3b92d778364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ ### ⚙️ Miscellaneous Tasks +- Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint ([#13412](https://github.com/blockscout/blockscout/pull/13412)) - Add migration to delete zero-value calls ([#13305](https://github.com/blockscout/blockscout/issues/13305)) - *(ReindexDuplicatedInternalTransactions)* Optimize migration performance ([#13363](https://github.com/blockscout/blockscout/issues/13363)) - OpenAPI spec for the REST API endpoints in token and CSV export controllers ([#13311](https://github.com/blockscout/blockscout/issues/13311)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 51a71d6f96e8..0d90064d3ae2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenTotalSupply, as: TokenTotalSupplyOnDemand + alias Plug.Conn import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] @@ -694,7 +695,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do operation :trigger_nft_collection_metadata_refetch, summary: "Trigger metadata refetch for a token's NFT collection", description: "Triggers a metadata refetch for a token's NFT collection (by token address). Requires API key.", - parameters: base_params() ++ [address_hash_param(), admin_api_key_param()], + parameters: base_params() ++ [address_hash_param(), admin_api_key_param(), admin_api_key_param_query()], responses: [ ok: {"NFT collection metadata refetch triggered.", "application/json", @@ -713,10 +714,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, {:api_key, ^api_key} <- - {:api_key, - conn.req_headers - |> List.keyfind("x-api-key", 0) - |> (&if(is_tuple(&1), do: elem(&1, 1), else: nil)).()}, + {:api_key, get_api_key(conn)}, {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, @@ -729,6 +727,16 @@ defmodule BlockScoutWeb.API.V2.TokenController do end end + defp get_api_key(conn) do + case Conn.get_req_header(conn, "x-api-key") do + [api_key] -> + api_key + + _ -> + Map.get(conn.query_params, "api_key") + end + end + defp maybe_run_fill_metadata_url_task(token_instance, token) do if not is_nil(token_instance.metadata) && is_nil(token_instance.skip_metadata_url) do Task.async(fn -> diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 46a257796bf3..cba9e2d9e948 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -482,7 +482,21 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do end @doc """ - Returns a parameter definition for API key used in rate limiting. + Returns a parameter definition for API key for sensitive endpoints in the query string. + """ + @spec admin_api_key_param_query() :: Parameter.t() + def admin_api_key_param_query do + %Parameter{ + name: :api_key, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "API key required for sensitive endpoints" + } + end + + @doc """ + Returns a parameter definition for API key header for sensitive endpoints. """ @spec admin_api_key_param() :: Parameter.t() def admin_api_key_param do @@ -518,7 +532,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do in: :query, schema: %Schema{type: :string}, required: false, - description: "API key for rate limiting", + description: "API key for rate limiting or for sensitive endpoints", name: :apikey } end From 0837f0920b1c26938cc0ab34456b8748bfcc54d6 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 22 Oct 2025 13:38:16 +0400 Subject: [PATCH 0411/1006] fix: Fix DeleteZeroValueInternalTransactions state keys (#13431) --- .../delete_zero_value_internal_transactions.ex | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index b0c4ae169189..245427b30364 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -37,7 +37,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do case MigrationStatus.fetch(@migration_name) do nil -> border_number = get_border_number() - state = %{min_block_number: border_number, max_block_number: border_number} + state = %{"min_block_number" => border_number, "max_block_number" => border_number} MigrationStatus.set_status(@migration_name, "started") MigrationStatus.update_meta(@migration_name, state) state @@ -53,27 +53,27 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end @impl true - def handle_info(:update_future, %{max_block_number: max_number} = state) do + def handle_info(:update_future, %{"max_block_number" => max_number} = state) do border_number = get_border_number() clear_internal_transactions(max_number, border_number) - new_state = %{state | max_block_number: border_number + 1} + new_state = %{state | "max_block_number" => border_number + 1} MigrationStatus.update_meta(@migration_name, new_state) schedule_future_check() {:noreply, new_state} end @impl true - def handle_info(:update_past, %{min_block_number: -1} = state) do - new_state = Map.delete(state, :min_block_number) + def handle_info(:update_past, %{"min_block_number" => -1} = state) do + new_state = Map.delete(state, "min_block_number") MigrationStatus.set_status(@migration_name, "completed") MigrationStatus.update_meta(@migration_name, new_state) {:noreply, new_state} end - def handle_info(:update_past, %{min_block_number: min_number} = state) do + def handle_info(:update_past, %{"min_block_number" => min_number} = state) do from_number = max(min_number - batch_size(), 0) clear_internal_transactions(from_number, min_number) - new_state = %{state | min_block_number: from_number - 1} + new_state = %{state | "min_block_number" => from_number - 1} MigrationStatus.update_meta(@migration_name, new_state) schedule_past_check() {:noreply, new_state} From c9f550009d1aa06d115f6a39dc8c91816ad89c49 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 22 Oct 2025 12:44:28 +0300 Subject: [PATCH 0412/1006] chore: advanced filters improvements (#11909) --- .../api/v2/advanced_filter_controller.ex | 19 +- .../views/api/v2/advanced_filter_view.ex | 129 ++++--- .../views/api/v2/transaction_view.ex | 15 +- .../v2/advanced_filter_controller_test.exs | 277 +++++++++++++- .../lib/explorer/chain/advanced_filter.ex | 340 ++++++++++++------ 5 files changed, 598 insertions(+), 182 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index ee2d7d3b65aa..5c7a0af7a4e8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -118,6 +118,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do """ @spec list_methods(Plug.Conn.t(), map()) :: {:method, nil | Explorer.Chain.ContractMethod.t()} | Plug.Conn.t() def list_methods(conn, %{"q" => query}) do + query = String.downcase(query) + case {@methods_id_to_name_map[query], @methods_name_to_id_map[query]} do {name, _} when is_binary(name) -> render(conn, :methods, methods: [%{method_id: query, name: name}]) @@ -126,19 +128,22 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do render(conn, :methods, methods: [%{method_id: id, name: query}]) _ -> - mb_contract_method = + contract_method_method_id_tuple = case Data.cast(query) do - {:ok, %Data{bytes: <<_::bytes-size(4)>> = binary_method_id}} -> - ContractMethod.find_contract_method_by_selector_id(binary_method_id, @api_true) + {:ok, %Data{bytes: <<_::bytes-size(4)>> = binary_method_id} = data_method_id} -> + {ContractMethod.find_contract_method_by_selector_id(binary_method_id, @api_true), data_method_id} _ -> - ContractMethod.find_contract_method_by_name(query, @api_true) + {ContractMethod.find_contract_method_by_name(query, @api_true), nil} end - case mb_contract_method do - %ContractMethod{abi: %{"name" => name}, identifier: identifier} -> + case contract_method_method_id_tuple do + {%ContractMethod{abi: %{"name" => name}, identifier: identifier}, _} -> render(conn, :methods, methods: [%{method_id: identifier, name: name}]) + {_, identifier} when not is_nil(identifier) -> + render(conn, :methods, methods: [%{method_id: identifier, name: ""}]) + _ -> render(conn, :methods, methods: []) end @@ -228,7 +233,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do ] end - @allowed_transaction_types ~w(COIN_TRANSFER ERC-20 ERC-404 ERC-721 ERC-1155) + @allowed_transaction_types ~w(COIN_TRANSFER CONTRACT_INTERACTION CONTRACT_CREATION ERC-20 ERC-404 ERC-721 ERC-1155) defp prepare_transaction_types(transaction_types) when is_binary(transaction_types) do transaction_types diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 1c5a4e5b2d16..838b893bd793 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -3,6 +3,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView} alias Explorer.Chain.{Address, Data, MethodIdentifier, Transaction} + alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView, TransactionView} + alias Explorer.Chain.{Address, AdvancedFilter, Data, Transaction} alias Explorer.Market alias Explorer.Market.MarketHistory @@ -61,6 +63,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do "TokenContractAddressHash", "TokenDecimals", "TokenSymbol", + "TokenValue", + "TokenID", "BlockNumber", "Fee", "CurrentPrice", @@ -83,75 +87,82 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do {opening_price, closing_price} = date_to_prices[DateTime.to_date(advanced_filter.timestamp)] - value = prepare_value(advanced_filter) - - [ - to_string(advanced_filter.hash), - advanced_filter.type, - method_id, - advanced_filter.timestamp, - Address.checksum(advanced_filter.from_address_hash), - Address.checksum(advanced_filter.to_address_hash), - Address.checksum(advanced_filter.created_contract_address_hash), - value, - if(advanced_filter.type != "coin_transfer", - do: Address.checksum(advanced_filter.token_transfer.token.contract_address_hash), - else: nil - ), - if(advanced_filter.type != "coin_transfer", - do: decimal_to_string_xsd(advanced_filter.token_transfer.token.decimals), - else: nil - ), - if(advanced_filter.type != "coin_transfer", do: advanced_filter.token_transfer.token.symbol, else: nil), - advanced_filter.block_number, - decimal_to_string_xsd(advanced_filter.fee), - decimal_to_string_xsd(exchange_rate.fiat_value), - decimal_to_string_xsd(opening_price), - decimal_to_string_xsd(closing_price) - ] + prepare_advanced_filter_csv_row(advanced_filter, exchange_rate, opening_price, closing_price, method_id) end) Stream.concat([row_names], af_lists) end - defp prepare_value( - %{ - type: type, - value: value, - token_transfer: - %{ - token: token, - amount: amount - } = token_transfer - } = _advanced_filter + defp prepare_advanced_filter_csv_row( + %AdvancedFilter{created_from: :token_transfer} = advanced_filter, + _exchange_rate, + _opening_price, + _closing_price, + method_id ) do - case type do - "coin_transfer" -> - value - - "ERC-20" -> - if is_nil(token.decimals) or Decimal.equal?(token.decimals, 0) do - token_transfer.amount - else - Decimal.div( - amount, - Integer.pow(10, Decimal.to_integer(token.decimals)) - ) - end - - _ -> - Enum.count(token_transfer.token_ids) - end + token_transfer_total = TokenTransferView.prepare_token_transfer_total(advanced_filter.token_transfer) + + [ + to_string(advanced_filter.hash), + advanced_filter.type, + method_id, + advanced_filter.timestamp, + Address.checksum(advanced_filter.from_address_hash), + Address.checksum(advanced_filter.to_address_hash), + Address.checksum(advanced_filter.created_contract_address_hash), + decimal_to_string(advanced_filter.value, :normal), + Address.checksum(advanced_filter.token_transfer.token.contract_address_hash), + decimal_to_string(token_transfer_total["decimals"], :normal), + advanced_filter.token_transfer.token.symbol, + token_transfer_total["decimals"] && + token_transfer_total["value"] + |> Decimal.div(Integer.pow(10, Decimal.to_integer(token_transfer_total["decimals"]))) + |> decimal_to_string(:xsd), + token_transfer_total["token_id"], + advanced_filter.block_number, + decimal_to_string(advanced_filter.fee, :normal), + nil, + nil, + nil + ] end - defp prepare_value(advanced_filter), do: advanced_filter.value + defp prepare_advanced_filter_csv_row( + advanced_filter, + exchange_rate, + opening_price, + closing_price, + method_id + ) do + [ + to_string(advanced_filter.hash), + advanced_filter.type, + method_id, + advanced_filter.timestamp, + Address.checksum(advanced_filter.from_address_hash), + Address.checksum(advanced_filter.to_address_hash), + Address.checksum(advanced_filter.created_contract_address_hash), + decimal_to_string(advanced_filter.value, :normal), + nil, + nil, + nil, + nil, + nil, + advanced_filter.block_number, + decimal_to_string(advanced_filter.fee, :normal), + decimal_to_string(exchange_rate.fiat_value, :xsd), + decimal_to_string(opening_price, :xsd), + decimal_to_string(closing_price, :xsd) + ] + end defp prepare_advanced_filter(advanced_filter, decoded_input) do %{ hash: advanced_filter.hash, type: advanced_filter.type, + status: TransactionView.format_status(advanced_filter.status), method: - if(advanced_filter.type != "coin_transfer", + if(advanced_filter.created_from == :token_transfer, do: Transaction.method_name( %Transaction{ @@ -192,12 +203,12 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do ), value: advanced_filter.value, total: - if(advanced_filter.type != "coin_transfer", + if(advanced_filter.created_from == :token_transfer, do: TokenTransferView.prepare_token_transfer_total(advanced_filter.token_transfer), else: nil ), token: - if(advanced_filter.type != "coin_transfer", + if(advanced_filter.created_from == :token_transfer, do: TokenView.render("token.json", %{token: advanced_filter.token_transfer.token}), else: nil ), @@ -220,6 +231,6 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do %{methods: method_ids, tokens: tokens_map} end - defp decimal_to_string_xsd(nil), do: nil - defp decimal_to_string_xsd(decimal), do: Decimal.to_string(decimal, :xsd) + defp decimal_to_string(nil, _), do: nil + defp decimal_to_string(decimal, type), do: Decimal.to_string(decimal, type) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 19ef782e459c..8b9a66ff06f1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -642,8 +642,19 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end) end - defp format_status({:error, reason}), do: reason - defp format_status(status), do: status + @spec format_status( + :pending + | :awaiting_internal_transactions + | :success + | {:error, :awaiting_internal_transactions} + | {:error, reason :: String.t()} + ) :: + :pending + | :awaiting_internal_transactions + | :success + | String.t() + def format_status({:error, reason}), do: reason + def format_status(status), do: status defp format_decoded_log_input({:error, :could_not_decode}), do: nil defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index aeef6eb025df..ef7d4ce6cfe7 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -261,6 +261,172 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do end end + test "filter by COIN_TRANSFER transaction_type", %{conn: conn} do + for i <- 1..50 do + value = if i < 20, do: 0, else: 1 + transaction = insert(:transaction, value: value) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + value: value, + block_hash: transaction.block_hash, + index: i, + block_index: i + ) + end + + request = get(conn, "/api/v2/advanced-filters", %{"transaction_types" => "coin_transfer"}) + assert response = json_response(request, 200) + + assert Enum.all?(response["items"], fn item -> + String.upcase(item["type"]) == "COIN_TRANSFER" and item["value"] > 0 + end) + + request_2nd_page = + get( + conn, + "/api/v2/advanced-filters", + Map.merge(%{"transaction_types" => "coin_transfer"}, response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response_2nd_page["items"]) == 12 + + assert Enum.all?(response_2nd_page["items"], fn item -> + String.upcase(item["type"]) == "COIN_TRANSFER" and item["value"] > 0 + end) + + check_paginated_response( + AdvancedFilter.list(transaction_types: ["COIN_TRANSFER"]), + response["items"], + response_2nd_page["items"] + ) + end + + test "filter by CONTRACT_INTERACTION transaction_type", %{conn: conn} do + contract_address = + insert(:address, contract_code: Factory.contract_code_info().bytecode) + + for i <- 1..50 do + if i < 20 do + transaction = insert(:transaction) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i, + block_index: i + ) + else + transaction = + insert(:transaction, to_address_hash: contract_address.hash, to_address: contract_address) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + to_address_hash: contract_address.hash, + to_address: contract_address, + block_hash: transaction.block_hash, + index: i, + block_index: i + ) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"transaction_types" => "contract_interaction"}) + assert response = json_response(request, 200) + + assert Enum.all?(response["items"], fn item -> + item["to"]["hash"] == to_string(contract_address) + end) + + request_2nd_page = + get( + conn, + "/api/v2/advanced-filters", + Map.merge(%{"transaction_types" => "contract_interaction"}, response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response_2nd_page["items"]) == 12 + + assert Enum.all?(response_2nd_page["items"], fn item -> + item["to"]["hash"] == to_string(contract_address) + end) + + check_paginated_response( + AdvancedFilter.list(transaction_types: ["CONTRACT_INTERACTION"]), + response["items"], + response_2nd_page["items"] + ) + end + + test "filter by CONTRACT_CREATION transaction_type", %{conn: conn} do + for i <- 1..62 do + address = insert(:address, contract_code: Factory.contract_code_info().bytecode) + + if i < 20 do + transaction = insert(:transaction) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + created_contract_address: address, + created_contract_address_hash: address.hash, + to_address_hash: nil, + to_address: nil, + index: i, + block_index: i + ) + else + transaction = + insert(:transaction, + created_contract_address: address, + created_contract_address_hash: address.hash, + to_address_hash: nil, + to_address: nil + ) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i, + block_index: i + ) + end + end + + request = get(conn, "/api/v2/advanced-filters", %{"transaction_types" => "contract_creation"}) + assert response = json_response(request, 200) + + assert Enum.all?(response["items"], fn item -> + is_nil(item["to"]) and not is_nil(item["created_contract"]) + end) + + request_2nd_page = + get( + conn, + "/api/v2/advanced-filters", + Map.merge(%{"transaction_types" => "contract_creation"}, response["next_page_params"]) + ) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + assert Enum.count(response_2nd_page["items"]) == 12 + + assert Enum.all?(response_2nd_page["items"], fn item -> + is_nil(item["to"]) and not is_nil(item["created_contract"]) + end) + + check_paginated_response( + AdvancedFilter.list(transaction_types: ["CONTRACT_CREATION"]), + response["items"], + response_2nd_page["items"] + ) + end + test "filter by methods", %{conn: conn} do EthereumJSONRPC.Mox |> TestHelper.mock_generic_proxy_requests() @@ -339,12 +505,22 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do method3_transaction = :transaction - |> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method3) + |> insert( + to_address_hash: contract_address.hash, + to_address: contract_address, + input: method3, + has_token_transfers: true + ) |> with_block() method4_transaction = :transaction - |> insert(to_address_hash: contract_address.hash, to_address: contract_address, input: method4) + |> insert( + to_address_hash: contract_address.hash, + to_address: contract_address, + input: method4, + has_token_transfers: true + ) |> with_block() 5 |> insert_list(:token_transfer, transaction: method3_transaction) @@ -782,6 +958,94 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do assert Enum.count(response["items"]) == 6 end + test "filter by from and to address (intersect corner case)", %{conn: conn} do + from_address = insert(:address) + to_address = insert(:address) + + transaction = + :transaction + |> insert( + from_address: from_address, + from_address_hash: from_address.hash, + to_address: to_address, + to_address_hash: to_address.hash + ) + |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: 51, + block_index: 51, + from_address: from_address, + from_address_hash: from_address.hash, + to_address: to_address, + to_address_hash: to_address.hash + ) + + insert(:token_transfer, + transaction: transaction, + block_number: transaction.block_number, + log_index: 51, + from_address: from_address, + from_address_hash: from_address.hash, + to_address: to_address, + to_address_hash: to_address.hash + ) + + for i <- 0..50 do + transaction = + :transaction |> insert(from_address: from_address, from_address_hash: from_address.hash) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1, + from_address: from_address, + from_address_hash: from_address.hash + ) + + insert(:token_transfer, + transaction: transaction, + block_number: transaction.block_number, + log_index: i, + from_address: from_address, + from_address_hash: from_address.hash + ) + + transaction = :transaction |> insert(to_address: to_address, to_address_hash: to_address.hash) |> with_block() + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + index: i + 1, + block_index: i + 1, + to_address: to_address, + to_address_hash: to_address.hash + ) + + insert(:token_transfer, + transaction: transaction, + block_number: transaction.block_number, + log_index: i, + to_address: to_address, + to_address_hash: to_address.hash + ) + end + + request = + get(conn, "/api/v2/advanced-filters", %{ + "from_address_hashes_to_include" => to_string(from_address.hash), + "to_address_hashes_to_include" => to_string(to_address.hash), + "address_relation" => "AnD" + }) + + assert response = json_response(request, 200) + + assert Enum.count(response["items"]) == 3 + end + test "filter by from or to address", %{conn: conn} do from_address = insert(:address) to_address = insert(:address) @@ -1043,7 +1307,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address: transaction_to_address, to_address_hash: transaction_to_address.hash, value: Enum.random(0..1_000_000), - input: method + input: method, + has_token_transfers: true ) |> with_block() @@ -1163,6 +1428,12 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do assert response = json_response(request, 200) assert response == [%{"method_id" => "0x3078f114", "name" => "getAccess"}] end + + test "returns method id without name if q is valid method id", %{conn: conn} do + request = get(conn, "/api/v2/advanced-filters/methods", %{"q" => "0x60fe47b1"}) + assert response = json_response(request, 200) + assert response == [%{"method_id" => "0x60fe47b1", "name" => ""}] + end end defp check_paginated_response(all_advanced_filters, first_page, second_page) do diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index d99a2f89717c..38b2a52b8ad8 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -31,6 +31,15 @@ defmodule Explorer.Chain.AdvancedFilter do field(:input, Data) field(:timestamp, :utc_datetime_usec) + field(:status) :: + :pending + | :awaiting_internal_transactions + | :success + | {:error, :awaiting_internal_transactions} + | {:error, reason :: String.t()} + + field(:created_from, Ecto.Enum, values: [:transaction, :internal_transaction, :token_transfer]) + belongs_to( :from_address, Address, @@ -91,7 +100,7 @@ defmodule Explorer.Chain.AdvancedFilter do | {:timeout, timeout()} ] - @spec list(options()) :: [__MODULE__.t()] + @spec list(options()) :: [t()] def list(options \\ []) do paging_options = Keyword.get(options, :paging_options) @@ -120,8 +129,10 @@ defmodule Explorer.Chain.AdvancedFilter do tasks = options |> Keyword.put(:block_numbers_age, block_numbers_age) - |> queries(paging_options) - |> Enum.map(fn query -> Task.async(fn -> Chain.select_repo(options).all(query, timeout: timeout) end) end) + |> query_functions(paging_options) + |> Enum.map(fn query_function -> + Task.async(fn -> query_function.(Chain.select_repo(options), timeout: timeout) end) + end) tasks |> Task.yield_many(timeout: timeout, on_timeout: :kill_task) @@ -145,48 +156,90 @@ defmodule Explorer.Chain.AdvancedFilter do to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()], created_contract_address: [:names, :smart_contract, proxy_implementations_association()] ) + |> sanitize_fee() + |> assign_type() + |> Enum.to_list() end - defp queries(options, paging_options) do + defp query_functions(options, paging_options) do [] |> maybe_add_transactions_queries(options, paging_options) |> maybe_add_token_transfers_queries(options, paging_options) end - defp maybe_add_transactions_queries(queries, options, paging_options) do + @transaction_types ["COIN_TRANSFER", "CONTRACT_INTERACTION", "CONTRACT_CREATION"] + defp maybe_add_transactions_queries(query_functions, options, paging_options) do transaction_types = options[:transaction_types] || [] tokens_to_include = options[:token_contract_address_hashes][:include] || [] tokens_to_exclude = options[:token_contract_address_hashes][:exclude] || [] - if (transaction_types == [] or "COIN_TRANSFER" in transaction_types) and + if (transaction_types == [] or Enum.any?(@transaction_types, &Enum.member?(transaction_types, &1))) and (tokens_to_include == [] or "native" in tokens_to_include) and "native" not in tokens_to_exclude do - [transactions_query(paging_options, options), internal_transactions_query(paging_options, options) | queries] + [ + transactions_query_function(paging_options, options), + internal_transactions_query_function(paging_options, options) | query_functions + ] else - queries + query_functions end end - defp maybe_add_token_transfers_queries(queries, options, paging_options) do + defp maybe_add_token_transfers_queries(query_functions, options, paging_options) do transaction_types = options[:transaction_types] || [] tokens_to_include = options[:token_contract_address_hashes][:include] || [] - if (transaction_types == [] or not (transaction_types |> Enum.reject(&(&1 == "COIN_TRANSFER")) |> Enum.empty?())) and + if (transaction_types == [] or not (transaction_types |> Enum.reject(&(&1 in @transaction_types)) |> Enum.empty?())) and (tokens_to_include == [] or not (tokens_to_include |> Enum.reject(&(&1 == "native")) |> Enum.empty?())) do - [token_transfers_query(paging_options, options) | queries] + [token_transfers_query_function(paging_options, options) | query_functions] else - queries + query_functions end end + defp sanitize_fee(advanced_filters) do + Stream.scan(advanced_filters, fn + %__MODULE__{hash: hash, created_from: created_from} = advanced_filter, %__MODULE__{hash: hash} + when created_from != :internal_transaction -> + %__MODULE__{advanced_filter | fee: Decimal.new(0)} + + advanced_filter, _ -> + advanced_filter + end) + end + + defp assign_type(advanced_filters) do + Stream.map(advanced_filters, fn advanced_filter -> + type = + cond do + advanced_filter.created_from in ~w(transaction internal_transaction)a and + is_nil(advanced_filter.to_address_hash) -> + "contract_creation" + + advanced_filter.created_from in ~w(transaction internal_transaction)a and + not is_nil(advanced_filter.to_address.contract_code) -> + "contract_interaction" + + advanced_filter.created_from in ~w(transaction internal_transaction)a -> + "coin_transfer" + + true -> + advanced_filter.token_transfer.token_type + end + + %{advanced_filter | type: type} + end) + end + defp to_advanced_filter(%Transaction{} = transaction) do %{value: decimal_transaction_value} = transaction.value %__MODULE__{ hash: transaction.hash, - type: "coin_transfer", + created_from: :transaction, input: transaction.input, timestamp: transaction.block_timestamp, + status: Chain.transaction_to_status(transaction), from_address_hash: transaction.from_address_hash, to_address_hash: transaction.to_address_hash, created_contract_address_hash: transaction.created_contract_address_hash, @@ -202,9 +255,10 @@ defmodule Explorer.Chain.AdvancedFilter do %__MODULE__{ hash: internal_transaction.transaction.hash, - type: "coin_transfer", + created_from: :internal_transaction, input: internal_transaction.input, timestamp: internal_transaction.transaction.block_timestamp, + status: Chain.transaction_to_status(internal_transaction.transaction), from_address_hash: internal_transaction.from_address_hash, to_address_hash: internal_transaction.to_address_hash, created_contract_address_hash: internal_transaction.created_contract_address_hash, @@ -221,9 +275,10 @@ defmodule Explorer.Chain.AdvancedFilter do defp to_advanced_filter(%TokenTransfer{} = token_transfer) do %__MODULE__{ hash: token_transfer.transaction.hash, - type: token_transfer.token_type, + created_from: :token_transfer, input: token_transfer.transaction.input, timestamp: token_transfer.transaction.block_timestamp, + status: Chain.transaction_to_status(token_transfer.transaction), from_address_hash: token_transfer.from_address_hash, to_address_hash: token_transfer.to_address_hash, created_contract_address_hash: nil, @@ -281,7 +336,7 @@ defmodule Explorer.Chain.AdvancedFilter do defp take_page_size(list, _), do: list - defp transactions_query(paging_options, options) do + defp transactions_query_function(paging_options, options) do query = if DenormalizationHelper.transactions_denormalization_finished?() do from(transaction in Transaction, @@ -305,14 +360,17 @@ defmodule Explorer.Chain.AdvancedFilter do ) end - query - |> page_transactions(paging_options) - |> limit_query(paging_options) - |> apply_transactions_filters( - options, - fn query -> query |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) end - ) - |> limit_query(paging_options) + filtered_and_paginated_query = + query + |> page_transactions(paging_options) + |> limit_query(paging_options) + |> apply_transactions_filters( + options, + fn query -> query |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) end + ) + |> limit_query(paging_options) + + fn repo, repo_options -> repo.all(filtered_and_paginated_query, repo_options) end end defp page_transactions(query, %PagingOptions{ @@ -332,7 +390,7 @@ defmodule Explorer.Chain.AdvancedFilter do defp page_transactions(query, _), do: query - defp internal_transactions_query(paging_options, options) do + defp internal_transactions_query_function(paging_options, options) do query = if DenormalizationHelper.transactions_denormalization_finished?() do from(internal_transaction in InternalTransaction, @@ -368,19 +426,22 @@ defmodule Explorer.Chain.AdvancedFilter do ) end - query - |> page_internal_transactions(paging_options) - |> limit_query(paging_options) - |> apply_transactions_filters(options, fn query -> + filtered_and_paginated_query = query - |> order_by([internal_transaction], - desc: internal_transaction.block_number, - desc: internal_transaction.transaction_index, - desc: internal_transaction.index - ) - end) - |> limit_query(paging_options) - |> preload([:transaction]) + |> page_internal_transactions(paging_options) + |> limit_query(paging_options) + |> apply_transactions_filters(options, fn query -> + query + |> order_by([internal_transaction], + desc: internal_transaction.block_number, + desc: internal_transaction.transaction_index, + desc: internal_transaction.index + ) + end) + |> limit_query(paging_options) + |> preload([:transaction]) + + fn repo, repo_options -> repo.all(filtered_and_paginated_query, repo_options) end end defp page_internal_transactions(query, %PagingOptions{ @@ -460,7 +521,7 @@ defmodule Explorer.Chain.AdvancedFilter do defp page_internal_transactions(query, _), do: query - defp token_transfers_query(paging_options, options) do + defp token_transfers_query_function(paging_options, options) do token_transfer_query = if DenormalizationHelper.transactions_denormalization_finished?() do from(token_transfer in TokenTransfer, @@ -515,13 +576,22 @@ defmodule Explorer.Chain.AdvancedFilter do |> apply_token_transfers_filters(options) |> page_token_transfers(paging_options) - token_transfer_query - |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) - |> limit_query(paging_options) - |> query_function.(false) - |> limit_query(paging_options) - |> preload([:transaction, [token: ^Reputation.reputation_association()]]) - |> select_merge([token_transfer], %{token_ids: [token_transfer.token_id], amounts: [token_transfer.amount]}) + filtered_and_paginated_query = + token_transfer_query + |> ExplorerHelper.maybe_hide_scam_addresses(:token_contract_address_hash, options) + |> limit_query(paging_options) + |> query_function.(false) + |> limit_query(paging_options) + |> select_merge([unnested_token_transfer: unnested_token_transfer], %{ + token_ids: [unnested_token_transfer.token_id], + amounts: [unnested_token_transfer.amount] + }) + + fn repo, repo_options -> + filtered_and_paginated_query + |> repo.all(repo_options) + |> repo.preload([:transaction, [token: Reputation.reputation_association()]]) + end end defp page_token_transfers(query_function, %PagingOptions{ @@ -723,8 +793,12 @@ defmodule Explorer.Chain.AdvancedFilter do defp apply_token_transfers_filters(query_function, options) do query_function - |> filter_by_transaction_type(options[:transaction_types]) - |> filter_token_transfers_by_methods(options[:methods]) + |> filter_token_transfer_by_types(options[:transaction_types]) + |> filter_token_transfers_by_methods( + options[:methods], + [include: nil, exclude: nil] == options[:from_address_hashes] and + options[:to_address_hashes] == [include: nil, exclude: nil] + ) |> filter_token_transfers_by_age(options) |> filter_by_token(options[:token_contract_address_hashes]) |> filter_token_transfers_by_addresses( @@ -737,6 +811,7 @@ defmodule Explorer.Chain.AdvancedFilter do defp apply_transactions_filters(query, options, order_by) do query + |> filter_transaction_by_types(options[:transaction_types]) |> filter_transactions_by_amount(options[:amount][:from], options[:amount][:to]) |> filter_transactions_by_methods(options[:methods]) |> only_collated_transactions() @@ -753,19 +828,64 @@ defmodule Explorer.Chain.AdvancedFilter do query |> where(not is_nil(as(:transaction).block_number) and not is_nil(as(:transaction).index)) end - defp filter_by_transaction_type(query_function, [_ | _] = transaction_types) do + defp filter_transaction_by_types(query, types) when types in [nil, []], do: query + + defp filter_transaction_by_types(query, types) do + if Enum.all?(@transaction_types, &Enum.member?(types, &1)) do + query + else + dynamic_condition = + types + |> Enum.reduce(nil, fn + type, dynamic_condition when type in @transaction_types -> + filter_transaction_by_type(type, dynamic_condition) + + _, dynamic_condition -> + dynamic_condition + end) + + query = + if "CONTRACT_INTERACTION" in types and not has_named_binding?(query, :to_address) do + join(query, :left, [t], a in assoc(t, :to_address), as: :to_address) + else + query + end + + query |> where(^dynamic_condition) + end + end + + defp filter_transaction_by_type("COIN_TRANSFER", nil), do: dynamic([t], t.value > ^0) + + defp filter_transaction_by_type("COIN_TRANSFER", dynamic_condition), + do: dynamic([t], t.value > ^0 or ^dynamic_condition) + + defp filter_transaction_by_type("CONTRACT_INTERACTION", nil), + do: dynamic([to_address: to_address], not is_nil(to_address.contract_code)) + + defp filter_transaction_by_type("CONTRACT_INTERACTION", dynamic_condition), + do: dynamic([to_address: to_address], not is_nil(to_address.contract_code) or ^dynamic_condition) + + defp filter_transaction_by_type("CONTRACT_CREATION", nil), do: dynamic([t], is_nil(t.to_address_hash)) + + defp filter_transaction_by_type("CONTRACT_CREATION", dynamic_condition), + do: dynamic([t], is_nil(t.to_address_hash) or ^dynamic_condition) + + defp filter_token_transfer_by_types(query_function, [_ | _] = types) do + types = types -- @transaction_types + if DenormalizationHelper.tt_denormalization_finished?() do fn query, unnested? -> - query |> where([token_transfer], token_transfer.token_type in ^transaction_types) |> query_function.(unnested?) + query |> where([token_transfer], token_transfer.token_type in ^types) |> query_function.(unnested?) end else fn query, unnested? -> - query |> where([token: token], token.type in ^transaction_types) |> query_function.(unnested?) + query |> where([token: token], token.type in ^types) |> query_function.(unnested?) end end end - defp filter_by_transaction_type(query_function, _), do: query_function + defp filter_token_transfer_by_types(query_function, _), do: query_function defp filter_transactions_by_methods(query, [_ | _] = methods) do prepared_methods = prepare_methods(methods) @@ -775,7 +895,31 @@ defmodule Explorer.Chain.AdvancedFilter do defp filter_transactions_by_methods(query, _), do: query - defp filter_token_transfers_by_methods(query_function, [_ | _] = methods) do + defp filter_token_transfers_by_methods(query_function, [_ | _] = methods, true) do + fn query, _unnested? -> + from( + method_ids in fragment("SELECT unnest(?) as method_id", type(^methods, {:array, Data})), + as: :method_ids, + cross_lateral_join: + token_transfer in subquery( + query + |> where(fragment("substring(? FOR 4)", as(:transaction).input) == parent_as(:method_ids).method_id) + |> exclude(:order_by) + |> order_by( + desc: as(:transaction).block_number, + desc: as(:transaction).index, + desc: as(:token_transfer).log_index + ) + |> query_function.(true) + ), + as: :unnested_token_transfer, + select: token_transfer, + order_by: [desc: token_transfer.block_number, desc: token_transfer.log_index] + ) + end + end + + defp filter_token_transfers_by_methods(query_function, [_ | _] = methods, false) do prepared_methods = prepare_methods(methods) fn query, unnested? -> @@ -785,7 +929,7 @@ defmodule Explorer.Chain.AdvancedFilter do end end - defp filter_token_transfers_by_methods(query_function, _), do: query_function + defp filter_token_transfers_by_methods(query_function, _, _), do: query_function defp prepare_methods(methods) do methods @@ -876,7 +1020,13 @@ defmodule Explorer.Chain.AdvancedFilter do end end - defp do_filter_token_transfers_by_both_addresses(query_function, {:include, from}, {:include, to}, relation) do + defp do_filter_token_transfers_by_both_addresses(query_function, {:include, from}, {:include, to}, :and) do + fn query, unnested? -> + query |> where([t], t.from_address_hash in ^from and t.to_address_hash in ^to) |> query_function.(unnested?) + end + end + + defp do_filter_token_transfers_by_both_addresses(query_function, {:include, from}, {:include, to}, _relation) do fn query, _unnested? -> from_queries = from @@ -890,7 +1040,16 @@ defmodule Explorer.Chain.AdvancedFilter do query |> where([token_transfer], token_transfer.to_address_hash == ^to_address) |> query_function.(true) end) - do_filter_token_transfers_by_both_addresses_to_include(from_queries, to_queries, relation) + union_query = + from_queries + |> Kernel.++(to_queries) + |> map_first(&subquery/1) + |> Enum.reduce(fn query, acc -> union(acc, ^query) end) + + from(token_transfer in subquery(union_query), + as: :unnested_token_transfer, + order_by: [desc: token_transfer.block_number, desc: token_transfer.log_index] + ) end end @@ -998,34 +1157,6 @@ defmodule Explorer.Chain.AdvancedFilter do end end - defp do_filter_token_transfers_by_both_addresses_to_include(from_queries, to_queries, relation) do - case relation do - :and -> - united_from_queries = - from_queries |> map_first(&subquery/1) |> Enum.reduce(fn query, acc -> union_all(acc, ^query) end) - - united_to_queries = - to_queries |> map_first(&subquery/1) |> Enum.reduce(fn query, acc -> union_all(acc, ^query) end) - - from(token_transfer in subquery(intersect_all(united_from_queries, ^united_to_queries)), - as: :unnested_token_transfer, - order_by: [desc: token_transfer.block_number, desc: token_transfer.log_index] - ) - - _ -> - union_query = - from_queries - |> Kernel.++(to_queries) - |> map_first(&subquery/1) - |> Enum.reduce(fn query, acc -> union(acc, ^query) end) - - from(token_transfer in subquery(union_query), - as: :unnested_token_transfer, - order_by: [desc: token_transfer.block_number, desc: token_transfer.log_index] - ) - end - end - defp filter_transactions_by_addresses(query, from_addresses, to_addresses, relation, order_by) do order_by = fn query -> query |> exclude(:order_by) |> order_by.() end @@ -1058,7 +1189,13 @@ defmodule Explorer.Chain.AdvancedFilter do |> order_by.() end - defp do_filter_transactions_by_both_addresses(query, {:include, from}, {:include, to}, relation, order_by) do + defp do_filter_transactions_by_both_addresses(query, {:include, from}, {:include, to}, :and, order_by) do + query + |> where([transaction], transaction.from_address_hash in ^from and transaction.to_address_hash in ^to) + |> order_by.() + end + + defp do_filter_transactions_by_both_addresses(query, {:include, from}, {:include, to}, _relation, order_by) do from_queries = from |> Enum.map(fn from_address -> @@ -1075,7 +1212,14 @@ defmodule Explorer.Chain.AdvancedFilter do |> order_by.() end) - do_filter_transactions_by_both_addresses_to_include(from_queries, to_queries, relation, order_by) + union_query = + from_queries + |> Kernel.++(to_queries) + |> map_first(&subquery/1) + |> Enum.reduce(fn query, acc -> union(acc, ^query) end) + + filtered_query = from(transaction in subquery(union_query)) + filtered_query |> order_by.() end defp do_filter_transactions_by_both_addresses(query, {:include, from}, {:exclude, to}, :and, order_by) do @@ -1168,32 +1312,6 @@ defmodule Explorer.Chain.AdvancedFilter do |> order_by.() end - defp do_filter_transactions_by_both_addresses_to_include(from_queries, to_queries, relation, order_by) do - case relation do - :and -> - united_from_queries = - from_queries |> map_first(&subquery/1) |> Enum.reduce(fn query, acc -> union_all(acc, ^query) end) - - united_to_queries = - to_queries |> map_first(&subquery/1) |> Enum.reduce(fn query, acc -> union_all(acc, ^query) end) - - filtered_query = from(transaction in subquery(intersect_all(united_from_queries, ^united_to_queries))) - - filtered_query - |> order_by.() - - _ -> - union_query = - from_queries - |> Kernel.++(to_queries) - |> map_first(&subquery/1) - |> Enum.reduce(fn query, acc -> union(acc, ^query) end) - - filtered_query = from(transaction in subquery(union_query)) - filtered_query |> order_by.() - end - end - @eth_decimals 1_000_000_000_000_000_000 defp filter_transactions_by_amount(query, from, to) when not is_nil(from) and not is_nil(to) do From c8a161b51763294d1ffdf043f4a2c35aa18bacef Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 22 Oct 2025 12:46:28 +0300 Subject: [PATCH 0413/1006] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b92d778364..6ad4d8d30703 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) - dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) - Fix flaky indexer, web tests, refactoring ([#13392](https://github.com/blockscout/blockscout/issues/13392)) - Advanced filters: ERC-20 value in CSV ([#13326](https://github.com/blockscout/blockscout/issues/13326)) @@ -40,6 +41,7 @@ ### ⚙️ Miscellaneous Tasks +- advanced filters improvements([#11909](https://github.com/blockscout/blockscout/pull/11909)) - Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint ([#13412](https://github.com/blockscout/blockscout/pull/13412)) - Add migration to delete zero-value calls ([#13305](https://github.com/blockscout/blockscout/issues/13305)) - *(ReindexDuplicatedInternalTransactions)* Optimize migration performance ([#13363](https://github.com/blockscout/blockscout/issues/13363)) From 14f49c06c8b50e7783222d20dc0b160cd6ec1fe3 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 22 Oct 2025 18:22:55 +0400 Subject: [PATCH 0414/1006] fix: Set timeout: :infinity for DeleteZeroValueInternalTransactions (#13434) --- .../migrator/delete_zero_value_internal_transactions.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 245427b30364..5740a19e72d0 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -104,7 +104,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do on: join_on_ctid(it, locked_it) ) - Repo.delete_all(delete_query) + Repo.delete_all(delete_query, timeout: :infinity) end) end From 95147be723a9be2120c7b659cd266a6d18531aef Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 22 Oct 2025 17:23:48 +0300 Subject: [PATCH 0415/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ad4d8d30703..3ed511b56878 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) - Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) - dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) - Fix flaky indexer, web tests, refactoring ([#13392](https://github.com/blockscout/blockscout/issues/13392)) From e302aac0547b8f5d23f6ca6848b43f1cca5252c1 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 23 Oct 2025 12:02:51 +0400 Subject: [PATCH 0416/1006] chore: Improve DeleteZeroValueInternalTransactions future updating (#13437) --- .../migrator/delete_zero_value_internal_transactions.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 5740a19e72d0..a711309b2abf 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -55,8 +55,9 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do @impl true def handle_info(:update_future, %{"max_block_number" => max_number} = state) do border_number = get_border_number() - clear_internal_transactions(max_number, border_number) - new_state = %{state | "max_block_number" => border_number + 1} + to_number = min(max_number + batch_size(), border_number) + clear_internal_transactions(max_number, to_number) + new_state = %{state | "max_block_number" => to_number + 1} MigrationStatus.update_meta(@migration_name, new_state) schedule_future_check() {:noreply, new_state} From 14abf2dd8c8abf254056cad8c533e9725d1283b0 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 23 Oct 2025 11:11:22 +0300 Subject: [PATCH 0417/1006] fix: Fix broken txn batch blocks API endpoint (#13438) --- .../lib/block_scout_web/schemas/api/v2/general.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index cba9e2d9e948..52dd44b33085 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -229,7 +229,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec batch_number_param() :: Parameter.t() def batch_number_param do %Parameter{ - name: :batch_number, + name: :batch_number_param, in: :path, schema: %Schema{type: :integer, minimum: 0}, required: true, From f5e304335381c14adcb7e232225836edbb923f77 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 23 Oct 2025 11:52:44 +0300 Subject: [PATCH 0418/1006] perf: Remove BENS, metadata preloads from the main page API endpoints (#13442) --- .../controllers/api/v2/main_page_controller.ex | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 0770404624fa..86d3e66fe48f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -8,8 +8,6 @@ defmodule BlockScoutWeb.API.V2.MainPageController do alias Explorer.Chain.Optimism.Deposit import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] - import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] case @chain_type do @@ -51,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(BlockView) - |> render(:blocks, %{blocks: blocks |> maybe_preload_ens() |> maybe_preload_metadata()}) + |> render(:blocks, %{blocks: blocks}) end def optimism_deposits(conn, _params) do @@ -73,7 +71,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: recent_transactions |> maybe_preload_ens() |> maybe_preload_metadata()}) + |> render(:transactions, %{transactions: recent_transactions}) end def watchlist_transactions(conn, _params) do @@ -84,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do |> put_status(200) |> put_view(TransactionView) |> render(:transactions_watchlist, %{ - transactions: transactions |> maybe_preload_ens() |> maybe_preload_metadata(), + transactions: transactions, watchlist_names: watchlist_names }) end From 76f7e61b45844dc0b2d14a7a68da8d40fcbc79df Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 23 Oct 2025 11:54:42 +0300 Subject: [PATCH 0419/1006] Update CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed511b56878..a1aae81b383a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438)) - Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) - Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) - dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) @@ -36,13 +37,15 @@ ### ⚡ Performance +- Remove BENS, metadata preloads from the main page API endpoints ([#13442](https://github.com/blockscout/blockscout/pull/13442)) - Batch preload token transfers in `/api/v2/celo/epochs` ([#13398](https://github.com/blockscout/blockscout/issues/13398)) - Optimize token balance synchronous import steps ([#13217](https://github.com/blockscout/blockscout/issues/13217)) - Optimize `EmptyBlocksSanitizer` queries ([#13132](https://github.com/blockscout/blockscout/issues/13132)) ### ⚙️ Miscellaneous Tasks -- advanced filters improvements([#11909](https://github.com/blockscout/blockscout/pull/11909)) +- Improve DeleteZeroValueInternalTransactions future updating ([#13437](https://github.com/blockscout/blockscout/pull/13437)) +- advanced filters improvements ([#11909](https://github.com/blockscout/blockscout/pull/11909)) - Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint ([#13412](https://github.com/blockscout/blockscout/pull/13412)) - Add migration to delete zero-value calls ([#13305](https://github.com/blockscout/blockscout/issues/13305)) - *(ReindexDuplicatedInternalTransactions)* Optimize migration performance ([#13363](https://github.com/blockscout/blockscout/issues/13363)) From be0f36114446dac355d53ebcacf33734771505f3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 24 Oct 2025 12:22:57 +0300 Subject: [PATCH 0420/1006] chore: Rollback metadata preloads at the main page (#13449) --- CHANGELOG.md | 2 +- .../controllers/api/v2/main_page_controller.ex | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1aae81b383a..d4beefebedf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,7 @@ ### ⚡ Performance -- Remove BENS, metadata preloads from the main page API endpoints ([#13442](https://github.com/blockscout/blockscout/pull/13442)) +- Remove BENS preload from the main page API endpoints ([#13442](https://github.com/blockscout/blockscout/pull/13442), [#13449](https://github.com/blockscout/blockscout/pull/13449)) - Batch preload token transfers in `/api/v2/celo/epochs` ([#13398](https://github.com/blockscout/blockscout/issues/13398)) - Optimize token balance synchronous import steps ([#13217](https://github.com/blockscout/blockscout/issues/13217)) - Optimize `EmptyBlocksSanitizer` queries ([#13132](https://github.com/blockscout/blockscout/issues/13132)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 86d3e66fe48f..732814fb6a12 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do alias Explorer.Chain.Optimism.Deposit import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] case @chain_type do @@ -49,7 +50,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(BlockView) - |> render(:blocks, %{blocks: blocks}) + |> render(:blocks, %{blocks: blocks |> maybe_preload_metadata()}) end def optimism_deposits(conn, _params) do @@ -71,7 +72,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do conn |> put_status(200) |> put_view(TransactionView) - |> render(:transactions, %{transactions: recent_transactions}) + |> render(:transactions, %{transactions: recent_transactions |> maybe_preload_metadata()}) end def watchlist_transactions(conn, _params) do @@ -82,7 +83,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do |> put_status(200) |> put_view(TransactionView) |> render(:transactions_watchlist, %{ - transactions: transactions, + transactions: transactions |> maybe_preload_metadata(), watchlist_names: watchlist_names }) end From a090d02b1cab98049c27606f4153c44c49ebd67f Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:52:25 +0300 Subject: [PATCH 0421/1006] feat: distributed elixir runtime (#13080) Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> --- apps/explorer/lib/explorer/application.ex | 14 ++++- apps/explorer/lib/explorer/helper.ex | 54 +++++++++++++++++++ apps/explorer/mix.exs | 1 + .../lib/indexer/nft_media_handler/queue.ex | 2 +- .../nft_media_handler/dispatcher_interface.ex | 40 +++++++------- config/runtime.exs | 14 ++++- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 2 + .../envs/common-nft-media-handler.env | 1 - mix.lock | 1 + 10 files changed, 104 insertions(+), 26 deletions(-) diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index c6a160ea0f80..a17438efc49a 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -103,7 +103,7 @@ defmodule Explorer.Application do opts = [strategy: :one_for_one, name: Explorer.Supervisor, max_restarts: 1_000] if Application.get_env(:nft_media_handler, :standalone_media_worker?) do - Supervisor.start_link([], opts) + Supervisor.start_link([libcluster()] |> List.flatten(), opts) else Supervisor.start_link(children, opts) end @@ -112,6 +112,7 @@ defmodule Explorer.Application do defp configurable_children do configurable_children_set = [ + configure_libcluster(), configure_mode_dependent_process(Explorer.Market.Fetcher.Coin, :api), configure_mode_dependent_process(Explorer.Market.Fetcher.Token, :indexer), configure_mode_dependent_process(Explorer.Market.Fetcher.History, :indexer), @@ -487,4 +488,15 @@ defmodule Explorer.Application do defp redix_opts do {System.get_env("ACCOUNT_REDIS_URL") || "redis://127.0.0.1:6379", [name: :redix]} end + + defp configure_libcluster do + if Application.get_env(:explorer, :mode) in [:indexer, :api] do + libcluster() + else + [] + end + end + + defp libcluster, + do: {Cluster.Supervisor, [Application.get_env(:libcluster, :topologies), [name: Explorer.ClusterSupervisor]]} end diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 4f581e618e43..0e4bf8b9fa39 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -2,6 +2,7 @@ defmodule Explorer.Helper do @moduledoc """ Auxiliary common functions. """ + require Logger alias ABI.TypeDecoder alias Explorer.Chain @@ -639,4 +640,57 @@ defmodule Explorer.Helper do def number_to_decimal(value) when is_float(value), do: Decimal.from_float(value) def number_to_decimal(value) when is_binary(value) or is_integer(value), do: Decimal.new(value) def number_to_decimal(%Decimal{} = value), do: value + + @doc """ + Determines whether the specified node is configured to run indexer operations. + + This function checks if the node's `:explorer` application mode is set to + either `:all` or `:indexer`. It performs a remote procedure call to retrieve + the application environment configuration from the target node. If the RPC + call fails or the mode is set to a different value, the function returns + `false`. + + ## Parameters + - `node`: The node to check for indexer configuration. + + ## Returns + - `true` if the node's mode is `:all` or `:indexer` + - `false` if the node's mode is any other value, not set, or if the RPC call + fails + """ + @spec indexer_node?(Node.t()) :: boolean() + def indexer_node?(node) do + (node |> :rpc.call(Application, :get_env, [:explorer, :mode]) |> process_rpc_response(node, nil)) in [ + :all, + :indexer + ] + end + + @doc """ + Processes the response from a remote procedure call, handling errors gracefully. + + This function examines the RPC response and returns either the successful + result or a fallback value if the RPC call failed. When a `{:badrpc, reason}` + error tuple is encountered, it logs an error message including the node name + and error details, then returns the provided fallback value. For successful + responses, the original response is returned unchanged. + + ## Parameters + - `response`: The result from an RPC call, either a successful value or a + `{:badrpc, reason}` error tuple + - `node`: The node that was called via RPC, used for error logging + - `fallback`: The value to return if the RPC call failed + + ## Returns + - The original response if the RPC call succeeded + - The fallback value if the RPC call failed with a `{:badrpc, reason}` error + """ + @spec process_rpc_response(res | {:badrpc, reason}, Node.t(), fallback) :: res | fallback + when res: any(), reason: any(), fallback: any() + def process_rpc_response({:badrpc, _reason} = error, node, fallback) do + Logger.error("Received an error from #{node}: #{inspect(error)}") + fallback + end + + def process_rpc_response(response, _node, _fallback), do: response end diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 51010b049495..8bdaa424e65f 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -88,6 +88,7 @@ defmodule Explorer.Mixfile do {:junit_formatter, ">= 0.0.0", only: [:test], runtime: false}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, + {:libcluster, "~> 3.5"}, {:math, "~> 0.7.0"}, {:mock, "~> 0.3.0", only: [:test], runtime: false}, {:mox, "~> 1.1.0"}, diff --git a/apps/indexer/lib/indexer/nft_media_handler/queue.ex b/apps/indexer/lib/indexer/nft_media_handler/queue.ex index b85846785706..84f95087576a 100644 --- a/apps/indexer/lib/indexer/nft_media_handler/queue.ex +++ b/apps/indexer/lib/indexer/nft_media_handler/queue.ex @@ -124,7 +124,7 @@ defmodule Indexer.NFTMediaHandler.Queue do end dets_insert_wrapper(in_progress, instances) - {:reply, urls, {queue, in_progress, continuation}} + {:reply, {urls, Application.get_env(:nft_media_handler, :r2_folder)}, {queue, in_progress, continuation}} end @doc """ diff --git a/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex b/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex index 27f8fdec1919..5574da0a5117 100644 --- a/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex +++ b/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex @@ -6,6 +6,8 @@ defmodule NFTMediaHandler.DispatcherInterface do require Logger use GenServer + alias Explorer.Helper + def start_link(_) do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) end @@ -15,13 +17,7 @@ defmodule NFTMediaHandler.DispatcherInterface do """ @impl true def init(_) do - nodes = :nft_media_handler |> Application.get_env(:nodes_map) |> Map.to_list() - - if Enum.empty?(nodes) do - {:stop, "NFT_MEDIA_HANDLER_NODES_MAP must contain at least one node"} - else - {:ok, %{used_nodes: [], unused_nodes: nodes}} - end + {:ok, %{used_nodes: [], unused_nodes: []}} end @doc """ @@ -33,8 +29,15 @@ defmodule NFTMediaHandler.DispatcherInterface do {used, unused, node_to_call} = case unused_nodes do [] -> - [to_call | remains] = used_nodes |> Enum.reverse() - {[to_call], remains, to_call} + Node.list() + |> Enum.filter(&Helper.indexer_node?/1) + |> case do + [] -> + raise "No indexer nodes discovered" + + [to_call | remains] -> + {[to_call], remains, to_call} + end [to_call | remains] -> {[to_call | used_nodes], remains, to_call} @@ -58,13 +61,15 @@ defmodule NFTMediaHandler.DispatcherInterface do function = :get_urls_to_fetch if Application.get_env(:nft_media_handler, :remote?) do - {node, folder} = GenServer.call(__MODULE__, :take_node_to_call) + node = GenServer.call(__MODULE__, :take_node_to_call) - {node |> :rpc.call(Indexer.NFTMediaHandler.Queue, :get_urls_to_fetch, args) |> process_rpc_response(node), node, - folder} + {urls, folder} = + node |> :rpc.call(Indexer.NFTMediaHandler.Queue, function, args) |> Helper.process_rpc_response(node, {[], nil}) + + {urls, node, folder} else - folder = Application.get_env(:nft_media_handler, :nodes_map)[:self] - {apply(Indexer.NFTMediaHandler.Queue, function, args), :self, folder} + {urls, folder} = apply(Indexer.NFTMediaHandler.Queue, function, args) + {urls, :self, folder} end end @@ -83,11 +88,4 @@ defmodule NFTMediaHandler.DispatcherInterface do defp remote_call(args, function, _node, false) do apply(Indexer.NFTMediaHandler.Queue, function, args) end - - defp process_rpc_response({:badrpc, _reason} = error, node) do - Logger.error("Received an error from #{node}: #{inspect(error)}") - [] - end - - defp process_rpc_response(response, _node), do: response end diff --git a/config/runtime.exs b/config/runtime.exs index 8d16333925ff..8dc7670ea4ed 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1591,14 +1591,13 @@ config :ex_aws, :s3, nmh_enabled? = ConfigHelper.parse_bool_env_var("NFT_MEDIA_HANDLER_ENABLED") nmh_remote? = ConfigHelper.parse_bool_env_var("NFT_MEDIA_HANDLER_REMOTE_DISPATCHER_NODE_MODE_ENABLED") nmh_worker? = ConfigHelper.parse_bool_env_var("NFT_MEDIA_HANDLER_IS_WORKER") -nodes_map = ConfigHelper.parse_json_with_atom_keys_env_var("NFT_MEDIA_HANDLER_NODES_MAP") config :nft_media_handler, enabled?: nmh_enabled?, tmp_dir: "./temp", remote?: nmh_remote?, worker?: nmh_worker?, - nodes_map: nodes_map, + r2_folder: System.get_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER"), standalone_media_worker?: nmh_enabled? && nmh_remote? && nmh_worker?, worker_concurrency: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_WORKER_CONCURRENCY", 10), worker_batch_size: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_WORKER_BATCH_SIZE", 10), @@ -1615,6 +1614,17 @@ config :nft_media_handler, Indexer.NFTMediaHandler.Backfiller, config :indexer, Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, disabled?: ConfigHelper.chain_type() != :zilliqa +config :libcluster, + topologies: [ + k8sDNS: [ + strategy: Cluster.Strategy.Kubernetes.DNS, + config: [ + service: System.get_env("K8S_SERVICE"), + application_name: "blockscout" + ] + ] + ] + Code.require_file("#{config_env()}.exs", "config/runtime") for config <- "../apps/*/config/runtime/#{config_env()}.exs" |> Path.expand(__DIR__) |> Path.wildcard() do diff --git a/cspell.json b/cspell.json index 42585360f687..947d6e22f6eb 100644 --- a/cspell.json +++ b/cspell.json @@ -356,6 +356,7 @@ "lastname", "lastword", "lformat", + "libcluster", "libp", "libraryaddress", "libraryname", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 0842266b802d..453beb805e6b 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -593,6 +593,7 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # SHRINK_INTERNAL_TRANSACTIONS_ENABLED= NFT_MEDIA_HANDLER_ENABLED=true NFT_MEDIA_HANDLER_REMOTE_DISPATCHER_NODE_MODE_ENABLED=true +NFT_MEDIA_HANDLER_BUCKET_FOLDER=/folder_1 RELEASE_NODE=producer@172.18.0.4 RELEASE_DISTRIBUTION=name RELEASE_COOKIE=secret_cookie @@ -601,6 +602,7 @@ RELEASE_COOKIE=secret_cookie # NFT_MEDIA_HANDLER_BACKFILL_QUEUE_SIZE= # NFT_MEDIA_HANDLER_BACKFILL_ENQUEUE_BUSY_WAITING_TIMEOUT= # NFT_MEDIA_HANDLER_CACHE_UNIQUENESS_MAX_SIZE= +# K8S_SERVICE=blockscout-headless # old UI vars diff --git a/docker-compose/envs/common-nft-media-handler.env b/docker-compose/envs/common-nft-media-handler.env index fd68542cf31b..aed0aef14ccc 100644 --- a/docker-compose/envs/common-nft-media-handler.env +++ b/docker-compose/envs/common-nft-media-handler.env @@ -8,7 +8,6 @@ NFT_MEDIA_HANDLER_ENABLED=true NFT_MEDIA_HANDLER_REMOTE_DISPATCHER_NODE_MODE_ENABLED=true NFT_MEDIA_HANDLER_IS_WORKER=true -NFT_MEDIA_HANDLER_NODES_MAP="{\"producer@172.18.0.4\": \"/folder_1\"}" # NFT_MEDIA_HANDLER_WORKER_CONCURRENCY= # NFT_MEDIA_HANDLER_WORKER_BATCH_SIZE= # NFT_MEDIA_HANDLER_WORKER_SPAWN_TASKS_TIMEOUT= diff --git a/mix.lock b/mix.lock index 353368c6f39a..740387df21f1 100644 --- a/mix.lock +++ b/mix.lock @@ -98,6 +98,7 @@ "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, + "libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, "logger_json": {:hex, :logger_json, "5.1.4", "9e30a4f2e31a8b9e402bdc20bd37cf9b67d3a31f19d0b33082a19a06b4c50f6d", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "3f20eea58e406a33d3eb7814c7dff5accb503bab2ee8601e84da02976fa3934c"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, From 4a99c481fe3214f97bddc360e133bbcb33ebedb2 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 24 Oct 2025 14:53:22 +0300 Subject: [PATCH 0422/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4beefebedf4..c99070713ab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- distributed elixir runtime ([#13080](https://github.com/blockscout/blockscout/pull/13080)) - Delete internal transactions on reorgs ([#13121](https://github.com/blockscout/blockscout/issues/13121)) - Implement websocket endpoints support in the Universal Proxy config ([#13167](https://github.com/blockscout/blockscout/issues/13167)) - Celo accounts api ([#12982](https://github.com/blockscout/blockscout/issues/12982)) From df9e657184eb7f78de9a436c98b7124f34b94a6d Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Fri, 24 Oct 2025 16:06:20 +0300 Subject: [PATCH 0423/1006] fix: celo accounts transformer (#13423) --- apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex | 8 ++++---- apps/indexer/lib/indexer/transform/addresses.ex | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex b/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex index ffc855746a4f..dcabe7cd6493 100644 --- a/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex +++ b/apps/explorer/lib/explorer/chain/celo/legacy/accounts.ex @@ -63,7 +63,6 @@ defmodule Explorer.Chain.Celo.Legacy.Accounts do logs |> Enum.filter(fn log -> Enum.member?(topics, log.first_topic) end) |> Enum.reduce([], fn log, accounts -> do_parse(log, accounts, get_topic) end) - |> Enum.map(fn address -> %{address_hash: address} end) end defp get_withdrawal_events(logs, topics) do @@ -91,12 +90,13 @@ defmodule Explorer.Chain.Celo.Legacy.Accounts do end defp do_parse(log, accounts, get_topic) do - account_address = parse_params(log, get_topic) + account_address_hash = parse_params(log, get_topic) + entry = %{address_hash: account_address_hash, block_number: log.block_number} - if Enum.member?(accounts, account_address) do + if Enum.any?(accounts, &(&1.address_hash == account_address_hash)) do accounts else - [account_address | accounts] + [entry | accounts] end rescue _ in [FunctionClauseError, MatchError] -> diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index 8ab87631ad0f..dbe6399c91c1 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -193,6 +193,7 @@ defmodule Indexer.Transform.Addresses do ], celo_pending_account_operations: [ [ + %{from: :block_number, to: :fetched_coin_balance_block_number}, %{from: :address_hash, to: :hash} ] ] From 26bc9db26ca7f6569334b0e13027a37ba2e0a591 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 24 Oct 2025 16:07:47 +0300 Subject: [PATCH 0424/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c99070713ab7..be504bfd5494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) - Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438)) - Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) - Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) From 077f1be811cdb91c194a99c7bf19a1fe63b3e795 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 24 Oct 2025 16:27:19 +0300 Subject: [PATCH 0425/1006] Update CHANGELOG --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index be504bfd5494..5c7e183c3b1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,13 @@ | `CELO_ACCOUNTS_CONTRACT` | The address of the `Accounts` core contract. Implemented in [#12893](https://github.com/blockscout/blockscout/pull/12893). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | | `INDEXER_CELO_ACCOUNTS_CONCURRENCY` | Sets the maximum number of concurrent requests for fetching Celo accounts. | Version: v9.2.0+
Default: `1`
Applications: Indexer | | `INDEXER_CELO_ACCOUNTS_BATCH_SIZE` | Specifies the number of account addresses processed per batch during fetching. | Version: v9.2.0+
Default: `100`
Applications: Indexer | +| `K8S_SERVICE` | Kubernetes service name for Elixir nodes clusterization, more info on how to configure it can be found here https://hexdocs.pm/libcluster/Cluster.Strategy.Kubernetes.DNS.html. Implemented in [#13080](https://github.com/blockscout/blockscout/pull/13080). | Version: v9.2.0\+
Default: (empty)
Applications: API, Indexer | +### Deprecated ENV variables + +| Variable | Description | Default | Version | Deprecated in Version | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------- | --------------------- | +| `NFT_MEDIA_HANDLER_NODES_MAP` | String in json map format, where key is erlang node and value is folder in R2/S3 bucket, example: `"{\"producer@172.18.0.4\": \"/folder_1\"}"`. If nft_media_handler runs in one pod with indexer, map should contain `self` key | | v6.10.0+ | v9.2.0+ | + ## 9.1.1 From e7775bae758eaa40f8b1f177ffdf7b9d92cd8006 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 27 Oct 2025 14:35:15 +0400 Subject: [PATCH 0426/1006] fix: Remove transaction_has_multiple_internal_transactions filter (#13453) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + .../views/api/v2/block_view.ex | 2 +- .../graphql/schema/query/transaction_test.exs | 2 +- .../explorer/chain/internal_transaction.ex | 29 ------------------- apps/explorer/lib/explorer/etherscan.ex | 1 - apps/explorer/lib/explorer/graphql.ex | 2 +- cspell.json | 1 + 7 files changed, 5 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c7e183c3b1a..173389058fcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) - celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) - Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438)) - Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 92f5c390bc47..7a67bcee785c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -108,7 +108,7 @@ defmodule BlockScoutWeb.API.V2.BlockView do defp count_internal_transactions(%Block{internal_transactions: internal_transactions}) when is_list(internal_transactions), - do: Enum.count(internal_transactions) + do: Enum.count(internal_transactions, &(&1.type != :call or &1.index > 0)) defp count_internal_transactions(_), do: nil diff --git a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs index f13049bde761..3b7fd38612e0 100644 --- a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs +++ b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs @@ -430,7 +430,7 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do variables = %{ "hash" => to_string(transaction.hash), "last" => 1, - "count" => 3 + "count" => 2 } [internal_transaction] = diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index eb4ac7466714..82116a660e23 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -649,7 +649,6 @@ defmodule Explorer.Chain.InternalTransaction do __MODULE__ |> for_parent_transaction(hash) |> Chain.join_associations(necessity_by_association) - |> where_transaction_has_multiple_internal_transactions() |> where_is_different_from_parent_transaction() |> where_nonpending_block() |> page_internal_transaction(paging_options) @@ -936,34 +935,6 @@ defmodule Explorer.Chain.InternalTransaction do |> where([internal_transaction], internal_transaction.call_type in ^call_types) end - @doc """ - Ensures the following conditions are true: - - * excludes internal transactions of type call with no siblings in the - transaction - * includes internal transactions of type create, reward, or selfdestruct - even when they are alone in the parent transaction - - """ - @spec where_transaction_has_multiple_internal_transactions(Ecto.Query.t()) :: Ecto.Query.t() - def where_transaction_has_multiple_internal_transactions(query) do - where( - query, - [internal_transaction, transaction], - internal_transaction.type != ^:call or - fragment( - """ - EXISTS (SELECT sibling.* - FROM internal_transactions AS sibling - WHERE sibling.transaction_hash = ? AND sibling.index != ? - ) - """, - transaction.hash, - internal_transaction.index - ) - ) - end - @doc """ Returns the ordered paginated list of internal transactions (consensus blocks only) from the DB with address, block preloads """ diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 2a99d1d6ea5b..0c28613f74ee 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -137,7 +137,6 @@ defmodule Explorer.Etherscan do end query - |> InternalTransaction.where_transaction_has_multiple_internal_transactions() |> InternalTransaction.where_is_different_from_parent_transaction() |> InternalTransaction.where_nonpending_block() |> InternalTransaction.include_zero_value(options.include_zero_value) diff --git a/apps/explorer/lib/explorer/graphql.ex b/apps/explorer/lib/explorer/graphql.ex index 3ebc7e575ed5..90ecfb41bd6b 100644 --- a/apps/explorer/lib/explorer/graphql.ex +++ b/apps/explorer/lib/explorer/graphql.ex @@ -70,7 +70,7 @@ defmodule Explorer.GraphQL do query |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_transaction_has_multiple_internal_transactions() + |> InternalTransaction.where_is_different_from_parent_transaction() end @doc """ diff --git a/cspell.json b/cspell.json index 947d6e22f6eb..b0b4be429aea 100644 --- a/cspell.json +++ b/cspell.json @@ -138,6 +138,7 @@ "Cldr", "clearfix", "clickover", + "clusterization", "codecv", "codeformat", "coef", From 561de7d57a9419b1724fe5ecbb87340c384c433f Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 28 Oct 2025 14:08:24 +0300 Subject: [PATCH 0427/1006] fix: AverageBlockTime for sub-second blocks (#13468) --- .../cache/counters/average_block_time.ex | 9 ++++++- .../counters/average_block_time_test.exs | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex b/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex index 7d6e715c7cce..e0ecb82df324 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex @@ -96,7 +96,14 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTime do timestamps = raw_timestamps - |> Enum.sort_by(fn {_, timestamp} -> timestamp end, &Timex.after?/2) + |> Enum.sort(fn + {number_a, timestamp_a}, {number_b, timestamp_b} -> + case Timex.compare(timestamp_a, timestamp_b) do + 1 -> true + -1 -> false + 0 -> number_a > number_b + end + end) |> Enum.map(fn {number, timestamp} -> {number, DateTime.to_unix(timestamp, :millisecond)} end) diff --git a/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs b/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs index ce7a9ee18956..4c5d2e0b5576 100644 --- a/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs +++ b/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs @@ -183,5 +183,31 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTimeTest do assert Timex.Duration.to_milliseconds(AverageBlockTime.average_block_time()) == 3000 end + + test "handles sub-second blocks" do + block_number = 99_999_999 + + first_timestamp = Timex.now() + + Enum.each(1..200//2, fn i -> + insert(:block, + number: block_number + i, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: div(i + 1, 2)) + ) + + insert(:block, + number: block_number + i + 1, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: div(i + 1, 2)) + ) + end) + + AverageBlockTime.refresh() + + avg_time = Timex.Duration.to_milliseconds(AverageBlockTime.average_block_time()) + + assert avg_time > 490 and avg_time < 510 + end end end From db35bdde6e80b1363874d285b41e2a497bf66a5c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 28 Oct 2025 14:48:40 +0300 Subject: [PATCH 0428/1006] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 173389058fcb..ab05fd1b8e71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) - Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) - celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) - Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438)) From 7be40c57fcc794699e6249a67877c6e45472261f Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:57:52 +0300 Subject: [PATCH 0429/1006] chore: add `CACHE_AVERAGE_BLOCK_WINDOW` (#13470) Co-authored-by: Victor Baranov --- CHANGELOG.md | 3 +++ .../chain/cache/counters/average_block_time.ex | 6 ++++-- .../chain/cache/counters/average_block_time_test.exs | 11 +++++++++-- config/runtime.exs | 3 ++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab05fd1b8e71..22b963b635fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ ### ⚙️ Miscellaneous Tasks +- add `CACHE_AVERAGE_BLOCK_TIME_WINDOW` ([#13470](https://github.com/blockscout/blockscout/pull/13470)) - Improve DeleteZeroValueInternalTransactions future updating ([#13437](https://github.com/blockscout/blockscout/pull/13437)) - advanced filters improvements ([#11909](https://github.com/blockscout/blockscout/pull/11909)) - Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint ([#13412](https://github.com/blockscout/blockscout/pull/13412)) @@ -87,6 +88,8 @@ | `INDEXER_CELO_ACCOUNTS_CONCURRENCY` | Sets the maximum number of concurrent requests for fetching Celo accounts. | Version: v9.2.0+
Default: `1`
Applications: Indexer | | `INDEXER_CELO_ACCOUNTS_BATCH_SIZE` | Specifies the number of account addresses processed per batch during fetching. | Version: v9.2.0+
Default: `100`
Applications: Indexer | | `K8S_SERVICE` | Kubernetes service name for Elixir nodes clusterization, more info on how to configure it can be found here https://hexdocs.pm/libcluster/Cluster.Strategy.Kubernetes.DNS.html. Implemented in [#13080](https://github.com/blockscout/blockscout/pull/13080). | Version: v9.2.0\+
Default: (empty)
Applications: API, Indexer | +| `CACHE_AVERAGE_BLOCK_TIME_WINDOW` | The number of blocks to be taken into account in the calculations. Introduced in [#13470](https://github.com/blockscout/blockscout/pull/13470). | Version: v9.2.0\+
Default: `100`
Applications: API, Indexer | + ### Deprecated ENV variables | Variable | Description | Default | Version | Deprecated in Version | diff --git a/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex b/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex index e0ecb82df324..37bb42bb1933 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/average_block_time.ex @@ -11,7 +11,6 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTime do alias Explorer.Repo alias Timex.Duration - @num_of_blocks 100 @offset 100 @doc """ @@ -73,10 +72,13 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTime do first_block_from_config = RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :block_ranges)) + num_of_blocks = + Application.get_env(:explorer, __MODULE__)[:num_of_blocks] + base_query = from(block in Block, where: block.number > ^first_block_from_config, - limit: ^@num_of_blocks, + limit: ^num_of_blocks, offset: ^@offset, order_by: [desc: block.number], select: {block.number, block.timestamp} diff --git a/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs b/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs index 4c5d2e0b5576..80ce0affa2e6 100644 --- a/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs +++ b/apps/explorer/test/explorer/chain/cache/counters/average_block_time_test.exs @@ -185,11 +185,18 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTimeTest do end test "handles sub-second blocks" do + old_envs = Application.get_env(:explorer, AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, Keyword.merge(old_envs, num_of_blocks: 101)) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, old_envs) + end) + block_number = 99_999_999 first_timestamp = Timex.now() - Enum.each(1..200//2, fn i -> + Enum.each(1..206//2, fn i -> insert(:block, number: block_number + i, consensus: true, @@ -207,7 +214,7 @@ defmodule Explorer.Chain.Cache.Counters.AverageBlockTimeTest do avg_time = Timex.Duration.to_milliseconds(AverageBlockTime.average_block_time()) - assert avg_time > 490 and avg_time < 510 + assert avg_time == 500 end end end diff --git a/config/runtime.exs b/config/runtime.exs index 8dc7670ea4ed..ca5d97e5da60 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -384,7 +384,8 @@ config :explorer, Explorer.Chain.Cache.Counters.TokenTransfersCount, config :explorer, Explorer.Chain.Cache.Counters.AverageBlockTime, enabled: true, period: :timer.minutes(10), - cache_period: ConfigHelper.parse_time_env_var("CACHE_AVERAGE_BLOCK_PERIOD", "30m") + cache_period: ConfigHelper.parse_time_env_var("CACHE_AVERAGE_BLOCK_PERIOD", "30m"), + num_of_blocks: ConfigHelper.parse_integer_env_var("CACHE_AVERAGE_BLOCK_TIME_WINDOW", 100) config :explorer, Explorer.Market.MarketHistoryCache, cache_period: ConfigHelper.parse_time_env_var("CACHE_MARKET_HISTORY_PERIOD", "6h") From 734074670ed9477c8b8c3e00d04e6dfee8069717 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 28 Oct 2025 17:03:26 +0300 Subject: [PATCH 0430/1006] Update CHANGELOG --- CHANGELOG.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b963b635fe..7e5283c6a6ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,7 +52,6 @@ - Improve DeleteZeroValueInternalTransactions future updating ([#13437](https://github.com/blockscout/blockscout/pull/13437)) - advanced filters improvements ([#11909](https://github.com/blockscout/blockscout/pull/11909)) - Allow api_key in the query string for api/v2/tokens/:address_hash/instances/refetch-metadata endpoint ([#13412](https://github.com/blockscout/blockscout/pull/13412)) -- Add migration to delete zero-value calls ([#13305](https://github.com/blockscout/blockscout/issues/13305)) - *(ReindexDuplicatedInternalTransactions)* Optimize migration performance ([#13363](https://github.com/blockscout/blockscout/issues/13363)) - OpenAPI spec for the REST API endpoints in token and CSV export controllers ([#13311](https://github.com/blockscout/blockscout/issues/13311)) - Edited the broken Discord badge ([#13115](https://github.com/blockscout/blockscout/issues/13115)) @@ -69,10 +68,6 @@ | `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_BATCH_SIZE` | Batch size for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | | `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_CONCURRENCY` | Concurrency for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `1`
Applications: Indexer | | `INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_THRESHOLD` | Threshold for internal transactions delete queue handler. Implemented in [#13121](https://github.com/blockscout/blockscout/pull/13121). | Version: v9.2.0\+
Default: `10m`
Applications: Indexer | -| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED` | Enable of delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `false`
Applications: Indexer | -| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE` | Specifies the block batch size selected for the delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | -| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` | Specifies the period for which recent zero-value calls won't be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `30`
Applications: Indexer | -| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL` | Specifies the interval between checking of new zero-value calls to be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.2.0\+
Default: `1m`
Applications: Indexer | | `INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL` | Defines a URL to Alt-DA server to retrieve L1 data from that. Example for Redstone: `https://da.redstonechain.com/get`. Implemented in [#13179](https://github.com/blockscout/blockscout/pull/13179). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | | `INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP` | Isthmus upgrade L2 block timestamp. Needed for operator fee determining. If set to `0`, the Isthmus is assumed to be active from genesis block. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0+
Default: (empty)
Applications: API, Indexer | | `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_BATCH_SIZE` | Batch size for OP operator fee fetcher. Defines max number of transactions handled per batch. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `100`
Applications: Indexer | @@ -80,9 +75,6 @@ | `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_ENQUEUE_BUSY_WAITING_TIMEOUT` | Timeout before new attempt to append item to OP operator fee fetcher queue if it's full. [Time format](backend-env-variables.md#time-format). Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1s`
Applications: Indexer | | `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_MAX_QUEUE_SIZE` | Maximum size of OP operator fee fetcher queue. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1000`
Applications: Indexer | | `INDEXER_OPTIMISM_OPERATOR_FEE_QUEUE_INIT_QUERY_LIMIT` | Limit of the init query for processing the OP operator fee fetcher queue. Implemented in [#13139](https://github.com/blockscout/blockscout/pull/13139). | Version: v9.2.0\+
Default: `1000`
Applications: Indexer | -| `CELO_UNRELEASED_TREASURY_CONTRACT` | The address of the `CeloUnreleasedTreasury` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | -| `CELO_VALIDATORS_CONTRACT` | The address of the `Validators` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | -| `CELO_EPOCH_MANAGER_CONTRACT` | The address of the `EpochManager` core contract. Implemented in [#12373](https://github.com/blockscout/blockscout/pull/12373). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | | `CELO_LOCKED_GOLD_CONTRACT` | The address of the `LockedGold` core contract. Implemented in [#12893](https://github.com/blockscout/blockscout/pull/12893). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | | `CELO_ACCOUNTS_CONTRACT` | The address of the `Accounts` core contract. Implemented in [#12893](https://github.com/blockscout/blockscout/pull/12893). | Version: v9.2.0+
Default: (empty)
Applications: Indexer | | `INDEXER_CELO_ACCOUNTS_CONCURRENCY` | Sets the maximum number of concurrent requests for fetching Celo accounts. | Version: v9.2.0+
Default: `1`
Applications: Indexer | From 2a3e2cf747882dbc5829288567cc73c5d589c165 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 29 Oct 2025 13:48:00 +0300 Subject: [PATCH 0431/1006] fix: Fix /api/v2/tokens endpoints: allow back `limit` param (#13473) --- CHANGELOG.md | 1 + .../controllers/api/v2/token_controller.ex | 1 + .../controllers/api/v2/transaction_controller.ex | 2 +- .../lib/block_scout_web/schemas/api/v2/general.ex | 14 ++++++++++++++ .../controllers/api/v2/token_controller_test.exs | 6 ++++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e5283c6a6ad..218779ce0d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Fix /api/v2/tokens endpoints: allow back limit param ([#13473](https://github.com/blockscout/blockscout/pull/13473)) - Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) - Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) - celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 0d90064d3ae2..c0f71a7608fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -536,6 +536,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do [ token_type_param(), q_param(), + limit_param(), sort_param(["fiat_value", "holders_count", "circulating_market_cap"]), order_param() ] ++ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 4af08fdb693e..9a9609bf96dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -266,7 +266,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end @doc """ - Function to handle GET requests to `/api/v2/transactions/:tx_hash/external-transactions` endpoint. + Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/external-transactions` endpoint. It renders the list of external transactions that are somehow linked (eg. preceded or initiated by) to the selected one. The most common use case is for side-chains and rollups. Currently implemented only for Neon chain but could also be extended for similar cases. diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 52dd44b33085..13b70652bea8 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -136,6 +136,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for a limit result items in the response. + """ + @spec limit_param() :: Parameter.t() + def limit_param do + %Parameter{ + name: :limit, + in: :query, + schema: %Schema{type: :string, nullable: true}, + required: false, + description: "Limit result items in the response" + } + end + @doc """ Returns a parameter definition for a filter type in the query. """ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index e581bc04f333..f0e8642e142a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -647,6 +647,12 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) end + test "accepts limit", %{conn: conn} do + request = get(conn, "/api/v2/tokens?limit=5") + + assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200) + end + test "get token with ok reputation", %{conn: conn} do init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) Application.put_env(:block_scout_web, :hide_scam_addresses, true) From c05f2f528a6fca55f12f226392c35ce7e6604893 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 30 Oct 2025 12:27:55 +0300 Subject: [PATCH 0432/1006] fix: ERC-1155 value in advanced filters csv (#13474) --- CHANGELOG.md | 1 + .../views/api/v2/advanced_filter_view.ex | 14 ++++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 218779ce0d1c..ec5ab64d6916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- ERC-1155 value in advanced filters csv ([#13474](https://github.com/blockscout/blockscout/pull/13474)) - Fix /api/v2/tokens endpoints: allow back limit param ([#13473](https://github.com/blockscout/blockscout/pull/13473)) - Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) - Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 838b893bd793..555cc6569352 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -114,10 +114,16 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do Address.checksum(advanced_filter.token_transfer.token.contract_address_hash), decimal_to_string(token_transfer_total["decimals"], :normal), advanced_filter.token_transfer.token.symbol, - token_transfer_total["decimals"] && - token_transfer_total["value"] - |> Decimal.div(Integer.pow(10, Decimal.to_integer(token_transfer_total["decimals"]))) - |> decimal_to_string(:xsd), + case token_transfer_total["decimals"] do + nil -> + decimal_to_string(token_transfer_total["value"], :xsd) + + decimals -> + token_transfer_total["value"] && + token_transfer_total["value"] + |> Decimal.div(Integer.pow(10, Decimal.to_integer(decimals))) + |> decimal_to_string(:xsd) + end, token_transfer_total["token_id"], advanced_filter.block_number, decimal_to_string(advanced_filter.fee, :normal), From ca1d48379a7e9377ae780237fa61f2232913e9ce Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 31 Oct 2025 14:25:44 +0300 Subject: [PATCH 0433/1006] fix: Remove batch_number_param from next_page_params (#13482) --- CHANGELOG.md | 2 +- apps/block_scout_web/lib/block_scout_web/paging_helper.ex | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec5ab64d6916..acc4b0e6d9dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) - Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) - celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) -- Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438)) +- Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438), [#13482](https://github.com/blockscout/blockscout/pull/13482)) - Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) - Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) - dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 31b9ce2813ff..2d8da12ce2e7 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -242,7 +242,9 @@ defmodule BlockScoutWeb.PagingHelper do "state_filter", "l2_block_range_start", "l2_block_range_end", - "batch_number" + # remove in favour batch_number_param in the future when all batch - related API endpoints are covered with OpenAPI spec. + "batch_number", + "batch_number_param" ]) end From 97e7be82de2e53cdbe6d21d82ebad28f24543f79 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 31 Oct 2025 15:11:15 +0300 Subject: [PATCH 0434/1006] fix: Fix batch_number_param (#13483) --- CHANGELOG.md | 2 +- apps/block_scout_web/lib/block_scout_web/paging_helper.ex | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acc4b0e6d9dc..754ad9688267 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) - Remove transaction_has_multiple_internal_transactions filter ([#13453](https://github.com/blockscout/blockscout/pull/13453)) - celo accounts transformer ([#13423](https://github.com/blockscout/blockscout/pull/13423)) -- Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438), [#13482](https://github.com/blockscout/blockscout/pull/13482)) +- Fix broken txn batch blocks API endpoint ([#13438](https://github.com/blockscout/blockscout/pull/13438), [#13483](https://github.com/blockscout/blockscout/pull/13483)) - Set timeout: :infinity for DeleteZeroValueInternalTransactions ([#13434](https://github.com/blockscout/blockscout/pull/13434)) - Fix DeleteZeroValueInternalTransactions state keys ([#13431](https://github.com/blockscout/blockscout/pull/13431)) - dump block_hash to binary when querying celo epoch distributions ([#13410](https://github.com/blockscout/blockscout/pull/13410)) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 2d8da12ce2e7..3d6e1c4d1216 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -222,6 +222,7 @@ defmodule BlockScoutWeb.PagingHelper do params |> Map.drop([ :address_hash_param, + :batch_number_param, :block_hash_or_number_param, :token_id_param, :token_id, @@ -242,9 +243,8 @@ defmodule BlockScoutWeb.PagingHelper do "state_filter", "l2_block_range_start", "l2_block_range_end", - # remove in favour batch_number_param in the future when all batch - related API endpoints are covered with OpenAPI spec. - "batch_number", - "batch_number_param" + # remove in favour :batch_number_param in the future when all batch - related API endpoints are covered with OpenAPI spec. + "batch_number" ]) end From 1861f778a48972649a22913e7b6fd9587f93cbe7 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 31 Oct 2025 17:27:04 +0300 Subject: [PATCH 0435/1006] fix: Fix token holders CSV export (#13485) --- CHANGELOG.md | 1 + .../controllers/api/v2/csv_export_controller.ex | 11 ++++++++++- .../lib/block_scout_web/schemas/api/v2/general.ex | 8 ++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754ad9688267..e836aa88bb69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Fix token holders CSV export ([#13485](https://github.com/blockscout/blockscout/pull/13485)) - ERC-1155 value in advanced filters csv ([#13474](https://github.com/blockscout/blockscout/pull/13474)) - Fix /api/v2/tokens endpoints: allow back limit param ([#13473](https://github.com/blockscout/blockscout/pull/13473)) - Incorrect average block time for sub-second blocks ([#13469](https://github.com/blockscout/blockscout/issues/13469)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index 3700c27de5e8..309c792be6e0 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -29,7 +29,16 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do operation :export_token_holders, summary: "Export token holders as CSV", description: "Exports the holders of a specific token as a CSV file.", - parameters: base_params() ++ [address_hash_param(), address_id_param()], + parameters: + base_params() ++ + [ + address_hash_param(), + address_id_param(), + from_period_param(), + to_period_param(), + filter_type_param(), + filter_value_param() + ], responses: [ ok: {"CSV file of token holders.", "application/csv", nil}, unprocessable_entity: JsonErrorResponse.response(), diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 13b70652bea8..c1f9e15040af 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -88,7 +88,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do %Parameter{ name: :from_period, in: :query, - schema: %Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, + schema: %Schema{ + anyOf: [%Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, NullString] + }, required: true, description: "Start of the time period (ISO 8601 format) in CSV export" } @@ -102,7 +104,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do %Parameter{ name: :to_period, in: :query, - schema: %Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, + schema: %Schema{ + anyOf: [%Schema{type: :string, nullable: false, pattern: @iso_date_or_datetime_pattern}, NullString] + }, required: true, description: "End of the time period (ISO 8601 format) In CSV export" } From 0ae70463fff5e92e0d1eede74e9a09b8b81826e2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:46:39 +0300 Subject: [PATCH 0436/1006] chore(deps-dev): bump benchee from 1.4.0 to 1.5.0 (#13464) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/block_scout_web/mix.exs | 2 +- apps/explorer/mix.exs | 2 +- mix.lock | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 7dc432e20ddb..a3e131f4ea71 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -86,7 +86,7 @@ defmodule BlockScoutWeb.Mixfile do {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, # Absinthe support for the Relay framework {:absinthe_relay, "~> 1.5"}, - {:benchee, "~> 1.4.0", only: :test}, + {:benchee, "~> 1.5.0", only: :test}, {:bypass, "~> 2.1", only: :test}, # To add (CORS)(https://www.w3.org/TR/cors/) {:cors_plug, "~> 3.0"}, diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 8bdaa424e65f..47d7b5afedfe 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -61,7 +61,7 @@ defmodule Explorer.Mixfile do {:mime, "~> 2.0"}, {:bcrypt_elixir, "~> 3.0"}, # benchmark optimizations - {:benchee, "~> 1.4.0", only: :test}, + {:benchee, "~> 1.5.0", only: :test}, # CSV output for benchee {:benchee_csv, "~> 1.0.0", only: :test}, {:bypass, "~> 2.1", only: :test}, diff --git a/mix.lock b/mix.lock index 740387df21f1..7cd6148608dc 100644 --- a/mix.lock +++ b/mix.lock @@ -6,7 +6,7 @@ "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "bamboo": {:hex, :bamboo, "2.5.0", "973f5cb1471a1d2d7d9da7b8e4f6096afb6a133f85394631184fd40be8adb8ab", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35c8635ff6677a81ab7258944ff15739280f3254a041b6f0229dddeb9b90ad3d"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, - "benchee": {:hex, :benchee, "1.4.0", "9f1f96a30ac80bab94faad644b39a9031d5632e517416a8ab0a6b0ac4df124ce", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "299cd10dd8ce51c9ea3ddb74bb150f93d25e968f93e4c1fa31698a8e4fa5d715"}, + "benchee": {:hex, :benchee, "1.5.0", "4d812c31d54b0ec0167e91278e7de3f596324a78a096fd3d0bea68bb0c513b10", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.1", [hex: :statistex, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "5b075393aea81b8ae74eadd1c28b1d87e8a63696c649d8293db7c4df3eb67535"}, "benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"}, "blake2": {:hex, :blake2, "1.0.4", "8263c69a191142922bc2510f1ffc0de0ae96e8c3bd5e2ad3fac7e87aed94c8b1", [:mix], [], "hexpm", "e9f4120d163ba14d86304195e50745fa18483e6ad2be94c864ae449bbdd6a189"}, "briefly": {:git, "https://github.com/CargoSense/briefly.git", "4836ba322ffb504a102a15cc6e35d928ef97120e", []}, @@ -166,7 +166,7 @@ "spandex_ecto": {:hex, :spandex_ecto, "0.7.0", "259ad2feb7c834e774ec623f99c0fbacca8d60a73be212f92b75e37f853c81be", [:mix], [{:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}], "hexpm", "c64784be79d95538013b7c60828830411c5c7aff1f4e8d66dfe564b3c83b500e"}, "spandex_phoenix": {:hex, :spandex_phoenix, "1.1.0", "9cff829d05258dd49a227c56711b19b69a8fd5d4873d8e9a92a4f4097e7322ab", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.3", [hex: :plug, repo: "hexpm", optional: false]}, {:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "265fe05c1736485fbb75d66ef7576682ebf6428c391dd54d22217f612fd4ddad"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, - "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, + "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, From bb05f491dd622030f03db506546b1b6766d915a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 11:46:53 +0300 Subject: [PATCH 0437/1006] chore(deps): bump ecto from 3.13.3 to 3.13.4 (#13460) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 7cd6148608dc..55a130c7bccc 100644 --- a/mix.lock +++ b/mix.lock @@ -44,7 +44,7 @@ "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.13.3", "6a983f0917f8bdc7a89e96f2bf013f220503a0da5d8623224ba987515b3f0d80", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1927db768f53a88843ff25b6ba7946599a8ca8a055f69ad8058a1432a399af94"}, + "ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, From c143d35203528a63385d0f4583867ef5ed0883a0 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Mon, 3 Nov 2025 11:47:31 +0300 Subject: [PATCH 0438/1006] chore: silence multiple cspell complaints (#13424) --- cspell.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cspell.json b/cspell.json index b0b4be429aea..1fe89931751c 100644 --- a/cspell.json +++ b/cspell.json @@ -21,7 +21,11 @@ ], "ignoreRegExpList": [ // Ignore filecoin f410f-like native addresses - "f410f[a-z2-7]{39}" + "f410f[a-z2-7]{39}", + // Ignore long base64/base58-style account identifiers (TON addresses, hashes, etc.) + "[A-Za-z0-9+\\-:]{40,}", + // Ignore escaped hex byte sequences inside strings + "\\\\x[0-9a-f]{2,}" ], // words - list of words to be always considered correct "words": [ @@ -737,6 +741,9 @@ "WITHDRAWABILITY", "wobserver", "wysdvjkizxonu", + "x-ratelimit-limit", + "x-ratelimit-remaining", + "x-ratelimit-reset", "xact", "xakgj", "xbaddress", From a1f80a0aabfbce2a00c5fb46aaa386994668af72 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 3 Nov 2025 11:52:09 +0300 Subject: [PATCH 0439/1006] fix: API v2 errors logging to the proper log file (#13133) --- .../controllers/api/v2/fallback_controller.ex | 7 +++++++ .../lib/block_scout_web/routers/account_router.ex | 15 +++++++-------- .../lib/block_scout_web/routers/api_router.ex | 6 ++++++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index e2343eb226cb..4d812f7f907f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -344,4 +344,11 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> put_status(code) |> json(response) end + + def call(conn, :unknown_action) do + conn + |> put_status(400) + |> put_view(ApiView) + |> render(:message, %{message: "Unknown API v2 action"}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex index 8f5641926cdd..606fd2cb3b07 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex @@ -36,7 +36,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do plug(BlockScoutWeb.ChecksumAddress) end - pipeline :account_api do + pipeline :account_api_v2 do plug( Plug.Parsers, parsers: [:urlencoded, :multipart, :json], @@ -46,14 +46,14 @@ defmodule BlockScoutWeb.Routers.AccountRouter do json_decoder: Poison ) - plug(BlockScoutWeb.Plug.Logger, application: :api) + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) plug(:fetch_session) plug(:protect_from_forgery) plug(CheckAccountAPI) end - pipeline :api do + pipeline :api_v2 do plug( Plug.Parsers, parsers: [:urlencoded, :multipart, :json], @@ -63,7 +63,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do json_decoder: Poison ) - plug(BlockScoutWeb.Plug.Logger, application: :api) + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) end @@ -112,7 +112,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do end scope "/v2", as: :account_v2 do - pipe_through(:account_api) + pipe_through(:account_api_v2) get("/authenticate", AuthenticateController, :authenticate_get) post("/authenticate", AuthenticateController, :authenticate_post) @@ -163,8 +163,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do end scope "/v2" do - pipe_through(:api) - pipe_through(:account_api) + pipe_through([:api_v2, :account_api_v2]) scope "/tags" do get("/address/:address_hash", TagsController, :tags_address) @@ -174,7 +173,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do end scope "/v2" do - pipe_through(:api) + pipe_through(:api_v2) post("/authenticate_via_wallet", AuthenticateController, :authenticate_via_wallet) post("/send_otp", AuthenticateController, :send_otp) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index cc6d11305b06..b84f0b260f55 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -547,6 +547,12 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/multichain-search-export", BlockScoutWeb.API.HealthController, :multichain_search_db_export) end + scope "/v2" do + pipe_through(:api_v2) + + get("/*path", BlockScoutWeb.API.V2.FallbackController, :unknown_action) + end + scope "/" do pipe_through(:api) alias BlockScoutWeb.API.{EthRPC, RPC} From 6658b4b0b8b3afc42edda7e7f758049bd6559bb6 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:53:04 +0400 Subject: [PATCH 0440/1006] fix: Add missing query binding to txlistinternal query (#13479) --- apps/explorer/lib/explorer/etherscan.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 0c28613f74ee..7ccc0f0502d4 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -241,6 +241,7 @@ defmodule Explorer.Etherscan do defp internal_transactions_query(options, consensus_blocks) do from( it in InternalTransaction, + as: :internal_transaction, inner_join: block in subquery(consensus_blocks), on: it.block_number == block.number, order_by: [ From 684a42b1cd94cb8db4630fc35f60560b8813a8af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:08:27 +0300 Subject: [PATCH 0441/1006] chore(deps-dev): bump ex_doc from 0.38.4 to 0.39.1 (#13463) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 0d18a68842e2..ef4218e87c41 100644 --- a/mix.exs +++ b/mix.exs @@ -99,7 +99,7 @@ defmodule BlockScout.Mixfile do {:tesla, "~> 1.15.3"}, {:mint, "~> 1.7.1"}, # Documentation - {:ex_doc, "~> 0.38.1", only: :dev, runtime: false}, + {:ex_doc, "~> 0.39.1", only: :dev, runtime: false}, {:number, "~> 1.0.3"} ] end diff --git a/mix.lock b/mix.lock index 55a130c7bccc..971a274dfdb1 100644 --- a/mix.lock +++ b/mix.lock @@ -60,7 +60,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.2", "29c1b17732e089f5cd71623add92d7bbd584b2c4a586e51f782303a592f7eb81", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6db5fc81a7de7efe9e9bd66fb2f436b5f82cdbf88deea38f513e8432533856dd"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.2", "a0fd72de6c33e7b836305a27da288404853365538bffb728cbe5055ad92a3ea3", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f3f0c8cd8f192844a447838f33dfb942f54307afb213ad776264fae92d92ed35"}, - "ex_doc": {:hex, :ex_doc, "0.38.4", "ab48dff7a8af84226bf23baddcdda329f467255d924380a0cf0cee97bb9a9ede", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "f7b62346408a83911c2580154e35613eb314e0278aeea72ed7fedef9c1f165b2"}, + "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, From 7447a676861579ee4db726a4cc1f3c9107ac330d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 12:08:48 +0300 Subject: [PATCH 0442/1006] chore(deps-dev): bump sobelow from 0.14.0 to 0.14.1 (#13462) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 971a274dfdb1..982e1d877187 100644 --- a/mix.lock +++ b/mix.lock @@ -160,7 +160,7 @@ "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"}, "siwe": {:git, "https://github.com/royal-markets/siwe-ex.git", "51c9c08240eb7eea3c35693011f8d260cd9bb3be", [ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, - "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, + "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, "spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"}, "spandex_datadog": {:hex, :spandex_datadog, "1.4.0", "0594b9655b0af00ab9137122616bc0208b68ceec01e9916ab13d6fbb33dcce35", [:mix], [{:msgpax, "~> 2.2.1 or ~> 2.3", [hex: :msgpax, repo: "hexpm", optional: false]}, {:spandex, "~> 3.2", [hex: :spandex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "360f8e1b4db238c1749c4872b1697b096429927fa42b8858d0bb782067380123"}, "spandex_ecto": {:hex, :spandex_ecto, "0.7.0", "259ad2feb7c834e774ec623f99c0fbacca8d60a73be212f92b75e37f853c81be", [:mix], [{:spandex, "~> 2.2 or ~> 3.0", [hex: :spandex, repo: "hexpm", optional: false]}], "hexpm", "c64784be79d95538013b7c60828830411c5c7aff1f4e8d66dfe564b3c83b500e"}, From 1ed89d5978bd244bc601ffafc4644d761233bd9d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 13:15:05 +0300 Subject: [PATCH 0443/1006] chore(deps): bump image from 0.62.0 to 0.62.1 (#13461) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 982e1d877187..c380028b7007 100644 --- a/mix.lock +++ b/mix.lock @@ -89,7 +89,7 @@ "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "image": {:hex, :image, "0.62.0", "037a09f4300e5eb5a7612b2739648009250e327b4357d94372f82578469067b9", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "3c28dd98b1d14f5408b5d5c1d46258f54119468979286379033cc1efa87155ac"}, + "image": {:hex, :image, "0.62.1", "1dd3d8d0d29d6562aa2141b5ef08c0f6a60e2a9f843fe475499b2f4f1ef60406", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "5a5a7acaf68cfaed8932d478b95152cd7d84071442cac558c59f2d31427e91ab"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, From b6638e98fc36b550e5110212a9465547219cf654 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 3 Nov 2025 14:39:25 +0300 Subject: [PATCH 0444/1006] fix: Fix REST API token holders pagination (#13500) --- .../block_scout_web/lib/block_scout_web/chain.ex | 2 +- .../controllers/api/v2/token_controller.ex | 16 ++++++++-------- .../block_scout_web/schemas/api/v2/general.ex | 10 +--------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 141df3fb7a64..62ee41703816 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -877,7 +877,7 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%CurrentTokenBalance{address_hash: address_hash, value: value}) do - %{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)} + %{address_hash: to_string(address_hash), value: to_string(Decimal.to_integer(value))} end defp paging_params(%CoinBalance{block_number: block_number}) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index c0f71a7608fb..d239ada73178 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -189,20 +189,20 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :holders, - summary: "List holders for a specific token instance", - description: "Retrieves holders for a specific token instance, with optional filtering and pagination.", + summary: "List holders for a specific token", + description: "Retrieves holders for a specific token, with optional filtering and pagination.", parameters: base_params() ++ [address_hash_param()] ++ - define_paging_params(["address_hash_2", "value", "items_count"]), + define_paging_params(["address_hash_param", "value", "items_count"]), responses: [ ok: {"List of token holders.", "application/json", paginated_response( items: Schemas.Token.Holder, next_page_params_example: %{ - "address_hash_2" => "0x48bb9b14483e43c7726df702b271d410e7460656", - "value" => 200_000_000_000_000, + "address_hash" => "0x48bb9b14483e43c7726df702b271d410e7460656", + "value" => "200000000000000", "items_count" => 50 }, title_prefix: "TokenHolders" @@ -446,17 +446,17 @@ defmodule BlockScoutWeb.API.V2.TokenController do parameters: base_params() ++ [address_hash_param(), token_id_param()] ++ - define_paging_params(["address_hash_2", "items_count", "token_id", "value"]), + define_paging_params(["address_hash_param", "items_count", "token_id", "value"]), responses: [ ok: {"List of token holders for the instance.", "application/json", paginated_response( items: Schemas.Token.Holder, next_page_params_example: %{ - "address_hash_2" => "0x1d2c163fbda9486c3a384b6fa5e34c96fe948e9a", + "address_hash" => "0x1d2c163fbda9486c3a384b6fa5e34c96fe948e9a", "items_count" => 50, "token_id" => "0", - "value" => 4_217_417_051_704_137_590_935 + "value" => "4217417051704137590935" }, title_prefix: "TokenInstanceHolders" )}, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index c1f9e15040af..17bfdda57e3c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -678,20 +678,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Address hash for paging", name: :hash }, - # todo: consider refactoring to eliminate _2 suffix - "address_hash_2" => %Parameter{ - in: :query, - schema: AddressHash, - required: false, - description: "Address hash for paging", - name: :address_hash - }, "address_hash_param" => %Parameter{ in: :query, schema: AddressHash, required: false, description: "Address hash for paging", - name: :hash + name: :address_hash }, "contract_address_hash" => %Parameter{ in: :query, From 435ccc7e714e699268fa37f2a0d2d26a5d319b46 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 3 Nov 2025 17:41:30 +0300 Subject: [PATCH 0445/1006] 9.2.1 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../workflows/publish-docker-image-custom-build.yml | 2 +- .../workflows/publish-docker-image-every-push.yml | 2 +- .../workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .../workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../workflows/publish-docker-image-for-lukso.yml | 2 +- .../workflows/publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../workflows/publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../workflows/publish-docker-image-for-zilliqa.yml | 2 +- .../workflows/publish-docker-image-for-zkevm.yml | 2 +- .../workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../publish-docker-image-staging-on-demand.yml | 2 +- .../publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 13 +++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 61 files changed, 73 insertions(+), 60 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 720848097eb4..53384ff29e7a 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 705f5c1b2fdb..fc62b7806ec9 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 9bb354259c77..e61e81ef9f49 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 19882dd4b423..3608a30aeba8 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 69788dae1377..c19cdb552a33 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index b1076bb9806e..8542199109ef 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index 128c5f23d975..d479dcb934c9 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index bb6927a2829c..b517f275d679 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index bb910cad30e0..7482cf1e0d52 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index ea82ce78fc9d..fc37db004138 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index b23ab24c47e7..8e02cc009ff0 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index f46c70bf7575..ae7ae7f8996b 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index fe74d5802b9c..12e08af56d63 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index d23f438aa4e7..ef69674a280a 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 735429e06b2e..f4a6373b1f10 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index f708e92b921c..c0f0439887ca 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 11a08167e157..3c3b77edd889 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index edc82924d485..7dd9419b1a57 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 70eecbd3ba7a..f42161945049 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 8056cdcb6f65..27e500e77020 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index d11c6de59c1c..45424ecb98c4 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index d2e8e8e1828c..0f36c8257a82 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 14d7b7200aee..9660fcf5a00b 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 9db93e1c8607..43a380064d79 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 700262da0f9a..159e6fe1081e 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 173996b6f0e0..a0b8a1bff2f3 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 2c8f67c198b0..319d82a29256 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 9c78114985fb..8113b2d6655c 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index a65389815ec0..2613934758fc 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index fe00cf4f7346..2c6cda31823a 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 48d36b5be515..ddbfbea1cef5 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index a34c4c988023..a8421391f80b 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index fc338ec85696..1d2ffb2d7b26 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index cad509fceda8..cbfc07a3b1a8 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 833aa6feeb58..512396c05ffd 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index d063b93f8e97..c67fadfff436 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index e0ed176f3009..0479fb4b3611 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 7c1ddbac5db0..83e704a31082 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index a03506b55d1b..105e0f39a798 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index c3eb2ccb7683..3f6d75eb519d 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 61640885d53b..31aee387069a 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 935d2bb8946e..32f3b92ed9e3 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 9411f6795524..15dd0e65880a 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index f13867b432e8..7099bc821ec7 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 86ccab2afc64..d6a593fe9da9 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index f66309b00614..bbc3e7ca15d7 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 96beb0ff7825..8bbb60f71f47 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 18e28370effd..a8997078e339 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 9388e58054bb..03096a205fb7 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index a8a2de164ecd..28b6e57320d7 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.0 + RELEASE_VERSION: 9.2.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index e836aa88bb69..a7b99951aa0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 9.2.1 + +### 🐛 Bug Fixes + +- Fix REST API token holders pagination ([#13500](https://github.com/blockscout/blockscout/issues/13500)) +- Add missing query binding to txlistinternal query ([#13479](https://github.com/blockscout/blockscout/issues/13479)) +- API v2 errors logging to the proper log file ([#13133](https://github.com/blockscout/blockscout/issues/13133)) + +### ⚙️ Miscellaneous Tasks + +- Silence multiple cspell complaints ([#13424](https://github.com/blockscout/blockscout/issues/13424)) + + ## 9.2.0 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index a3e131f4ea71..b2c7b9fe2ffa 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.0", + version: "9.2.1", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 9a7b46f814c7..b075b5e2319d 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.0" + version: "9.2.1" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index f58f4d154f86..4e83f3761269 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.2.0"}] + @default_headers [{"User-Agent", "blockscout-9.2.1"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 47d7b5afedfe..658b5b8ff62a 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.0", + version: "9.2.1", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index f4f409466bf0..066bf33c29fd 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.2.0", + version: "9.2.1", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 7fb4643c8596..d5c79b09e612 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.2.0", + version: "9.2.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 3a30ec43182e..b55214ba34a9 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.2.0", + version: "9.2.1", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 8d8b8db72323..85b28a646948 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.2.0' +RELEASE_VERSION ?= '9.2.1' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index ef4218e87c41..605cdb96ba4c 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.2.0", + version: "9.2.1", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 4bb050e68bbf..c6e30ab18ea8 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.2.0" + set version: "9.2.1" set applications: [ :runtime_tools, block_scout_web: :permanent, From f3c575ffa830c7463b10f4d2dbf1a6b25870577b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:32:40 +0300 Subject: [PATCH 0446/1006] chore(deps): bump absinthe from 1.7.10 to 1.7.11 (#13504) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c380028b7007..102d7dcd7130 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "absinthe": {:hex, :absinthe, "1.7.10", "b33471b593260f148d05e4d771d1857e07b70a680f89cfa75184098bef4ec893", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffda95735364c041a65a4b0e02ffb04eabb1e52ab664fa7eeecefb341449e8c2"}, + "absinthe": {:hex, :absinthe, "1.7.11", "c8118eab5dd7c680087d47379784aab866661a5bbdb250a2c085b5234d9e6e38", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "04c128cb55238742ca30fe735a1584c282acd83fe8b7681ee33ab70dc058baeb"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, From 12539fe6155763d26d99edb5fdd73058fa144031 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:32:59 +0300 Subject: [PATCH 0447/1006] chore(deps-dev): bump wallaby from 0.30.10 to 0.30.11 (#13503) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 102d7dcd7130..3e1de4650d01 100644 --- a/mix.lock +++ b/mix.lock @@ -35,7 +35,7 @@ "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, - "db_connection": {:hex, :db_connection, "2.8.0", "64fd82cfa6d8e25ec6660cea73e92a4cbc6a18b31343910427b702838c4b33b2", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "008399dae5eee1bf5caa6e86d204dcb44242c82b1ed5e22c881f2c34da201b15"}, + "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, "ddskerl": {:hex, :ddskerl, "0.4.2", "a51a90be9ac9b36a94017670bed479c623b10ca9d4bda1edf3a0e48caeeada2a", [:rebar3], [], "hexpm", "63f907373d7e548151d584d4da8a38928fd26ec9477b94c0ffaad87d7cb69fe7"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, @@ -180,7 +180,7 @@ "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, "vix": {:hex, :vix, "0.35.0", "f6319b715e3b072e53eba456a21af5f2ff010a7a7b19b884600ea98a0609b18c", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "a3e80067a89d0631b6cf2b93594e03c1b303a2c7cddbbdd28040750d521984e5"}, - "wallaby": {:hex, :wallaby, "0.30.10", "574afb8796521252daf49a4cd76a1c389d53cae5897f2d4b5f55dfae159c8e50", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "a8f89b92d8acce37a94b5dfae6075c2ef00cb3689d6333f5f36c04b381c077b2"}, + "wallaby": {:hex, :wallaby, "0.30.11", "93b635bb3d01b11b5a80f83a5d9be59e87ccc8c8390136b7c526dd4fc7e485e3", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "407b50972e3827ce77e3b8292c36dcbd6b21b6837cc4f12ee8767e92a72610ac"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, From 5c61c35737ef02bf0e8c2e3e2e247bef4551f052 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:33:16 +0300 Subject: [PATCH 0448/1006] chore(deps): bump cldr_utils from 2.29.0 to 2.29.1 (#13502) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 3e1de4650d01..651bd77b645f 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "cldr_utils": {:hex, :cldr_utils, "2.29.0", "b64e01c8712ed06db4053302e7e3f6d57fd792cb40dd03144978e663ee40b018", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "d21f6a1937461e3b0634e53159d64b07852c54069304fb7aef666aeb46ba5175"}, + "cldr_utils": {:hex, :cldr_utils, "2.29.1", "11ff0a50a36a7e5f3bd9fc2fb8486a4c1bcca3081d9c080bf9e48fe0e6742e2d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3844a0a0ed7f42e6590ddd8bd37eb4b1556b112898f67dea3ba068c29aabd6c2"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, From 17148d5086b197395438b4fac74aaac84f5b229b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Nov 2025 12:52:48 +0300 Subject: [PATCH 0449/1006] chore(deps): bump phoenix_live_view from 1.1.14 to 1.1.16 (#13465) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 651bd77b645f..7e788e0495c3 100644 --- a/mix.lock +++ b/mix.lock @@ -134,8 +134,8 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.14", "cae84abc4cd00dde4bb200b8516db556704c585c267aff9cd4955ff83cceb86c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b827980e2bc00fddd8674e3b567519a4e855b5de04bf8607140414f1101e2627"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.16", "e42f95337b912a73a1c4ddb077af2eb13491712d7ab79b67e13de4237dfcac50", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f2a0093895b8ef4880af76d41de4a9cf7cff6c66ad130e15a70bdabc4d279feb"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, From 111b1a18d37207fa3074211435e37f34716afbb4 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 5 Nov 2025 23:42:20 +0300 Subject: [PATCH 0450/1006] fix: Fix next page params for tokens list API endpoint (#13520) --- apps/block_scout_web/lib/block_scout_web/chain.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 62ee41703816..bf083aa814c8 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -808,12 +808,12 @@ defmodule BlockScoutWeb.Chain do fiat_value: fiat_value }) do %{ - "market_cap" => circulating_market_cap, - "holders_count" => holders_count, - "contract_address_hash" => contract_address_hash, - "name" => token_name, - "is_name_null" => is_nil(token_name), - "fiat_value" => fiat_value + market_cap: circulating_market_cap, + holders_count: holders_count, + contract_address_hash: contract_address_hash, + name: token_name, + is_name_null: is_nil(token_name), + fiat_value: fiat_value } end From 4c20fc3d89b4ea63c8de975c3a76d8636617ac80 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 5 Nov 2025 23:45:44 +0300 Subject: [PATCH 0451/1006] 9.2.2 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .github/workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .github/workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 6 ++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 61 files changed, 66 insertions(+), 60 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 53384ff29e7a..731b9b7a3270 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index fc62b7806ec9..848133b694b9 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index e61e81ef9f49..2d2884719295 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 3608a30aeba8..cb3a62481591 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index c19cdb552a33..25b93a779972 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 8542199109ef..bb7302ac5fa6 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index d479dcb934c9..67f1372a7ada 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index b517f275d679..ebf56ff30a1b 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 7482cf1e0d52..e5bfc51f7b24 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index fc37db004138..b0d2859957d9 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 8e02cc009ff0..34fdc5dc6f71 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index ae7ae7f8996b..ca8999bdac09 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 12e08af56d63..ad4384d6f5e7 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index ef69674a280a..cd932d6746b6 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index f4a6373b1f10..d611f2e4bc2e 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index c0f0439887ca..22bb365556bb 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 3c3b77edd889..027c0cc95f15 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 7dd9419b1a57..6f0833864ed1 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index f42161945049..bb93181a8fc2 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 27e500e77020..a33b682c9d11 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 45424ecb98c4..48799bc527f5 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 0f36c8257a82..d7ed77faa437 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 9660fcf5a00b..aa1187e3cff3 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 43a380064d79..76dfe75049d5 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 159e6fe1081e..f4f9a9d0870f 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index a0b8a1bff2f3..56ab0211a99a 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 319d82a29256..fe8948235b46 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 8113b2d6655c..f2490e9c1dd0 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index 2613934758fc..a7237878dede 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index 2c6cda31823a..ad4bd4726fdb 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index ddbfbea1cef5..ddd94cb766e1 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index a8421391f80b..35eae8cf8468 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 1d2ffb2d7b26..cd361ec5f5c0 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index cbfc07a3b1a8..93f5c67f519a 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 512396c05ffd..3dc4250aee30 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index c67fadfff436..f381ed3f53c7 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 0479fb4b3611..5c76b75e9369 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 83e704a31082..d650d462e8de 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 105e0f39a798..24c5aabb6330 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 3f6d75eb519d..09367af296ca 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 31aee387069a..c89a7c137bbb 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 32f3b92ed9e3..fe353ebc35c7 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 15dd0e65880a..b513b3c3d7e7 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 7099bc821ec7..6e8b700792c9 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index d6a593fe9da9..c49b8e8c64a4 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index bbc3e7ca15d7..4c73f56d285f 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 8bbb60f71f47..43afa0bac809 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index a8997078e339..2fd4ad024dad 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 03096a205fb7..eca5696a465c 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 28b6e57320d7..a66309ae7985 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: ubuntu-latest env: - RELEASE_VERSION: 9.2.1 + RELEASE_VERSION: 9.2.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index a7b99951aa0d..e8503f4904d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 9.2.2 + +### 🐛 Bug Fixes + +- Fix next page params for tokens list API endpoint ([#13520](https://github.com/blockscout/blockscout/issues/13520)) + ## 9.2.1 ### 🐛 Bug Fixes diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index b2c7b9fe2ffa..596715881071 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.1", + version: "9.2.2", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index b075b5e2319d..670013bec089 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -23,7 +23,7 @@ defmodule EthereumJSONRPC.MixProject do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.1" + version: "9.2.2" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 4e83f3761269..905bbd387294 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.2.1"}] + @default_headers [{"User-Agent", "blockscout-9.2.2"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 658b5b8ff62a..de2a060509b8 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -24,7 +24,7 @@ defmodule Explorer.Mixfile do dialyzer: :test ], start_permanent: Mix.env() == :prod, - version: "9.2.1", + version: "9.2.2", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 066bf33c29fd..ef8827242c9d 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.2.1", + version: "9.2.2", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index d5c79b09e612..9e02ecfe9138 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.2.1", + version: "9.2.2", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index b55214ba34a9..ee732b654fd8 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.2.1", + version: "9.2.2", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 85b28a646948..3a6653f78529 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.2.1' +RELEASE_VERSION ?= '9.2.2' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 605cdb96ba4c..dd6a3994bfe1 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.2.1", + version: "9.2.2", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index c6e30ab18ea8..0d6743c05104 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.2.1" + set version: "9.2.2" set applications: [ :runtime_tools, block_scout_web: :permanent, From 8cb1b0d90e415c4115dce91e942aff94868f4ecb Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 6 Nov 2025 17:55:56 +0300 Subject: [PATCH 0452/1006] fix: Fix coin balance history - related normalize_balances_by_day/2 function (#13515) --- .../controllers/api/v2/address_controller_test.exs | 1 - apps/explorer/lib/explorer/chain.ex | 13 ++++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 98f1ef0f2d1d..46bec0cfb141 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2679,7 +2679,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "days" => 10, "items" => [ %{"date" => _, "value" => "2000"}, - %{"date" => _, "value" => "1000"}, %{"date" => _, "value" => "1000"} ] } = response diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 7a74817eba93..aabe1dc5b280 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3137,12 +3137,19 @@ defmodule Explorer.Chain do result = balances_by_day |> Enum.filter(fn day -> day.value end) - |> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :date, fn x -> to_string(x) end) end))).() - |> (&if(api?, do: &1, else: Enum.map(&1, fn day -> Map.update!(day, :value, fn x -> Wei.to(x, :ether) end) end))).() + |> (&if(api?, + do: &1, + else: + Enum.map(&1, fn day -> + day + |> Map.update!(:date, fn x -> to_string(x) end) + |> Map.update!(:value, fn x -> Wei.to(x, :ether) end) + end) + )).() today = Date.to_string(NaiveDateTime.utc_now()) - if not Enum.empty?(result) && !Enum.any?(result, fn map -> map[:date] == today end) do + if not Enum.empty?(result) && !Enum.any?(result, fn map -> to_string(map[:date]) == today end) do List.flatten([result | [%{date: today, value: List.last(result)[:value]}]]) else result From c7ba9d5f79c540b21ac9d7838a965dc34b0bed79 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 9 Nov 2025 20:28:36 +0300 Subject: [PATCH 0453/1006] chore: Increase genesis file content fetch timeout (#13527) --- apps/explorer/lib/explorer/chain_spec/genesis_data.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index f3a6f431d6b0..3b91f1bc11c3 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -173,7 +173,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Fetches JSON data from a provided URL. @spec fetch_from_url(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t() | any()} defp fetch_from_url(url) do - case HttpClient.get(url) do + case HttpClient.get(url, timeout: 60_000, recv_timeout: 60_000) do {:ok, %{body: body, status_code: 200}} -> {:ok, Jason.decode!(body)} From fab3d4109c0f7def377352962ae9b4ddb0ca415f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:36:37 +0300 Subject: [PATCH 0454/1006] chore(deps): bump phoenix_live_view from 1.1.16 to 1.1.17 (#13543) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 7e788e0495c3..e6a1b3687414 100644 --- a/mix.lock +++ b/mix.lock @@ -16,7 +16,7 @@ "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "cafezinho": {:hex, :cafezinho, "0.4.4", "36c31fc5456b1284180d8a9d968c7eaaf474782df5af023a8f8b66937c5b2785", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "3ca334f2a2992ec081868c39ad0487eb97d213292702aae6b197fd5e6f04fd58"}, - "castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"}, + "castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, @@ -134,7 +134,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.16", "e42f95337b912a73a1c4ddb077af2eb13491712d7ab79b67e13de4237dfcac50", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f2a0093895b8ef4880af76d41de4a9cf7cff6c66ad130e15a70bdabc4d279feb"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, From f57257463f9b958ad09f759145cabee4310bd12c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:36:59 +0300 Subject: [PATCH 0455/1006] chore(deps): bump ecto from 3.13.4 to 3.13.5 (#13542) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index e6a1b3687414..b267caf9096f 100644 --- a/mix.lock +++ b/mix.lock @@ -44,7 +44,7 @@ "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.13.4", "27834b45d58075d4a414833d9581e8b7bb18a8d9f264a21e42f653d500dbeeb5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5ad7d1505685dfa7aaf86b133d54f5ad6c42df0b4553741a1ff48796736e88b2"}, + "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, From ec0f9d77f59c4844c80cc46dc218924fd0ef834a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 21:41:07 +0300 Subject: [PATCH 0456/1006] chore(deps): bump ex_cldr_units from 3.19.2 to 3.20.0 (#13544) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index b267caf9096f..8b54792179b5 100644 --- a/mix.lock +++ b/mix.lock @@ -55,11 +55,11 @@ "ex_aws": {:hex, :ex_aws, "2.6.0", "346e87e35e5df0b3c016a96fb30adf6001de102981a71648dfc3ce3ad04765af", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "30729ee9cbaacda674a4e4260d74206fa89bcd712267c4eaf42a0fc34592c0b3"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, - "ex_cldr": {:hex, :ex_cldr, "2.43.2", "92370062f72d8efbeba3a7dc60a331fbb70f061cbc87041cb9f3c6701c6e2ddc", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "095137a7bd081166f77d23291b0649db2136ca013245cb73955fb0515031272a"}, + "ex_cldr": {:hex, :ex_cldr, "2.44.0", "19af18b3c9477ba8888cfd2b0874894dd007b649d323380d1094297d80394b10", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "58260baac6308156595cd0a2b81c09ac8e05dd64fd2a19c8b0ef309fdcdcd7ee"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.35.2", "29c1b17732e089f5cd71623add92d7bbd584b2c4a586e51f782303a592f7eb81", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.42", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "6db5fc81a7de7efe9e9bd66fb2f436b5f82cdbf88deea38f513e8432533856dd"}, - "ex_cldr_units": {:hex, :ex_cldr_units, "3.19.2", "a0fd72de6c33e7b836305a27da288404853365538bffb728cbe5055ad92a3ea3", [:mix], [{:cldr_utils, "~> 2.25", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.35", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "f3f0c8cd8f192844a447838f33dfb942f54307afb213ad776264fae92d92ed35"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.36.0", "d0de502db2fa24eeae16749af1af0093f24fb823e8715c66076cd8c6a3a960e3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "17640b8daf2580a0a11317a720a26079e774d4c36f939d82f4e9f7075269897d"}, + "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.0", "7ade6ae1017a8f650f83f79a378ad9ad5bcc4eacfa2704349590c1f91f2eac19", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "eb93f99005f8d3f1b1b12c85a45323937350e11d3e79f8375ab51e2ff44392f2"}, "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, @@ -72,7 +72,7 @@ "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, - "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, + "expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"}, "exvcr": {:hex, :exvcr, "0.16.0", "11579f43c88ae81f57c82ce4f09e3ebda4c40117c859ed39e61a653c3a0b4ff4", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "8f576af22369942f7a1482baff1f31e2f45983cf6fac45d49d2bd2e84b4d5be8"}, "ezstd": {:hex, :ezstd, "1.2.3", "98748f4099e6e2a067f77ace43041ebaa53c13194b08ce22370e4c93079e9e16", [:rebar3], [], "hexpm", "de32e0b41ba36a9ed46db8215da74777d2f141bb75f67bfc05dbb4b7c3386dee"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, From 03f35231b96df80e97e80983d0b73aa58ccadb54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:51:16 +0300 Subject: [PATCH 0457/1006] chore(deps-dev): bump dialyxir from 1.4.6 to 1.4.7 (#13545) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 8b54792179b5..454fe6d1a3f2 100644 --- a/mix.lock +++ b/mix.lock @@ -40,14 +40,14 @@ "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.6", "7cca478334bf8307e968664343cbdb432ee95b4b68a9cba95bdabb0ad5bdfd9a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "8cf5615c5cd4c2da6c501faae642839c8405b49f8aa057ad4ae401cb808ef64d"}, + "dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"}, "digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"}, "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, - "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"}, "evision": {:hex, :evision, "0.2.14", "6005e44b38ea61a33109694c5b097d42e81c767723c55d0c230d9662d6c87987", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "630714395f2c242e488944b4c9c75a5de96cbb95d3cbb69f0525bdb3e0a88775"}, From 1f105dd35c80ed11798b8e6e6ea4d30f2e89df9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:51:33 +0300 Subject: [PATCH 0458/1006] chore(deps): bump absinthe_relay from 1.5.2 to 1.6.0 (#13548) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 454fe6d1a3f2..9ea9b06b6846 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,8 @@ %{ - "absinthe": {:hex, :absinthe, "1.7.11", "c8118eab5dd7c680087d47379784aab866661a5bbdb250a2c085b5234d9e6e38", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "04c128cb55238742ca30fe735a1584c282acd83fe8b7681ee33ab70dc058baeb"}, + "absinthe": {:hex, :absinthe, "1.8.0", "c2d941e951210ebd2494b4263d4354ca0a6d886d6dbf0cefaa21fe716a888a95", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ac63d2f07db21913e58d662e4415768986e1bd0d2cd48e2417cfe200441161b"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, - "absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"}, + "absinthe_relay": {:hex, :absinthe_relay, "1.6.0", "73590bdb0dcc192622f39fa11da6fca9df4e339b603c45ec7a93493eb94f1829", [:mix], [{:absinthe, ">= 1.7.10", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "32d6397a7af3fd02678ef9bc8e2f574487f14593cb3e4f9110fb1c695d4d2ac0"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "bamboo": {:hex, :bamboo, "2.5.0", "973f5cb1471a1d2d7d9da7b8e4f6096afb6a133f85394631184fd40be8adb8ab", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "35c8635ff6677a81ab7258944ff15739280f3254a041b6f0229dddeb9b90ad3d"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, From 0d83aa9a1f581d432f0ec0ea8a75600d83f28d85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 22:51:55 +0300 Subject: [PATCH 0459/1006] chore(deps): bump phoenix_ecto from 4.6.5 to 4.7.0 (#13547) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 9ea9b06b6846..53b15a4ae21e 100644 --- a/mix.lock +++ b/mix.lock @@ -130,7 +130,7 @@ "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, "phoenix": {:hex, :phoenix, "1.6.16", "e5bdd18c7a06da5852a25c7befb72246de4ddc289182285f8685a40b7b5f5451", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e15989ff34f670a96b95ef6d1d25bad0d9c50df5df40b671d8f4a669e050ac39"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.5", "c4ef322acd15a574a8b1a08eff0ee0a85e73096b53ce1403b6563709f15e1cea", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "26ec3208eef407f31b748cadd044045c6fd485fbff168e35963d2f9dfff28d4b"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, From 69326c2e689e8760d1cbb035209cff67e8b84dd1 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 11 Nov 2025 12:05:34 +0300 Subject: [PATCH 0460/1006] fix: duplicated block numbers in int txs queue (#13554) --- apps/explorer/lib/explorer/chain/import/runner/blocks.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index c7e97e5ddfec..5e005507a6f2 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -865,9 +865,11 @@ defmodule Explorer.Chain.Import.Runner.Blocks do defp save_internal_transactions_for_delete(repo, non_consensus_blocks, %{timeout: timeout, timestamps: timestamps}) do insert_params = - Enum.map(non_consensus_blocks, fn {number, _hash} -> + non_consensus_blocks + |> Enum.map(fn {number, _hash} -> Map.put(timestamps, :block_number, number) end) + |> Enum.uniq() {_total, result} = repo.insert_all( From edb5c2fb131df6d64492e6e5a880d140a15d575b Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 11 Nov 2025 13:07:46 +0400 Subject: [PATCH 0461/1006] fix: Reset ResetSanitizeDuplicatedLogsMigration status (#13556) --- ...1111085348_reset_sanitize_duplicated_logs_migration.exs | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/explorer/priv/repo/migrations/20251111085348_reset_sanitize_duplicated_logs_migration.exs diff --git a/apps/explorer/priv/repo/migrations/20251111085348_reset_sanitize_duplicated_logs_migration.exs b/apps/explorer/priv/repo/migrations/20251111085348_reset_sanitize_duplicated_logs_migration.exs new file mode 100644 index 000000000000..c098f716e66b --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251111085348_reset_sanitize_duplicated_logs_migration.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.ResetSanitizeDuplicatedLogsMigration do + use Ecto.Migration + + def change do + execute("DELETE FROM migrations_status WHERE migration_name = 'sanitize_duplicated_log_index_logs'") + end +end From 75b4585c49f244061f13f7b947bb735d581ef7c2 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 11 Nov 2025 19:27:58 +0300 Subject: [PATCH 0462/1006] chore: Transaction controller OpenAPI spec (#13419) --- .../lib/block_scout_web/chain.ex | 85 +++- .../controllers/address_token_controller.ex | 4 +- .../controllers/api/v2/address_controller.ex | 8 +- .../api/v2/advanced_filter_controller.ex | 4 +- .../controllers/api/v2/arbitrum_controller.ex | 4 +- .../controllers/api/v2/block_controller.ex | 4 +- .../controllers/api/v2/celo_controller.ex | 4 +- .../api/v2/ethereum/deposit_controller.ex | 4 +- .../controllers/api/v2/mud_controller.ex | 8 +- .../api/v2/smart_contract_controller.ex | 3 +- .../api/v2/transaction_controller.ex | 448 ++++++++++++++++-- .../api/v2/validator_controller.ex | 5 +- .../controllers/api/v2/zksync_controller.ex | 3 +- .../verified_contracts_controller.ex | 3 +- .../lib/block_scout_web/paging_helper.ex | 33 ++ .../lib/block_scout_web/routers/api_router.ex | 10 +- .../schemas/api/v2/address_nullable.ex | 18 + .../schemas/api/v2/beacon/deposit.ex | 4 +- .../schemas/api/v2/beacon/deposit/response.ex | 18 + .../block_scout_web/schemas/api/v2/blob.ex | 18 + .../schemas/api/v2/blob/response.ex | 18 + .../block_scout_web/schemas/api/v2/general.ex | 96 ++++ .../schemas/api/v2/transaction.ex | 2 +- .../schemas/api/v2/transaction/raw_trace.ex | 53 +++ .../schemas/api/v2/transaction/response.ex | 87 ++++ .../api/v2/transaction/state_change.ex | 34 ++ .../schemas/api/v2/transaction/summary.ex | 57 +++ .../transaction/summary_just_request_body.ex | 61 +++ .../lib/block_scout_web/schemas/helper.ex | 7 +- .../views/api/v2/optimism_view.ex | 6 - .../test/block_scout_web/chain_test.exs | 4 +- ...address_token_transfer_controller_test.exs | 4 +- .../api/v2/csv_export_controller_test.exs | 4 +- .../api/v2/transaction_controller_test.exs | 113 ++++- .../controllers/block_controller_test.exs | 2 +- .../withdrawal_controller_test.exs | 2 +- .../chain/optimism/interop_message.ex | 4 - 37 files changed, 1137 insertions(+), 105 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address_nullable.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/raw_trace.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/response.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/state_change.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index bf083aa814c8..be132193f151 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -132,12 +132,12 @@ defmodule BlockScoutWeb.Chain do end end - @spec next_page_params(any, list(), map(), (any -> map())) :: nil | map - def next_page_params(next_page, list, params, paging_function \\ &paging_params/1) + @spec next_page_params(any, list(), map(), bool(), (any -> map())) :: nil | map + def next_page_params(next_page, list, params, increment_items_count? \\ false, paging_function \\ &paging_params/1) - def next_page_params([], _list, _params, _), do: nil + def next_page_params([], _list, _params, _increment_items_count?, _), do: nil - def next_page_params(_, list, params, paging_function) do + def next_page_params(_, list, params, increment_items_count?, paging_function) do paging_params = paging_function.(List.last(list)) string_keys = map_to_string_keys(paging_params) @@ -148,17 +148,54 @@ defmodule BlockScoutWeb.Chain do |> Map.drop(string_keys) |> Map.merge(paging_params) - current_items_count_string = Map.get(next_page_params, "items_count") + items_count = next_items_count(next_page_params, list, increment_items_count?) - items_count = - if is_binary(current_items_count_string) do - {current_items_count, _} = Integer.parse(current_items_count_string) - current_items_count + Enum.count(list) - else - Enum.count(list) + cond do + Map.has_key?(next_page_params, "items_count") -> + Map.put(next_page_params, "items_count", items_count) + + Map.has_key?(next_page_params, :items_count) -> + Map.put(next_page_params, :items_count, items_count) + + true -> + Map.put(next_page_params, :items_count, items_count) + end + end + + defp get_items_count_from_next_page_params(next_page_params) do + cond do + Map.has_key?(next_page_params, "items_count") -> + Map.get(next_page_params, "items_count") + + Map.has_key?(next_page_params, :items_count) -> + Map.get(next_page_params, :items_count) + + true -> + nil + end + end + + defp next_items_count(_next_page_params, list, false) do + Enum.count(list) + end + + defp next_items_count(next_page_params, list, true) do + current_items_count_object = get_items_count_from_next_page_params(next_page_params) + + current_items_count = + cond do + is_binary(current_items_count_object) -> + {current_items_count, _} = Integer.parse(current_items_count_object) + current_items_count + + is_integer(current_items_count_object) -> + current_items_count_object + + true -> + 0 end - Map.put(next_page_params, "items_count", items_count) + current_items_count + Enum.count(list) end @doc """ @@ -574,6 +611,10 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{items_count: items_count, state_changes: _}) when is_integer(items_count) do + [paging_options: %{@default_paging_options | key: {items_count}}] + end + def paging_options(%{"l1_block_number" => block_number, "transaction_hash" => transaction_hash}) do with {block_number, ""} <- Integer.parse(block_number), {:ok, transaction_hash} <- string_to_full_hash(transaction_hash) do @@ -939,7 +980,8 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%StateChange{}) do - %{"state_changes" => nil} + # todo: remove in the future as this param is unused in the pagination of state changes + %{state_changes: nil} end # clause for Polygon Edge Deposits and Withdrawals @@ -1011,6 +1053,17 @@ defmodule BlockScoutWeb.Chain do def token_transfers_next_page_params([], _list, _params), do: nil + @batch_transfer_fields_to_delete_from_next_page_params [ + "batch_log_index", + "batch_block_hash", + "batch_transaction_hash", + "index_in_batch", + :batch_log_index, + :batch_block_hash, + :batch_transaction_hash, + :index_in_batch + ] + def token_transfers_next_page_params(next_page, list, params) do next_token_transfer = List.first(next_page) current_token_transfer = List.last(list) @@ -1023,11 +1076,12 @@ defmodule BlockScoutWeb.Chain do |> last_token_transfer_before_current(current_token_transfer) |> (&if(is_nil(&1), do: %{}, else: paging_params(&1))).() + # todo: consider removing it, when all controllers will get OpenAPI specs string_keys = map_to_string_keys(new_params) params |> delete_parameters_from_next_page_params() - |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]) + |> Map.drop(@batch_transfer_fields_to_delete_from_next_page_params ++ string_keys) |> Map.merge(new_params) |> Map.merge(%{ batch_log_index: current_token_transfer.log_index, @@ -1038,11 +1092,12 @@ defmodule BlockScoutWeb.Chain do else new_params = paging_params(List.last(list)) + # todo: consider removing it, when all controllers will get OpenAPI specs string_keys = map_to_string_keys(new_params) params |> delete_parameters_from_next_page_params() - |> Map.drop(["batch_log_index", "batch_block_hash", "batch_transaction_hash", "index_in_batch" | string_keys]) + |> Map.drop(@batch_transfer_fields_to_delete_from_next_page_params ++ string_keys) |> Map.merge(new_params) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex index 6c6ad85901de..b250c9e58a7a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.AddressTokenController do use BlockScoutWeb, :controller import BlockScoutWeb.Chain, - only: [next_page_params: 4, paging_options: 1, split_list_by_page: 1, paging_params_with_fiat_value: 1] + only: [next_page_params: 5, paging_options: 1, split_list_by_page: 1, paging_params_with_fiat_value: 1] import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] @@ -24,7 +24,7 @@ defmodule BlockScoutWeb.AddressTokenController do {tokens, next_page} = split_list_by_page(token_balances_plus_one) next_page_path = - case next_page_params(next_page, tokens, params, &paging_params_with_fiat_value/1) do + case next_page_params(next_page, tokens, params, false, &paging_params_with_fiat_value/1) do nil -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 1bef35ef89a1..ba2533151289 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do import BlockScoutWeb.Chain, only: [ next_page_params: 3, - next_page_params: 4, + next_page_params: 5, token_transfers_next_page_params: 3, paging_options: 1, split_list_by_page: 1, @@ -413,6 +413,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> next_page_params( transactions, params, + false, &Transaction.address_transactions_next_page_params/1 ) @@ -945,6 +946,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> next_page_params( tokens, params, + false, &paging_params_with_fiat_value/1 ) @@ -1243,6 +1245,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> next_page_params( nfts, params, + false, &Instance.nft_list_next_page_params/1 ) @@ -1319,6 +1322,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> next_page_params( collections, params, + false, &Instance.nft_collections_next_page_params/1 ) @@ -1391,6 +1395,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page, rewards, filtered_params, + false, &%{ epoch_number: &1.epoch_number, amount: &1.amount, @@ -1514,6 +1519,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> next_page_params( deposits, params, + false, DepositController.paging_function() ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 5c7a0af7a4e8..ba37553eff23 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do use BlockScoutWeb, :controller - import BlockScoutWeb.Chain, only: [split_list_by_page: 1, next_page_params: 4, fetch_scam_token_toggle: 2] + import BlockScoutWeb.Chain, only: [split_list_by_page: 1, next_page_params: 5, fetch_scam_token_toggle: 2] import Explorer.PagingOptions, only: [default_paging_options: 0] alias BlockScoutWeb.API.V2.{AdvancedFilterView, CsvExportController} @@ -70,7 +70,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do |> Transaction.decode_transactions(true, @api_true) next_page_params = - next_page |> next_page_params(advanced_filters, Map.take(params, ["items_count"]), &paging_params/1) + next_page |> next_page_params(advanced_filters, Map.take(params, ["items_count"]), false, &paging_params/1) render(conn, :advanced_filters, advanced_filters: advanced_filters, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex index 510bf91e8011..3c88c4b250ca 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/arbitrum_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do import BlockScoutWeb.Chain, only: [ - next_page_params: 4, + next_page_params: 5, paging_options: 1, split_list_by_page: 1, parse_block_hash_or_number_param: 1 @@ -42,6 +42,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do next_page, messages, params, + false, fn %Message{message_id: message_id} -> %{"id" => message_id} end ) @@ -260,6 +261,7 @@ defmodule BlockScoutWeb.API.V2.ArbitrumController do next_page, batches, params, + false, fn %L1Batch{number: number} -> %{"number" => number} end ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 93ab33ec9c5d..40bc1c2df08a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do import BlockScoutWeb.Chain, only: [ next_page_params: 3, - next_page_params: 4, + next_page_params: 5, paging_options: 1, param_to_block_number: 1, put_key_value_to_paging_options: 3, @@ -478,6 +478,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> next_page_params( internal_transactions, params, + false, &InternalTransaction.internal_transaction_to_block_paging_options/1 ) @@ -671,6 +672,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> next_page_params( deposits, params, + false, DepositController.paging_function() ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index 53c54d2e9179..4c54cf725d3b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do import BlockScoutWeb.Chain, only: [ - next_page_params: 4, + next_page_params: 5, split_list_by_page: 1 ] @@ -53,6 +53,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do next_page, epochs, filtered_params, + false, &%{number: &1.number} ) @@ -132,6 +133,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do next_page, rewards, filtered_params, + false, &%{ amount: &1.amount, account_address_hash: &1.account_address_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex index f8d63ed2746b..b300b3149694 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do use BlockScoutWeb, :controller use OpenApiSpex.ControllerSpecs - import BlockScoutWeb.Chain, only: [next_page_params: 4, split_list_by_page: 1] + import BlockScoutWeb.Chain, only: [next_page_params: 5, split_list_by_page: 1] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] @@ -69,7 +69,7 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do next_page_params = next_page - |> next_page_params(deposits, params, paging_function()) + |> next_page_params(deposits, params, false, paging_function()) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex index 4e998ba106ad..135e2c56d624 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.MudController do import BlockScoutWeb.Chain, only: [ - next_page_params: 4, + next_page_params: 5, split_list_by_page: 1 ] @@ -44,7 +44,7 @@ defmodule BlockScoutWeb.API.V2.MudController do |> Enum.into(%{}, &{&1.hash, &1}) next_page_params = - next_page_params(next_page, worlds, conn.query_params, fn item -> + next_page_params(next_page, worlds, conn.query_params, false, fn item -> %{"world" => item} end) @@ -86,7 +86,7 @@ defmodule BlockScoutWeb.API.V2.MudController do |> split_list_by_page() next_page_params = - next_page_params(next_page, tables, conn.query_params, fn item -> + next_page_params(next_page, tables, conn.query_params, false, fn item -> %{"table_id" => item |> elem(0)} end) @@ -159,7 +159,7 @@ defmodule BlockScoutWeb.API.V2.MudController do blocks = Mud.preload_records_timestamps(records, @api_true) next_page_params = - next_page_params(next_page, records, conn.query_params, fn item -> + next_page_params(next_page, records, conn.query_params, false, fn item -> keys = [item.key_bytes, item.key0, item.key1] |> Enum.filter(&(!is_nil(&1))) ["key_bytes", "key0", "key1"] |> Enum.zip(keys) |> Enum.into(%{}) end) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 0d984065f948..d450e8a3d66d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do import BlockScoutWeb.Chain, only: [ fetch_scam_token_toggle: 2, - next_page_params: 4, + next_page_params: 5, split_list_by_page: 1 ] @@ -144,6 +144,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do |> next_page_params( addresses, params, + false, pager ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 9a9609bf96dd..f8b7a3577fae 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -1,6 +1,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do use BlockScoutWeb, :controller use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Account.AuthController, only: [current_user: 1] @@ -8,6 +9,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do only: [ next_page_params: 3, next_page_params: 4, + next_page_params: 5, put_key_value_to_paging_options: 3, token_transfers_next_page_params: 3, paging_options: 1, @@ -42,6 +44,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do alias BlockScoutWeb.API.V2.{BlobView, Ethereum.DepositController, Ethereum.DepositView} alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.{ForbiddenResponse, NotFoundResponse} alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit @@ -58,6 +61,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["transactions"]) + case @chain_type do :ethereum -> @chain_type_transaction_necessity_by_association %{ @@ -127,11 +134,22 @@ defmodule BlockScoutWeb.API.V2.TransactionController do @api_true [api?: true] + operation :transaction, + summary: "Retrieve detailed information about a specific transaction", + description: + "Retrieves detailed information for a specific transaction, including token transfers, internal transactions, and metadata.", + parameters: [transaction_hash_param() | base_params()], + responses: [ + ok: {"Transaction details", "application/json", BlockScoutWeb.Schemas.API.V2.Transaction.Response}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param` endpoint. """ @spec transaction(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def transaction(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def transaction(conn, %{transaction_hash_param: transaction_hash_string} = params) do necessity_by_association_with_actions = @transaction_necessity_by_association |> Map.put(:transaction_actions, :optional) @@ -193,6 +211,28 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :transactions, + summary: "List transactions with optional filtering and sorting", + description: "Retrieves a paginated list of transactions with optional filtering and sorting options.", + parameters: + base_params() ++ + [transaction_filter_param()] ++ + define_paging_params(["block_number", "index", "items_count", "hash", "inserted_at"]), + responses: [ + ok: + {"List of transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 23_532_302, + "index" => 375, + "items_count" => 50 + }, + title_prefix: "Transactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions` endpoint. """ @@ -223,12 +263,31 @@ defmodule BlockScoutWeb.API.V2.TransactionController do }) end + operation :polygon_zkevm_batch, + summary: "List L2 transactions in a Polygon ZkEVM batch", + description: "Retrieves L2 transactions bound to a specific Polygon ZkEVM batch number.", + parameters: [batch_number_param() | base_params()], + responses: [ + ok: + {"Polygon ZkEVM batch transactions", "application/json", + %Schema{ + title: "PolygonZkEVMBatchTransactions", + type: :object, + properties: %{ + items: %Schema{type: :array, items: Schemas.Transaction.Response} + }, + nullable: false, + additionalProperties: false + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/zkevm-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. """ @spec polygon_zkevm_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def polygon_zkevm_batch(conn, %{"batch_number" => batch_number} = _params) do + def polygon_zkevm_batch(conn, %{batch_number_param: batch_number} = _params) do options = [necessity_by_association: @transaction_necessity_by_association] |> Keyword.merge(@api_true) @@ -247,6 +306,27 @@ defmodule BlockScoutWeb.API.V2.TransactionController do }) end + operation :zksync_batch, + summary: "List L2 transactions in a ZkSync batch", + description: "Retrieves L2 transactions bound to a specific ZkSync batch number.", + parameters: + base_params() ++ + [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"ZkSync batch transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 65_361_291, + "index" => 1, + "items_count" => 50 + }, + title_prefix: "ZkSyncBatchTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/zksync-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. @@ -256,6 +336,27 @@ defmodule BlockScoutWeb.API.V2.TransactionController do handle_batch_transactions(conn, params, &ZkSyncReader.batch_transactions/2) end + operation :arbitrum_batch, + summary: "List L2 transactions in an Arbitrum batch", + description: "Retrieves L2 transactions bound to a specific Arbitrum batch number.", + parameters: + base_params() ++ + [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"Arbitrum batch transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 391_483_842, + "index" => 0, + "items_count" => 50 + }, + title_prefix: "ArbitrumBatchTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/arbitrum-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. @@ -265,14 +366,27 @@ defmodule BlockScoutWeb.API.V2.TransactionController do handle_batch_transactions(conn, params, &ArbitrumSettlementReader.batch_transactions/2) end + operation :external_transactions, + summary: "List external transactions linked to a transaction", + description: + "Retrieves external transactions that are linked to the specified transaction (e.g., Solana transactions in `neon` chain type).", + parameters: [transaction_hash_param() | base_params()], + responses: [ + ok: + {"Linked external transactions", "application/json", + %Schema{type: :array, items: %Schema{type: :string}, nullable: false}}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/external-transactions` endpoint. It renders the list of external transactions that are somehow linked (eg. preceded or initiated by) to the selected one. The most common use case is for side-chains and rollups. Currently implemented only for Neon chain but could also be extended for similar cases. """ - @spec external_transactions(Plug.Conn.t(), %{required(String.t()) => String.t()}) :: Plug.Conn.t() - def external_transactions(conn, %{"transaction_hash_param" => transaction_hash} = _params) do + @spec external_transactions(Plug.Conn.t(), %{required(atom()) => String.t()}) :: Plug.Conn.t() + def external_transactions(conn, %{transaction_hash_param: transaction_hash} = _params) do with {:format, {:ok, hash}} <- {:format, Chain.string_to_full_hash(transaction_hash)} do case NeonSolanaTransactions.maybe_fetch(hash) do {:ok, linked_transactions} -> @@ -293,12 +407,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :optimism_batch, + summary: "List L2 transactions in an Optimism batch", + description: "Retrieves L2 transactions bound to a specific Optimism batch number.", + parameters: + base_params() ++ + [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"Optimism batch transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 142_678_440, + "index" => 5, + "items_count" => 50 + }, + title_prefix: "OptimismBatchTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/optimism-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. """ @spec optimism_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def optimism_batch(conn, %{"batch_number" => batch_number_string} = params) do + def optimism_batch(conn, %{batch_number_param: batch_number_string} = params) do {batch_number, ""} = Integer.parse(batch_number_string) l2_block_number_from = OptimismTransactionBatch.edge_l2_block_number(batch_number, :min, @api_true) @@ -307,12 +442,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do handle_block_range_transactions(conn, params, l2_block_number_from, l2_block_number_to) end + operation :scroll_batch, + summary: "List L2 transactions in a Scroll batch", + description: "Retrieves L2 transactions bound to a specific Scroll batch number.", + parameters: + base_params() ++ + [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"Scroll batch transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 14_127_868, + "index" => 0, + "items_count" => 50 + }, + title_prefix: "ScrollBatchTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/scroll-batch/:batch_number` endpoint. It renders the list of L2 transactions bound to the specified batch. """ @spec scroll_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def scroll_batch(conn, %{"batch_number" => batch_number_string} = params) do + def scroll_batch(conn, %{batch_number_param: batch_number_string} = params) do {batch_number, ""} = Integer.parse(batch_number_string) {l2_block_number_from, l2_block_number_to} = @@ -388,7 +544,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do # ## Returns # - Updated connection object with the transactions data rendered. @spec handle_batch_transactions(Plug.Conn.t(), map(), function()) :: Plug.Conn.t() - defp handle_batch_transactions(conn, %{"batch_number" => batch_number} = params, batch_transactions_fun) do + defp handle_batch_transactions(conn, %{batch_number_param: batch_number} = params, batch_transactions_fun) do full_options = [ necessity_by_association: @transaction_necessity_by_association @@ -417,7 +573,32 @@ defmodule BlockScoutWeb.API.V2.TransactionController do }) end - def execution_node(conn, %{"execution_node_hash_param" => execution_node_hash_string} = params) do + operation :execution_node, + summary: "List transactions executed on a specific execution node", + description: "Retrieves transactions that were executed on the specified execution node.", + parameters: + [execution_node_hash_param() | base_params()] ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"List of transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 14_127_868, + "index" => 0, + "items_count" => 50 + }, + title_prefix: "ExecutionNodeTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/transactions/:execution_node_hash_param/execution-node` endpoint. + It renders the list of transactions that were executed on the specified execution node. + """ + @spec execution_node(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} + def execution_node(conn, %{execution_node_hash_param: execution_node_hash_string} = params) do with {:format, {:ok, execution_node_hash}} <- {:format, Chain.string_to_address_hash(execution_node_hash_string)} do full_options = [necessity_by_association: @transaction_necessity_by_association] @@ -441,11 +622,22 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :raw_trace, + summary: "Get raw trace for a transaction", + description: + "Retrieves the raw trace for a specific transaction, including internal operations and execution details.", + parameters: [transaction_hash_param() | base_params()], + responses: [ + ok: {"Raw trace for transaction", "application/json", BlockScoutWeb.Schemas.API.V2.Transaction.RawTrace}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/raw-trace` endpoint. """ @spec raw_trace(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def raw_trace(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def raw_trace(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do if is_nil(transaction.block_number) do conn @@ -468,11 +660,40 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :token_transfers, + summary: "List token transfers for a transaction", + description: "Retrieves token transfers for a specific transaction.", + parameters: + base_params() ++ + [transaction_hash_param(), token_type_param()] ++ + define_paging_params([ + "index", + "block_number", + "batch_log_index", + "batch_block_hash", + "batch_transaction_hash", + "index_in_batch" + ]), + responses: [ + ok: + {"Token transfers for transaction", "application/json", + paginated_response( + items: Schemas.TokenTransfer, + next_page_params_example: %{ + "index" => 442, + "block_number" => 21_307_214 + }, + title_prefix: "TransactionTokenTransfers" + )}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/token-transfers` endpoint. """ @spec token_transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def token_transfers(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def token_transfers(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do paging_options = paging_options(params) @@ -505,11 +726,34 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :internal_transactions, + summary: "List internal transactions for a transaction", + description: "Retrieves internal transactions for a specific transaction.", + parameters: + [transaction_hash_param() | base_params()] ++ + define_paging_params(["index", "block_number", "transaction_index", "items_count"]), + responses: [ + ok: + {"Internal transactions for transaction", "application/json", + paginated_response( + items: Schemas.InternalTransaction, + next_page_params_example: %{ + "index" => 50, + "block_number" => 22_133_247, + "transaction_index" => 68, + "items_count" => 50 + }, + title_prefix: "TransactionInternalTransactions" + )}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/internal-transactions` endpoint. """ @spec internal_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def internal_transactions(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def internal_transactions(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = @internal_transaction_necessity_by_association @@ -534,11 +778,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :logs, + summary: "List logs for a transaction", + description: "Retrieves logs for a specific transaction.", + parameters: + [transaction_hash_param() | base_params()] ++ + define_paging_params(["index", "block_number", "items_count"]), + responses: [ + ok: + {"Logs for transaction", "application/json", + paginated_response( + items: Schemas.Log, + next_page_params_example: %{ + "index" => 124, + "block_number" => 21_925_703, + "items_count" => 50 + }, + title_prefix: "TransactionLogs" + )}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/logs` endpoint. """ @spec logs(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def logs(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def logs(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = [ @@ -567,11 +833,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :state_changes, + summary: "List state changes for a transaction", + description: "Retrieves state changes for a specific transaction.", + parameters: + [transaction_hash_param() | base_params()] ++ define_state_changes_paging_params(["state_changes", "items_count"]), + responses: [ + ok: { + "State changes for transaction", + "application/json", + paginated_response( + items: Schemas.Transaction.StateChange, + next_page_params_example: %{ + "state_changes" => nil, + "items_count" => 50 + }, + title_prefix: "TransactionStateChanges" + ) + }, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/state-changes` endpoint. """ @spec state_changes(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def state_changes(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def state_changes(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do state_changes_plus_next_page = transaction @@ -586,7 +874,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params = next_page - |> next_page_params(state_changes, params) + |> next_page_params(state_changes, params, true) conn |> put_status(200) @@ -594,6 +882,26 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :watchlist_transactions, + summary: "List transactions in a user's watchlist", + description: "Retrieves transactions in the authenticated user's watchlist.", + parameters: base_params() ++ define_paging_params(["block_number", "index", "items_count"]), + responses: [ + ok: + {"Watchlist transactions", "application/json", + paginated_response( + items: Schemas.Transaction.Response, + next_page_params_example: %{ + "block_number" => 23_617_990, + "index" => 128, + "items_count" => 50 + }, + title_prefix: "WatchlistTransactions" + )}, + forbidden: ForbiddenResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/watchlist` endpoint. """ @@ -623,7 +931,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end - def summary(conn, %{"transaction_hash_param" => transaction_hash_string, "just_request_body" => "true"} = params) do + operation :summary, + summary: "Get interpreted summary for a transaction", + description: "Retrieves interpreted summary for a specific transaction.", + parameters: base_params() ++ [transaction_hash_param(), just_request_body_param()], + responses: [ + ok: + {"Transaction summary", "application/json", + %Schema{ + anyOf: [ + BlockScoutWeb.Schemas.API.V2.Transaction.Summary, + BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody + ] + }}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/summary` endpoint. + """ + @spec summary(Plug.Conn.t(), map()) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | {:transaction_interpreter_enabled, boolean} + | Plug.Conn.t() + def summary(conn, %{transaction_hash_param: transaction_hash_string, just_request_body: true} = params) do options = [ necessity_by_association: %{ @@ -641,16 +975,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end - @doc """ - Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/summary` endpoint. - """ - @spec summary(Plug.Conn.t(), map()) :: - {:format, :error} - | {:not_found, {:error, :not_found}} - | {:restricted_access, true} - | {:transaction_interpreter_enabled, boolean} - | Plug.Conn.t() - def summary(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def summary(conn, %{transaction_hash_param: transaction_hash_string} = params) do options = [ necessity_by_association: %{ @@ -676,11 +1001,31 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :blobs, + summary: "List blobs for a transaction", + description: "Retrieves blobs for a specific transaction (Ethereum only).", + parameters: [transaction_hash_param() | base_params()], + responses: [ + ok: + {"Blobs for transaction", "application/json", + %Schema{ + title: "TransactionBlobs", + type: :object, + properties: %{ + items: %Schema{type: :array, items: BlockScoutWeb.Schemas.API.V2.Blob.Response} + }, + nullable: false, + additionalProperties: false + }}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/blobs` endpoint. """ @spec blobs(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def blobs(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def blobs(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = @api_true @@ -693,6 +1038,35 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end end + operation :stats, + summary: "Get transaction statistics", + description: "Retrieves statistics for transactions, including counts and fee summaries for the last 24 hours.", + responses: [ + ok: + {"Transaction statistics", "application/json", + %Schema{ + type: :object, + properties: %{ + transactions_count_24h: Schemas.General.IntegerString, + pending_transactions_count: Schemas.General.IntegerString, + transaction_fees_sum_24h: Schemas.General.IntegerString, + transaction_fees_avg_24h: Schemas.General.IntegerString + }, + required: [ + :transactions_count_24h, + :pending_transactions_count, + :transaction_fees_sum_24h, + :transaction_fees_avg_24h + ], + additionalProperties: false + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/transactions/stats` endpoint. + """ + @spec stats(Plug.Conn.t(), map()) :: Plug.Conn.t() def stats(conn, _params) do transactions_count = Transactions24hCount.fetch_count(@api_true) pending_transactions_count = NewPendingTransactionsCount.fetch(@api_true) @@ -712,6 +1086,25 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ) end + operation :beacon_deposits, + summary: "List beacon deposits in a transaction", + description: "Retrieves beacon deposits included in a specific transaction with pagination support.", + parameters: [transaction_hash_param() | base_params()] ++ define_paging_params(["index", "items_count"]), + responses: [ + ok: + {"Beacon deposits for transaction", "application/json", + paginated_response( + items: BlockScoutWeb.Schemas.API.V2.Beacon.Deposit.Response, + next_page_params_example: %{ + "index" => 2_287_943, + "items_count" => 50 + }, + title_prefix: "Transactions" + )}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Handles `api/v2/transactions/:transaction_hash_param/beacon/deposits` endpoint. Fetches beacon deposits included in a specific transaction with pagination support. @@ -740,7 +1133,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do | {:not_found, {:error, :not_found}} | {:restricted_access, true} | Plug.Conn.t() - def beacon_deposits(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + def beacon_deposits(conn, %{transaction_hash_param: transaction_hash_string} = params) do with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = [ @@ -761,6 +1154,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do |> next_page_params( deposits, params, + false, DepositController.paging_function() ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex index 63674c20080e..0ab0fac316a5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/validator_controller.ex @@ -22,7 +22,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do only: [ split_list_by_page: 1, paging_options: 1, - next_page_params: 4 + next_page_params: 5 ] @api_true api?: true @@ -52,6 +52,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do |> next_page_params( validators, params, + false, &ValidatorStability.next_page_params/1 ) @@ -102,6 +103,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do |> next_page_params( validators, params, + false, &ValidatorBlackfort.next_page_params/1 ) @@ -171,6 +173,7 @@ defmodule BlockScoutWeb.API.V2.ValidatorController do |> next_page_params( validators, params, + false, &ValidatorZilliqa.next_page_params/1 ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex index 8f96f0e1c2df..877f1ef9fcf1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/zksync_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.ZkSyncController do import BlockScoutWeb.Chain, only: [ - next_page_params: 4, + next_page_params: 5, paging_options: 1, split_list_by_page: 1 ] @@ -56,6 +56,7 @@ defmodule BlockScoutWeb.API.V2.ZkSyncController do next_page, batches, params, + false, fn %TransactionBatch{number: number} -> %{"number" => number} end ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex index 1e3235600c08..7245892e9d8a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do use BlockScoutWeb, :controller import BlockScoutWeb.Chain, - only: [next_page_params: 4, split_list_by_page: 1, fetch_page_number: 1] + only: [next_page_params: 5, split_list_by_page: 1, fetch_page_number: 1] import BlockScoutWeb.PagingHelper, only: [current_filter: 1, search_query: 1] @@ -46,6 +46,7 @@ defmodule BlockScoutWeb.VerifiedContractsController do next_page, verified_contracts, params, + false, &%{id: &1.id} ) do nil -> nil diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 3d6e1c4d1216..19f159f33aa1 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -34,6 +34,10 @@ defmodule BlockScoutWeb.PagingHelper do end end + def paging_options(%{block_number: block_number, index: index}, [:validated | _]) do + [paging_options: %{@default_paging_options | key: {block_number, index}}] + end + def paging_options(%{"inserted_at" => inserted_at_string, "hash" => hash_string}, [:pending | _]) do with {:ok, inserted_at, _} <- DateTime.from_iso8601(inserted_at_string), {:ok, hash} <- string_to_full_hash(hash_string) do @@ -44,6 +48,16 @@ defmodule BlockScoutWeb.PagingHelper do end end + def paging_options(%{inserted_at: inserted_at, hash: hash_string}, [:pending | _]) do + case string_to_full_hash(hash_string) do + {:ok, hash} -> + [paging_options: %{@default_paging_options | key: {inserted_at, hash}, is_pending_transaction: true}] + + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(_params, _filter), do: [paging_options: @default_paging_options] @spec stability_validators_state_options(map()) :: [{:state, list()}, ...] @@ -92,6 +106,11 @@ defmodule BlockScoutWeb.PagingHelper do if(filter == [], do: [fallback], else: filter) end + def filter_options(%{filter: filter}, fallback) do + filter = filter |> parse_filter(@allowed_filter_labels) |> Enum.map(&String.to_existing_atom/1) + if(filter == [], do: [fallback], else: filter) + end + def filter_options(_params, fallback), do: [fallback] def chain_ids_filter_options(%{"chain_ids" => chain_id}) do @@ -122,6 +141,10 @@ defmodule BlockScoutWeb.PagingHelper do [type: type |> parse_filter(General.allowed_transaction_types()) |> Enum.map(&String.to_existing_atom/1)] end + def type_filter_options(%{type: type}) do + [type: type |> parse_filter(General.allowed_transaction_types()) |> Enum.map(&String.to_existing_atom/1)] + end + def type_filter_options(_params), do: [type: []] @spec internal_transaction_type_options(any()) :: [{:type, list()}] @@ -129,6 +152,10 @@ defmodule BlockScoutWeb.PagingHelper do [type: type |> parse_filter(InternalTransactionType.values()) |> Enum.map(&String.to_existing_atom/1)] end + def internal_transaction_type_options(%{type: type}) do + [type: type |> parse_filter(InternalTransactionType.values()) |> Enum.map(&String.to_existing_atom/1)] + end + def internal_transaction_type_options(_params), do: [type: []] @spec internal_transaction_call_type_options(any()) :: [{:call_type, list()}] @@ -142,6 +169,10 @@ defmodule BlockScoutWeb.PagingHelper do [method: parse_method_filter(method)] end + def method_filter_options(%{method: method}) do + [method: parse_method_filter(method)] + end + def method_filter_options(_params), do: [method: []] def parse_filter("[" <> filter, allowed_labels) do @@ -224,6 +255,8 @@ defmodule BlockScoutWeb.PagingHelper do :address_hash_param, :batch_number_param, :block_hash_or_number_param, + :transaction_hash_param, + :batch_number_param, :token_id_param, :token_id, :type, diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index b84f0b260f55..a1ae824e4d73 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -152,23 +152,23 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/stats", V2.TransactionController, :stats) if @chain_type == :polygon_zkevm do - get("/zkevm-batch/:batch_number", V2.TransactionController, :polygon_zkevm_batch) + get("/zkevm-batch/:batch_number_param", V2.TransactionController, :polygon_zkevm_batch) end if @chain_type == :zksync do - get("/zksync-batch/:batch_number", V2.TransactionController, :zksync_batch) + get("/zksync-batch/:batch_number_param", V2.TransactionController, :zksync_batch) end if @chain_type == :arbitrum do - get("/arbitrum-batch/:batch_number", V2.TransactionController, :arbitrum_batch) + get("/arbitrum-batch/:batch_number_param", V2.TransactionController, :arbitrum_batch) end if @chain_type == :optimism do - get("/optimism-batch/:batch_number", V2.TransactionController, :optimism_batch) + get("/optimism-batch/:batch_number_param", V2.TransactionController, :optimism_batch) end if @chain_type == :scroll do - get("/scroll-batch/:batch_number", V2.TransactionController, :scroll_batch) + get("/scroll-batch/:batch_number_param", V2.TransactionController, :scroll_batch) end if @chain_type == :suave do diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address_nullable.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address_nullable.ex new file mode 100644 index 000000000000..46cefeb0f93f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address_nullable.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.AddressNullable do + @moduledoc """ + This module defines the schema for nullable address struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address + alias BlockScoutWeb.Schemas.Helper + + OpenApiSpex.schema( + Address.schema() + |> Helper.extend_schema( + title: "AddressNullable", + description: "AddressNullable", + nullable: true + ) + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex index cc5645bb3efe..74cf3cabf4f6 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit do """ require OpenApiSpex - alias BlockScoutWeb.Schemas.API.V2.{Address, Beacon.Deposit.Status, General} + alias BlockScoutWeb.Schemas.API.V2.{Address, AddressNullable, Beacon.Deposit.Status, General} alias OpenApiSpex.Schema OpenApiSpex.schema(%{ @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit do block_timestamp: General.Timestamp, pubkey: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{96})$", nullable: false}, withdrawal_credentials: %Schema{type: :string, pattern: General.full_hash_pattern(), nullable: false}, - withdrawal_address: %Schema{allOf: [Address], nullable: true}, + withdrawal_address: AddressNullable, amount: General.IntegerString, signature: %Schema{type: :string, pattern: ~r"^0x([A-Fa-f0-9]{192})$", nullable: false}, status: Status, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex new file mode 100644 index 000000000000..9a099afdef18 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit.Response do + @moduledoc """ + This module defines the schema for beacon deposit response from /api/v2/transactions/:transaction_hash_param/beacon/deposits. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Beacon.Deposit + alias BlockScoutWeb.Schemas.Helper + + OpenApiSpex.schema( + Deposit.schema() + |> Helper.extend_schema( + title: "BeaconDepositResponse", + description: "BeaconDeposit response", + additionalProperties: false + ) + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob.ex new file mode 100644 index 000000000000..438386e8878e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Blob do + @moduledoc "OpenAPI schema for Blob responses." + + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + blob_data: General.HexString, + hash: General.FullHash, + kzg_commitment: General.HexString, + kzg_proof: General.HexString + }, + required: [:blob_data, :hash, :kzg_commitment, :kzg_proof], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex new file mode 100644 index 000000000000..4816f95363f5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex @@ -0,0 +1,18 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Blob.Response do + @moduledoc """ + This module defines the schema for blob response from /api/v2/transactions/:transaction_hash_param/blobs. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Blob + alias BlockScoutWeb.Schemas.Helper + + OpenApiSpex.schema( + Blob.schema() + |> Helper.extend_schema( + title: "BlobResponse", + description: "Blob response", + additionalProperties: false + ) + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 17bfdda57e3c..571da8dd9bbe 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -195,6 +195,32 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for an execution_node hash in the path. + """ + @spec execution_node_hash_param() :: Parameter.t() + def execution_node_hash_param do + %Parameter{ + name: :execution_node_hash_param, + in: :path, + schema: AddressHash, + required: true + } + end + + @doc """ + Returns a parameter definition for a transaction hash in the path. + """ + @spec transaction_hash_param() :: Parameter.t() + def transaction_hash_param do + %Parameter{ + name: :transaction_hash_param, + in: :path, + schema: FullHash, + required: true + } + end + @doc """ Returns a parameter definition for a block hash or number in the path. """ @@ -241,6 +267,39 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for filtering transactions by type (validated or pending). + """ + @spec transaction_filter_param() :: Parameter.t() + def transaction_filter_param do + %Parameter{ + name: :filter, + in: :query, + schema: %Schema{type: :string, enum: ["validated", "pending"]}, + required: false, + description: """ + Filter by transaction type: + * validated - Only show validated transactions + * pending - Only show pending transactions + If omitted, default value "validated" is used. + """ + } + end + + @doc """ + Returns a parameter definition for a request body used in the summary endpoint. + """ + @spec just_request_body_param() :: Parameter.t() + def just_request_body_param do + %Parameter{ + name: :just_request_body, + in: :query, + schema: %Schema{type: :boolean}, + required: false, + description: "If true, returns only the request body in the summary endpoint" + } + end + @doc """ Returns a parameter definition for a batch number in the path. """ @@ -861,6 +920,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do description: "Type for paging", name: :type }, + "filter" => %Parameter{ + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Filter for paging", + name: :filter + }, "deposit_index" => %Parameter{ in: :query, schema: %Schema{type: :integer, minimum: 0, maximum: 9_223_372_036_854_775_807}, @@ -870,6 +936,26 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } } + @state_changes_paging_params %{ + # "items_count" is used for pagination for the list of transactions's state changes and it can be higher than 50. + # Thus, we extracted it to a separate map. + "items_count" => %Parameter{ + in: :query, + schema: %Schema{type: :integer, minimum: 1}, + required: false, + description: "Cumulative number of items to skip for keyset-based pagination of state changes", + name: :items_count + }, + # todo: remove in the future as this param is unused in the pagination of state changes + "state_changes" => %Parameter{ + in: :query, + schema: %Schema{type: :string, nullable: true}, + required: false, + description: "State changes for paging", + name: :state_changes + } + } + @doc """ Returns a list of paging parameters based on the provided field names. """ @@ -880,6 +966,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do end) end + @doc """ + Returns a list of pagination parameters for `/api/v2/transactions/:transaction_hash_param/state-changes` API endpoint + """ + @spec define_state_changes_paging_params([String.t()]) :: [Parameter.t()] + def define_state_changes_paging_params(fields) do + Enum.map(fields, fn field -> + Map.get(@state_changes_paging_params, field) || raise "Unknown paging param: #{field}" + end) + end + @doc """ Returns the list of allowed transaction type labels based on the configured chain type. """ diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex index f34b9ff5e8f2..dda00876db5f 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -184,7 +184,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do items: @optimism_withdrawal_schema, nullable: false }, - op_interop: %Schema{ + op_interop_messages: %Schema{ type: :object, nullable: false, properties: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/raw_trace.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/raw_trace.ex new file mode 100644 index 000000000000..ec6f91626a09 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/raw_trace.ex @@ -0,0 +1,53 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.RawTrace do + @moduledoc """ + This module defines the schema for a transaction raw trace API response. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + action_schema = %Schema{ + type: :object, + properties: %{ + callType: %Schema{type: :string, enum: ["call", "callcode", "delegatecall", "staticcall"]}, + from: General.AddressHash, + gas: General.HexString, + input: General.HexString, + init: General.HexString, + to: General.AddressHash, + value: General.HexString + }, + required: [:from, :gas, :input, :value], + additionalProperties: false + } + + result_schema = %Schema{ + type: :object, + properties: %{ + gasUsed: General.HexString, + output: General.HexString + }, + required: [:gasUsed, :output], + additionalProperties: false + } + + trace_schema = %Schema{ + type: :object, + properties: %{ + action: action_schema, + result: result_schema, + subtraces: %Schema{type: :integer, minimum: 0}, + traceAddress: %Schema{type: :array, items: %Schema{type: :integer}}, + transactionHash: General.FullHashNullable, + type: %Schema{type: :string, enum: ["call", "create", "create2", "reward", "selfdestruct", "stop", "invalid"]} + }, + required: [:action, :subtraces, :traceAddress, :type], + additionalProperties: false + } + + OpenApiSpex.schema(%Schema{ + type: :array, + items: trace_schema + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/response.ex new file mode 100644 index 000000000000..a270b64b7132 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/response.ex @@ -0,0 +1,87 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Response.ChainTypeCustomizations do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.Helper + alias OpenApiSpex.Schema + + @op_interop_message_schema %Schema{ + type: :object, + description: "Interop message included in an Optimism transaction", + nullable: false, + properties: %{ + nonce: %Schema{type: :integer, minimum: 0}, + payload: General.HexString, + relay_chain: %Schema{ + type: :object, + properties: %{ + instance_url: %Schema{type: :string, nullable: true}, + chain_id: General.IntegerString, + chain_name: %Schema{type: :string, nullable: true}, + chain_logo: %Schema{type: :string, nullable: true} + }, + nullable: true + }, + relay_transaction_hash: General.FullHash, + sender_address_hash: General.AddressHash, + status: %Schema{type: :string, enum: ["Sent", "Relayed", "Failed"]}, + target_address_hash: General.AddressHash, + unique_id: %Schema{type: :string} + }, + required: [:payload], + example: %{ + "nonce" => 0, + "payload" => "0x30787849009c24f10a91a327a9f2ed94ebc49ee9", + "relay_chain" => nil, + "relay_transaction_hash" => "0x0000000000000000000000000000000000000000000000000000000000000002", + "sender_address_hash" => "0x0000000000000000000000000000000000000003", + "status" => "Relayed", + "target_address_hash" => "0x0000000000000000000000000000000000000004", + "unique_id" => "0000000100000000" + } + } + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :optimism -> + schema + |> Helper.extend_schema( + properties: %{op_interop_messages: %Schema{type: :array, items: @op_interop_message_schema, nullable: false}} + ) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Response do + @moduledoc """ + This module defines the schema for transaction response from /api/v2/transactions/:transaction_hash_param. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Transaction + alias BlockScoutWeb.Schemas.API.V2.Transaction.Response.ChainTypeCustomizations + alias BlockScoutWeb.Schemas.Helper + + OpenApiSpex.schema( + Transaction.schema() + |> Helper.extend_schema( + title: "TransactionResponse", + description: "Transaction response" + ) + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/state_change.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/state_change.ex new file mode 100644 index 000000000000..294286713e03 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/state_change.ex @@ -0,0 +1,34 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.StateChange do + @moduledoc """ + This module defines the schema for a transaction state change API response. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + required: [ + :address, + :balance_after, + :balance_before, + :change, + :is_miner, + :token, + :type + ], + properties: %{ + # Should reference Address schema if available + address: Address, + balance_after: General.IntegerStringNullable, + balance_before: General.IntegerStringNullable, + change: General.IntegerStringNullable, + is_miner: %Schema{type: :boolean}, + token: %Schema{allOf: [Token], nullable: true}, + token_id: General.IntegerStringNullable, + type: %Schema{type: :string, enum: ["token", "coin"]} + }, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary.ex new file mode 100644 index 000000000000..29cc3d47303d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary.ex @@ -0,0 +1,57 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.Summary do + @moduledoc """ + This module defines the schema for a transaction summary API response. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + summary_variable_schema = %Schema{ + type: :object, + required: [:type, :value], + properties: %{ + type: %Schema{type: :string}, + value: %Schema{ + anyOf: [ + %Schema{type: :string}, + %Schema{type: :number}, + # For address or other complex types + %Schema{type: :object} + ] + } + }, + additionalProperties: false + } + + summary_template_variables_schema = %Schema{ + type: :object, + additionalProperties: summary_variable_schema + } + + summary_schema = %Schema{ + type: :object, + required: [:summary_template, :summary_template_variables], + properties: %{ + summary_template: %Schema{type: :string}, + summary_template_variables: summary_template_variables_schema + }, + additionalProperties: false + } + + OpenApiSpex.schema(%{ + type: :object, + required: [:data, :success], + properties: %{ + data: %Schema{ + type: :object, + required: [:summaries], + properties: %{ + summaries: %Schema{type: :array, items: summary_schema} + }, + additionalProperties: false + }, + success: %Schema{type: :boolean} + }, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex new file mode 100644 index 000000000000..eeabdd4fffa8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex @@ -0,0 +1,61 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody do + @moduledoc """ + OpenAPI schema for the transaction summary just request body response. + """ + require OpenApiSpex + alias OpenApiSpex.Schema + alias BlockScoutWeb.Schemas.API.V2.{Address, InternalTransaction, Log, TokenTransfer} + + logs_data_schema = %Schema{ + type: :object, + required: [:items], + properties: %{ + items: %Schema{type: :array, items: %Schema{allOf: [Log], nullable: true}} + }, + additionalProperties: false + } + + data_schema = %Schema{ + type: :object, + required: [ + :status, + :type, + :value, + :hash, + :from, + :to, + :method, + :token_transfers, + :internal_transactions, + :transaction_types, + :decoded_input, + :raw_input + ], + properties: %{ + status: %Schema{type: :string}, + type: %Schema{type: :integer, nullable: true}, + value: %Schema{type: :string}, + hash: %Schema{type: :string}, + from: %Schema{allOf: [Address], nullable: true}, + to: %Schema{allOf: [Address], nullable: true}, + method: %Schema{type: :string, nullable: true}, + token_transfers: %Schema{type: :array, items: %Schema{allOf: [TokenTransfer], nullable: true}}, + internal_transactions: %Schema{type: :array, items: %Schema{allOf: [InternalTransaction], nullable: true}}, + transaction_types: %Schema{type: :array, items: %Schema{type: :string}}, + decoded_input: %Schema{type: :object, nullable: true}, + raw_input: %Schema{type: :string} + }, + additionalProperties: false + } + + OpenApiSpex.schema(%{ + type: :object, + required: [:data], + properties: %{ + chain_id: %Schema{type: :integer, nullable: true}, + data: data_schema, + logs_data: logs_data_schema + }, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex index 6fc972895330..85f69e09c602 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex @@ -23,6 +23,11 @@ defmodule BlockScoutWeb.Schemas.Helper do do: Map.put(updated_schema_with_title, :description, Keyword.get(options, :description)), else: updated_schema_with_title - updated_schema_with_description + updated_schema_with_nullable = + if Keyword.has_key?(options, :nullable), + do: Map.put(updated_schema_with_description, :nullable, Keyword.get(options, :nullable)), + else: updated_schema_with_description + + updated_schema_with_nullable end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 4e83afcaa4fe..0230b55d8bc6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -266,11 +266,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "init_transaction_hash" => message.init_transaction_hash, "relay_transaction_hash" => message.relay_transaction_hash, "sender_address_hash" => message.sender_address_hash, - # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `sender_address_hash` property - "sender" => message.sender_address_hash, "target_address_hash" => message.target_address_hash, - # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `target_address_hash` property - "target" => message.target_address_hash, "payload" => message.payload } @@ -516,8 +512,6 @@ defmodule BlockScoutWeb.API.V2.OptimismView do else out_json |> Map.put("op_interop_messages", interop_messages) - # TODO: remove the deprecated `op_interop` map after frontend switches to the new `op_interop_messages` - |> Map.put("op_interop", Enum.at(interop_messages, 0)) end end diff --git a/apps/block_scout_web/test/block_scout_web/chain_test.exs b/apps/block_scout_web/test/block_scout_web/chain_test.exs index b0192d46b29e..12dd4de7cbe1 100644 --- a/apps/block_scout_web/test/block_scout_web/chain_test.exs +++ b/apps/block_scout_web/test/block_scout_web/chain_test.exs @@ -7,8 +7,8 @@ defmodule BlockScoutWeb.ChainTest do describe "next_page_params/4" do # https://github.com/blockscout/blockscout/issues/12984 test "does not return duplicated keys" do - assert Chain.next_page_params([nil], [%{id: 123}], %{"id" => 178}, fn x -> x end) == %{ - "items_count" => 1, + assert Chain.next_page_params([nil], [%{id: 123}], %{"id" => 178}, false, fn x -> x end) == %{ + items_count: 1, id: 123 } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs index e6ea85081b45..3f272a5e481a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs @@ -207,9 +207,9 @@ defmodule BlockScoutWeb.AddressTokenTransferControllerTest do :index, Address.checksum(address.hash), Address.checksum(token.contract_address_hash), + items_count: "50", block_number: page_last_transfer.block_number, - index: page_last_transfer.index, - items_count: "50" + index: page_last_transfer.index ) assert Map.get(json_response(conn, 200), "next_page_path") == expected_path diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index 4711b0806662..45c35875a417 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -236,8 +236,8 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do {:ok, now} = DateTime.now("Etc/UTC") - from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() - to_period = now |> DateTime.to_iso8601() + from_period = DateTime.add(now, -1, :minute) |> DateTime.to_iso8601() |> to_string() + to_period = now |> DateTime.to_iso8601() |> to_string() conn = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}/transactions/csv", %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index c11c5dba1754..87cbec68e69d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -338,7 +338,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return existing transaction", %{conn: conn} do @@ -545,7 +553,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do request = get(conn, "/api/v2/transactions/#{transaction.hash}") - assert %{"op_interop" => %{"payload" => "0x30787849009c24f10a91a327a9f2ed94ebc49ee9"}} = + assert %{"op_interop_messages" => [%{"payload" => "0x30787849009c24f10a91a327a9f2ed94ebc49ee9"}]} = json_response(request, 200) end end @@ -562,7 +570,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x/internal-transactions") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return empty list", %{conn: conn} do @@ -683,7 +699,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x/logs") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return empty list", %{conn: conn} do @@ -907,7 +931,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x/token-transfers") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return empty list", %{conn: conn} do @@ -1350,7 +1382,27 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x/state-changes") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) + end + + test "accepts pagination params", %{conn: conn} do + transaction = + :transaction + |> insert() + |> with_block(status: :ok) + + request = + get(conn, "/api/v2/transactions/#{to_string(transaction.hash)}/state-changes?items_count=50&state_changes=null") + + assert %{} = json_response(request, 200) end test "return existing transaction", %{conn: conn} do @@ -1918,17 +1970,22 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do |> with_block(status: :ok) raw_trace = %{ + "action" => %{ + "callType" => "call", + "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", + "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", + "gas" => "0x8600", + "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", + "value" => "0x174876e800" + }, + "result" => %{ + "gasUsed" => "0x7d37", + "output" => "0x" + }, + "subtraces" => 0, "traceAddress" => [], - "type" => "call", - "callType" => "call", - "from" => "0xa931c862e662134b85e4dc4baf5c70cc9ba74db4", - "to" => "0x1469b17ebf82fedf56f04109e5207bdc4554288c", - "gas" => "0x8600", - "gasUsed" => "0x7d37", - "input" => "0xb118e2db0000000000000000000000000000000000000000000000000000000000000008", - "output" => "0x", - "value" => "0x174876e800", - "transactionHash" => to_string(transaction.hash) + "transactionHash" => to_string(transaction.hash), + "type" => "call" } expect(EthereumJSONRPC.Mox, :json_rpc, fn _, _ -> {:ok, [raw_trace]} end) @@ -2267,12 +2324,17 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do service_url: "http://localhost:4000" ) + original_chain_id = Application.get_env(:block_scout_web, :chain_id) + Application.put_env(:block_scout_web, :chain_id, 1) + on_exit(fn -> Application.put_env( :block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, original_config ) + + Application.put_env(:block_scout_web, :chain_id, original_chain_id) end) end @@ -2286,7 +2348,15 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "return 422 on invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/0x/summary?just_request_body=true") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return 403 when transaction interpretation service is disabled", %{conn: conn} do @@ -2998,7 +3068,14 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do test "returns 422 for invalid transaction hash", %{conn: conn} do request = get(conn, "/api/v2/transactions/invalid_hash/external-transactions") assert response = json_response(request, 422) - assert response["message"] == "Invalid parameter(s)" + + assert response["errors"] == [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/transaction_hash_param"}, + "title" => "Invalid value" + } + ] end end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs index 0ebc98bfc98a..210c00687026 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs @@ -98,7 +98,7 @@ defmodule BlockScoutWeb.BlockControllerTest do conn = get(conn, blocks_path(conn, :index), %{"type" => "JSON"}) - expected_path = blocks_path(conn, :index, block_number: number, block_type: "Block", items_count: "50") + expected_path = blocks_path(conn, :index, block_number: number, items_count: "50", block_type: "Block") assert Map.get(json_response(conn, 200), "next_page_path") == expected_path end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs index c152318a1cc5..5b9caf835d70 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs @@ -40,7 +40,7 @@ defmodule BlockScoutWeb.WithdrawalControllerTest do conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) - expected_path = withdrawal_path(conn, :index, index: index, items_count: "50") + expected_path = withdrawal_path(conn, :index, items_count: "50", index: index) assert Map.get(json_response(conn, 200), "next_page_path") == expected_path end diff --git a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex index 86e70ab32ac9..e95a7ff7c0f8 100644 --- a/apps/explorer/lib/explorer/chain/optimism/interop_message.ex +++ b/apps/explorer/lib/explorer/chain/optimism/interop_message.ex @@ -737,11 +737,7 @@ defmodule Explorer.Chain.Optimism.InteropMessage do "nonce" => message.nonce, "status" => message.status, "sender_address_hash" => message.sender_address_hash, - # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `sender_address_hash` property - "sender" => message.sender_address_hash, "target_address_hash" => message.target_address_hash, - # todo: keep next line for compatibility with frontend and remove when new frontend is bound to `target_address_hash` property - "target" => message.target_address_hash, "payload" => message.payload }, chain_info From 8c830f026a3d0d4fa43377cf14ec7d1e408f3b45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 19:29:27 +0300 Subject: [PATCH 0463/1006] chore(deps-dev): bump credo from 1.7.12 to 1.7.13 (#13420) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Baranov --- .../lib/block_scout_web/controllers/address_controller.ex | 2 +- .../block_scout_web/controllers/api/rpc/celo_controller.ex | 2 +- .../api/v1/verified_smart_contract_controller.ex | 2 +- .../block_scout_web/controllers/api/v2/celo_controller.ex | 2 +- .../controllers/api/v2/internal_transaction_controller.ex | 2 +- .../controllers/api/v2/main_page_controller.ex | 2 +- .../block_scout_web/controllers/api/v2/stats_controller.ex | 2 +- .../block_scout_web/controllers/api/v2/token_controller.ex | 2 +- .../controllers/api/v2/verification_controller.ex | 2 +- .../lib/block_scout_web/graphql/resolvers/transaction.ex | 2 +- .../microservice_interfaces/transaction_interpretation.ex | 2 +- .../lib/block_scout_web/models/transaction_state_helper.ex | 2 +- apps/block_scout_web/lib/block_scout_web/rate_limit.ex | 2 +- .../lib/block_scout_web/routers/admin_router.ex | 2 +- .../lib/block_scout_web/schemas/api/v2/address/response.ex | 2 +- .../block_scout_web/schemas/api/v2/internal_transaction.ex | 2 +- .../lib/block_scout_web/schemas/api/v2/token_instance.ex | 2 +- .../lib/block_scout_web/views/address_view.ex | 2 +- .../block_scout_web/views/api/v2/advanced_filter_view.ex | 4 +--- .../lib/block_scout_web/views/api/v2/arbitrum_view.ex | 2 +- .../lib/block_scout_web/views/api/v2/block_view.ex | 2 +- .../lib/block_scout_web/views/api/v2/celo_view.ex | 4 ++-- .../lib/block_scout_web/views/api/v2/scroll_view.ex | 2 +- .../lib/block_scout_web/views/api/v2/smart_contract_view.ex | 2 +- .../block_scout_web/lib/block_scout_web/views/block_view.ex | 2 +- .../lib/block_scout_web/views/transaction_view.ex | 2 +- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rsk.ex | 2 +- .../ethereum_jsonrpc/lib/ethereum_jsonrpc/zilliqa/helper.ex | 2 +- apps/explorer/lib/explorer/account/api/key.ex | 2 +- .../explorer/lib/explorer/account/watchlist_notification.ex | 2 +- apps/explorer/lib/explorer/chain/address.ex | 6 +++--- apps/explorer/lib/explorer/chain/address/token_balance.ex | 2 +- apps/explorer/lib/explorer/chain/advanced_filter.ex | 2 +- .../lib/explorer/chain/arbitrum/reader/api/messages.ex | 2 +- .../lib/explorer/chain/arbitrum/reader/api/settlement.ex | 2 +- .../lib/explorer/chain/arbitrum/reader/indexer/messages.ex | 2 +- apps/explorer/lib/explorer/chain/beacon/deposit.ex | 2 +- apps/explorer/lib/explorer/chain/beacon/reader.ex | 2 +- apps/explorer/lib/explorer/chain/blackfort/validator.ex | 2 +- apps/explorer/lib/explorer/chain/block/reward.ex | 2 +- apps/explorer/lib/explorer/chain/celo/election_reward.ex | 2 +- apps/explorer/lib/explorer/chain/contract_method.ex | 2 +- .../explorer/chain/csv_export/address/token_transfers.ex | 2 +- .../lib/explorer/chain/csv_export/address/transactions.ex | 4 ++-- apps/explorer/lib/explorer/chain/import.ex | 2 +- apps/explorer/lib/explorer/chain/import/runner/addresses.ex | 2 +- .../chain/import/runner/beacon/blob_transactions.ex | 2 +- .../chain/multichain_search_db/balances_export_queue.ex | 2 +- apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex | 2 +- apps/explorer/lib/explorer/chain/scroll/reader.ex | 2 +- .../chain/smart_contract/proxy/verification_status.ex | 2 +- .../explorer/chain/smart_contract/verification_status.ex | 2 +- apps/explorer/lib/explorer/chain/stability/validator.ex | 2 +- apps/explorer/lib/explorer/chain/validator.ex | 2 +- apps/explorer/lib/explorer/chain_spec/poa/importer.ex | 4 ++-- apps/explorer/lib/explorer/etherscan.ex | 4 ++-- apps/explorer/lib/explorer/etherscan/contracts.ex | 2 +- .../explorer/microservice_interfaces/multichain_search.ex | 2 +- apps/explorer/lib/explorer/migrator/celo_accounts.ex | 2 +- apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex | 2 +- .../migrator/reindex_blocks_with_missing_transactions.ex | 2 +- .../explorer/lib/explorer/third_party_integrations/auth0.ex | 2 +- .../lib/explorer/third_party_integrations/auth0/internal.ex | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- .../token_instance_owner_address_migration/helper.ex | 2 +- .../indexer/fetcher/arbitrum/workers/batches/discovery.ex | 2 +- apps/indexer/lib/indexer/fetcher/beacon/blob.ex | 2 +- .../validator_and_group_payments_prior_l2_migration.ex | 2 +- apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex | 2 +- .../fetcher/multichain_search_db/balances_export_queue.ex | 2 +- apps/indexer/lib/indexer/fetcher/scroll/batch.ex | 2 +- apps/indexer/lib/indexer/fetcher/shibarium/l1.ex | 2 +- .../lib/indexer/fetcher/token_instance/sanitize_erc1155.ex | 2 +- apps/indexer/lib/indexer/transform/token_transfers.ex | 2 +- apps/indexer/lib/indexer/transform/transaction_actions.ex | 2 +- mix.lock | 4 ++-- 76 files changed, 83 insertions(+), 85 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index c8ff8f208430..a9bf0bbbec9e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -16,9 +16,9 @@ defmodule BlockScoutWeb.AddressController do } alias Explorer.{Chain, Market} + alias Explorer.Chain.{Address, Wei} alias Explorer.Chain.Address.Counters alias Explorer.Chain.Cache.Counters.AddressesCount - alias Explorer.Chain.{Address, Wei} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand alias Phoenix.View diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex index 3680103ed474..4e34e5ec56bd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.RPC.CeloController do use BlockScoutWeb, :controller - alias Explorer.Helper alias Explorer.Chain.Celo.{ElectionReward, Epoch} + alias Explorer.Helper @max_safe_epoch_number 32_768 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex index 6a93491f21eb..ab3d52e122a2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/verified_smart_contract_controller.ex @@ -1,8 +1,8 @@ defmodule BlockScoutWeb.API.V1.VerifiedSmartContractController do use BlockScoutWeb, :controller - alias Explorer.Chain.Hash.Address, as: AddressHash alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.Hash.Address, as: AddressHash alias Explorer.SmartContract.Solidity.Publisher def create(conn, params) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index 4c54cf725d3b..bc8ad6ceff56 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -11,8 +11,8 @@ defmodule BlockScoutWeb.API.V2.CeloController do import Explorer.PagingOptions, only: [default_paging_options: 0] - alias Explorer.Chain.Hash alias Explorer.Chain.Celo.{ElectionReward, Epoch} + alias Explorer.Chain.Hash alias Explorer.PagingOptions action_fallback(BlockScoutWeb.API.V2.FallbackController) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 39e9df1bb035..df2fb742cc3b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -1,8 +1,8 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do use BlockScoutWeb, :controller + alias Explorer.{Chain, Helper, PagingOptions} alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.InternalTransaction - alias Explorer.{Chain, Helper, PagingOptions} import BlockScoutWeb.Chain, only: [ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 732814fb6a12..b278d14dc9f6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -2,8 +2,8 @@ defmodule BlockScoutWeb.API.V2.MainPageController do use BlockScoutWeb, :controller use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - alias Explorer.{Chain, PagingOptions} alias BlockScoutWeb.API.V2.{BlockView, OptimismView, TransactionView} + alias Explorer.{Chain, PagingOptions} alias Explorer.{Chain, Repo} alias Explorer.Chain.Optimism.Deposit diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 5c0d51f03200..daede28dd3de 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -5,8 +5,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController alias Explorer.{Chain, Market} - alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime, BlocksCount, GasUsageSum, TransactionsCount} + alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats alias Plug.Conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index d239ada73178..1046b2927b94 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -6,8 +6,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse - alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} alias Explorer.{Chain, Helper, PagingOptions} + alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} alias Explorer.Migrator.BackfillMetadataURL alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand alias Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch, as: TokenInstanceMetadataRefetchOnDemand diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex index a691ec638f69..d7220b40971c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/verification_controller.ex @@ -10,11 +10,11 @@ defmodule BlockScoutWeb.API.V2.VerificationController do alias BlockScoutWeb.API.V2.ApiView alias Explorer.Chain alias Explorer.Chain.SmartContract + alias Explorer.SmartContract.{CompilerVersion, RustVerifierInterface, Solidity.CodeCompiler, StylusVerifierInterface} alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Explorer.SmartContract.Solidity.PublishHelper alias Explorer.SmartContract.Stylus.PublisherWorker, as: StylusPublisherWorker alias Explorer.SmartContract.Vyper.PublisherWorker, as: VyperPublisherWorker - alias Explorer.SmartContract.{CompilerVersion, RustVerifierInterface, Solidity.CodeCompiler, StylusVerifierInterface} alias Indexer.Fetcher.OnDemand.ContractCode action_fallback(BlockScoutWeb.API.V2.FallbackController) diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex index c1406eed85fc..9230fa0834dc 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex @@ -2,8 +2,8 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.Transaction do @moduledoc false alias Absinthe.Relay.Connection - alias Explorer.{GraphQL, Repo} alias Explorer.Chain.{Address, TokenTransfer} + alias Explorer.{GraphQL, Repo} def get_by(_, %{hash: hash}, _), do: GraphQL.get_transaction_by_hash(hash) diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 1acc2fe60dac..bc8c17d66b45 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -6,8 +6,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do alias BlockScoutWeb.API.V2.{Helper, InternalTransactionView, TokenTransferView, TokenView, TransactionView} alias Ecto.Association.NotLoaded alias Explorer.{Chain, HttpClient} - alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{Data, InternalTransaction, Log, TokenTransfer, Transaction} + alias Explorer.Helper, as: ExplorerHelper import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index f774c3ef0c48..3c91f71f9edb 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -8,10 +8,10 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] - alias Explorer.Chain.Transaction.StateChange alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address.CoinBalance, BlockNumberHelper, InternalTransaction, Transaction, Wei} alias Explorer.Chain.Cache.StateChanges + alias Explorer.Chain.Transaction.StateChange alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Indexer.Fetcher.OnDemand.TokenBalance, as: TokenBalanceOnDemand diff --git a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex index 1a51936ec24b..00d59f25a27c 100644 --- a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex +++ b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex @@ -2,8 +2,8 @@ defmodule BlockScoutWeb.RateLimit do @moduledoc """ Rate limiting """ - alias BlockScoutWeb.RateLimit.Hammer alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias BlockScoutWeb.RateLimit.Hammer alias Explorer.Account.Api.Key, as: ApiKey alias Plug.Conn diff --git a/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex index d452980ae9ff..e0f0c5f4a727 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/admin_router.ex @@ -5,8 +5,8 @@ defmodule BlockScoutWeb.Routers.AdminRouter do use BlockScoutWeb, :router - alias BlockScoutWeb.Plug.FetchUserFromSession alias BlockScoutWeb.Plug.Admin.{CheckOwnerRegistered, RequireAdminRole} + alias BlockScoutWeb.Plug.FetchUserFromSession pipeline :browser do plug( diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index 1fe47a70219c..ed28d1972827 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -112,8 +112,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response do """ require OpenApiSpex - alias BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex index 78331fe33e1a..26d2ad574d74 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex @@ -4,8 +4,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.InternalTransaction do """ require OpenApiSpex - alias OpenApiSpex.Schema alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema OpenApiSpex.schema(%{ type: :object, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex index f8e02ca3607c..071d2f38b67c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_instance.ex @@ -4,8 +4,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenInstance do """ require OpenApiSpex - alias OpenApiSpex.Schema alias BlockScoutWeb.Schemas.API.V2.{Address, General, Token} + alias OpenApiSpex.Schema OpenApiSpex.schema(%{ type: :object, diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 74337756e236..e3ef168a216c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -8,8 +8,8 @@ defmodule BlockScoutWeb.AddressView do alias BlockScoutWeb.API.V2.TransactionView, as: APIV2TransactionView alias Explorer.Account.CustomABI alias Explorer.{Chain, CustomContractsHelper, Repo} - alias Explorer.Chain.Address.Counters alias Explorer.Chain.{Address, Hash, InternalTransaction, Log, SmartContract, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.Address.Counters alias Explorer.Chain.Block.Reward alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Market.Token, as: TokenExchangeRate diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 555cc6569352..0d43749c90b7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -1,10 +1,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do use BlockScoutWeb, :view - alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView} - alias Explorer.Chain.{Address, Data, MethodIdentifier, Transaction} alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView, TransactionView} - alias Explorer.Chain.{Address, AdvancedFilter, Data, Transaction} + alias Explorer.Chain.{Address, AdvancedFilter, Data, MethodIdentifier, Transaction} alias Explorer.Market alias Explorer.Market.MarketHistory diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex index 46e33442c49b..c49d5c39b6d4 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/arbitrum_view.ex @@ -3,9 +3,9 @@ defmodule BlockScoutWeb.API.V2.ArbitrumView do alias BlockScoutWeb.API.V2.ApiView alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper - alias Explorer.Chain.{Block, Hash, Transaction, Wei} alias Explorer.Chain.Arbitrum.{L1Batch, LifecycleTransaction} alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: SettlementReader + alias Explorer.Chain.{Block, Hash, Transaction, Wei} @doc """ Function to render error\\text responses for GET requests diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 7a67bcee785c..78e319c1db43 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -2,8 +2,8 @@ defmodule BlockScoutWeb.API.V2.BlockView do use BlockScoutWeb, :view use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - alias BlockScoutWeb.BlockView alias BlockScoutWeb.API.V2.{ApiView, Helper} + alias BlockScoutWeb.BlockView alias Explorer.Chain.Block def render("message.json", assigns) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex index 2a427947386e..893f8ffc98c8 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/celo_view.ex @@ -10,10 +10,10 @@ defmodule BlockScoutWeb.API.V2.CeloView do alias BlockScoutWeb.API.V2.{Helper, TokenTransferView, TokenView, TransactionView} alias Explorer.Chain + alias Explorer.Chain.{Address, Address.Reputation, Block, Token, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Cache.{CeloCoreContracts, CeloEpochs} - alias Explorer.Chain.Celo.Helper, as: CeloHelper alias Explorer.Chain.Celo.{Account, Epoch, EpochReward} - alias Explorer.Chain.{Address, Address.Reputation, Block, Token, TokenTransfer, Transaction, Wei} + alias Explorer.Chain.Celo.Helper, as: CeloHelper @address_params [ necessity_by_association: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex index 4599c2a02a57..ed3c1fa99f35 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/scroll_view.ex @@ -2,8 +2,8 @@ defmodule BlockScoutWeb.API.V2.ScrollView do use BlockScoutWeb, :view alias BlockScoutWeb.API.V2.TransactionView - alias Explorer.Chain.Scroll.{Batch, L1FeeParam, Reader} alias Explorer.Chain.{Data, Transaction} + alias Explorer.Chain.Scroll.{Batch, L1FeeParam, Reader} @api_true [api?: true] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index 1ee8ae9eefba..3d48b4ac0dc9 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -5,9 +5,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do import Explorer.SmartContract.Reader, only: [zip_tuple_values_with_types: 2] alias ABI.FunctionSelector + alias BlockScoutWeb.{AddressContractView, SmartContractView} alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper alias BlockScoutWeb.API.V2.TransactionView - alias BlockScoutWeb.{AddressContractView, SmartContractView} alias Ecto.Changeset alias Explorer.Chain alias Explorer.Chain.{Address, SmartContract, SmartContractAdditionalSource} diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex index 11311a747b55..65f9ee9b58dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex @@ -5,9 +5,9 @@ defmodule BlockScoutWeb.BlockView do alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.Cache.Counters.{BlockBurntFeeCount, BlockPriorityFeeCount} alias Explorer.Chain.{Block, Wei} alias Explorer.Chain.Block.Reward + alias Explorer.Chain.Cache.Counters.{BlockBurntFeeCount, BlockPriorityFeeCount} @dialyzer :no_match diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index f5dcb2c6a717..e763da414218 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -5,8 +5,8 @@ defmodule BlockScoutWeb.TransactionView do alias BlockScoutWeb.Account.AuthController alias BlockScoutWeb.Cldr.Number alias Explorer.{Chain, CustomContractsHelper, Repo} - alias Explorer.Chain.Block.Reward alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Wei} + alias Explorer.Chain.Block.Reward alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Market.Token alias Timex.Duration diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rsk.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rsk.ex index baff2def36f8..d19975cb7d2a 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rsk.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rsk.ex @@ -3,8 +3,8 @@ defmodule EthereumJSONRPC.RSK do Ethereum JSONRPC methods that are/are not supported by [RSK](https://www.rsk.co/). """ - alias EthereumJSONRPC.RSK.Traces alias EthereumJSONRPC.{Geth, TraceBlock} + alias EthereumJSONRPC.RSK.Traces @behaviour EthereumJSONRPC.Variant diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zilliqa/helper.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zilliqa/helper.ex index dc630270e34b..cbc800d47200 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zilliqa/helper.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/zilliqa/helper.ex @@ -2,8 +2,8 @@ defmodule EthereumJSONRPC.Zilliqa.Helper do @moduledoc """ Helper functions for processing consensus data. """ - alias EthereumJSONRPC.Zilliqa alias EthereumJSONRPC.{Block, Blocks} + alias EthereumJSONRPC.Zilliqa alias EthereumJSONRPC.Zilliqa.{ AggregateQuorumCertificate, diff --git a/apps/explorer/lib/explorer/account/api/key.ex b/apps/explorer/lib/explorer/account/api/key.ex index 2f86c1fabde2..8afd77756b9c 100644 --- a/apps/explorer/lib/explorer/account/api/key.ex +++ b/apps/explorer/lib/explorer/account/api/key.ex @@ -4,9 +4,9 @@ defmodule Explorer.Account.Api.Key do """ use Explorer.Schema + alias Ecto.{Changeset, UUID} alias Ecto.Multi alias Explorer.Account.Identity - alias Ecto.{Changeset, UUID} alias Explorer.Repo import Ecto.Changeset diff --git a/apps/explorer/lib/explorer/account/watchlist_notification.ex b/apps/explorer/lib/explorer/account/watchlist_notification.ex index 9d5c9d4ca74f..e84c8fcb8e9c 100644 --- a/apps/explorer/lib/explorer/account/watchlist_notification.ex +++ b/apps/explorer/lib/explorer/account/watchlist_notification.ex @@ -10,8 +10,8 @@ defmodule Explorer.Account.WatchlistNotification do import Explorer.Chain, only: [hash_to_lower_case_string: 1] alias Ecto.Multi - alias Explorer.Repo alias Explorer.Account.{Watchlist, WatchlistAddress} + alias Explorer.Repo typed_schema "account_watchlist_notifications" do field(:amount, :decimal, null: false) diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 542977167d2c..8e2ad724f9f5 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -150,12 +150,12 @@ defmodule Explorer.Chain.Address do alias Ecto.Association.NotLoaded alias Ecto.Changeset - alias Explorer.Chain.Cache.Accounts - alias Explorer.Chain.SmartContract.Proxy.EIP7702 + alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} alias Explorer.Chain.{Address, Data, Hash, InternalTransaction, SmartContract, Transaction} + alias Explorer.Chain.Cache.Accounts alias Explorer.Chain.Fetcher.{CheckBytecodeMatchingOnDemand, LookUpSmartContractSourcesOnDemand} + alias Explorer.Chain.SmartContract.Proxy.EIP7702 alias Explorer.Chain.SmartContract.Proxy.Models.Implementation - alias Explorer.{Chain, PagingOptions, Repo, SortingHelper} import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 85a7314eb874..29f4e77696c2 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -12,9 +12,9 @@ defmodule Explorer.Chain.Address.TokenBalance do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Block, Hash, Token} alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.{Address, Block, Hash, Token} @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index 38b2a52b8ad8..8f34147a5738 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -8,8 +8,8 @@ defmodule Explorer.Chain.AdvancedFilter do import Ecto.Query import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] - alias Explorer.Helper, as: ExplorerHelper alias Explorer.{Chain, Helper, PagingOptions} + alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.{ Address, diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/messages.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/messages.ex index cd51e8e0e3d3..ca7c8f9c400e 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/messages.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/messages.ex @@ -21,8 +21,8 @@ defmodule Explorer.Chain.Arbitrum.Reader.API.Messages do import Ecto.Query, only: [from: 2, limit: 2, where: 3] import Explorer.Chain, only: [select_repo: 1] - alias Explorer.Chain.Arbitrum.Message alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.Arbitrum.Message @api_true [api?: true] diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex index be77593cfc2b..75f18b62d542 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/api/settlement.ex @@ -31,10 +31,10 @@ defmodule Explorer.Chain.Arbitrum.Reader.API.Settlement do L1Batch } + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.Arbitrum.Reader.Common alias Explorer.Chain.Block, as: FullBlock alias Explorer.Chain.Cache.BackgroundMigrations, as: MigrationStatuses - alias Explorer.{Chain, PagingOptions} @api_true [api?: true] diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex index be2edad3ccf0..6c6d746c8deb 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex @@ -18,9 +18,9 @@ defmodule Explorer.Chain.Arbitrum.Reader.Indexer.Messages do Message } + alias Explorer.{Chain, Repo} alias Explorer.Chain.Block, as: FullBlock alias Explorer.Chain.{Hash, Log, Transaction} - alias Explorer.{Chain, Repo} # https://github.com/OffchainLabs/go-ethereum/blob/dff302de66598c36b964b971f72d35a95148e650/core/types/transaction.go#L44C2-L50 @message_to_l2_eth_deposit 100 diff --git a/apps/explorer/lib/explorer/chain/beacon/deposit.ex b/apps/explorer/lib/explorer/chain/beacon/deposit.ex index e9aef4045ad2..a3a69c44e46d 100644 --- a/apps/explorer/lib/explorer/chain/beacon/deposit.ex +++ b/apps/explorer/lib/explorer/chain/beacon/deposit.ex @@ -5,8 +5,8 @@ defmodule Explorer.Chain.Beacon.Deposit do use Explorer.Schema - alias Explorer.Chain.{Address, Block, Data, Hash, Log, Transaction, Wei} alias Explorer.{Chain, Repo, SortingHelper} + alias Explorer.Chain.{Address, Block, Data, Hash, Log, Transaction, Wei} @deposit_event_signature "0x649BBC62D0E31342AFEA4E5CD82D4049E7E1EE912FC0889AA790803BE39038C5" diff --git a/apps/explorer/lib/explorer/chain/beacon/reader.ex b/apps/explorer/lib/explorer/chain/beacon/reader.ex index 65fe20b977bc..36172e6d8e0e 100644 --- a/apps/explorer/lib/explorer/chain/beacon/reader.ex +++ b/apps/explorer/lib/explorer/chain/beacon/reader.ex @@ -18,8 +18,8 @@ defmodule Explorer.Chain.Beacon.Reader do import Explorer.Chain, only: [select_repo: 1] alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Block, DenormalizationHelper, Hash, Transaction} alias Explorer.Chain.Beacon.{Blob, BlobTransaction} + alias Explorer.Chain.{Block, DenormalizationHelper, Hash, Transaction} @doc """ Finds `t:Explorer.Chain.Beacon.Blob.t/0` by its `hash`. diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index c82fa0c57f92..abcffecdf0a8 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -5,9 +5,9 @@ defmodule Explorer.Chain.Blackfort.Validator do use Explorer.Schema + alias Explorer.{Chain, Helper, HttpClient, Repo, SortingHelper} alias Explorer.Chain.{Address, Import} alias Explorer.Chain.Hash.Address, as: HashAddress - alias Explorer.{Chain, Helper, HttpClient, Repo, SortingHelper} require Logger diff --git a/apps/explorer/lib/explorer/chain/block/reward.ex b/apps/explorer/lib/explorer/chain/block/reward.ex index f558c0eced42..524cf65b8d5a 100644 --- a/apps/explorer/lib/explorer/chain/block/reward.ex +++ b/apps/explorer/lib/explorer/chain/block/reward.ex @@ -7,8 +7,8 @@ defmodule Explorer.Chain.Block.Reward do alias Explorer.Application.Constants alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.Block.Reward.AddressType alias Explorer.Chain.{Address, Block, Hash, Validator, Wei} + alias Explorer.Chain.Block.Reward.AddressType alias Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand alias Explorer.Chain.SmartContract alias Explorer.SmartContract.Reader diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index c5994ce9207e..bbabb4560dd5 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -32,9 +32,9 @@ defmodule Explorer.Chain.Celo.ElectionReward do import Ecto.Query, only: [from: 2, where: 3, group_by: 3, select: 3] import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] - alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.{Chain, SortingHelper} alias Explorer.Chain.{Address, Address.Reputation, Celo.Epoch, Hash, Token, Wei} + alias Explorer.Chain.Cache.CeloCoreContracts @type type :: :voter | :validator | :group | :delegated_payment @types_enum ~w(voter validator group delegated_payment)a diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 15359cfb1780..0ef47726521b 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -8,8 +8,8 @@ defmodule Explorer.Chain.ContractMethod do import Ecto.Query, only: [from: 2] use Explorer.Schema - alias Explorer.Chain.{Data, Hash, MethodIdentifier, SmartContract} alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Data, Hash, MethodIdentifier, SmartContract} typed_schema "contract_methods" do field(:identifier, MethodIdentifier) diff --git a/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex b/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex index f70f4696d9e3..2117431689f0 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address/token_transfers.ex @@ -11,10 +11,10 @@ defmodule Explorer.Chain.CsvExport.Address.TokenTransfers do where: 3 ] - alias Explorer.{PagingOptions, Repo} alias Explorer.Chain.{Address, DenormalizationHelper, Hash, TokenTransfer, Transaction} alias Explorer.Chain.CsvExport.Helper alias Explorer.Helper, as: ExplorerHelper + alias Explorer.{PagingOptions, Repo} @spec export(Hash.Address.t(), String.t(), String.t(), Keyword.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() diff --git a/apps/explorer/lib/explorer/chain/csv_export/address/transactions.ex b/apps/explorer/lib/explorer/chain/csv_export/address/transactions.ex index 657b792ac4c5..25d7507c0f6c 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address/transactions.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address/transactions.ex @@ -3,10 +3,10 @@ defmodule Explorer.Chain.CsvExport.Address.Transactions do Exports transactions to a csv file. """ - alias Explorer.Market - alias Explorer.Market.MarketHistory alias Explorer.Chain.{Address, DenormalizationHelper, Hash, Transaction, Wei} alias Explorer.Chain.CsvExport.Helper + alias Explorer.Market + alias Explorer.Market.MarketHistory @spec export(Hash.Address.t(), String.t(), String.t(), Keyword.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 690c0a2dd806..9fce0a8fb796 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -5,8 +5,8 @@ defmodule Explorer.Chain.Import do alias Ecto.Changeset alias Explorer.Account.Notify - alias Explorer.Chain.Events.Publisher alias Explorer.Chain.{Block, Import} + alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Import.Stage alias Explorer.Repo diff --git a/apps/explorer/lib/explorer/chain/import/runner/addresses.ex b/apps/explorer/lib/explorer/chain/import/runner/addresses.ex index 7f416a810066..265a4bb44b8f 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/addresses.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/addresses.ex @@ -7,9 +7,9 @@ defmodule Explorer.Chain.Import.Runner.Addresses do import Explorer.Chain.Import.Runner.Helper, only: [chain_type_dependent_import: 3] alias Ecto.{Multi, Repo} + alias Explorer.Chain.{Address, Import, Transaction} alias Explorer.Chain.Filecoin.PendingAddressOperation, as: FilecoinPendingAddressOperation alias Explorer.Chain.Import.Runner - alias Explorer.Chain.{Address, Import, Transaction} alias Explorer.Prometheus.Instrumenter require Ecto.Query diff --git a/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex index e3f61616cd19..251f81ea5534 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/beacon/blob_transactions.ex @@ -7,8 +7,8 @@ defmodule Explorer.Chain.Import.Runner.Beacon.BlobTransactions do import Ecto.Query, only: [from: 2] - alias Explorer.Chain.Beacon.BlobTransaction alias Ecto.{Multi, Repo} + alias Explorer.Chain.Beacon.BlobTransaction alias Explorer.Chain.{Hash, Import} alias Explorer.Prometheus.Instrumenter diff --git a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex index f1ed8bf05374..5aa7d248629b 100644 --- a/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex +++ b/apps/explorer/lib/explorer/chain/multichain_search_db/balances_export_queue.ex @@ -6,8 +6,8 @@ defmodule Explorer.Chain.MultichainSearchDb.BalancesExportQueue do use Explorer.Schema import Ecto.Query alias Ecto.Multi - alias Explorer.Repo alias Explorer.Chain.{Hash, Wei} + alias Explorer.Repo @required_attrs ~w(address_hash token_contract_address_hash_or_native)a @optional_attrs ~w(value token_id retries_number)a diff --git a/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex index 9e983a6d846f..f35f39f13e19 100644 --- a/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex +++ b/apps/explorer/lib/explorer/chain/polygon_zkevm/reader.ex @@ -12,8 +12,8 @@ defmodule Explorer.Chain.PolygonZkevm.Reader do import Explorer.Chain, only: [select_repo: 1] - alias Explorer.Chain.PolygonZkevm.{BatchTransaction, Bridge, BridgeL1Token, LifecycleTransaction, TransactionBatch} alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.PolygonZkevm.{BatchTransaction, Bridge, BridgeL1Token, LifecycleTransaction, TransactionBatch} alias Explorer.Prometheus.Instrumenter alias Indexer.Helper diff --git a/apps/explorer/lib/explorer/chain/scroll/reader.ex b/apps/explorer/lib/explorer/chain/scroll/reader.ex index a53487debdd1..62b33728f534 100644 --- a/apps/explorer/lib/explorer/chain/scroll/reader.ex +++ b/apps/explorer/lib/explorer/chain/scroll/reader.ex @@ -14,9 +14,9 @@ defmodule Explorer.Chain.Scroll.Reader do import Explorer.Chain, only: [select_repo: 1] - alias Explorer.Chain.Scroll.{Batch, BatchBundle, Bridge, L1FeeParam} alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Block, Transaction} + alias Explorer.Chain.Scroll.{Batch, BatchBundle, Bridge, L1FeeParam} alias Explorer.Prometheus.Instrumenter @doc """ diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex index ac483648e67e..6e95cc136317 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/verification_status.ex @@ -7,9 +7,9 @@ defmodule Explorer.Chain.SmartContract.Proxy.VerificationStatus do import Ecto.Changeset + alias Explorer.{Chain, Repo} alias Explorer.Chain.Hash alias Explorer.Chain.SmartContract.Proxy.Models.Implementation - alias Explorer.{Chain, Repo} @typep status :: integer() | atom() diff --git a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex index 4ababe2d5824..beef2a0c8d89 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex @@ -7,8 +7,8 @@ defmodule Explorer.Chain.SmartContract.VerificationStatus do import Ecto.Changeset - alias Explorer.Chain.Hash alias Explorer.{Chain, Repo} + alias Explorer.Chain.Hash alias Explorer.SmartContract.Solidity.PublisherWorker, as: SolidityPublisherWorker alias Que.Persistence, as: QuePersistence diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex index 8ba82db8c595..c2eee52fdc9b 100644 --- a/apps/explorer/lib/explorer/chain/stability/validator.ex +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -5,9 +5,9 @@ defmodule Explorer.Chain.Stability.Validator do use Explorer.Schema + alias Explorer.{Chain, Repo, SortingHelper} alias Explorer.Chain.{Address, Block, Import} alias Explorer.Chain.Hash.Address, as: HashAddress - alias Explorer.{Chain, Repo, SortingHelper} alias Explorer.SmartContract.Reader require Logger diff --git a/apps/explorer/lib/explorer/chain/validator.ex b/apps/explorer/lib/explorer/chain/validator.ex index 76447d851921..4006d2a6acc9 100644 --- a/apps/explorer/lib/explorer/chain/validator.ex +++ b/apps/explorer/lib/explorer/chain/validator.ex @@ -4,8 +4,8 @@ defmodule Explorer.Chain.Validator do """ use Explorer.Schema - alias Explorer.Chain.Hash.Address alias Explorer.{Chain, Repo} + alias Explorer.Chain.Hash.Address @primary_key false typed_schema "validators" do diff --git a/apps/explorer/lib/explorer/chain_spec/poa/importer.ex b/apps/explorer/lib/explorer/chain_spec/poa/importer.ex index d97da4b8a979..1abfea0b0347 100644 --- a/apps/explorer/lib/explorer/chain_spec/poa/importer.ex +++ b/apps/explorer/lib/explorer/chain_spec/poa/importer.ex @@ -5,11 +5,11 @@ defmodule Explorer.ChainSpec.POA.Importer do require Logger + alias Explorer.Chain.Block.{EmissionReward, Range} alias Explorer.Chain.Wei + alias Explorer.ChainSpec.GenesisData alias Explorer.Repo alias Explorer.SmartContract.Reader - alias Explorer.Chain.Block.{EmissionReward, Range} - alias Explorer.ChainSpec.GenesisData import Ecto.Query diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 7ccc0f0502d4..02fc5f4cec6a 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -7,11 +7,11 @@ defmodule Explorer.Etherscan do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] - alias Explorer.Etherscan.Logs alias Explorer.{Chain, Repo} - alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} alias Explorer.Chain.{Address, Block, DenormalizationHelper, Hash, InternalTransaction, TokenTransfer, Transaction} + alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Etherscan.Logs @default_options %{ order_by_direction: :desc, diff --git a/apps/explorer/lib/explorer/etherscan/contracts.ex b/apps/explorer/lib/explorer/etherscan/contracts.ex index ce33dc54fa5c..4906690a92d3 100644 --- a/apps/explorer/lib/explorer/etherscan/contracts.ex +++ b/apps/explorer/lib/explorer/etherscan/contracts.ex @@ -11,10 +11,10 @@ defmodule Explorer.Etherscan.Contracts do where: 3 ] - alias Explorer.Repo alias Explorer.Chain.{Address, Hash, SmartContract} alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + alias Explorer.Repo @doc """ Returns address with preloaded SmartContract and proxy info if it exists diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 6d2e80422739..91d625af435b 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -4,9 +4,9 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do """ alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Block, Hash, Token, Transaction, Wei} alias Explorer.Chain.Block.Range + alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.MultichainSearchDb.{ BalancesExportQueue, diff --git a/apps/explorer/lib/explorer/migrator/celo_accounts.ex b/apps/explorer/lib/explorer/migrator/celo_accounts.ex index dabd43691c1f..9d8cf35a0fa5 100644 --- a/apps/explorer/lib/explorer/migrator/celo_accounts.ex +++ b/apps/explorer/lib/explorer/migrator/celo_accounts.ex @@ -10,8 +10,8 @@ defmodule Explorer.Migrator.CeloAccounts do import Ecto.Query alias Explorer.{Chain, Repo} - alias Explorer.Chain.Celo.Legacy.{Accounts, Events} alias Explorer.Chain.Celo.{Account, PendingAccountOperation} + alias Explorer.Chain.Celo.Legacy.{Accounts, Events} alias Explorer.Chain.Log alias Explorer.Migrator.FillingMigration diff --git a/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex b/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex index c3789927be49..3e3295000809 100644 --- a/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex +++ b/apps/explorer/lib/explorer/migrator/celo_l2_epochs.ex @@ -15,9 +15,9 @@ defmodule Explorer.Migrator.CeloL2Epochs do import Ecto.Query alias Explorer.Chain.Celo.Epoch - alias Explorer.{Helper, Repo} alias Explorer.Chain.{Import, Log} alias Explorer.Chain.Import.Runner.Celo.Epochs + alias Explorer.{Helper, Repo} alias Explorer.Migrator.FillingMigration @migration_name "celo_l2_epochs" diff --git a/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex index 31c3ec94deec..7a0cb767439f 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex @@ -10,10 +10,10 @@ defmodule Explorer.Migrator.ReindexBlocksWithMissingTransactions do import Ecto.Query - alias Explorer.Repo alias Explorer.Chain.{Block, Transaction} alias Explorer.Chain.Cache.BlockNumber alias Explorer.Migrator.{FillingMigration, MigrationStatus} + alias Explorer.Repo @migration_name "reindex_blocks_with_missing_transactions" diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex index e48fdd44e6f7..b703ca4917af 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex @@ -4,8 +4,8 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do """ require Logger - alias Explorer.Account.Identity alias Explorer.{Account, Helper, HttpClient} + alias Explorer.Account.Identity alias Explorer.ThirdPartyIntegrations.Auth0.Internal alias Ueberauth.Auth alias Ueberauth.Strategy.Auth0.OAuth diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex index 685f2a43df47..8f1dcb408fdb 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex @@ -17,8 +17,8 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Internal do require Logger - alias Explorer.Account.Identity alias Explorer.{Account, Helper, Repo} + alias Explorer.Account.Identity alias Explorer.Chain.Address alias Explorer.ThirdPartyIntegrations.Auth0 alias Explorer.ThirdPartyIntegrations.Auth0.{Legacy, Migrated} diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 905bbd387294..4c9836efd7b2 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -5,9 +5,9 @@ defmodule Explorer.Token.MetadataRetriever do require Logger - alias Explorer.{HttpClient, MetadataURIValidator} alias Explorer.Chain.{Hash, Token} alias Explorer.Helper, as: ExplorerHelper + alias Explorer.{HttpClient, MetadataURIValidator} alias Explorer.SmartContract.Reader @no_uri_error "no uri" diff --git a/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex b/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex index 0f55492d61b1..0a556e6dd282 100644 --- a/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex +++ b/apps/explorer/lib/explorer/token_instance_owner_address_migration/helper.ex @@ -8,8 +8,8 @@ defmodule Explorer.TokenInstanceOwnerAddressMigration.Helper do ] alias Explorer.{Chain, Repo} - alias Explorer.Chain.Token.Instance alias Explorer.Chain.{SmartContract, TokenTransfer} + alias Explorer.Chain.Token.Instance require Logger diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex index 4cf769aaff91..87a369f990ce 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/discovery.ex @@ -22,8 +22,8 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Discovery do alias Explorer.Chain.Arbitrum alias Explorer.Chain.Events.Publisher - alias Indexer.Fetcher.Arbitrum.DA.Common, as: DataAvailabilityInfo alias Indexer.Fetcher.Arbitrum.DA.{Anytrust, Celestia, Eigenda} + alias Indexer.Fetcher.Arbitrum.DA.Common, as: DataAvailabilityInfo alias Indexer.Fetcher.Arbitrum.Utils.Db.Messages, as: DbMessages alias Indexer.Fetcher.Arbitrum.Utils.Db.ParentChainTransactions, as: DbParentChainTransactions alias Indexer.Fetcher.Arbitrum.Utils.Db.Settlement, as: DbSettlement diff --git a/apps/indexer/lib/indexer/fetcher/beacon/blob.ex b/apps/indexer/lib/indexer/fetcher/beacon/blob.ex index b42a49f4824e..88e7067f2bd5 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/blob.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/blob.ex @@ -8,9 +8,9 @@ defmodule Indexer.Fetcher.Beacon.Blob do require Logger - alias Explorer.Repo alias Explorer.Chain.Beacon.{Blob, Reader} alias Explorer.Chain.Data + alias Explorer.Repo alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.Beacon.Blob.Supervisor, as: BlobSupervisor alias Indexer.Fetcher.Beacon.Client diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex index 91d3ab9289b3..23f7eb25634a 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/validator_and_group_payments_prior_l2_migration.ex @@ -4,8 +4,8 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.ValidatorAndGroupPaymentsPri """ import Ecto.Query, only: [from: 2] - alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.Chain.{Block, Celo.Epoch, Log} + alias Explorer.Chain.Cache.CeloCoreContracts alias Explorer.Repo alias Indexer.Transform.Celo.ValidatorEpochPaymentDistributions diff --git a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex index 98fbbbaa29d2..665ed7fb9aec 100644 --- a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex +++ b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex @@ -14,9 +14,9 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do alias EthereumJSONRPC.Block.ByNumber alias EthereumJSONRPC.Blocks - alias Explorer.Repo alias Explorer.Chain.{Block, Hash, PendingOperationsHelper, Transaction} alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Repo @update_timeout 60_000 diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex index ce9c23ce5fa4..edf3b4696174 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -8,8 +8,8 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do use Indexer.Fetcher, restart: :permanent use Spandex.Decorators - alias Explorer.Chain.Wei alias Explorer.Chain.{Hash, MultichainSearchDb.BalancesExportQueue} + alias Explorer.Chain.Wei alias Explorer.MicroserviceInterfaces.MultichainSearch alias Explorer.Repo diff --git a/apps/indexer/lib/indexer/fetcher/scroll/batch.ex b/apps/indexer/lib/indexer/fetcher/scroll/batch.ex index da6d6054dcb1..ba1f02263ea6 100644 --- a/apps/indexer/lib/indexer/fetcher/scroll/batch.ex +++ b/apps/indexer/lib/indexer/fetcher/scroll/batch.ex @@ -24,10 +24,10 @@ defmodule Indexer.Fetcher.Scroll.Batch do alias ABI.{FunctionSelector, TypeDecoder} alias Ecto.Multi alias EthereumJSONRPC.Logs + alias Explorer.{Chain, Repo} alias Explorer.Chain.Block.Range, as: BlockRange alias Explorer.Chain.RollupReorgMonitorQueue alias Explorer.Chain.Scroll.{Batch, BatchBundle, Reader} - alias Explorer.{Chain, Repo} alias Indexer.Fetcher.RollupL1ReorgMonitor alias Indexer.Fetcher.Scroll.Helper, as: ScrollHelper alias Indexer.Helper diff --git a/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex index ff283612d6dc..9fece6bc1a89 100644 --- a/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex +++ b/apps/indexer/lib/indexer/fetcher/shibarium/l1.ex @@ -23,9 +23,9 @@ defmodule Indexer.Fetcher.Shibarium.L1 do import Indexer.Fetcher.Shibarium.Helper, only: [calc_operation_hash: 5, prepare_insert_items: 2, recalculate_cached_count: 0] + alias Explorer.{Chain, Repo} alias Explorer.Chain.RollupReorgMonitorQueue alias Explorer.Chain.Shibarium.Bridge - alias Explorer.{Chain, Repo} alias Indexer.Fetcher.RollupL1ReorgMonitor alias Indexer.Helper alias Indexer.Transform.Addresses diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex index 2361f98753d4..9cbea8da889d 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/sanitize_erc1155.ex @@ -7,9 +7,9 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155 do use GenServer, restart: :transient + alias Explorer.{Chain, Repo} alias Explorer.Chain.Token.Instance alias Explorer.Migrator.MigrationStatus - alias Explorer.{Chain, Repo} alias Indexer.Fetcher.TokenInstance.Sanitize @migration_name "backfill_erc1155" diff --git a/apps/indexer/lib/indexer/transform/token_transfers.ex b/apps/indexer/lib/indexer/transform/token_transfers.ex index 03e894d9a323..74892e9eb130 100644 --- a/apps/indexer/lib/indexer/transform/token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/token_transfers.ex @@ -8,8 +8,8 @@ defmodule Indexer.Transform.TokenTransfers do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.Helper, only: [decode_data: 2, truncate_address_hash: 1] - alias Explorer.Repo alias Explorer.Chain.{Hash, Token, TokenTransfer} + alias Explorer.Repo alias Indexer.Fetcher.TokenTotalSupplyUpdater @doc """ diff --git a/apps/indexer/lib/indexer/transform/transaction_actions.ex b/apps/indexer/lib/indexer/transform/transaction_actions.ex index 14805430f26f..42c0ee541e00 100644 --- a/apps/indexer/lib/indexer/transform/transaction_actions.ex +++ b/apps/indexer/lib/indexer/transform/transaction_actions.ex @@ -9,8 +9,8 @@ defmodule Indexer.Transform.TransactionActions do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.Helper, only: [decode_data: 2] - alias Explorer.Chain.Cache.{ChainId, TransactionActionTokensData, TransactionActionUniswapPools} alias Explorer.Chain.{Address, Hash, Token, TransactionAction} + alias Explorer.Chain.Cache.{ChainId, TransactionActionTokensData, TransactionActionUniswapPools} alias Explorer.Repo alias Indexer.Helper, as: IndexerHelper diff --git a/mix.lock b/mix.lock index 53b15a4ae21e..39608f573693 100644 --- a/mix.lock +++ b/mix.lock @@ -32,7 +32,7 @@ "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, - "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, + "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, @@ -76,7 +76,7 @@ "exvcr": {:hex, :exvcr, "0.16.0", "11579f43c88ae81f57c82ce4f09e3ebda4c40117c859ed39e61a653c3a0b4ff4", [:mix], [{:exjsx, "~> 4.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:finch, "~> 0.16", [hex: :finch, repo: "hexpm", optional: true]}, {:httpoison, "~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: true]}, {:httpotion, "~> 3.1", [hex: :httpotion, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:meck, "~> 0.9", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "8f576af22369942f7a1482baff1f31e2f45983cf6fac45d49d2bd2e84b4d5be8"}, "ezstd": {:hex, :ezstd, "1.2.3", "98748f4099e6e2a067f77ace43041ebaa53c13194b08ce22370e4c93079e9e16", [:rebar3], [], "hexpm", "de32e0b41ba36a9ed46db8215da74777d2f141bb75f67bfc05dbb4b7c3386dee"}, "file_info": {:hex, :file_info, "0.0.4", "2e0e77f211e833f38ead22cb29ce53761d457d80b3ffe0ffe0eb93880b0963b2", [:mix], [{:mimetype_parser, "~> 0.1.2", [hex: :mimetype_parser, repo: "hexpm", optional: false]}], "hexpm", "50e7ad01c2c8b9339010675fe4dc4a113b8d6ca7eddce24d1d74fd0e762781a5"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "floki": {:hex, :floki, "0.38.0", "62b642386fa3f2f90713f6e231da0fa3256e41ef1089f83b6ceac7a3fd3abf33", [:mix], [], "hexpm", "a5943ee91e93fb2d635b612caf5508e36d37548e84928463ef9dd986f0d1abd9"}, "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, From ed8549d6cb3e7ed2a66befe336650b75b246c8a1 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Tue, 11 Nov 2025 19:33:01 +0300 Subject: [PATCH 0464/1006] fix: null-checks for distribution field in celo epochs api (#13457) --- .../lib/block_scout_web/views/api/rpc/celo_view.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex index 5efc953d81c1..9b3bc9cebcd3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/celo_view.ex @@ -20,9 +20,17 @@ defmodule BlockScoutWeb.API.RPC.CeloView do "endProcessingBlockHash" => epoch.end_processing_block && to_string(epoch.end_processing_block.hash), "endProcessingBlockNumber" => epoch.end_processing_block && to_string(epoch.end_processing_block.number), "carbonOffsettingTargetEpochRewards" => - distribution.carbon_offsetting_transfer && distribution.carbon_offsetting_transfer.amount, - "communityTargetEpochRewards" => distribution.community_transfer && distribution.community_transfer.amount, - "reserveBolster" => distribution.reserve_bolster_transfer && distribution.reserve_bolster_transfer.amount, + distribution && + distribution.carbon_offsetting_transfer && + distribution.carbon_offsetting_transfer.amount, + "communityTargetEpochRewards" => + distribution && + distribution.community_transfer && + distribution.community_transfer.amount, + "reserveBolster" => + distribution && + distribution.reserve_bolster_transfer && + distribution.reserve_bolster_transfer.amount, "voterTargetEpochRewards" => aggregated_rewards.voter && aggregated_rewards.voter.total, "validatorTargetEpochRewards" => aggregated_rewards.validator && aggregated_rewards.validator.total } From ed1386c6af2a5f9c3f4f5cf7ae4dd0bf0961529f Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Wed, 12 Nov 2025 11:20:45 +0300 Subject: [PATCH 0465/1006] feat: scope celo under optimism chain type (#13375) --- .github/workflows/config.yml | 4 +- .github/workflows/generate-swagger.yml | 4 +- .github/workflows/pre-release-celo.yml | 4 +- .../publish-docker-image-for-celo.yml | 2 +- .github/workflows/release-celo.yml | 4 +- .../controllers/api/v2/address_controller.ex | 10 +- .../controllers/api/v2/block_controller.ex | 21 +- .../api/v2/main_page_controller.ex | 8 +- .../controllers/api/v2/stats_controller.ex | 17 +- .../api/v2/transaction_controller.ex | 11 +- .../lib/block_scout_web/graphql/schema.ex | 6 +- .../block_scout_web/graphql/schema/types.ex | 6 +- .../lib/block_scout_web/notifier.ex | 8 +- .../lib/block_scout_web/routers/api_router.ex | 9 +- .../schemas/api/v2/address/response.ex | 10 +- .../block_scout_web/schemas/api/v2/block.ex | 21 +- .../schemas/api/v2/token_transfer.ex | 7 +- .../schemas/api/v2/transaction.ex | 39 ++-- .../transaction/summary_just_request_body.ex | 2 +- .../views/api/v2/address_view.ex | 10 +- .../views/api/v2/block_view.ex | 37 ++-- .../views/api/v2/transaction_view.ex | 69 +++++-- .../api/v2/address_controller_test.exs | 4 +- .../api/v2/block_controller_test.exs | 2 +- .../api/v2/transaction_controller_test.exs | 9 +- .../lib/ethereum_jsonrpc/transaction.ex | 61 ++++-- apps/explorer/config/config.exs | 1 + apps/explorer/lib/explorer/chain/address.ex | 34 ++-- .../lib/explorer/chain/address/counters.ex | 5 +- apps/explorer/lib/explorer/chain/block.ex | 42 ++-- .../explorer/chain/cache/gas_price_oracle.ex | 7 +- .../explorer/chain/import/runner/blocks.ex | 6 +- .../explorer/chain/import/runner/helper.ex | 17 ++ .../lib/explorer/chain/import/runner/logs.ex | 30 +-- .../chain/import/runner/token_transfers.ex | 29 +-- .../chain/import/runner/transactions.ex | 52 +++-- .../chain/import/stage/chain_type_specific.ex | 42 ++-- apps/explorer/lib/explorer/chain/log.ex | 21 +- .../lib/explorer/chain/token_transfer.ex | 26 +-- .../lib/explorer/chain/transaction.ex | 73 ++++--- .../chain/cache/gas_price_oracle_test.exs | 5 +- .../chain/import/runner/blocks_test.exs | 6 +- .../explorer/chain/token_transfer_test.exs | 4 +- .../explorer/migrator/celo_accounts_test.exs | 4 +- .../explorer/migrator/celo_l2_epochs_test.exs | 4 +- apps/indexer/lib/indexer/block/fetcher.ex | 93 ++++++--- .../delegated_payments_prior_l2_migration.ex | 2 +- .../lib/indexer/fetcher/celo/epoch_logs.ex | 5 +- .../indexer/fetcher/internal_transaction.ex | 7 +- .../transform/address_coin_balances.ex | 4 +- .../lib/indexer/transform/celo/l1_epochs.ex | 4 +- .../lib/indexer/transform/celo/l2_epochs.ex | 4 +- .../transform/celo/transaction_gas_tokens.ex | 4 +- .../celo/transaction_token_transfers.ex | 5 +- .../test/indexer/block/fetcher_test.exs | 5 +- .../indexer/block/realtime/fetcher_test.exs | 7 +- .../fetcher/internal_transaction_test.exs | 5 +- config/config_helper.exs | 186 ++++++++++++++---- config/runtime.exs | 15 +- 59 files changed, 758 insertions(+), 381 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 8814caeaeb4c..f21729cfd1cc 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -53,10 +53,10 @@ jobs: "default", "arbitrum", "blackfort", - "celo", "ethereum", "filecoin", "optimism", + "optimism-celo", "polygon_zkevm", "rsk", "scroll", @@ -73,7 +73,7 @@ jobs: "default", "ethereum", "optimism", - "celo" + "optimism-celo" ]; const labels = ${{ github.event_name == 'pull_request' && toJson(github.event.pull_request.labels.*.name) || '[]' }}; diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 731b9b7a3270..6fa536069ee3 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -35,10 +35,10 @@ jobs: "default", "arbitrum", "blackfort", - "celo", "ethereum", "filecoin", "optimism", + "optimism-celo", "polygon_zkevm", "rsk", "scroll", @@ -201,4 +201,4 @@ jobs: if: always() run: | rm -rf temp-specs - rm -rf api-specs \ No newline at end of file + rm -rf api-specs diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 2d2884719295..379762b01455 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -48,7 +48,7 @@ jobs: build-args: | BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo + CHAIN_TYPE=optimism-celo - name: Build and push Docker image for CELO (indexer) uses: docker/build-push-action@v6 @@ -65,4 +65,4 @@ jobs: DISABLE_API=true BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}-alpha.${{ inputs.number }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo + CHAIN_TYPE=optimism-celo diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 6f0833864ed1..fbeadebcd2cf 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest env: RELEASE_VERSION: 9.2.2 - DOCKER_CHAIN_NAME: celo + DOCKER_CHAIN_NAME: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index d650d462e8de..54c5718698f3 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -46,7 +46,7 @@ jobs: API_GRAPHQL_MAX_COMPLEXITY=${{ env.API_GRAPHQL_MAX_COMPLEXITY }} BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo + CHAIN_TYPE=optimism-celo - name: Build and push Docker image for CELO (indexer) uses: docker/build-push-action@v6 @@ -64,5 +64,5 @@ jobs: DISABLE_API=true BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - CHAIN_TYPE=celo + CHAIN_TYPE=optimism-celo diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index ba2533151289..2a8b26c78cc2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -1,6 +1,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] use OpenApiSpex.ControllerSpecs @@ -55,8 +55,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias Indexer.Fetcher.OnDemand.ContractCode, as: ContractCodeOnDemand alias Indexer.Fetcher.OnDemand.TokenBalance, as: TokenBalanceOnDemand - case @chain_type do - :celo -> + case @chain_identity do + {:optimism, :celo} -> @chain_type_transaction_necessity_by_association %{ [gas_token: reputation_association()] => :optional } @@ -89,8 +89,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do api?: true ] - case @chain_type do - :celo -> + case @chain_identity do + {:optimism, :celo} -> @chain_type_address_necessity_by_association %{ [ celo_account: [ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 40bc1c2df08a..fd0861b239b3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -1,6 +1,10 @@ defmodule BlockScoutWeb.API.V2.BlockController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Chain, @@ -54,7 +58,14 @@ defmodule BlockScoutWeb.API.V2.BlockController do } :optimism -> - @chain_type_transaction_necessity_by_association %{} + if @chain_identity == {:optimism, :celo} do + @chain_type_transaction_necessity_by_association %{ + [gas_token: reputation_association()] => :optional + } + else + @chain_type_transaction_necessity_by_association %{} + end + @chain_type_block_necessity_by_association %{ :op_frame_sequence => :optional } @@ -68,12 +79,6 @@ defmodule BlockScoutWeb.API.V2.BlockController do :zksync_execute_transaction => :optional } - :celo -> - @chain_type_transaction_necessity_by_association %{ - [gas_token: reputation_association()] => :optional - } - @chain_type_block_necessity_by_association %{} - :arbitrum -> @chain_type_transaction_necessity_by_association %{} @chain_type_block_necessity_by_association %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index b278d14dc9f6..5bb96e30d04b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -1,6 +1,8 @@ defmodule BlockScoutWeb.API.V2.MainPageController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] alias BlockScoutWeb.API.V2.{BlockView, OptimismView, TransactionView} alias Explorer.{Chain, PagingOptions} @@ -11,8 +13,8 @@ defmodule BlockScoutWeb.API.V2.MainPageController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] - case @chain_type do - :celo -> + case @chain_identity do + {:optimism, :celo} -> @chain_type_transaction_necessity_by_association %{ [gas_token: reputation_association()] => :optional } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index daede28dd3de..8715fc4a3790 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.StatsController do + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + use Phoenix.Controller, namespace: BlockScoutWeb - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController @@ -79,6 +82,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do "network_utilization_percentage" => network_utilization_percentage() } |> add_chain_type_fields() + |> add_chain_identity_fields() |> backward_compatibility(conn) ) end @@ -194,13 +198,18 @@ defmodule BlockScoutWeb.API.V2.StatsController do response |> Map.put("last_output_root_size", fetch(@api_true)) end - :celo -> - defp add_chain_type_fields(response) do + _ -> + defp add_chain_type_fields(response), do: response + end + + case @chain_identity do + {:optimism, :celo} -> + defp add_chain_identity_fields(response) do alias Explorer.Chain.Cache.CeloEpochs response |> Map.put("celo", %{"epoch_number" => CeloEpochs.last_block_epoch_number()}) end _ -> - defp add_chain_type_fields(response), do: response + defp add_chain_identity_fields(response), do: response end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index f8b7a3577fae..df52c2ebfb9a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.TransactionController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Account.AuthController, only: [current_user: 1] @@ -65,13 +68,13 @@ defmodule BlockScoutWeb.API.V2.TransactionController do tags(["transactions"]) - case @chain_type do - :ethereum -> + case @chain_identity do + {:ethereum, nil} -> @chain_type_transaction_necessity_by_association %{ :beacon_blob_transaction => :optional } - :celo -> + {:optimism, :celo} -> @chain_type_transaction_necessity_by_association %{ [gas_token: reputation_association()] => :optional } diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex index e94ed7ddffbc..727aea396e35 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.GraphQL.Schema do use Absinthe.Schema use Absinthe.Relay.Schema, :modern - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] alias Absinthe.Middleware.Dataloader, as: AbsintheDataloaderMiddleware alias Absinthe.Plugin, as: AbsinthePlugin @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.GraphQL.Schema do import_types(BlockScoutWeb.GraphQL.Schema.Types) - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do import_types(BlockScoutWeb.GraphQL.Celo.Schema.Types) end @@ -108,7 +108,7 @@ defmodule BlockScoutWeb.GraphQL.Schema do resolve(&Transaction.get_by/3) end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do require BlockScoutWeb.GraphQL.Celo.QueryFields alias BlockScoutWeb.GraphQL.Celo.QueryFields diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex index bcb49f5826db..f710239f0c61 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex @@ -1,11 +1,11 @@ defmodule BlockScoutWeb.GraphQL.Schema.Transaction do @moduledoc false - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] alias BlockScoutWeb.GraphQL.Resolvers.{Block, InternalTransaction} - case @chain_type do - :celo -> + case @chain_identity do + {:optimism, :celo} -> @chain_type_fields quote( do: [ field(:gas_token_contract_address_hash, :address_hash) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index ab812e4f28c5..03faf243c111 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -2,7 +2,9 @@ defmodule BlockScoutWeb.Notifier do @moduledoc """ Responds to events by sending appropriate channel updates to front-end. """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] require Logger @@ -63,8 +65,8 @@ defmodule BlockScoutWeb.Notifier do nil end - case @chain_type do - :celo -> + case @chain_identity do + {:optimism, :celo} -> @chain_type_transaction_associations [ gas_token: Reputation.reputation_association() ] diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index a1ae824e4d73..dd816fb7c55f 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -17,6 +17,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity], graphql_enabled: [:block_scout_web, [Api.GraphQL, :enabled]], graphql_max_complexity: [:block_scout_web, [Api.GraphQL, :max_complexity]], graphql_token_limit: [:block_scout_web, [Api.GraphQL, :token_limit]], @@ -141,7 +142,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/csv-export", V2.ConfigController, :csv_export) get("/public-metrics", V2.ConfigController, :public_metrics) - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do get("/celo", V2.ConfigController, :celo) end end @@ -251,7 +252,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/:address_hash_param/nft", V2.AddressController, :nft_list) get("/:address_hash_param/nft/collections", V2.AddressController, :nft_collections) - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do get("/:address_hash_param/celo/election-rewards", V2.AddressController, :celo_election_rewards) get("/:address_hash_param/celo/election-rewards/csv", V2.CsvExportController, :celo_election_rewards_csv) end @@ -322,7 +323,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do scope "/celo/epochs" do get("/", V2.CeloController, :epochs) get("/:number", V2.CeloController, :epoch) @@ -573,7 +574,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do "transaction" => {RPC.TransactionController, []} } |> then(fn options -> - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do Map.put(options, "epoch", {BlockScoutWeb.API.RPC.CeloController, []}) else options diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index ed28d1972827..7ea622783edb 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -5,6 +5,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations alias BlockScoutWeb.Schemas.{API.V2.Address, Helper} alias OpenApiSpex.Schema + use Utils.RuntimeEnvHelper, chain_identity: [:explorer, :chain_identity] + @filecoin_robust_address_schema %Schema{ type: :string, description: "Robust f0/f1/f2/f3/f4 Filecoin address", @@ -78,22 +80,22 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations """ @spec chain_type_fields(map()) :: map() def chain_type_fields(schema) do - case Application.get_env(:explorer, :chain_type) do - :filecoin -> + case chain_identity() do + {:filecoin, nil} -> schema |> Helper.extend_schema( properties: %{creator_filecoin_robust_address: @filecoin_robust_address_schema}, required: [:creator_filecoin_robust_address] ) - :zilliqa -> + {:zilliqa, nil} -> schema |> Helper.extend_schema( properties: %{zilliqa: @zilliqa_schema}, required: [:zilliqa] ) - :celo -> + {:optimism, :celo} -> schema |> Helper.extend_schema( properties: %{celo: @celo_schema}, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex index c62571030822..fc414dcc6532 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block.ex @@ -5,6 +5,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + @zksync_schema %Schema{ type: :object, nullable: false, @@ -245,7 +249,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do """ @spec chain_type_fields(map()) :: map() def chain_type_fields(schema) do - case Application.get_env(:explorer, :chain_type) do + chain_type() + |> case do :rsk -> schema |> Helper.extend_schema( @@ -281,15 +286,23 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.ChainTypeCustomizations do required: [:blob_transactions_count, :blob_gas_used, :excess_blob_gas, :beacon_deposits_count] ) - :celo -> - schema |> Helper.extend_schema(properties: %{celo: @celo_schema}, required: [:celo]) - :zilliqa -> schema |> Helper.extend_schema(properties: %{zilliqa: @zilliqa_schema}) _ -> schema end + |> chain_identity_fields() + end + + defp chain_identity_fields(schema) do + case chain_identity() do + {:optimism, :celo} -> + schema |> Helper.extend_schema(properties: %{celo: @celo_schema}) + + _ -> + schema + end end end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index f57b1bcf6ca8..f9a39847acfd 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -1,13 +1,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TransactionHashCustomization do @moduledoc false + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + require OpenApiSpex alias OpenApiSpex.Schema alias BlockScoutWeb.Schemas.API.V2.General def schema do - case Application.get_env(:explorer, :chain_type) do - :celo -> + case chain_identity() do + {:optimism, :celo} -> General.FullHashNullable _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex index dda00876db5f..cc11fed2369d 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction.ex @@ -6,6 +6,10 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + @zksync_schema %Schema{ type: :object, nullable: false, @@ -149,7 +153,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do @spec chain_type_fields(map()) :: map() # credo:disable-for-next-line def chain_type_fields(schema) do - case Application.get_env(:explorer, :chain_type) do + chain_type() + |> case do :polygon_zkevm -> schema |> Helper.extend_schema( @@ -270,36 +275,44 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.ChainTypeCustomizations do } ) - :celo -> + :zilliqa -> schema |> Helper.extend_schema( properties: %{ - celo: %Schema{ + zilliqa: %Schema{ type: :object, nullable: false, - properties: %{gas_token: %Schema{allOf: [Token], nullable: true}}, - required: [:gas_token], + properties: %{ + is_scilla: %Schema{type: :boolean, nullable: false} + }, + required: [:is_scilla], additionalProperties: false } }, - required: [:celo] + required: [:zilliqa] ) - :zilliqa -> + _ -> + schema + end + |> chain_identity_fields() + end + + defp chain_identity_fields(schema) do + case chain_identity() do + {:optimism, :celo} -> schema |> Helper.extend_schema( properties: %{ - zilliqa: %Schema{ + celo: %Schema{ type: :object, nullable: false, - properties: %{ - is_scilla: %Schema{type: :boolean, nullable: false} - }, - required: [:is_scilla], + properties: %{gas_token: %Schema{allOf: [Token], nullable: true}}, + required: [:gas_token], additionalProperties: false } }, - required: [:zilliqa] + required: [:celo] ) _ -> diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex index eeabdd4fffa8..7506da594aa4 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex @@ -3,8 +3,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody do OpenAPI schema for the transaction summary just request body response. """ require OpenApiSpex - alias OpenApiSpex.Schema alias BlockScoutWeb.Schemas.API.V2.{Address, InternalTransaction, Log, TokenTransfer} + alias OpenApiSpex.Schema logs_data_schema = %Schema{ type: :object, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 228e5a9bca30..1051e52793e2 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -1,6 +1,6 @@ defmodule BlockScoutWeb.API.V2.AddressView do use BlockScoutWeb, :view - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] import BlockScoutWeb.Account.AuthController, only: [current_user: 1] @@ -272,8 +272,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do map(), %{address: Address.t(), creation_transaction_from_address: Address.t()} ) :: map() - case @chain_type do - :filecoin -> + case @chain_identity do + {:filecoin, nil} -> defp chain_type_fields(result, %{creation_transaction_from_address: creation_transaction_from_address}) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.FilecoinView.put_filecoin_robust_address(result, %{ @@ -282,13 +282,13 @@ defmodule BlockScoutWeb.API.V2.AddressView do }) end - :celo -> + {:optimism, :celo} -> defp chain_type_fields(result, %{address: address}) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.CeloView.extend_address_json_response(result, address) end - :zilliqa -> + {:zilliqa, nil} -> defp chain_type_fields(result, %{address: address}) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.ZilliqaView.extend_address_json_response(result, address) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index 78e319c1db43..fdaf689cf192 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.BlockView do use BlockScoutWeb, :view - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] alias BlockScoutWeb.API.V2.{ApiView, Helper} alias BlockScoutWeb.BlockView @@ -74,28 +77,25 @@ defmodule BlockScoutWeb.API.V2.BlockView do "is_pending_update" => block.refetch_needed } |> chain_type_fields(block, single_block?) + |> chain_identity_fields(block, single_block?) end - def prepare_rewards(rewards, block, single_block?) do + defp prepare_rewards(rewards, block, single_block?) do Enum.map(rewards, &prepare_reward(&1, block, single_block?)) end - def prepare_reward(reward, block, single_block?) do + defp prepare_reward(reward, block, single_block?) do %{ "reward" => reward.reward, "type" => if(single_block?, do: BlockView.block_reward_text(reward, block.miner.hash), else: reward.address_type) } end - def prepare_uncles(uncles_relations) when is_list(uncles_relations) do - Enum.map(uncles_relations, &prepare_uncle/1) + defp prepare_uncles(uncles_relations) when is_list(uncles_relations) do + Enum.map(uncles_relations, &%{"hash" => &1.uncle_hash}) end - def prepare_uncles(_), do: [] - - def prepare_uncle(uncle_relation) do - %{"hash" => uncle_relation.uncle_hash} - end + defp prepare_uncles(_), do: [] def burnt_fees_percentage(_, %Decimal{coef: 0}), do: nil @@ -162,12 +162,6 @@ defmodule BlockScoutWeb.API.V2.BlockView do BlockScoutWeb.API.V2.EthereumView.extend_block_json_response(result, block, single_block?) end - :celo -> - defp chain_type_fields(result, block, single_block?) do - # credo:disable-for-next-line Credo.Check.Design.AliasUsage - BlockScoutWeb.API.V2.CeloView.extend_block_json_response(result, block, single_block?) - end - :zilliqa -> defp chain_type_fields(result, block, single_block?) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage @@ -179,4 +173,15 @@ defmodule BlockScoutWeb.API.V2.BlockView do result end end + + case @chain_identity do + {:optimism, :celo} -> + defp chain_identity_fields(result, block, single_block?) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.CeloView.extend_block_json_response(result, block, single_block?) + end + + _ -> + defp chain_identity_fields(result, _block, _single_block?), do: result + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 8b9a66ff06f1..f1d7ce7f30eb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do use BlockScoutWeb, :view - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] alias BlockScoutWeb.API.V2.{ApiView, Helper, InternalTransactionView, TokenTransferView, TokenView} @@ -891,8 +894,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end defp with_chain_type_transformations(transactions) do - chain_type = Application.get_env(:explorer, :chain_type) - do_with_chain_type_transformations(chain_type, transactions) + do_with_chain_type_transformations(chain_type(), transactions) end defp do_with_chain_type_transformations(:stability, transactions) do @@ -905,13 +907,26 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end defp with_chain_type_fields(result, transaction, single_transaction?, conn, watchlist_names) do - chain_type = Application.get_env(:explorer, :chain_type) - do_with_chain_type_fields(chain_type, result, transaction, single_transaction?, conn, watchlist_names) + result + |> do_with_chain_type_fields( + chain_type(), + transaction, + single_transaction?, + conn, + watchlist_names + ) + |> do_with_chain_identity_fields( + chain_identity(), + transaction, + single_transaction?, + conn, + watchlist_names + ) end defp do_with_chain_type_fields( - :polygon_zkevm, result, + :polygon_zkevm, transaction, true = _single_transaction?, _conn, @@ -921,27 +936,27 @@ defmodule BlockScoutWeb.API.V2.TransactionView do BlockScoutWeb.API.V2.PolygonZkevmView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:zksync, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :zksync, transaction, true = _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.ZkSyncView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:arbitrum, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :arbitrum, transaction, true = _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.ArbitrumView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:optimism, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :optimism, transaction, true = _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.OptimismView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:scroll, result, transaction, true = _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :scroll, transaction, true = _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.ScrollView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:suave, result, transaction, true = single_transaction?, conn, watchlist_names) do + defp do_with_chain_type_fields(result, :suave, transaction, true = single_transaction?, conn, watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.SuaveView.extend_transaction_json_response( transaction, @@ -952,27 +967,45 @@ defmodule BlockScoutWeb.API.V2.TransactionView do ) end - defp do_with_chain_type_fields(:stability, result, transaction, _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :stability, transaction, _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.StabilityView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:ethereum, result, transaction, _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :ethereum, transaction, _single_transaction?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage BlockScoutWeb.API.V2.EthereumView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(:celo, result, transaction, _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_type_fields(result, :zilliqa, transaction, _single_tx?, _conn, _watchlist_names) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage - BlockScoutWeb.API.V2.CeloView.extend_transaction_json_response(result, transaction) + BlockScoutWeb.API.V2.ZilliqaView.extend_transaction_json_response(result, transaction) + end + + defp do_with_chain_type_fields(result, _chain_type, _transaction, _single_transaction?, _conn, _watchlist_names) do + result end - defp do_with_chain_type_fields(:zilliqa, result, transaction, _single_tx?, _conn, _watchlist_names) do + defp do_with_chain_identity_fields( + result, + {:optimism, :celo}, + transaction, + _single_transaction?, + _conn, + _watchlist_names + ) do # credo:disable-for-next-line Credo.Check.Design.AliasUsage - BlockScoutWeb.API.V2.ZilliqaView.extend_transaction_json_response(result, transaction) + BlockScoutWeb.API.V2.CeloView.extend_transaction_json_response(result, transaction) end - defp do_with_chain_type_fields(_chain_type, result, _transaction, _single_transaction?, _conn, _watchlist_names) do + defp do_with_chain_identity_fields( + result, + _chain_identity, + _transaction, + _single_transaction?, + _conn, + _watchlist_names + ) do result end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 46bec0cfb141..69242b1d2c75 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do use BlockScoutWeb.ConnCase use EthereumJSONRPC.Case, async: false use BlockScoutWeb.ChannelCase - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] alias ABI.{TypeDecoder, TypeEncoder} alias Explorer.{Chain, Repo, TestHelper} @@ -2167,7 +2167,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do check_paginated_response(response, response_2nd_page, token_transfers_1 ++ token_transfers_2) end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do test "get token balance when a token transfer has no transaction", %{conn: conn} do address = insert(:address) block = insert(:block) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index 1f91a1164ca2..46203e37fa59 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -321,7 +321,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert no_refetch_block_response["is_pending_update"] == false end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do test "get block with Celo base fee information when chain type is celo", %{conn: conn} do # Store original configuration original_celo_config = Application.get_env(:explorer, Explorer.Chain.Cache.CeloCoreContracts) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 87cbec68e69d..429364eeaa76 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do use BlockScoutWeb.ConnCase - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] import Explorer.Chain, only: [hash_to_lower_case_string: 1] import Mox @@ -10,8 +13,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.{Repo, TestHelper} - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - require Logger setup do @@ -1783,7 +1784,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do end end - if Application.compile_env(:explorer, :chain_type) == :celo do + if @chain_identity == {:optimism, :celo} do describe "celo gas token" do test "when gas is paid with token and token is present in db", %{conn: conn} do token = insert(:token) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex index 81c772dc8e2f..d11b02dd2fc7 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transaction.ex @@ -7,7 +7,13 @@ defmodule EthereumJSONRPC.Transaction do [`eth_getTransactionByBlockHashAndIndex`](https://github.com/ethereum/wiki/wiki/JSON-RPC/e8e0771b9f3677693649d945956bc60e886ceb2b#eth_gettransactionbyblockhashandindex), and [`eth_getTransactionByBlockNumberAndIndex`](https://github.com/ethereum/wiki/wiki/JSON-RPC/e8e0771b9f3677693649d945956bc60e886ceb2b#eth_gettransactionbyblocknumberandindex) """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] import EthereumJSONRPC, only: [ @@ -64,15 +70,6 @@ defmodule EthereumJSONRPC.Transaction do ] ) - :celo -> - @chain_type_fields quote( - do: [ - gas_token_contract_address_hash: EthereumJSONRPC.address(), - gas_fee_recipient_address_hash: EthereumJSONRPC.address(), - gateway_fee: non_neg_integer() - ] - ) - :arbitrum -> @chain_type_fields quote( do: [ @@ -84,6 +81,20 @@ defmodule EthereumJSONRPC.Transaction do @chain_type_fields quote(do: []) end + case @chain_identity do + {:optimism, :celo} -> + @chain_identity_fields quote( + do: [ + gas_token_contract_address_hash: EthereumJSONRPC.address(), + gas_fee_recipient_address_hash: EthereumJSONRPC.address(), + gateway_fee: non_neg_integer() + ] + ) + + _ -> + @chain_identity_fields quote(do: []) + end + # todo: Check if it's possible to simplify by avoiding t -> elixir -> params conversions # and directly convert t -> params. @type elixir :: %{ @@ -140,7 +151,10 @@ defmodule EthereumJSONRPC.Transaction do * `"executionNode"` - `t:EthereumJSONRPC.address/0` of execution node (used by Suave). * `"requestRecord"` - map of wrapped transaction data (used by Suave). """ - :celo -> """ + _ -> "" + end} + #{case @chain_identity do + {:optimism, :celo} -> """ * `"feeCurrency"` - `t:EthereumJSONRPC.address/0` of the currency used to pay for gas. * `"gatewayFee"` - `t:EthereumJSONRPC.quantity/0` of the gateway fee. * `"gatewayFeeRecipient"` - `t:EthereumJSONRPC.address/0` of the gateway fee recipient. @@ -160,6 +174,7 @@ defmodule EthereumJSONRPC.Transaction do @type params :: %{ unquote_splicing(@chain_type_fields), + unquote_splicing(@chain_identity_fields), block_hash: EthereumJSONRPC.hash(), block_number: non_neg_integer(), from_address_hash: EthereumJSONRPC.address(), @@ -307,6 +322,7 @@ defmodule EthereumJSONRPC.Transaction do elixir |> do_elixir_to_params() |> chain_type_fields(elixir) + |> chain_identity_fields(elixir) end # Converts a map of the transaction parameters to the map with the corresponding atom parameters. @@ -529,7 +545,7 @@ defmodule EthereumJSONRPC.Transaction do end defp chain_type_fields(params, elixir) do - case Application.get_env(:explorer, :chain_type) do + case chain_type() do :ethereum -> put_if_present(params, elixir, [ {"blobVersionedHashes", :blob_versioned_hashes}, @@ -574,16 +590,23 @@ defmodule EthereumJSONRPC.Transaction do }) end - :celo -> + :arbitrum -> put_if_present(params, elixir, [ - {"feeCurrency", :gas_token_contract_address_hash}, - {"gatewayFee", :gateway_fee}, - {"gatewayFeeRecipient", :gas_fee_recipient_address_hash} + {"requestId", :request_id} ]) - :arbitrum -> + _ -> + params + end + end + + defp chain_identity_fields(params, elixir) do + case chain_identity() do + {:optimism, :celo} -> put_if_present(params, elixir, [ - {"requestId", :request_id} + {"feeCurrency", :gas_token_contract_address_hash}, + {"gatewayFee", :gateway_fee}, + {"gatewayFeeRecipient", :gas_fee_recipient_address_hash} ]) _ -> @@ -742,7 +765,7 @@ defmodule EthereumJSONRPC.Transaction do do: {key, value |> Enum.map(&SignedAuthorization.to_params/1)} # Celo-specific fields - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do defp entry_to_elixir({key, value}) when key in ~w(feeCurrency gatewayFeeRecipient), do: {key, value} diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 431c96188ae8..ba8ad635c221 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -12,6 +12,7 @@ import Config # General application configuration config :explorer, chain_type: ConfigHelper.chain_type(), + chain_identity: ConfigHelper.chain_identity(), ecto_repos: ConfigHelper.repos(), token_functions_reader_max_retries: 3, # for not fully indexed blockchains diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 8e2ad724f9f5..85ca17fef299 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -5,7 +5,9 @@ defmodule Explorer.Chain.Address.Schema do Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.Addresses """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.{ Address, @@ -67,22 +69,27 @@ defmodule Explorer.Chain.Address.Schema do ] end - :celo -> - quote do - [ - has_one( - :celo_account, - Explorer.Chain.Celo.Account, - foreign_key: :address_hash, - references: :hash - ) - ] - end - _ -> [] end) + @chain_identity_fields (case @chain_identity do + {:optimism, :celo} -> + quote do + [ + has_one( + :celo_account, + Explorer.Chain.Celo.Account, + foreign_key: :address_hash, + references: :hash + ) + ] + end + + _ -> + [] + end) + defmacro generate do quote do @primary_key false @@ -132,6 +139,7 @@ defmodule Explorer.Chain.Address.Schema do timestamps() unquote_splicing(@chain_type_fields) + unquote_splicing(@chain_identity_fields) end end end diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index 3f72d5c51df0..f042f9c9222f 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -2,6 +2,9 @@ defmodule Explorer.Chain.Address.Counters do @moduledoc """ Functions related to Explorer.Chain.Address counters """ + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import Ecto.Query, only: [from: 2, limit: 2, select: 3, union_all: 2, where: 3] import Explorer.Chain, @@ -453,7 +456,7 @@ defmodule Explorer.Chain.Address.Counters do ) celo_election_rewards_count_task = - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do configure_task( :celo_election_rewards, cached_counters, diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 06176d964a1b..e3f19fc82df0 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -5,7 +5,9 @@ defmodule Explorer.Chain.Block.Schema do Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.Blocks """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.{ Address, @@ -75,22 +77,6 @@ defmodule Explorer.Chain.Block.Schema do 2 ) - :celo -> - elem( - quote do - has_one(:celo_initiated_epoch, CeloEpoch, - foreign_key: :start_processing_block_hash, - references: :hash - ) - - has_one(:celo_terminated_epoch, CeloEpoch, - foreign_key: :end_processing_block_hash, - references: :hash - ) - end, - 2 - ) - :arbitrum -> elem( quote do @@ -138,6 +124,27 @@ defmodule Explorer.Chain.Block.Schema do [] end) + @chain_identity_fields (case @chain_identity do + {:optimism, :celo} -> + elem( + quote do + has_one(:celo_initiated_epoch, CeloEpoch, + foreign_key: :start_processing_block_hash, + references: :hash + ) + + has_one(:celo_terminated_epoch, CeloEpoch, + foreign_key: :end_processing_block_hash, + references: :hash + ) + end, + 2 + ) + + _ -> + [] + end) + defmacro generate do quote do @primary_key false @@ -186,6 +193,7 @@ defmodule Explorer.Chain.Block.Schema do has_one(:pending_operations, PendingBlockOperation, foreign_key: :block_hash, references: :hash) unquote_splicing(@chain_type_fields) + unquote_splicing(@chain_identity_fields) end end end diff --git a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex index 28a1f3105d4c..9e2acc103f63 100644 --- a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex +++ b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex @@ -5,6 +5,9 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do require Logger + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import Ecto.Query, only: [ from: 2, @@ -172,7 +175,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do new_acc = fee_query - |> filter_unknown_transaction_types(Application.get_env(:explorer, :chain_type)) + |> filter_unknown_transaction_types(chain_identity()) |> Repo.all(timeout: :infinity) |> merge_gas_prices(acc, from_block_actual) @@ -185,7 +188,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {{:error, error}, get_gas_prices_acc()} end - defp filter_unknown_transaction_types(query, :celo) do + defp filter_unknown_transaction_types(query, {:optimism, :celo}) do query |> where([transaction: t], is_nil(t.type) or t.type != 123) end diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 5e005507a6f2..132f956da123 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do require Ecto.Query import Ecto.Query, only: [dynamic: 1, dynamic: 2, from: 2, where: 3, subquery: 1] - import Explorer.Chain.Import.Runner.Helper, only: [chain_type_dependent_import: 3] + import Explorer.Chain.Import.Runner.Helper, only: [chain_identity_dependent_import: 3] import Explorer.QueryHelper, only: [select_ctid: 1, join_on_ctid: 2] alias Ecto.{Changeset, Multi, Repo} @@ -242,8 +242,8 @@ defmodule Explorer.Chain.Import.Runner.Blocks do :blocks_update_token_holder_counts ) end) - |> chain_type_dependent_import( - :celo, + |> chain_identity_dependent_import( + {:optimism, :celo}, &Multi.run( &1, :celo_delete_epoch_rewards, diff --git a/apps/explorer/lib/explorer/chain/import/runner/helper.ex b/apps/explorer/lib/explorer/chain/import/runner/helper.ex index 6fd5f53e85e5..5022ddbb3534 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/helper.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/helper.ex @@ -19,4 +19,21 @@ defmodule Explorer.Chain.Import.Runner.Helper do multi end end + + @doc """ + Executes the import function if the configured chain identity matches the + specified `chain_identity`. + """ + @spec chain_identity_dependent_import( + Ecto.Multi.t(), + chain_identity :: atom() | tuple(), + (Ecto.Multi.t() -> Ecto.Multi.t()) + ) :: Ecto.Multi.t() + def chain_identity_dependent_import(multi, chain_identity, multi_run) do + if Application.get_env(:explorer, :chain_identity) == chain_identity do + multi_run.(multi) + else + multi + end + end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/logs.ex b/apps/explorer/lib/explorer/chain/import/runner/logs.ex index cb8466f2b2ac..10e811c55762 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/logs.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/logs.ex @@ -3,6 +3,9 @@ defmodule Explorer.Chain.Import.Runner.Logs do Bulk imports `t:Explorer.Chain.Log.t/0`. """ + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + require Ecto.Query alias Ecto.{Changeset, Multi, Repo} @@ -65,16 +68,19 @@ defmodule Explorer.Chain.Import.Runner.Logs do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce Log ShareLocks order (see docs: sharelocks.md) - ordered_changes_list = - case Application.get_env(:explorer, :chain_type) do - :celo -> Enum.sort_by(changes_list, &{&1.block_hash, &1.index}) - _ -> Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.index}) - end - - conflict_target = - case Application.get_env(:explorer, :chain_type) do - :celo -> [:index, :block_hash] - _ -> [:transaction_hash, :index, :block_hash] + {ordered_changes_list, conflict_target} = + case chain_identity() do + {:optimism, :celo} -> + { + Enum.sort_by(changes_list, &{&1.block_hash, &1.index}), + [:index, :block_hash] + } + + _ -> + { + Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.index}), + [:transaction_hash, :index, :block_hash] + } end {:ok, _} = @@ -91,8 +97,8 @@ defmodule Explorer.Chain.Import.Runner.Logs do end defp default_on_conflict do - case Application.get_env(:explorer, :chain_type) do - :celo -> + case chain_identity() do + {:optimism, :celo} -> from( log in Log, update: [ diff --git a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex index acbbf3159207..dc0ca4a64cef 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex @@ -3,6 +3,9 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do Bulk imports `t:Explorer.Chain.TokenTransfer.t/0`. """ + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + require Ecto.Query import Ecto.Query, only: [from: 2] @@ -61,16 +64,20 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) # Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md) - ordered_changes_list = - case Application.get_env(:explorer, :chain_type) do - :celo -> Enum.sort_by(changes_list, &{&1.block_hash, &1.log_index}) - _ -> Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.log_index}) - end - conflict_target = - case Application.get_env(:explorer, :chain_type) do - :celo -> [:log_index, :block_hash] - _ -> [:transaction_hash, :log_index, :block_hash] + {ordered_changes_list, conflict_target} = + case chain_identity() do + {:optimism, :celo} -> + { + Enum.sort_by(changes_list, &{&1.block_hash, &1.log_index}), + [:log_index, :block_hash] + } + + _ -> + { + Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.log_index}), + [:transaction_hash, :log_index, :block_hash] + } end {:ok, inserted} = @@ -89,8 +96,8 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do end defp default_on_conflict do - case Application.get_env(:explorer, :chain_type) do - :celo -> + case chain_identity() do + {:optimism, :celo} -> from( token_transfer in TokenTransfer, update: [ diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 0eb4dda8cdc6..4108cb586ed4 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -2,7 +2,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do @moduledoc """ Bulk imports `t:Explorer.Chain.Transaction.t/0`. """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] require Ecto.Query @@ -146,8 +146,8 @@ defmodule Explorer.Chain.Import.Runner.Transactions do end # todo: avoid code duplication - case @chain_type do - :suave -> + case @chain_identity do + {:suave, nil} -> defp default_on_conflict do from( transaction in Transaction, @@ -245,7 +245,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do ) end - :optimism -> + {:optimism, nil} -> defp default_on_conflict do from( transaction in Transaction, @@ -331,7 +331,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do ) end - :arbitrum -> + {:arbitrum, nil} -> defp default_on_conflict do from( transaction in Transaction, @@ -403,7 +403,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do ) end - :celo -> + {:scroll, nil} -> defp default_on_conflict do from( transaction in Transaction, @@ -436,18 +436,16 @@ defmodule Explorer.Chain.Import.Runner.Transactions do max_priority_fee_per_gas: fragment("EXCLUDED.max_priority_fee_per_gas"), max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), type: fragment("EXCLUDED.type"), + l1_fee: fragment("EXCLUDED.l1_fee"), + queue_index: fragment("EXCLUDED.queue_index"), # Don't update `hash` as it is part of the primary key and used for the conflict target inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at), - # Celo custom fields - gas_token_contract_address_hash: fragment("EXCLUDED.gas_token_contract_address_hash"), - gas_fee_recipient_address_hash: fragment("EXCLUDED.gas_fee_recipient_address_hash"), - gateway_fee: fragment("EXCLUDED.gateway_fee") + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) ] ], where: fragment( - "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.gas_token_contract_address_hash, EXCLUDED.gas_fee_recipient_address_hash, EXCLUDED.gateway_fee) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.queue_index) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", transaction.block_hash, transaction.block_number, transaction.block_consensus, @@ -473,14 +471,13 @@ defmodule Explorer.Chain.Import.Runner.Transactions do transaction.max_priority_fee_per_gas, transaction.max_fee_per_gas, transaction.type, - transaction.gas_token_contract_address_hash, - transaction.gas_fee_recipient_address_hash, - transaction.gateway_fee + transaction.l1_fee, + transaction.queue_index ) ) end - :scroll -> + {:optimism, :celo} -> defp default_on_conflict do from( transaction in Transaction, @@ -514,15 +511,23 @@ defmodule Explorer.Chain.Import.Runner.Transactions do max_fee_per_gas: fragment("EXCLUDED.max_fee_per_gas"), type: fragment("EXCLUDED.type"), l1_fee: fragment("EXCLUDED.l1_fee"), - queue_index: fragment("EXCLUDED.queue_index"), + l1_fee_scalar: fragment("EXCLUDED.l1_fee_scalar"), + l1_gas_price: fragment("EXCLUDED.l1_gas_price"), + l1_gas_used: fragment("EXCLUDED.l1_gas_used"), + l1_transaction_origin: fragment("EXCLUDED.l1_transaction_origin"), + l1_block_number: fragment("EXCLUDED.l1_block_number"), # Don't update `hash` as it is part of the primary key and used for the conflict target inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at), + # Celo custom fields + gas_token_contract_address_hash: fragment("EXCLUDED.gas_token_contract_address_hash"), + gas_fee_recipient_address_hash: fragment("EXCLUDED.gas_fee_recipient_address_hash"), + gateway_fee: fragment("EXCLUDED.gateway_fee") ] ], where: fragment( - "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.queue_index) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_transaction_origin, EXCLUDED.l1_block_number, EXCLUDED.gas_token_contract_address_hash, EXCLUDED.gas_fee_recipient_address_hash, EXCLUDED.gateway_fee) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", transaction.block_hash, transaction.block_number, transaction.block_consensus, @@ -549,7 +554,14 @@ defmodule Explorer.Chain.Import.Runner.Transactions do transaction.max_fee_per_gas, transaction.type, transaction.l1_fee, - transaction.queue_index + transaction.l1_fee_scalar, + transaction.l1_gas_price, + transaction.l1_gas_used, + transaction.l1_transaction_origin, + transaction.l1_block_number, + transaction.gas_token_contract_address_hash, + transaction.gas_fee_recipient_address_hash, + transaction.gateway_fee ) ) end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 8586b9b365bf..0278c4bca77a 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -3,6 +3,10 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Imports any chain type specific tables. """ + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + alias Explorer.Chain.Import.{Runner, Stage} @behaviour Stage @@ -55,14 +59,6 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Runner.Scroll.BridgeOperations, Runner.Scroll.L1FeeParams ], - celo: [ - Runner.Celo.PendingAccountOperations, - Runner.Celo.Accounts, - Runner.Celo.ValidatorGroupVotes, - Runner.Celo.Epochs, - Runner.Celo.ElectionRewards, - Runner.Celo.EpochRewards - ], zilliqa: [ Runner.Zilliqa.AggregateQuorumCertificates, Runner.Zilliqa.NestedQuorumCertificates, @@ -73,17 +69,37 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do ] } + @runners_by_chain_identity %{ + {:optimism, :celo} => [ + Runner.Celo.PendingAccountOperations, + Runner.Celo.Accounts, + Runner.Celo.ValidatorGroupVotes, + Runner.Celo.Epochs, + Runner.Celo.ElectionRewards, + Runner.Celo.EpochRewards + ] + } + @impl Stage def runners do - chain_type = Application.get_env(:explorer, :chain_type) - Map.get(@runners_by_chain_type, chain_type, []) + chain_type_runners = Map.get(@runners_by_chain_type, chain_type(), []) + chain_identity_runners = Map.get(@runners_by_chain_identity, chain_identity(), []) + chain_type_runners ++ chain_identity_runners end @impl Stage def all_runners do - @runners_by_chain_type - |> Map.values() - |> Enum.concat() + chain_type_runners = + @runners_by_chain_type + |> Map.values() + |> Enum.concat() + + chain_identity_runners = + @runners_by_chain_identity + |> Map.values() + |> Enum.concat() + + chain_type_runners ++ chain_identity_runners end @impl Stage diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 0858ea8875bf..70ba284ae7af 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -1,6 +1,7 @@ defmodule Explorer.Chain.Log.Schema do @moduledoc false - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.{ Address, @@ -18,10 +19,10 @@ defmodule Explorer.Chain.Log.Schema do # However, on Celo, logs may exist where `transaction_hash` equals block_hash. # In these instances, we set `transaction_hash` to `nil`. This action, though, # violates the primary key constraint. To resolve this issue, we've excluded - # `transaction_hash` from the composite primary key when dealing with `:celo` - # chain type. - @transaction_field (case @chain_type do - :celo -> + # `transaction_hash` from the composite primary key when dealing with + # `:optimism-celo` chain type. + @transaction_field (case @chain_identity do + {:optimism, :celo} -> quote do [ belongs_to(:transaction, Transaction, @@ -80,7 +81,7 @@ defmodule Explorer.Chain.Log do @moduledoc "Captures a Web3 log entry generated by a transaction" use Explorer.Schema - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] require Explorer.Chain.Log.Schema require Logger @@ -91,8 +92,8 @@ defmodule Explorer.Chain.Log do alias Explorer.SmartContract.SigProviderInterface @required_attrs ~w(address_hash data block_hash index)a - |> (&(case @chain_type do - :celo -> + |> (&(case @chain_identity do + {:optimism, :celo} -> &1 _ -> @@ -100,8 +101,8 @@ defmodule Explorer.Chain.Log do end)).() @optional_attrs ~w(first_topic second_topic third_topic fourth_topic block_number)a - |> (&(case @chain_type do - :celo -> + |> (&(case @chain_identity do + {:optimism, :celo} -> [:transaction_hash | &1] _ -> diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 6e7694a7c57f..4cea3d7c1164 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -5,7 +5,8 @@ defmodule Explorer.Chain.TokenTransfer.Schema do Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.TokenTransfers """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.{ Address, @@ -16,10 +17,10 @@ defmodule Explorer.Chain.TokenTransfer.Schema do alias Explorer.Chain.Token.Instance - # Remove `transaction_hash` from primary key for `:celo` chain type. See + # Remove `transaction_hash` from primary key for `optimism-celo` chain type. See # `Explorer.Chain.Log.Schema` for more details. - @transaction_field (case @chain_type do - :celo -> + @transaction_field (case @chain_identity do + {:optimism, :celo} -> quote do [ belongs_to(:transaction, Transaction, @@ -134,8 +135,9 @@ defmodule Explorer.Chain.TokenTransfer do """ use Explorer.Schema - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] + use Utils.RuntimeEnvHelper, chain_identity: [:explorer, :chain_identity] require Explorer.Chain.TokenTransfer.Schema @@ -186,16 +188,16 @@ defmodule Explorer.Chain.TokenTransfer do Explorer.Chain.TokenTransfer.Schema.generate() @required_attrs ~w(block_number log_index from_address_hash to_address_hash token_contract_address_hash block_hash token_type)a - |> (&(case @chain_type do - :celo -> + |> (&(case @chain_identity do + {:optimism, :celo} -> &1 _ -> [:transaction_hash | &1] end)).() @optional_attrs ~w(amount amounts token_ids block_consensus)a - |> (&(case @chain_type do - :celo -> + |> (&(case @chain_identity do + {:optimism, :celo} -> [:transaction_hash | &1] _ -> @@ -691,9 +693,9 @@ defmodule Explorer.Chain.TokenTransfer do where: tt.log_index == parent_as(:log).index ) - chain_type() + chain_identity() |> case do - :celo -> + {:optimism, :celo} -> query |> where( [tt], diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index c4e57f136f61..d3eb9ed23d65 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -5,7 +5,9 @@ defmodule Explorer.Chain.Transaction.Schema do Changes in the schema should be reflected in the bulk import module: - Explorer.Chain.Import.Runner.Transactions """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain @@ -138,28 +140,6 @@ defmodule Explorer.Chain.Transaction.Schema do 2 ) - :celo -> - elem( - quote do - field(:gateway_fee, Wei) - - belongs_to(:gas_fee_recipient, Address, - foreign_key: :gas_fee_recipient_address_hash, - references: :hash, - type: Hash.Address - ) - - belongs_to(:gas_token_contract_address, Address, - foreign_key: :gas_token_contract_address_hash, - references: :hash, - type: Hash.Address - ) - - has_one(:gas_token, through: [:gas_token_contract_address, :token]) - end, - 2 - ) - :arbitrum -> elem( quote do @@ -202,6 +182,33 @@ defmodule Explorer.Chain.Transaction.Schema do [] end) + @chain_identity_fields (case @chain_identity do + {:optimism, :celo} -> + elem( + quote do + field(:gateway_fee, Wei) + + belongs_to(:gas_fee_recipient, Address, + foreign_key: :gas_fee_recipient_address_hash, + references: :hash, + type: Hash.Address + ) + + belongs_to(:gas_token_contract_address, Address, + foreign_key: :gas_token_contract_address_hash, + references: :hash, + type: Hash.Address + ) + + has_one(:gas_token, through: [:gas_token_contract_address, :token]) + end, + 2 + ) + + _ -> + [] + end) + defmacro generate do quote do @primary_key false @@ -292,6 +299,7 @@ defmodule Explorer.Chain.Transaction.Schema do has_one(:pending_operation, PendingTransactionOperation, foreign_key: :transaction_hash, references: :hash) unquote_splicing(@chain_type_fields) + unquote_splicing(@chain_identity_fields) end end end @@ -304,6 +312,7 @@ defmodule Explorer.Chain.Transaction do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity], decode_not_a_contract_calls: [:explorer, :decode_not_a_contract_calls] require Logger @@ -354,13 +363,18 @@ defmodule Explorer.Chain.Transaction do :arbitrum -> ~w(gas_used_for_l1)a - :celo -> - ~w(gateway_fee gas_fee_recipient_address_hash gas_token_contract_address_hash)a - _ -> ~w()a end) + @chain_identity_optional_attrs (case @chain_identity do + {:optimism, :celo} -> + ~w(gateway_fee gas_fee_recipient_address_hash gas_token_contract_address_hash)a + + _ -> + ~w()a + end) + @required_attrs ~w(from_address_hash gas hash input nonce value)a @typedoc """ @@ -682,7 +696,8 @@ defmodule Explorer.Chain.Transaction do attrs_to_cast = @required_attrs ++ @optional_attrs ++ - @chain_type_optional_attrs + @chain_type_optional_attrs ++ + @chain_identity_optional_attrs transaction |> cast(attrs, attrs_to_cast) @@ -812,7 +827,7 @@ defmodule Explorer.Chain.Transaction do def decoded_input_data(%NotLoaded{}, _, _, _, _), do: {:error, :not_loaded} - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do # Celo's Epoch logs does not have an associated transaction and linked to # the block instead, so we discard these token transfers for transaction # decoding @@ -2261,7 +2276,7 @@ defmodule Explorer.Chain.Transaction do ) end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do defp decode_remaining_transaction({nil, nil}, _, _, _, _), do: nil end diff --git a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs index ed5b745e8ad2..94797d77d887 100644 --- a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs +++ b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs @@ -1,6 +1,9 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do use Explorer.DataCase, async: false + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Wei alias Explorer.Chain.Cache.Counters.AverageBlockTime @@ -672,7 +675,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do GasPriceOracle.get_average_gas_price(1, 35, 60, 90) end - if Application.compile_env(:explorer, :chain_type) == :celo do + if @chain_identity == {:optimism, :celo} do test "ignores transactions with unsupported types" do block = insert(:block, diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index 00b5b5b85aab..eaa61c9ec23e 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -1,6 +1,8 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do use Explorer.DataCase - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] import Ecto.Query, only: [from: 2, select: 2, where: 2] @@ -746,7 +748,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do }} = Multi.new() |> Blocks.run(changes_list, options) |> Repo.transaction() end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do test "removes celo epoch rewards and sets fetched? = false when starting block loses consensus", %{ consensus_block: %{miner_hash: miner_hash} = parent_block, options: options diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 70a2ecd3f959..a6c5be2b9f9d 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -2,7 +2,7 @@ defmodule Explorer.Chain.TokenTransferTest do use Explorer.DataCase use Utils.CompileTimeEnvHelper, - chain_type: [:explorer, :chain_type] + chain_identity: [:explorer, :chain_identity] import Explorer.Factory @@ -354,7 +354,7 @@ defmodule Explorer.Chain.TokenTransferTest do end end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do test "returns block numbers for Celo epoch blocks with nil transaction_hash" do log = insert(:token_transfer_log, diff --git a/apps/explorer/test/explorer/migrator/celo_accounts_test.exs b/apps/explorer/test/explorer/migrator/celo_accounts_test.exs index 5218a773835f..ebe19a8a38a7 100644 --- a/apps/explorer/test/explorer/migrator/celo_accounts_test.exs +++ b/apps/explorer/test/explorer/migrator/celo_accounts_test.exs @@ -2,9 +2,9 @@ defmodule Explorer.Migrator.CeloAccountsTest do use Explorer.DataCase, async: false use Utils.CompileTimeEnvHelper, - chain_type: [:explorer, :chain_type] + chain_identity: [:explorer, :chain_identity] - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do alias Explorer.Factory alias Explorer.Migrator.{CeloAccounts, MigrationStatus} alias Explorer.Chain.{Address, Hash} diff --git a/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs b/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs index fe01497d2961..6a00bcd6cc05 100644 --- a/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs +++ b/apps/explorer/test/explorer/migrator/celo_l2_epochs_test.exs @@ -2,9 +2,9 @@ defmodule Explorer.Migrator.CeloL2EpochsTest do use Explorer.DataCase, async: false use Utils.CompileTimeEnvHelper, - chain_type: [:explorer, :chain_type] + chain_identity: [:explorer, :chain_identity] - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do alias Explorer.Migrator.{CeloL2Epochs, MigrationStatus} alias Explorer.Chain.Celo.Epoch alias Explorer.Repo diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index f80740fb98f8..7d7b289c433d 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -3,7 +3,9 @@ defmodule Indexer.Block.Fetcher do Fetches and indexes block ranges. """ - use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] use Spandex.Decorators @@ -298,20 +300,36 @@ defmodule Indexer.Block.Fetcher do end defp import_options(basic_import_options, chain_specific_import_options) do - chain_type = Application.get_env(:explorer, :chain_type) - do_import_options(chain_type, basic_import_options, chain_specific_import_options) + do_import_options( + chain_type(), + basic_import_options, + chain_specific_import_options + ) end - defp do_import_options(:ethereum, basic_import_options, %{transactions_with_receipts: transactions_with_receipts}) do + defp do_import_options(:ethereum, basic_import_options, %{ + transactions_with_receipts: transactions_with_receipts + }) do basic_import_options |> Map.put_new(:beacon_blob_transactions, %{ params: transactions_with_receipts |> Enum.filter(&Map.has_key?(&1, :max_fee_per_blob_gas)) }) end - defp do_import_options(:optimism, basic_import_options, %{optimism_withdrawals: optimism_withdrawals}) do - basic_import_options - |> Map.put_new(:optimism_withdrawals, %{params: optimism_withdrawals}) + defp do_import_options( + :optimism, + basic_import_options, + %{optimism_withdrawals: optimism_withdrawals} = chain_specific_import_options + ) do + import_options = + basic_import_options + |> Map.put_new(:optimism_withdrawals, %{params: optimism_withdrawals}) + + do_chain_identity_import_options( + chain_identity(), + import_options, + chain_specific_import_options + ) end defp do_import_options(:polygon_zkevm, basic_import_options, %{ @@ -326,28 +344,11 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:scroll_l1_fee_params, %{params: scroll_l1_fee_params}) end - defp do_import_options(:shibarium, basic_import_options, %{shibarium_bridge_operations: shibarium_bridge_operations}) do - basic_import_options - |> Map.put_new(:shibarium_bridge_operations, %{params: shibarium_bridge_operations}) - end - - defp do_import_options(:celo, basic_import_options, %{ - celo_gas_tokens: celo_gas_tokens, - celo_epochs: celo_epochs, - celo_pending_account_operations: celo_pending_account_operations + defp do_import_options(:shibarium, basic_import_options, %{ + shibarium_bridge_operations: shibarium_bridge_operations }) do - tokens = - basic_import_options - |> Map.get(:tokens, %{}) - |> Map.get(:params, []) - basic_import_options - |> Map.put_new(:celo_pending_account_operations, %{params: celo_pending_account_operations}) - |> Map.put_new(:celo_epochs, %{params: celo_epochs}) - |> Map.put( - :tokens, - %{params: (tokens ++ celo_gas_tokens) |> Enum.uniq()} - ) + |> Map.put_new(:shibarium_bridge_operations, %{params: shibarium_bridge_operations}) end defp do_import_options(:arbitrum, basic_import_options, %{arbitrum_messages: arbitrum_xlevel_messages}) do @@ -371,7 +372,41 @@ defmodule Indexer.Block.Fetcher do |> Map.put_new(:stability_validators, %{params: stability_validators}) end - defp do_import_options(_chain_type, basic_import_options, _chain_specific_import_options) do + defp do_import_options(_chain_identity, basic_import_options, _chain_specific_import_options) do + basic_import_options + end + + defp do_chain_identity_import_options( + {:optimism, :celo}, + basic_import_options, + %{ + celo_gas_tokens: celo_gas_tokens, + celo_epochs: celo_epochs, + celo_pending_account_operations: celo_pending_account_operations + } = chain_specific_import_options + ) do + tokens = + basic_import_options + |> Map.get(:tokens, %{}) + |> Map.get(:params, []) + + import_options = + do_import_options( + {:optimism, nil}, + basic_import_options, + chain_specific_import_options + ) + + import_options + |> Map.put_new(:celo_pending_account_operations, %{params: celo_pending_account_operations}) + |> Map.put_new(:celo_epochs, %{params: celo_epochs}) + |> Map.put( + :tokens, + %{params: (tokens ++ celo_gas_tokens) |> Enum.uniq()} + ) + end + + defp do_chain_identity_import_options(_, basic_import_options, _chain_specific_import_options) do basic_import_options end @@ -844,7 +879,7 @@ defmodule Indexer.Block.Fetcher do end defp parse_celo_pending_account_operations(logs) do - if chain_type() == :celo do + if chain_identity() == {:optimism, :celo} do logs |> CeloAccountsTransform.parse() |> Map.take([:accounts, :attestations_fulfilled, :attestations_requested]) diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex index cb29fc1f9aa3..1d8d1832abc1 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations/delegated_payments_prior_l2_migration.ex @@ -159,7 +159,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations.DelegatedPaymentsPriorL2Migr &%{ contract_address: accounts_contract_address, method_id: @get_payment_delegation_method_id, - args: [&1], + args: [to_string(&1)], block_number: block_number } ) diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex index f34f6c614f62..aea0faba42e8 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_logs.ex @@ -9,6 +9,9 @@ defmodule Indexer.Fetcher.Celo.EpochLogs do pre_migration_block_number?: 1 ] + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + alias EthereumJSONRPC.{Logs, Transport} alias Explorer.Chain.Cache.CeloCoreContracts alias Indexer.Helper, as: IndexerHelper @@ -44,7 +47,7 @@ defmodule Indexer.Fetcher.Celo.EpochLogs do def fetch(blocks, json_rpc_named_arguments) def fetch(blocks, json_rpc_named_arguments) do - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do do_fetch(blocks, json_rpc_named_arguments) else [] diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index a3f0027ec522..4e9604febd89 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -9,6 +9,9 @@ defmodule Indexer.Fetcher.InternalTransaction do use Indexer.Fetcher, restart: :permanent use Spandex.Decorators + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + require Logger import Indexer.Block.Fetcher, @@ -371,7 +374,7 @@ defmodule Indexer.Fetcher.InternalTransaction do celo_token_transfers_params = %{token_transfers: celo_token_transfers, tokens: celo_tokens} = - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do block_number_to_block_hash = transactions_params_or_unique_numbers |> data_to_block_numbers(data_type) @@ -560,7 +563,7 @@ defmodule Indexer.Fetcher.InternalTransaction do end defp async_import_celo_token_balances(%{token_transfers: token_transfers, tokens: tokens}) do - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do token_transfers_with_token = token_transfers_merge_token(token_transfers, tokens) address_token_balances = diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex index a5ddb2f2428d..1efb3e76a981 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex @@ -2,7 +2,7 @@ defmodule Indexer.Transform.AddressCoinBalances do @moduledoc """ Extracts `Explorer.Chain.Address.CoinBalance` params from other schema's params. """ - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.TokenTransfer @@ -120,7 +120,7 @@ defmodule Indexer.Transform.AddressCoinBalances do |> (&transactions_params_chain_type_fields_reducer(transaction_params, &1)).() end - if @chain_type == :celo do + if @chain_identity == {:optimism, :celo} do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @burn_address_hash_string burn_address_hash_string() diff --git a/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex b/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex index 88c60a891d2e..c73b9f820bed 100644 --- a/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex +++ b/apps/indexer/lib/indexer/transform/celo/l1_epochs.ex @@ -12,13 +12,13 @@ defmodule Indexer.Transform.Celo.L1Epochs do """ use Utils.RuntimeEnvHelper, - chain_type: [:explorer, :chain_type] + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.Celo.Helper @spec parse([EthereumJSONRPC.Block.params()]) :: [map()] def parse(blocks) do - if chain_type() == :celo do + if chain_identity() == {:optimism, :celo} do do_parse(blocks) else [] diff --git a/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex b/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex index 0edc0c43ee22..c4a11ef604e6 100644 --- a/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex +++ b/apps/indexer/lib/indexer/transform/celo/l2_epochs.ex @@ -12,7 +12,7 @@ defmodule Indexer.Transform.Celo.L2Epochs do """ use Utils.RuntimeEnvHelper, - chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity], epoch_manager_contract_address: [ :explorer, [:celo, :epoch_manager_contract_address] @@ -28,7 +28,7 @@ defmodule Indexer.Transform.Celo.L2Epochs do @spec parse([map()]) :: [map()] def parse(logs) do - if chain_type() == :celo do + if chain_identity() == {:optimism, :celo} do do_parse(logs) else [] diff --git a/apps/indexer/lib/indexer/transform/celo/transaction_gas_tokens.ex b/apps/indexer/lib/indexer/transform/celo/transaction_gas_tokens.ex index 7857a932157d..ef31255dcb59 100644 --- a/apps/indexer/lib/indexer/transform/celo/transaction_gas_tokens.ex +++ b/apps/indexer/lib/indexer/transform/celo/transaction_gas_tokens.ex @@ -2,6 +2,8 @@ defmodule Indexer.Transform.Celo.TransactionGasTokens do @moduledoc """ Helper functions for extracting tokens specified as gas fee currency. """ + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] alias Explorer.Chain.Hash @@ -19,7 +21,7 @@ defmodule Indexer.Transform.Celo.TransactionGasTokens do } ] def parse(transactions) do - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do transactions |> Enum.reduce( MapSet.new(), diff --git a/apps/indexer/lib/indexer/transform/celo/transaction_token_transfers.ex b/apps/indexer/lib/indexer/transform/celo/transaction_token_transfers.ex index 659a9c8143f2..9351d9c2832a 100644 --- a/apps/indexer/lib/indexer/transform/celo/transaction_token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/celo/transaction_token_transfers.ex @@ -11,6 +11,9 @@ defmodule Indexer.Transform.Celo.TransactionTokenTransfers do """ require Logger + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import Indexer.Transform.TokenTransfers, only: [ filter_tokens_for_supply_update: 1 @@ -47,7 +50,7 @@ defmodule Indexer.Transform.Celo.TransactionTokenTransfers do } def parse_transactions(transactions) do token_transfers = - if Application.get_env(:explorer, :chain_type) == :celo do + if chain_identity() == {:optimism, :celo} do transactions |> Enum.filter(fn transaction -> transaction.value > 0 end) |> Enum.map(fn transaction -> diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index cf9c5e033524..64b1a02aa618 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -3,6 +3,9 @@ defmodule Indexer.Block.FetcherTest do use EthereumJSONRPC.Case, async: false use Explorer.DataCase + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import Mox import EthereumJSONRPC, only: [integer_to_quantity: 1] @@ -805,7 +808,7 @@ defmodule Indexer.Block.FetcherTest do end end - if Application.compile_env(:explorer, :chain_type) == :celo do + if @chain_identity == {:optimism, :celo} do describe "import_range/2 celo" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index f0fa21cc99bb..089e9630c996 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -2,6 +2,9 @@ defmodule Indexer.Block.Realtime.FetcherTest do use EthereumJSONRPC.Case, async: false use Explorer.DataCase + use Utils.RuntimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import Mox alias Explorer.{Chain, Factory} @@ -568,7 +571,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do errors: [] }} = Indexer.Block.Fetcher.fetch_and_import_range(block_fetcher, 3_946_079..3_946_080) - unless Application.get_env(:explorer, :chain_type) == :celo do + unless chain_identity() == {:optimism, :celo} do assert [ %Address{hash: ^first_address_hash}, %Address{hash: ^second_address_hash}, @@ -828,7 +831,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do errors: [] }} = Indexer.Block.Fetcher.fetch_and_import_range(block_fetcher, 3_946_079..3_946_080) - unless Application.get_env(:explorer, :chain_type) == :celo do + unless chain_identity() == {:optimism, :celo} do assert [ %Address{hash: ^first_address_hash}, %Address{hash: ^second_address_hash}, diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs index 8d864bb2c347..89b0ba925e59 100644 --- a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs +++ b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs @@ -2,6 +2,9 @@ defmodule Indexer.Fetcher.InternalTransactionTest do use EthereumJSONRPC.Case, async: false use Explorer.DataCase + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + import ExUnit.CaptureLog import Mox @@ -628,7 +631,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do # Due to token-duality feature in Celo network (native coin transfers are # treated as token transfers), we need to fetch updated token balances after # parsing the internal transactions - if Application.compile_env(:explorer, :chain_type) == :celo do + if @chain_identity == {:optimism, :celo} do defp start_token_balance_fetcher(json_rpc_named_arguments) do TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) end diff --git a/config/config_helper.exs b/config/config_helper.exs index 5dd1c8c2e767..53528439ce47 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -9,27 +9,28 @@ defmodule ConfigHelper do def repos do base_repos = [Explorer.Repo, Explorer.Repo.Account] - chain_type_repo = + chain_identity_repos = %{ - arbitrum: Explorer.Repo.Arbitrum, - blackfort: Explorer.Repo.Blackfort, - celo: Explorer.Repo.Celo, - ethereum: Explorer.Repo.Beacon, - filecoin: Explorer.Repo.Filecoin, - optimism: Explorer.Repo.Optimism, - polygon_zkevm: Explorer.Repo.PolygonZkevm, - rsk: Explorer.Repo.RSK, - scroll: Explorer.Repo.Scroll, - shibarium: Explorer.Repo.Shibarium, - stability: Explorer.Repo.Stability, - suave: Explorer.Repo.Suave, - zilliqa: Explorer.Repo.Zilliqa, - zksync: Explorer.Repo.ZkSync, - neon: Explorer.Repo.Neon + {:arbitrum, nil} => [Explorer.Repo.Arbitrum], + {:blackfort, nil} => [Explorer.Repo.Blackfort], + {:ethereum, nil} => [Explorer.Repo.Beacon], + {:filecoin, nil} => [Explorer.Repo.Filecoin], + {:optimism, nil} => [Explorer.Repo.Optimism], + {:polygon_zkevm, nil} => [Explorer.Repo.PolygonZkevm], + {:rsk, nil} => [Explorer.Repo.RSK], + {:scroll, nil} => [Explorer.Repo.Scroll], + {:shibarium, nil} => [Explorer.Repo.Shibarium], + {:stability, nil} => [Explorer.Repo.Stability], + {:suave, nil} => [Explorer.Repo.Suave], + {:zilliqa, nil} => [Explorer.Repo.Zilliqa], + {:zksync, nil} => [Explorer.Repo.ZkSync], + {:neon, nil} => [Explorer.Repo.Neo], + {:optimism, :celo} => [ + Explorer.Repo.Optimism, + Explorer.Repo.Celo + ] } - |> Map.get(chain_type()) - - chain_type_repos = (chain_type_repo && [chain_type_repo]) || [] + |> Map.get(chain_identity(), []) ext_repos = [ @@ -41,7 +42,7 @@ defmodule ConfigHelper do |> Enum.filter(&elem(&1, 0)) |> Enum.map(&elem(&1, 1)) - base_repos ++ chain_type_repos ++ ext_repos + base_repos ++ chain_identity_repos ++ ext_repos end @doc """ @@ -187,6 +188,34 @@ defmodule ConfigHelper do "Invalid value \"#{value}\" of #{env_var} environment variable is provided. Supported values are #{inspect(catalog)}" end + @doc """ + Parses value of env var through catalogued values map. If a value is not in + the map, nil is returned. Also, the application shutdown option is supported, + if a value is wrong. + """ + @spec parse_catalog_map_value(String.t(), %{binary() => any()}, bool(), String.t() | nil) :: any() | nil + def parse_catalog_map_value(env_var, catalog, shutdown_on_wrong_key?, default_key \\ nil) do + key = env_var |> safe_get_env(default_key) + + if key !== "" do + case Map.fetch(catalog, key) do + {:ok, value} -> + value + + :error -> + if shutdown_on_wrong_key? do + Logger.error(wrong_value_error(key, env_var, catalog)) + exit(:shutdown) + else + Logger.warning(wrong_value_error(key, env_var, catalog)) + nil + end + end + else + nil + end + end + def safe_get_env(env_var, default_value) do env_var |> System.get_env(default_value) @@ -347,28 +376,101 @@ defmodule ConfigHelper do end end - @supported_chain_types [ - "default", - "arbitrum", - "blackfort", - "celo", - "ethereum", - "filecoin", - "optimism", - "polygon_zkevm", - "rsk", - "scroll", - "shibarium", - "stability", - "suave", - "zetachain", - "zilliqa", - "zksync", - "neon" - ] - - @spec chain_type() :: atom() | nil - def chain_type, do: parse_catalog_value("CHAIN_TYPE", @supported_chain_types, true, "default") + @supported_chain_identities %{ + "default" => :default, + "arbitrum" => :arbitrum, + "blackfort" => :blackfort, + "ethereum" => :ethereum, + "filecoin" => :filecoin, + "optimism" => :optimism, + "polygon_zkevm" => :polygon_zkevm, + "rsk" => :rsk, + "scroll" => :scroll, + "shibarium" => :shibarium, + "stability" => :stability, + "suave" => :suave, + "zetachain" => :zetachain, + "zilliqa" => :zilliqa, + "zksync" => :zksync, + "neon" => :neon, + "optimism-celo" => {:optimism, :celo} + } + + @doc """ + Returns the primary chain type. + + This function extracts the base chain type from the chain identity (see + chain_identity/1), which is used to determine the core feature set and + indexing behavior. For example, both `:optimism` and `{:optimism, :celo}` + identities will return `:optimism` as the chain type, allowing them to share + the same Optimism-specific logic. + + ## Examples + + iex> System.put_env("CHAIN_TYPE", "optimism") + iex> chain_type() + :optimism + + iex> System.put_env("CHAIN_TYPE", "optimism-celo") + iex> chain_type() + :optimism + + ## Returns + + * `atom()` - The primary chain type identifier + """ + @spec chain_type() :: atom() + def chain_type do + {type, _} = chain_identity() + type + end + + @doc """ + Returns the full chain identity as a tuple of base type and optional variant. + + Chain identity is a concept that allows networks to inherit a base chain's + feature set while adding variant-specific customizations. The identity is + returned as a two-element tuple `{base_type, variant}` where: + + - `base_type` - The primary chain type (e.g., `:optimism`, `:arbitrum`) + - `variant` - An optional sub-type for specialized networks (e.g., `:celo`) + + This enables Celo to run on the OP Stack with the identity `{:optimism, + :celo}`, inheriting all Optimism features (deposits, withdrawals, batches, + etc.) while allowing Celo-specific endpoints, caches, and schema fields to be + gated on the full identity tuple. + + The identity is configured via the `CHAIN_TYPE` environment variable and must + match one of the supported values in `@supported_chain_identities`. + + ## Examples + + iex> System.put_env("CHAIN_TYPE", "optimism") + iex> chain_identity() + {:optimism, nil} + + iex> System.put_env("CHAIN_TYPE", "optimism-celo") + iex> chain_identity() + {:optimism, :celo} + + iex> System.put_env("CHAIN_TYPE", "ethereum") + iex> chain_identity() + {:ethereum, nil} + + ## Returns + + * `{atom(), atom() | nil}` - A tuple containing the base chain type and + optional variant + """ + @spec chain_identity() :: {atom(), atom() | nil} + def chain_identity do + "CHAIN_TYPE" + |> parse_catalog_map_value(@supported_chain_identities, true, "default") + |> case do + type when is_atom(type) -> {type, nil} + identity -> identity + end + end @supported_modes ["all", "indexer", "api"] diff --git a/config/runtime.exs b/config/runtime.exs index ca5d97e5da60..3c04d964d9ea 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -275,6 +275,7 @@ config :explorer, mode: app_mode, ecto_repos: ConfigHelper.repos(), chain_type: ConfigHelper.chain_type(), + chain_identity: ConfigHelper.chain_identity(), coin: coin, coin_name: System.get_env("COIN_NAME") || "ETH", allowed_solidity_evm_versions: @@ -808,15 +809,15 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_FILECOIN_PENDING_ADDRESS_OPERATIONS_CONCURRENCY", 1) -config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_type() == :celo +config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} config :explorer, Explorer.Migrator.CeloL2Epochs, enabled: - ConfigHelper.chain_type() == :celo && + ConfigHelper.chain_identity() == {:optimism, :celo} && !is_nil(celo_l2_migration_block) && !is_nil(celo_epoch_manager_contract_address) -config :explorer, Explorer.Chain.Cache.CeloEpochs, enabled: ConfigHelper.chain_type() == :celo +config :explorer, Explorer.Chain.Cache.CeloEpochs, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} config :explorer, Explorer.Migrator.ShrinkInternalTransactions, enabled: ConfigHelper.parse_bool_env_var("SHRINK_INTERNAL_TRANSACTIONS_ENABLED"), @@ -1509,11 +1510,11 @@ config :indexer, Indexer.Fetcher.Celo.ValidatorGroupVotes, config :indexer, Indexer.Fetcher.Celo.ValidatorGroupVotes.Supervisor, enabled: - ConfigHelper.chain_type() == :celo and + ConfigHelper.chain_identity() == {:optimism, :celo} and not ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CELO_VALIDATOR_GROUP_VOTES_FETCHER") celo_epoch_fetchers_enabled? = - ConfigHelper.chain_type() == :celo and + ConfigHelper.chain_identity() == {:optimism, :celo} and not ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_CELO_EPOCH_FETCHER") config :indexer, Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, @@ -1525,8 +1526,8 @@ config :indexer, Indexer.Fetcher.Celo.Legacy.Account, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_CELO_ACCOUNTS_BATCH_SIZE", 100) config :indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor, - enabled: ConfigHelper.chain_type() == :celo, - disabled?: not (ConfigHelper.chain_type() == :celo) + enabled: ConfigHelper.chain_identity() == {:optimism, :celo}, + disabled?: not (ConfigHelper.chain_identity() == {:optimism, :celo}) config :indexer, Indexer.Fetcher.Filecoin.BeryxAPI, base_url: ConfigHelper.safe_get_env("BERYX_API_BASE_URL", "https://api.zondax.ch/fil/data/v3/mainnet"), From b514a5aebeec3207e66b2b039a10d36e1df109b6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 12 Nov 2025 15:00:12 +0300 Subject: [PATCH 0466/1006] Explorer.Repo.Neo -> Explorer.Repo.Neon --- config/config_helper.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config_helper.exs b/config/config_helper.exs index 53528439ce47..f39a86881d13 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -24,7 +24,7 @@ defmodule ConfigHelper do {:suave, nil} => [Explorer.Repo.Suave], {:zilliqa, nil} => [Explorer.Repo.Zilliqa], {:zksync, nil} => [Explorer.Repo.ZkSync], - {:neon, nil} => [Explorer.Repo.Neo], + {:neon, nil} => [Explorer.Repo.Neon], {:optimism, :celo} => [ Explorer.Repo.Optimism, Explorer.Repo.Celo From 80a07cafaff9e673819a981881f216bc1f0943a6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 12 Nov 2025 18:43:19 +0300 Subject: [PATCH 0467/1006] Worldchain custom docker image workflow --- ...h-docker-image-for-optimism-worldchain.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/publish-docker-image-for-optimism-worldchain.yml diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml new file mode 100644 index 000000000000..bc69eee3fdf7 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -0,0 +1,62 @@ +name: Optimism Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-optimism +permissions: + contents: read + packages: write + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: ubuntu-latest + env: + RELEASE_VERSION: 9.2.2 + DOCKER_CHAIN_NAME: optimism + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-worldchain + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + + - name: Build and push Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-${{ env.SHORT_SHA }}-worldchain-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + From 4968d2e8d5c937bc78a0b3a43a99db1936eac296 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 12 Nov 2025 18:44:14 +0300 Subject: [PATCH 0468/1006] Rename Worldchain custom docker image workflow --- .../workflows/publish-docker-image-for-optimism-worldchain.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index bc69eee3fdf7..0f68a27e001b 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -1,4 +1,4 @@ -name: Optimism Publish Docker image +name: Optimism Worldchain Publish Docker image on: workflow_dispatch: From 3c87d3c6fe70d2f272ded601a2fb38ebce2dd116 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 12 Nov 2025 19:14:39 +0300 Subject: [PATCH 0469/1006] Change target branch in publish-docker-image-for-optimism-worldchain.yml --- .../workflows/publish-docker-image-for-optimism-worldchain.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index 0f68a27e001b..cec976c9666b 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - production-optimism + - production-optimism-worldchain permissions: contents: read packages: write From d742da95a89da38d0acb75b60ccdbf9dc08afdd7 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 13 Nov 2025 13:42:21 +0300 Subject: [PATCH 0470/1006] perf: implement celo aggregated election rewards (#13418) --- .../controllers/api/rpc/celo_controller.ex | 9 +- .../controllers/api/v2/celo_controller.ex | 4 +- apps/explorer/lib/explorer/application.ex | 1 + .../chain/celo/aggregated_election_reward.ex | 128 +++++ .../explorer/chain/celo/election_reward.ex | 95 +--- .../explorer/chain/import/runner/blocks.ex | 8 + .../celo/aggregated_election_rewards.ex | 80 +++ .../import/runner/celo/election_rewards.ex | 4 +- .../chain/import/stage/chain_type_specific.ex | 3 +- .../celo_aggregated_election_rewards.ex | 94 ++++ ...reate_celo_aggregated_election_rewards.exs | 27 + .../celo_aggregated_election_rewards_test.exs | 522 ++++++++++++++++++ apps/explorer/test/support/factory.ex | 24 + .../fetcher/celo/epoch_block_operations.ex | 26 +- config/runtime.exs | 3 + 15 files changed, 934 insertions(+), 94 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/celo/aggregated_election_reward.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/celo/aggregated_election_rewards.ex create mode 100644 apps/explorer/lib/explorer/migrator/celo_aggregated_election_rewards.ex create mode 100644 apps/explorer/priv/celo/migrations/20251016095435_create_celo_aggregated_election_rewards.exs create mode 100644 apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex index 4e34e5ec56bd..3c348419ffb6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/celo_controller.ex @@ -1,6 +1,7 @@ defmodule BlockScoutWeb.API.RPC.CeloController do use BlockScoutWeb, :controller - alias Explorer.Chain.Celo.{ElectionReward, Epoch} + + alias Explorer.Chain.Celo.{AggregatedElectionReward, Epoch} alias Explorer.Helper @max_safe_epoch_number 32_768 @@ -20,7 +21,11 @@ defmodule BlockScoutWeb.API.RPC.CeloController do {:format, {:ok, epoch_number}} <- {:format, Helper.safe_parse_non_negative_integer(epoch_number, @max_safe_epoch_number)}, {:epoch, {:ok, epoch}} <- {:epoch, Epoch.from_number(epoch_number, options)} do - aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, options) + aggregated_rewards = + AggregatedElectionReward.epoch_number_to_rewards_aggregated_by_type( + epoch.number, + options + ) conn |> render(:celo_epoch, epoch: epoch, aggregated_rewards: aggregated_rewards) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex index bc8ad6ceff56..98a2724d5922 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/celo_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do import Explorer.PagingOptions, only: [default_paging_options: 0] - alias Explorer.Chain.Celo.{ElectionReward, Epoch} + alias Explorer.Chain.Celo.{AggregatedElectionReward, ElectionReward, Epoch} alias Explorer.Chain.Hash alias Explorer.PagingOptions @@ -80,7 +80,7 @@ defmodule BlockScoutWeb.API.V2.CeloController do with {:ok, number} <- parse_epoch_number(number_string), {:ok, epoch} <- Epoch.from_number(number, options) do - aggregated_rewards = ElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, api?: true) + aggregated_rewards = AggregatedElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch.number, api?: true) conn |> render(:celo_epoch, %{ diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index a17438efc49a..213d12360c3d 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -164,6 +164,7 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.SmartContractLanguage, :indexer), configure_mode_dependent_process(Explorer.Migrator.CeloL2Epochs, :indexer), configure_mode_dependent_process(Explorer.Migrator.CeloAccounts, :indexer), + configure_mode_dependent_process(Explorer.Migrator.CeloAggregatedElectionRewards, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeErc1155TokenBalancesWithoutTokenIds, :indexer), Explorer.Migrator.BackfillMultichainSearchDB |> configure_mode_dependent_process(:indexer) diff --git a/apps/explorer/lib/explorer/chain/celo/aggregated_election_reward.ex b/apps/explorer/lib/explorer/chain/celo/aggregated_election_reward.ex new file mode 100644 index 000000000000..d358f00093f5 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/celo/aggregated_election_reward.ex @@ -0,0 +1,128 @@ +defmodule Explorer.Chain.Celo.AggregatedElectionReward do + @moduledoc """ + Schema for aggregated election rewards in the Celo blockchain. + """ + + use Explorer.Schema + + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + + alias Explorer.Chain + alias Explorer.Chain.{Token, Wei} + + alias Explorer.Chain.Celo.{ElectionReward, Epoch} + + @required_attrs ~w(epoch_number type sum count)a + + @primary_key false + typed_schema "celo_aggregated_election_rewards" do + field(:sum, Wei, null: false) + field(:count, :integer, null: false) + + field( + :type, + Ecto.Enum, + values: ElectionReward.types(), + null: false, + primary_key: true + ) + + belongs_to(:epoch, Epoch, + primary_key: true, + foreign_key: :epoch_number, + references: :number, + type: :integer + ) + + timestamps() + end + + @spec changeset(__MODULE__.t(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = rewards, attrs) do + rewards + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:epoch_number) + end + + @doc """ + Retrieves aggregated election rewards by epoch number. + + ## Parameters + - `epoch_number` (`integer()`): The epoch number to aggregate election rewards + for. + - `options` (`Keyword.t()`): Optional parameters for fetching data. + + ## Returns + - A map of aggregated election rewards by type. + + ## Examples + + iex> epoch_number = 1 + iex> Explorer.Chain.Celo.AggregatedElectionReward.epoch_number_to_rewards_aggregated_by_type(epoch_number) + %{ + voter: %{total: %Explorer.Chain.Wei{value: #Decimal<2500>}, count: 2, token: %Explorer.Chain.Token{}}, + validator: %{total: %Explorer.Chain.Wei{value: #Decimal<0>}, count: 0, token: %Explorer.Chain.Token{}}, + group: %{total: %Explorer.Chain.Wei{value: #Decimal<0>}, count: 0, token: %Explorer.Chain.Token{}}, + delegated_payment: %{total: %Explorer.Chain.Wei{value: #Decimal<0>}, count: 0, token: %Explorer.Chain.Token{}} + } + """ + @spec epoch_number_to_rewards_aggregated_by_type(integer(), Keyword.t()) :: + %{atom() => %{total: Wei.t(), count: integer(), token: Token.t() | nil}} + def epoch_number_to_rewards_aggregated_by_type(epoch_number, options \\ []) do + reward_type_to_aggregated_rewards = + __MODULE__ + |> where([r], r.epoch_number == ^epoch_number) + |> Chain.select_repo(options).all() + |> Map.new(&{&1.type, %{total: &1.sum, count: &1.count}}) + + reward_type_to_token = election_reward_tokens_by_type() + zero = %Wei{value: Decimal.new(0)} + + ElectionReward.types() + |> Map.new(&{&1, %{total: zero, count: 0}}) + |> Map.merge(reward_type_to_aggregated_rewards) + |> Map.new(fn {type, aggregated_reward} -> + token = reward_type_to_token[type] + aggregated_reward_with_token = Map.put(aggregated_reward, :token, token) + {type, aggregated_reward_with_token} + end) + end + + # Retrieves the token for each type of election reward. + # + # ## Parameters + # - `options` (`Keyword.t()`): Optional parameters for fetching data. + # + # ## Returns + # - `%{atom() => Token.t() | nil}`: A map of reward types to token. + # + # ## Examples + # + # iex> epoch_number = %Hash.Full{ + # ...> byte_count: 32, + # ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> + # ...> } + # iex> Explorer.Chain.Celo.ElectionReward.election_reward_token_addresses_by_type(epoch_number) + # %{voter_reward: %Token{}, ...} + @spec election_reward_tokens_by_type :: %{atom() => Token.t() | nil} + defp election_reward_tokens_by_type do + reward_type_to_token_address_hash = ElectionReward.reward_type_to_token_address_hash() + + tokens = + reward_type_to_token_address_hash + |> Map.values() + |> Token.get_by_contract_address_hashes( + api?: true, + necessity_by_association: %{ + reputation_association() => :optional + } + ) + + reward_type_to_token_address_hash + |> Map.new(fn {type, address_hash} -> + token = Enum.find(tokens, &(&1.contract_address_hash == address_hash)) + {type, token} + end) + end +end diff --git a/apps/explorer/lib/explorer/chain/celo/election_reward.ex b/apps/explorer/lib/explorer/chain/celo/election_reward.ex index bbabb4560dd5..5a0afc644de5 100644 --- a/apps/explorer/lib/explorer/chain/celo/election_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/election_reward.ex @@ -29,8 +29,7 @@ defmodule Explorer.Chain.Celo.ElectionReward do use Explorer.Schema import Explorer.PagingOptions, only: [default_paging_options: 0] - import Ecto.Query, only: [from: 2, where: 3, group_by: 3, select: 3] - import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + import Ecto.Query, only: [from: 2, where: 3] alias Explorer.{Chain, SortingHelper} alias Explorer.Chain.{Address, Address.Reputation, Celo.Epoch, Hash, Token, Wei} @@ -169,88 +168,6 @@ defmodule Explorer.Chain.Celo.ElectionReward do Map.fetch(@reward_type_string_to_atom, type_string) end - @doc """ - Retrieves aggregated election rewards by block hash. - - ## Parameters - - `block_hash` (`Hash.Full.t()`): The block hash to aggregate election - rewards. - - `options` (`Keyword.t()`): Optional parameters for fetching data. - - ## Returns - - `%{atom() => Wei.t() | nil}`: A map of aggregated election rewards by type. - - ## Examples - - iex> block_hash = %Hash.Full{ - ...> byte_count: 32, - ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - ...> } - iex> Explorer.Chain.Celo.Reader.epoch_number_to_rewards_aggregated_by_type(block_hash) - %{voter_reward: %{total: %Decimal{}, count: 2}, ...} - """ - @spec epoch_number_to_rewards_aggregated_by_type(integer(), Keyword.t()) :: - %{atom() => %{total: Decimal.t(), count: integer(), token: map() | nil}} - def epoch_number_to_rewards_aggregated_by_type(epoch_number, options \\ []) do - reward_type_to_aggregated_rewards = - __MODULE__ - |> where([r], r.epoch_number == ^epoch_number) - |> group_by([r], r.type) - |> select([r], {r.type, sum(r.amount), count(r)}) - |> Chain.select_repo(options).all() - |> Map.new(fn {type, total, count} -> - {type, %{total: total, count: count}} - end) - - reward_type_to_token = election_reward_tokens_by_type() - - @types_enum - |> Map.new(&{&1, %{total: Decimal.new(0), count: 0}}) - |> Map.merge(reward_type_to_aggregated_rewards) - |> Map.new(fn {type, aggregated_reward} -> - token = reward_type_to_token[type] - aggregated_reward_with_token = Map.put(aggregated_reward, :token, token) - {type, aggregated_reward_with_token} - end) - end - - # Retrieves the token for each type of election reward. - # - # ## Parameters - # - `options` (`Keyword.t()`): Optional parameters for fetching data. - # - # ## Returns - # - `%{atom() => Token.t() | nil}`: A map of reward types to token. - # - # ## Examples - # - # iex> epoch_number = %Hash.Full{ - # ...> byte_count: 32, - # ...> bytes: <<0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b :: big-integer-size(32)-unit(8)>> - # ...> } - # iex> Explorer.Chain.Celo.ElectionReward.election_reward_token_addresses_by_type(epoch_number) - # %{voter_reward: %Token{}, ...} - @spec election_reward_tokens_by_type :: %{atom() => Token.t() | nil} - defp election_reward_tokens_by_type do - reward_type_to_token_address_hash = reward_type_to_token_address_hash() - - tokens = - reward_type_to_token_address_hash - |> Map.values() - |> Token.get_by_contract_address_hashes( - api?: true, - necessity_by_association: %{ - reputation_association() => :optional - } - ) - - reward_type_to_token_address_hash - |> Map.new(fn {type, address_hash} -> - token = Enum.find(tokens, &(&1.contract_address_hash == address_hash)) - {type, token} - end) - end - @doc """ Retrieves election rewards by epoch number and reward type. @@ -338,7 +255,15 @@ defmodule Explorer.Chain.Celo.ElectionReward do |> with_loaded_token_reputations() end - defp reward_type_to_token_address_hash do + @doc """ + Retrieves a mapping of reward types to their corresponding token address + hashes. + ## Returns + - `%{type => Hash.Address.t()}`: A map of reward types to + token address hashes. + """ + @spec reward_type_to_token_address_hash :: %{type => Hash.Address.t()} + def reward_type_to_token_address_hash do Map.new( @reward_type_atom_to_token_atom, fn {type, token_atom} -> diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 132f956da123..1c529bb57f90 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -37,6 +37,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Chain.Celo.AggregatedElectionReward, as: CeloAggregatedElectionReward alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward alias Explorer.Chain.Celo.Epoch, as: CeloEpoch alias Explorer.Chain.Celo.EpochReward, as: CeloEpochReward @@ -1230,8 +1231,15 @@ defmodule Explorer.Chain.Import.Runner.Blocks do on: reward.epoch_number == epoch.number ) + aggregated_election_rewards_query = + from(reward in CeloAggregatedElectionReward, + join: epoch in subquery(ordered_query), + on: reward.epoch_number == epoch.number + ) + repo.delete_all(epoch_rewards_query, timeout: timeout) repo.delete_all(election_rewards_query, timeout: timeout) + repo.delete_all(aggregated_election_rewards_query, timeout: timeout) {_count, updated_epochs} = repo.update_all( diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/aggregated_election_rewards.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/aggregated_election_rewards.ex new file mode 100644 index 000000000000..24cd225b5b33 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/aggregated_election_rewards.ex @@ -0,0 +1,80 @@ +defmodule Explorer.Chain.Import.Runner.Celo.AggregatedElectionRewards do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Celo.AggregatedElectionReward.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Celo.AggregatedElectionReward + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [AggregatedElectionReward.t()] + + @impl Import.Runner + def ecto_schema_module, do: AggregatedElectionReward + + @impl Import.Runner + def option_key, do: :celo_aggregated_election_rewards + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + multi + |> Multi.run(:insert_celo_aggregated_election_rewards, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :celo_aggregated_election_rewards, + :celo_aggregated_election_rewards + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [AggregatedElectionReward.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce Celo.AggregatedElectionReward ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.type, &1.epoch_number}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: AggregatedElectionReward, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: [:type, :epoch_number], + on_conflict: :replace_all + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex b/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex index 8f32c87fee83..2e1715f599c5 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/celo/election_rewards.ex @@ -64,8 +64,8 @@ defmodule Explorer.Chain.Import.Runner.Celo.ElectionRewards do Enum.sort_by( changes_list, &{ - &1.epoch_number, &1.type, + &1.epoch_number, &1.account_address_hash, &1.associated_account_address_hash } @@ -80,8 +80,8 @@ defmodule Explorer.Chain.Import.Runner.Celo.ElectionRewards do timeout: timeout, timestamps: timestamps, conflict_target: [ - :epoch_number, :type, + :epoch_number, :account_address_hash, :associated_account_address_hash ], diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 0278c4bca77a..01448381142a 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -76,7 +76,8 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do Runner.Celo.ValidatorGroupVotes, Runner.Celo.Epochs, Runner.Celo.ElectionRewards, - Runner.Celo.EpochRewards + Runner.Celo.EpochRewards, + Runner.Celo.AggregatedElectionRewards ] } diff --git a/apps/explorer/lib/explorer/migrator/celo_aggregated_election_rewards.ex b/apps/explorer/lib/explorer/migrator/celo_aggregated_election_rewards.ex new file mode 100644 index 000000000000..c14fd18fab30 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/celo_aggregated_election_rewards.ex @@ -0,0 +1,94 @@ +defmodule Explorer.Migrator.CeloAggregatedElectionRewards do + @moduledoc """ + Backfills the `celo_aggregated_election_rewards` table with aggregated + statistics from the `celo_election_rewards` table for all finalized epochs. + + This migration calculates the sum and count of rewards grouped by epoch number + and reward type, creating pre-computed aggregates that significantly improve + query performance for epoch reward statistics. + + Only epochs with at least one election reward are processed. Epochs with zero + rewards are skipped. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Celo.{AggregatedElectionReward, ElectionReward, Epoch} + alias Explorer.Migrator.FillingMigration + + @migration_name "celo_aggregated_election_rewards" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(state) do + limit = batch_size() * concurrency() + + epoch_numbers = + unprocessed_data_query() + |> select([e], e.number) + |> limit(^limit) + |> Repo.all(timeout: :infinity) + + {epoch_numbers, state} + end + + @impl FillingMigration + def unprocessed_data_query do + # Get all epochs that have been finalized (have end_processing_block_hash) + # but don't yet have aggregated election rewards. + # Only process epochs that have at least one election reward to avoid + # reprocessing epochs with no rewards indefinitely. + aggregated_epoch_numbers = + from(aer in AggregatedElectionReward, + select: aer.epoch_number, + distinct: true + ) + + epochs_with_rewards = + from(er in ElectionReward, + select: er.epoch_number, + distinct: true + ) + + from( + e in Epoch, + join: r in subquery(epochs_with_rewards), + on: r.epoch_number == e.number, + where: not is_nil(e.end_processing_block_hash), + where: e.number not in subquery(aggregated_epoch_numbers), + order_by: [asc: e.number] + ) + end + + @impl FillingMigration + def update_batch(epoch_numbers) when is_list(epoch_numbers) do + query = + from( + er in ElectionReward, + where: er.epoch_number in ^epoch_numbers, + group_by: [er.epoch_number, er.type], + select: %{ + epoch_number: er.epoch_number, + type: er.type, + sum: sum(er.amount), + count: count(er.amount) + } + ) + + aggregates = Repo.all(query, timeout: :infinity) + + Chain.import(%{ + celo_aggregated_election_rewards: %{ + params: aggregates + } + }) + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/priv/celo/migrations/20251016095435_create_celo_aggregated_election_rewards.exs b/apps/explorer/priv/celo/migrations/20251016095435_create_celo_aggregated_election_rewards.exs new file mode 100644 index 000000000000..be390bd0b579 --- /dev/null +++ b/apps/explorer/priv/celo/migrations/20251016095435_create_celo_aggregated_election_rewards.exs @@ -0,0 +1,27 @@ +defmodule Explorer.Repo.Celo.Migrations.CreateCeloAggregatedElectionRewards do + use Ecto.Migration + + def change do + create table(:celo_aggregated_election_rewards, primary_key: false) do + add(:sum, :numeric, precision: 100, null: false) + add(:count, :integer, null: false) + add(:type, :celo_election_reward_type, null: false, primary_key: true) + + add( + :epoch_number, + references( + :celo_epochs, + column: :number, + on_delete: :delete_all + ), + null: false, + primary_key: true + ) + + timestamps() + end + + # Index for efficient lookups by epoch_number + create(index(:celo_aggregated_election_rewards, [:epoch_number])) + end +end diff --git a/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs b/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs new file mode 100644 index 000000000000..0ed08b45462d --- /dev/null +++ b/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs @@ -0,0 +1,522 @@ +defmodule Explorer.Migrator.CeloAggregatedElectionRewardsTest do + use Explorer.DataCase, async: false + + use Utils.CompileTimeEnvHelper, + chain_identity: [:explorer, :chain_identity] + + if @chain_identity == {:optimism, :celo} do + alias Explorer.Chain.Celo.{AggregatedElectionReward, Epoch} + alias Explorer.Migrator.{CeloAggregatedElectionRewards, MigrationStatus} + alias Explorer.Repo + + import Ecto.Query + + describe "celo_aggregated_election_rewards migration" do + setup do + # Clean up migration status before each test + Repo.delete_all(MigrationStatus) + :ok + end + + test "does not fail when there are no epochs" do + # Ensure no epochs exist + assert Repo.aggregate(Epoch, :count) == 0 + + # Run migration + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == nil + start_supervised!(CeloAggregatedElectionRewards) + + # Wait for migration to complete + Process.sleep(500) + + # Verify migration completed successfully + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + + # Verify no aggregated rewards were created + assert Repo.aggregate(AggregatedElectionReward, :count) == 0 + end + + test "skips epochs without end_processing_block_hash (unfinalized epochs)" do + # Create finalized epoch + finalized_epoch = insert(:celo_epoch, number: 100, end_processing_block_hash: insert(:block).hash) + + # Create unfinalized epoch (no end_processing_block_hash) + _unfinalized_epoch = insert(:celo_epoch, number: 101, end_processing_block_hash: nil) + + # Create addresses for the rewards + account_address = insert(:address) + associated_address = insert(:address) + + # Add rewards for finalized epoch + insert(:celo_election_reward, + epoch_number: finalized_epoch.number, + type: :voter, + amount: 1000, + account_address_hash: account_address.hash, + associated_account_address_hash: associated_address.hash + ) + + # Add rewards for unfinalized epoch + insert(:celo_election_reward, + epoch_number: 101, + type: :voter, + amount: 2000, + account_address_hash: account_address.hash, + associated_account_address_hash: associated_address.hash + ) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(500) + + # Verify only finalized epoch was processed + aggregated_rewards = Repo.all(AggregatedElectionReward) + # Only 1 reward type (voter) has data, so only 1 aggregate is saved (no zeros) + assert length(aggregated_rewards) == 1 + + epoch_numbers = Enum.map(aggregated_rewards, & &1.epoch_number) |> Enum.uniq() + assert epoch_numbers == [100] + end + + test "backfills aggregated rewards for a single epoch with all reward types" do + epoch = insert(:celo_epoch, number: 42, end_processing_block_hash: insert(:block).hash) + + # Create addresses for the rewards + account1 = insert(:address) + account2 = insert(:address) + associated1 = insert(:address) + associated2 = insert(:address) + + # Insert rewards for all types + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :voter, + amount: 1000, + account_address_hash: account1.hash, + associated_account_address_hash: associated1.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :voter, + amount: 1500, + account_address_hash: account2.hash, + associated_account_address_hash: associated1.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :validator, + amount: 2000, + account_address_hash: account1.hash, + associated_account_address_hash: associated1.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :group, + amount: 3000, + account_address_hash: account1.hash, + associated_account_address_hash: associated1.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :group, + amount: 3500, + account_address_hash: account1.hash, + associated_account_address_hash: associated2.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :group, + amount: 4000, + account_address_hash: account2.hash, + associated_account_address_hash: associated1.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :delegated_payment, + amount: 500, + account_address_hash: account1.hash, + associated_account_address_hash: associated1.hash + ) + + # Ensure migration has not run yet + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == nil + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + # Wait for migration to complete + Process.sleep(500) + + # Verify aggregated rewards were created correctly + aggregated_rewards = + from(aer in AggregatedElectionReward, + where: aer.epoch_number == ^epoch.number, + order_by: [asc: aer.type] + ) + |> Repo.all() + + assert length(aggregated_rewards) == 4 + + # Check voter rewards + voter_reward = Enum.find(aggregated_rewards, &(&1.type == :voter)) + assert voter_reward.count == 2 + assert Decimal.to_integer(voter_reward.sum.value) == 2500 + + # Check validator rewards + validator_reward = Enum.find(aggregated_rewards, &(&1.type == :validator)) + assert validator_reward.count == 1 + assert Decimal.to_integer(validator_reward.sum.value) == 2000 + + # Check group rewards + group_reward = Enum.find(aggregated_rewards, &(&1.type == :group)) + assert group_reward.count == 3 + assert Decimal.to_integer(group_reward.sum.value) == 10500 + + # Check delegated_payment rewards + delegated_reward = Enum.find(aggregated_rewards, &(&1.type == :delegated_payment)) + assert delegated_reward.count == 1 + assert Decimal.to_integer(delegated_reward.sum.value) == 500 + + # Confirm migration status is completed + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + end + + test "only saves reward types with data (no zero values for types with actual rewards)" do + epoch = insert(:celo_epoch, number: 50, end_processing_block_hash: insert(:block).hash) + + # Create addresses + account = insert(:address) + associated = insert(:address) + + # Only insert voter rewards, leaving other types empty + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :voter, + amount: 5000, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(500) + + # Verify only reward types with data are saved (no zero values for other types) + aggregated_rewards = + from(aer in AggregatedElectionReward, + where: aer.epoch_number == ^epoch.number, + order_by: [asc: aer.type] + ) + |> Repo.all() + + # Only voter type has data, so only 1 aggregate is saved + assert length(aggregated_rewards) == 1 + + # Check voter has data + voter_reward = Enum.at(aggregated_rewards, 0) + assert voter_reward.type == :voter + assert voter_reward.count == 1 + assert Decimal.to_integer(voter_reward.sum.value) == 5000 + + # Verify other types are NOT saved (no zero-value entries for types with actual data) + for type <- [:validator, :group, :delegated_payment] do + reward = Enum.find(aggregated_rewards, &(&1.type == type)) + assert reward == nil + end + end + + test "backfills aggregated rewards for multiple epochs" do + # Create multiple finalized epochs + epoch1 = insert(:celo_epoch, number: 10, end_processing_block_hash: insert(:block).hash) + epoch2 = insert(:celo_epoch, number: 11, end_processing_block_hash: insert(:block).hash) + epoch3 = insert(:celo_epoch, number: 12, end_processing_block_hash: insert(:block).hash) + + # Create addresses + account = insert(:address) + associated = insert(:address) + + # Add rewards for epoch 1 + insert(:celo_election_reward, + epoch_number: epoch1.number, + type: :voter, + amount: 100, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch1.number, + type: :validator, + amount: 200, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + # Add rewards for epoch 2 + insert(:celo_election_reward, + epoch_number: epoch2.number, + type: :group, + amount: 300, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + # Add rewards for epoch 3 + account2 = insert(:address) + + insert(:celo_election_reward, + epoch_number: epoch3.number, + type: :delegated_payment, + amount: 400, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch3.number, + type: :delegated_payment, + amount: 500, + account_address_hash: account2.hash, + associated_account_address_hash: associated.hash + ) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(1000) + + # Verify all epochs were processed + aggregated_rewards = Repo.all(AggregatedElectionReward) + + # Each epoch has different reward types with data: + # epoch1: voter, validator (2 types) + # epoch2: group (1 type) + # epoch3: delegated_payment (1 type) + # Total: 4 aggregated records (only non-zero values) + assert length(aggregated_rewards) == 4 + + epoch_numbers = Enum.map(aggregated_rewards, & &1.epoch_number) |> Enum.uniq() |> Enum.sort() + assert epoch_numbers == [10, 11, 12] + + # Verify epoch 1 + epoch1_rewards = + Enum.filter(aggregated_rewards, &(&1.epoch_number == epoch1.number)) + |> Enum.sort_by(& &1.type) + + voter1 = Enum.find(epoch1_rewards, &(&1.type == :voter)) + assert voter1.count == 1 + assert Decimal.to_integer(voter1.sum.value) == 100 + + validator1 = Enum.find(epoch1_rewards, &(&1.type == :validator)) + assert validator1.count == 1 + assert Decimal.to_integer(validator1.sum.value) == 200 + + # Verify epoch 3 + epoch3_rewards = Enum.filter(aggregated_rewards, &(&1.epoch_number == epoch3.number)) + delegated3 = Enum.find(epoch3_rewards, &(&1.type == :delegated_payment)) + assert delegated3.count == 2 + assert Decimal.to_integer(delegated3.sum.value) == 900 + + # Confirm migration status is completed + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + end + + test "is idempotent - does not duplicate data when run multiple times" do + epoch = insert(:celo_epoch, number: 99, end_processing_block_hash: insert(:block).hash) + + account = insert(:address) + associated = insert(:address) + + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :voter, + amount: 7777, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + # Run migration first time + start_supervised!(CeloAggregatedElectionRewards) + + Process.sleep(500) + + # Verify data was created (only 1 type has data, so only 1 aggregate) + assert Repo.aggregate(AggregatedElectionReward, :count) == 1 + + initial_rewards = + from(aer in AggregatedElectionReward, order_by: [asc: aer.type]) + |> Repo.all() + + # Reset migration status to simulate running again + MigrationStatus.set_status("celo_aggregated_election_rewards", nil) + + # Run migration second time + start_supervised!(CeloAggregatedElectionRewards) + + Process.sleep(500) + + # Verify data was not duplicated + assert Repo.aggregate(AggregatedElectionReward, :count) == 1 + + final_rewards = + from(aer in AggregatedElectionReward, order_by: [asc: aer.type]) + |> Repo.all() + + # Data should be identical + Enum.zip(initial_rewards, final_rewards) + |> Enum.each(fn {initial, final} -> + assert initial.epoch_number == final.epoch_number + assert initial.type == final.type + assert initial.count == final.count + assert Decimal.equal?(initial.sum.value, final.sum.value) + end) + end + + test "processes epochs in ascending order" do + # Create epochs out of order + epoch3 = insert(:celo_epoch, number: 33, end_processing_block_hash: insert(:block).hash) + epoch1 = insert(:celo_epoch, number: 31, end_processing_block_hash: insert(:block).hash) + epoch2 = insert(:celo_epoch, number: 32, end_processing_block_hash: insert(:block).hash) + + # Create addresses + account = insert(:address) + associated = insert(:address) + + # Add minimal rewards + for epoch <- [epoch1, epoch2, epoch3] do + insert(:celo_election_reward, + epoch_number: epoch.number, + type: :voter, + amount: 100 * epoch.number, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + end + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(1000) + + # Verify all epochs were processed + aggregated_rewards = + from(aer in AggregatedElectionReward, + where: aer.type == :voter, + order_by: [asc: aer.epoch_number] + ) + |> Repo.all() + + assert length(aggregated_rewards) == 3 + assert Enum.at(aggregated_rewards, 0).epoch_number == 31 + assert Enum.at(aggregated_rewards, 1).epoch_number == 32 + assert Enum.at(aggregated_rewards, 2).epoch_number == 33 + end + + test "skips epoch with no election rewards" do + # Create epoch with no rewards + epoch = insert(:celo_epoch, number: 999, end_processing_block_hash: insert(:block).hash) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(500) + + # Verify no aggregates were created (epoch was skipped) + aggregated_rewards = + from(aer in AggregatedElectionReward, + where: aer.epoch_number == ^epoch.number + ) + |> Repo.all() + + assert length(aggregated_rewards) == 0 + + # Verify migration completed (no epochs with rewards to process) + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + + # Verify unprocessed_data_query doesn't return the epoch without rewards + unprocessed_epochs = + CeloAggregatedElectionRewards.unprocessed_data_query() + |> select([e], e.number) + |> Repo.all() + + refute epoch.number in unprocessed_epochs + end + + test "completes migration even when all epochs have no rewards" do + # Create multiple finalized epochs with no rewards + _epoch1 = insert(:celo_epoch, number: 100, end_processing_block_hash: insert(:block).hash) + _epoch2 = insert(:celo_epoch, number: 101, end_processing_block_hash: insert(:block).hash) + _epoch3 = insert(:celo_epoch, number: 102, end_processing_block_hash: insert(:block).hash) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(1000) + + # Verify no aggregates were created (all epochs skipped) + aggregated_rewards = Repo.all(AggregatedElectionReward) + assert length(aggregated_rewards) == 0 + + # Most importantly: verify migration completed and doesn't spin forever + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + + # Verify unprocessed_data_query returns nothing (epochs with no rewards are skipped) + unprocessed_count = + CeloAggregatedElectionRewards.unprocessed_data_query() + |> Repo.aggregate(:count) + + assert unprocessed_count == 0 + end + + test "processes only epochs with rewards and skips epochs without rewards" do + # Create a mix of epochs with and without rewards + epoch_with_rewards = insert(:celo_epoch, number: 200, end_processing_block_hash: insert(:block).hash) + _epoch_without_rewards1 = insert(:celo_epoch, number: 201, end_processing_block_hash: insert(:block).hash) + epoch_with_rewards2 = insert(:celo_epoch, number: 202, end_processing_block_hash: insert(:block).hash) + _epoch_without_rewards2 = insert(:celo_epoch, number: 203, end_processing_block_hash: insert(:block).hash) + + # Create addresses + account = insert(:address) + associated = insert(:address) + + # Add rewards only for specific epochs + insert(:celo_election_reward, + epoch_number: epoch_with_rewards.number, + type: :voter, + amount: 1000, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + insert(:celo_election_reward, + epoch_number: epoch_with_rewards2.number, + type: :validator, + amount: 2000, + account_address_hash: account.hash, + associated_account_address_hash: associated.hash + ) + + # Run migration + start_supervised!(CeloAggregatedElectionRewards) + Process.sleep(1000) + + # Verify only epochs with rewards were processed + aggregated_rewards = Repo.all(AggregatedElectionReward) + assert length(aggregated_rewards) == 2 + + epoch_numbers = Enum.map(aggregated_rewards, & &1.epoch_number) |> Enum.sort() + assert epoch_numbers == [200, 202] + + # Verify epochs without rewards were not processed + for epoch_num <- [201, 203] do + rewards = Repo.all(from(aer in AggregatedElectionReward, where: aer.epoch_number == ^epoch_num)) + assert length(rewards) == 0 + end + + # Verify migration completed + assert MigrationStatus.get_status("celo_aggregated_election_rewards") == "completed" + end + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index e506a0a82921..0ffee5775752 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -59,6 +59,9 @@ defmodule Explorer.Factory do alias Explorer.Chain.Zilliqa.Hash.BLSPublicKey alias Explorer.Chain.Zilliqa.Staker, as: ZilliqaStaker + alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward + alias Explorer.Chain.Celo.Epoch, as: CeloEpoch + alias Explorer.Migrator.MigrationStatus alias Explorer.SmartContract.Helper @@ -1397,6 +1400,27 @@ defmodule Explorer.Factory do def random_bool, do: Enum.random([true, false]) + def celo_epoch_factory do + %CeloEpoch{ + number: sequence("celo_epoch_number", & &1), + fetched?: false, + start_block_number: nil, + end_block_number: nil, + start_processing_block_hash: nil, + end_processing_block_hash: nil + } + end + + def celo_election_reward_factory do + %CeloElectionReward{ + amount: Enum.random(1..100_000), + type: Enum.random([:voter, :validator, :group, :delegated_payment]), + epoch_number: sequence("celo_election_reward_epoch_number", & &1), + account_address_hash: insert(:address).hash, + associated_account_address_hash: insert(:address).hash + } + end + def validator_stability_factory do address = insert(:address) diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex index 21adea918d25..89a87e66562e 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex @@ -14,6 +14,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do alias Indexer.Transform.Addresses alias Explorer.Chain.Import.Runner.Celo.{ + AggregatedElectionRewards, ElectionRewards, EpochRewards, Epochs @@ -118,6 +119,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do defp fetch(epoch, json_rpc_named_arguments) do election_rewards_params = fetch_election_rewards_params(epoch, json_rpc_named_arguments) + aggregated_election_rewards_params = aggregate_election_rewards(election_rewards_params) epoch_params = fetch_epoch_params(epoch) {:ok, distributions_params} = Distributions.fetch(epoch) @@ -148,12 +150,14 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do [ Epochs, ElectionRewards, - EpochRewards + EpochRewards, + AggregatedElectionRewards ], %{ celo_epoch_rewards: %{params: [distributions_params]}, celo_election_rewards: %{params: election_rewards_params}, - celo_epochs: %{params: epochs_params} + celo_epochs: %{params: epochs_params}, + celo_aggregated_election_rewards: %{params: aggregated_election_rewards_params} } ) @@ -184,6 +188,24 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do end end + defp aggregate_election_rewards(election_rewards_params) do + election_rewards_params + |> Enum.group_by(&{&1.epoch_number, &1.type}) + |> Enum.map(fn {{epoch_number, type}, rewards} -> + %{ + epoch_number: epoch_number, + type: type, + count: length(rewards), + sum: + rewards + |> Enum.reduce( + Decimal.new(0), + fn r, acc -> Decimal.add(acc, r.amount) end + ) + } + end) + end + defp fetch_election_rewards_params(epoch, json_rpc_named_arguments) do {:ok, voter_payments} = VoterPayments.fetch( diff --git a/config/runtime.exs b/config/runtime.exs index 3c04d964d9ea..6f7d2288be09 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -811,6 +811,9 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} +config :explorer, Explorer.Migrator.CeloAggregatedElectionRewards, + enabled: ConfigHelper.chain_identity() == {:optimism, :celo} + config :explorer, Explorer.Migrator.CeloL2Epochs, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} && From 79c67578ece4422f4f8ea13587725b13f229f670 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:57:52 +0300 Subject: [PATCH 0471/1006] chore: Move not auth cookies to headers (#13478) --- apps/block_scout_web/config/config.exs | 3 +- .../lib/block_scout_web/chain.ex | 33 +++++-- .../controllers/api/v2/api_key_controller.ex | 61 ++++++++++-- .../lib/block_scout_web/endpoint.ex | 16 +++- .../lib/block_scout_web/rate_limit.ex | 26 ++++-- .../test/block_scout_web/rate_limit_test.exs | 93 +++++++++++++++++++ 6 files changed, 202 insertions(+), 30 deletions(-) diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 24f380eadd14..808135c5ef62 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -17,7 +17,8 @@ config :block_scout_web, # 604800 seconds, 1 week session_cookie_ttl: 60 * 60 * 24 * 7, invalid_session_key: "invalid_session", - api_v2_temp_token_key: "api_v2_temp_token", + api_v2_temp_token_cookie_key: "api_v2_temp_token", + api_v2_temp_token_header_key: "api-v2-temp-token", http_client: Explorer.HttpClient.Tesla config :block_scout_web, diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index be132193f151..1cd0aec0ab61 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -56,6 +56,7 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.Optimism.OutputRoot, as: OptimismOutputRoot alias Explorer.Chain.Scroll.Bridge, as: ScrollBridge alias Explorer.PagingOptions + alias Plug.Conn @page_size page_size() @default_paging_options default_paging_options() @@ -1140,20 +1141,36 @@ defmodule BlockScoutWeb.Chain do end @doc """ - Fetches the scam token toggle from conn.cookies["show_scam_tokens"]. And put it to the params keyword. + Determines the scam token toggle value and adds it to the params keyword list. - ## Parameters + The function checks for the scam token toggle in the following order: + 1. Looks for the `"show-scam-tokens"` request header + 2. Falls back to the `"show_scam_tokens"` cookie if the header is not present + 3. Parses the retrieved value as a boolean (defaults to `false` if the value + is neither `"true"`, `"false"`, `true`, nor `false`) - - params: Initial params to append scam token toggle info. - - conn: The connection. + ## Parameters + - `params`: Initial params keyword list to append scam token toggle info. + - `conn`: The connection struct. ## Returns - - Provided params keyword with the new field `show_scam_tokens?`. + The provided params keyword list with the added `show_scam_tokens?` field + set to a boolean value. """ @spec fetch_scam_token_toggle(Keyword.t(), Plug.Conn.t()) :: Keyword.t() - def fetch_scam_token_toggle(params, conn), - do: Keyword.put(params, :show_scam_tokens?, conn.cookies["show_scam_tokens"] |> parse_boolean()) + def fetch_scam_token_toggle(params, conn) do + Keyword.put( + params, + :show_scam_tokens?, + conn + |> Conn.get_req_header("show-scam-tokens") + |> case do + [show_scam_tokens?] -> show_scam_tokens? + _ -> conn.cookies["show_scam_tokens"] + end + |> parse_boolean() + ) + end defp map_to_string_keys(map) do map diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex index 6d23516eee70..77f3f6f88bb4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/api_key_controller.ex @@ -1,28 +1,69 @@ defmodule BlockScoutWeb.API.V2.APIKeyController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, api_v2_temp_token_key: [:block_scout_web, :api_v2_temp_token_key] + + use Utils.CompileTimeEnvHelper, + api_v2_temp_token_cookie_key: [:block_scout_web, :api_v2_temp_token_cookie_key], + api_v2_temp_token_header_key: [:block_scout_web, :api_v2_temp_token_header_key] alias BlockScoutWeb.{AccessHelper, CaptchaHelper} + alias Plug.Crypto action_fallback(BlockScoutWeb.API.V2.FallbackController) - plug(:fetch_cookies, signed: [@api_v2_temp_token_key]) + plug(:fetch_cookies, signed: [@api_v2_temp_token_cookie_key]) @doc """ - Function to handle POST requests to `/api/v2/key` endpoint. It expects body with `recaptcha_response`. And puts cookie with temporary API v2 token. Which is handled here: https://github.com/blockscout/blockscout/blob/cd19739347f267d8a6ad81bbba2dbdad08bcc134/apps/block_scout_web/lib/block_scout_web/views/access_helper.ex#L170 + Handles POST requests to `/api/v2/key` endpoint to generate a temporary API v2 token after CAPTCHA verification. + + The function verifies the CAPTCHA response and, upon successful verification, + generates a temporary API v2 token tied to the client's IP address. The token + is delivered either as a signed response header or as a signed cookie, + depending on the `in_header` parameter. The token is validated by + `BlockScoutWeb.RateLimit.get_ui_v2_token/2`. + + ## Parameters + - `conn`: The connection struct. + - `params`: A map that may contain: + * `"recaptcha_bypass_token"`, `"recaptcha_v3_response"`, or + `"recaptcha_response"` for CAPTCHA verification + * `"in_header"` - if set to `"true"`, the token is placed in a response + header; otherwise, it is placed in a cookie + + ## Returns + - A connection struct with a JSON response `{"message": "OK"}` and either: + * A signed token in the response header (if `params["in_header"]` is + `"true"`) + * A signed token in a cookie (otherwise) + - `{:recaptcha, false}` if CAPTCHA verification fails. """ @spec get_key(Plug.Conn.t(), nil | map) :: {:recaptcha, any} | Plug.Conn.t() def get_key(conn, params) do ttl = div(Application.get_env(:block_scout_web, :api_rate_limit)[:api_v2_token_ttl], 1000) with {:recaptcha, true} <- {:recaptcha, CaptchaHelper.recaptcha_passed?(params)} do - conn - |> put_resp_cookie(@api_v2_temp_token_key, %{ip: AccessHelper.conn_to_ip_string(conn)}, - max_age: ttl, - sign: true, - same_site: "Lax", - domain: Application.get_env(:block_scout_web, :cookie_domain) - ) + params["in_header"] + |> case do + "true" -> + put_resp_header( + conn, + @api_v2_temp_token_header_key, + Crypto.sign( + conn.secret_key_base, + @api_v2_temp_token_header_key <> "-header", + %{ip: AccessHelper.conn_to_ip_string(conn)}, + keys: Plug.Keys, + max_age: ttl + ) + ) + + _ -> + put_resp_cookie(conn, @api_v2_temp_token_cookie_key, %{ip: AccessHelper.conn_to_ip_string(conn)}, + max_age: ttl, + sign: true, + same_site: "Lax", + domain: Application.get_env(:block_scout_web, :cookie_domain) + ) + end |> json(%{ message: "OK" }) diff --git a/apps/block_scout_web/lib/block_scout_web/endpoint.ex b/apps/block_scout_web/lib/block_scout_web/endpoint.ex index 22f88cab8307..570349c2d05e 100644 --- a/apps/block_scout_web/lib/block_scout_web/endpoint.ex +++ b/apps/block_scout_web/lib/block_scout_web/endpoint.ex @@ -6,7 +6,8 @@ defmodule BlockScoutWeb.Endpoint do disable_api?: [:block_scout_web, :disable_api?], sql_sandbox: [:block_scout_web, :sql_sandbox], cookie_domain: [:block_scout_web, :cookie_domain], - session_cookie_ttl: [:block_scout_web, :session_cookie_ttl] + session_cookie_ttl: [:block_scout_web, :session_cookie_ttl], + api_v2_temp_token_header_key: [:block_scout_web, :api_v2_temp_token_header_key] alias Explorer.ThirdPartyIntegrations.UniversalProxy @@ -88,8 +89,17 @@ defmodule BlockScoutWeb.Endpoint do "recaptcha-v2-response", "recaptcha-v3-response", "recaptcha-bypass-token", - "scoped-recaptcha-bypass-token" - ] ++ CORSPlug.defaults()[:headers] + "scoped-recaptcha-bypass-token", + "show-scam-tokens", + @api_v2_temp_token_header_key + ] ++ CORSPlug.defaults()[:headers], + expose: [ + "bypass-429-option", + "x-ratelimit-reset", + "x-ratelimit-limit", + "x-ratelimit-remaining", + @api_v2_temp_token_header_key + ] ) plug(BlockScoutWeb.Router) diff --git a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex index 00d59f25a27c..32e97f4245f2 100644 --- a/apps/block_scout_web/lib/block_scout_web/rate_limit.ex +++ b/apps/block_scout_web/lib/block_scout_web/rate_limit.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.RateLimit do alias BlockScoutWeb.{AccessHelper, CaptchaHelper} alias BlockScoutWeb.RateLimit.Hammer alias Explorer.Account.Api.Key, as: ApiKey - alias Plug.Conn + alias Plug.{Conn, Crypto} require Logger @@ -337,15 +337,25 @@ defmodule BlockScoutWeb.RateLimit do end defp get_ui_v2_token(conn, ip_string) do - api_v2_temp_token_key = Application.get_env(:block_scout_web, :api_v2_temp_token_key) - conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_key]) - - case conn.cookies[api_v2_temp_token_key] do - %{ip: ^ip_string} -> - conn.req_cookies[api_v2_temp_token_key] + api_v2_temp_token_cookie_key = Application.get_env(:block_scout_web, :api_v2_temp_token_cookie_key) + conn = Conn.fetch_cookies(conn, signed: [api_v2_temp_token_cookie_key]) + api_v2_temp_token_header_key = Application.get_env(:block_scout_web, :api_v2_temp_token_header_key) + + case Conn.get_req_header(conn, api_v2_temp_token_header_key) do + [token] -> + case Crypto.verify(conn.secret_key_base, api_v2_temp_token_header_key <> "-header", token, keys: Plug.Keys) do + {:ok, %{ip: ^ip_string}} -> token + _ -> nil + end _ -> - nil + case conn.cookies[api_v2_temp_token_cookie_key] do + %{ip: ^ip_string} -> + conn.req_cookies[api_v2_temp_token_cookie_key] + + _ -> + nil + end end end diff --git a/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs b/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs index f490a76c7880..c1d794cf18fb 100644 --- a/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs +++ b/apps/block_scout_web/test/block_scout_web/rate_limit_test.exs @@ -320,6 +320,99 @@ defmodule BlockScoutWeb.RateLimitTest do assert count == 1 assert limit == 100 assert period == 60_000 + + assert {:allow, count, limit, period} = + RateLimit.rate_limit_with_config(conn |> Map.put(:remote_ip, {192, 168, 1, 2}), config) + + assert count == 1 + assert limit == 1 + assert period == 60_000 + end + + test "applies rate limit by temporary token in header" do + config = %{ + temporary_token: %{ + period: 60_000, + limit: 100 + }, + static_api_key: %{ + period: 60_000, + limit: 200 + }, + account_api_key: %{ + period: 60_000, + limit: 300 + }, + whitelisted_ip: %{ + period: 60_000, + limit: 400 + }, + ip: %{ + period: 60_000, + limit: 1 + } + } + + Application.put_env( + :block_scout_web, + :recaptcha, + Keyword.put(Application.get_env(:block_scout_web, :recaptcha), :bypass_token, "test_token") + ) + + conn = + build_conn() + |> Map.put(:remote_ip, {192, 168, 1, 1}) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 1 + assert period == 60_000 + + assert {:deny, time_to_reset, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert time_to_reset > 0 and time_to_reset < 60_000 + assert limit == 1 + assert period == 60_000 + + # First make request to get temporary token + conn = + build_conn() + |> Map.put(:remote_ip, {192, 168, 1, 1}) + |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + |> post("/api/v2/key", %{"recaptcha_bypass_token" => "test_token", "in_header" => "true"}) + + # Extract token from response + [token] = Plug.Conn.get_resp_header(conn, "api-v2-temp-token") + assert conn.resp_cookies["api_v2_temp_token"] == nil + + # Now make request with the token + conn = + conn + |> Map.put(:remote_ip, {192, 168, 1, 1}) + |> Map.put(:req_headers, [{"user-agent", "test-agent"}, {"api-v2-temp-token", token}]) + + assert {:allow, count, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert count == 1 + assert limit == 100 + assert period == 60_000 + + # Token from header does not work in cookies + conn = + conn + |> Map.put(:remote_ip, {192, 168, 1, 1}) + |> Map.put(:req_headers, [{"user-agent", "test-agent"}]) + |> Map.put(:req_cookies, %{"api_v2_temp_token" => token}) + + assert {:deny, time_to_reset, limit, period} = RateLimit.rate_limit_with_config(conn, config) + assert time_to_reset > 0 and time_to_reset < 60_000 + assert limit == 1 + assert period == 60_000 + + assert {:allow, count, limit, period} = + RateLimit.rate_limit_with_config(conn |> Map.put(:remote_ip, {192, 168, 1, 2}), config) + + assert count == 1 + assert limit == 1 + assert period == 60_000 end test "handles recaptcha bypass" do From 415ff99416c6fb8c14035455a3ffa0ec8522f286 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 13 Nov 2025 17:18:42 +0400 Subject: [PATCH 0472/1006] feat: Support OP Jovian upgrade, other enhancements (#13538) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../controllers/api/v2/optimism_controller.ex | 6 +- .../transaction/summary_just_request_body.ex | 1 + .../views/api/v2/optimism_view.ex | 3 +- .../lib/ethereum_jsonrpc/receipt.ex | 20 ++- .../lib/ethereum_jsonrpc/receipts.ex | 3 +- apps/explorer/lib/explorer/chain/block.ex | 3 +- .../runner/optimism/eip1559_config_updates.ex | 6 +- .../chain/import/runner/transactions.ex | 6 +- .../lib/explorer/chain/optimism/deposit.ex | 38 +++- .../explorer/chain/optimism/dispute_game.ex | 9 +- .../chain/optimism/eip1559_config_update.ex | 11 +- .../lib/explorer/chain/transaction.ex | 32 +++- .../20251104063632_jovian_support.exs | 23 +++ .../fetcher/optimism/eip1559_config_update.ex | 166 +++++++++++++++--- .../fetcher/optimism/withdrawal_event.ex | 2 + config/runtime.exs | 3 +- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 1 + 18 files changed, 276 insertions(+), 58 deletions(-) create mode 100644 apps/explorer/priv/optimism/migrations/20251104063632_jovian_support.exs diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 7e7e95c9d575..4b4daa4edb4c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -263,7 +263,11 @@ defmodule BlockScoutWeb.API.V2.OptimismController do """ @spec deposits_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def deposits_count(conn, _params) do - items_count(conn, Deposit) + count = Deposit.count(@api_true) + + conn + |> put_status(200) + |> render(:optimism_items_count, %{count: count}) end @doc """ diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex index 7506da594aa4..61637071fe01 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/transaction/summary_just_request_body.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody do OpenAPI schema for the transaction summary just request body response. """ require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.{Address, InternalTransaction, Log, TokenTransfer} alias OpenApiSpex.Schema diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 0230b55d8bc6..0f0b8be36ee1 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -153,7 +153,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do "l1_block_timestamp" => deposit.l1_block_timestamp, "l1_transaction_hash" => deposit.l1_transaction_hash, "l1_transaction_origin" => deposit.l1_transaction_origin, - "l2_transaction_gas_limit" => deposit.l2_transaction.gas + "l2_transaction_gas_limit" => deposit.l2_transaction_gas_limit } end), next_page_params: next_page_params @@ -447,6 +447,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do |> add_optional_transaction_field(transaction, :l1_fee_scalar) |> add_optional_transaction_field(transaction, :l1_gas_price) |> add_optional_transaction_field(transaction, :l1_gas_used) + |> add_optional_transaction_field(transaction, :da_footprint_gas_scalar) |> add_optimism_fields(transaction) end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex index 23f0bbd0f010..0366cd0b9a5c 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipt.ex @@ -4,6 +4,7 @@ defmodule EthereumJSONRPC.Receipt do [`eth_getTransactionReceipt`](https://github.com/ethereum/wiki/wiki/JSON-RPC/e8e0771b9f3677693649d945956bc60e886ceb2b#eth_gettransactionreceipt). """ use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, op_isthmus_timestamp: [:indexer, [Indexer.Fetcher.Optimism, :isthmus_timestamp_l2]] import EthereumJSONRPC, only: [quantity_to_integer: 1] @@ -26,7 +27,8 @@ defmodule EthereumJSONRPC.Receipt do l1_gas_price: non_neg_integer(), l1_gas_used: non_neg_integer(), operator_fee_scalar: non_neg_integer() | nil, - operator_fee_constant: non_neg_integer() | nil + operator_fee_constant: non_neg_integer() | nil, + da_footprint_gas_scalar: non_neg_integer() | nil ] ) @@ -131,7 +133,8 @@ defmodule EthereumJSONRPC.Receipt do l1_gas_price: 0,\ l1_gas_used: 0,\ operator_fee_scalar: nil,\ - operator_fee_constant: nil\ + operator_fee_constant: nil,\ + da_footprint_gas_scalar: nil\ """ :scroll -> """ l1_fee: 0\ @@ -185,7 +188,8 @@ defmodule EthereumJSONRPC.Receipt do l1_gas_price: 0,\ l1_gas_used: 0,\ operator_fee_scalar: nil,\ - operator_fee_constant: nil\ + operator_fee_constant: nil,\ + da_footprint_gas_scalar: nil\ """ :scroll -> """ l1_fee: 0\ @@ -249,10 +253,8 @@ defmodule EthereumJSONRPC.Receipt do :optimism -> defp chain_type_fields(params, elixir) do - isthmus_timestamp_l2 = Application.get_env(:indexer, Indexer.Fetcher.Optimism)[:isthmus_timestamp_l2] - {operator_fee_scalar_default, operator_fee_constant_default} = - if is_nil(isthmus_timestamp_l2) do + if is_nil(op_isthmus_timestamp()) do {nil, nil} else {0, 0} @@ -265,7 +267,8 @@ defmodule EthereumJSONRPC.Receipt do l1_gas_price: Map.get(elixir, "l1GasPrice", 0), l1_gas_used: Map.get(elixir, "l1GasUsed", 0), operator_fee_scalar: Map.get(elixir, "operatorFeeScalar", operator_fee_scalar_default), - operator_fee_constant: Map.get(elixir, "operatorFeeConstant", operator_fee_constant_default) + operator_fee_constant: Map.get(elixir, "operatorFeeConstant", operator_fee_constant_default), + da_footprint_gas_scalar: Map.get(elixir, "daFootprintGasScalar") }) end @@ -409,7 +412,8 @@ defmodule EthereumJSONRPC.Receipt do defp entry_to_elixir({key, quantity}) when key in ~w(blockNumber cumulativeGasUsed gasUsed transactionIndex blobGasUsed blobGasPrice l1Fee l1GasPrice l1GasUsed effectiveGasPrice gasUsedForL1 - l1BlobBaseFeeScalar l1BlobBaseFee l1BaseFeeScalar operatorFeeScalar operatorFeeConstant) do + l1BlobBaseFeeScalar l1BlobBaseFee l1BaseFeeScalar operatorFeeScalar + operatorFeeConstant daFootprintGasScalar) do result = if is_nil(quantity) do nil diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex index 79f8509bb23c..e8a9bf765d53 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex @@ -113,7 +113,8 @@ defmodule EthereumJSONRPC.Receipts do l1_gas_price: 0,\ l1_gas_used: 0,\ operator_fee_scalar: nil,\ - operator_fee_constant: nil\ + operator_fee_constant: nil,\ + da_footprint_gas_scalar: nil\ """ :scroll -> """ l1_fee: 0\ diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index e3f19fc82df0..a569a8fcc83a 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -479,7 +479,8 @@ defmodule Explorer.Chain.Block do # credo:disable-for-next-line Credo.Check.Design.AliasUsage config = Explorer.Chain.Optimism.EIP1559ConfigUpdate.actual_config_for_block(block_number), false <- is_nil(config) do - config + {denominator, multiplier, _min_base_fee} = config + {denominator, multiplier} else _ -> {Application.get_env(:explorer, :base_fee_max_change_denominator), diff --git a/apps/explorer/lib/explorer/chain/import/runner/optimism/eip1559_config_updates.ex b/apps/explorer/lib/explorer/chain/import/runner/optimism/eip1559_config_updates.ex index 65ad40ee00dd..afa33faa0a84 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/optimism/eip1559_config_updates.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/optimism/eip1559_config_updates.ex @@ -88,16 +88,18 @@ defmodule Explorer.Chain.Import.Runner.Optimism.EIP1559ConfigUpdates do l2_block_hash: fragment("EXCLUDED.l2_block_hash"), base_fee_max_change_denominator: fragment("EXCLUDED.base_fee_max_change_denominator"), elasticity_multiplier: fragment("EXCLUDED.elasticity_multiplier"), + min_base_fee: fragment("EXCLUDED.min_base_fee"), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", update.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", update.updated_at) ] ], where: fragment( - "(EXCLUDED.l2_block_hash, EXCLUDED.base_fee_max_change_denominator, EXCLUDED.elasticity_multiplier) IS DISTINCT FROM (?, ?, ?)", + "(EXCLUDED.l2_block_hash, EXCLUDED.base_fee_max_change_denominator, EXCLUDED.elasticity_multiplier, EXCLUDED.min_base_fee) IS DISTINCT FROM (?, ?, ?, ?)", update.l2_block_hash, update.base_fee_max_change_denominator, - update.elasticity_multiplier + update.elasticity_multiplier, + update.min_base_fee ) ) end diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 4108cb586ed4..27e13af507b9 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -286,6 +286,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do l1_block_number: fragment("EXCLUDED.l1_block_number"), operator_fee_scalar: fragment("EXCLUDED.operator_fee_scalar"), operator_fee_constant: fragment("EXCLUDED.operator_fee_constant"), + da_footprint_gas_scalar: fragment("EXCLUDED.da_footprint_gas_scalar"), # Don't update `hash` as it is part of the primary key and used for the conflict target inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", transaction.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", transaction.updated_at) @@ -293,7 +294,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do ], where: fragment( - "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_transaction_origin, EXCLUDED.l1_block_number, EXCLUDED.operator_fee_scalar, EXCLUDED.operator_fee_constant) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.block_hash, EXCLUDED.block_number, EXCLUDED.block_consensus, EXCLUDED.block_timestamp, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code_indexed_at, EXCLUDED.cumulative_gas_used, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_price, EXCLUDED.gas_used, EXCLUDED.index, EXCLUDED.input, EXCLUDED.nonce, EXCLUDED.r, EXCLUDED.s, EXCLUDED.status, EXCLUDED.to_address_hash, EXCLUDED.v, EXCLUDED.value, EXCLUDED.earliest_processing_start, EXCLUDED.revert_reason, EXCLUDED.max_priority_fee_per_gas, EXCLUDED.max_fee_per_gas, EXCLUDED.type, EXCLUDED.l1_fee, EXCLUDED.l1_fee_scalar, EXCLUDED.l1_gas_price, EXCLUDED.l1_gas_used, EXCLUDED.l1_transaction_origin, EXCLUDED.l1_block_number, EXCLUDED.operator_fee_scalar, EXCLUDED.operator_fee_constant, EXCLUDED.da_footprint_gas_scalar) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", transaction.block_hash, transaction.block_number, transaction.block_consensus, @@ -326,7 +327,8 @@ defmodule Explorer.Chain.Import.Runner.Transactions do transaction.l1_transaction_origin, transaction.l1_block_number, transaction.operator_fee_scalar, - transaction.operator_fee_constant + transaction.operator_fee_constant, + transaction.da_footprint_gas_scalar ) ) end diff --git a/apps/explorer/lib/explorer/chain/optimism/deposit.ex b/apps/explorer/lib/explorer/chain/optimism/deposit.ex index 021c4a11d463..25149344af9b 100644 --- a/apps/explorer/lib/explorer/chain/optimism/deposit.ex +++ b/apps/explorer/lib/explorer/chain/optimism/deposit.ex @@ -3,7 +3,7 @@ defmodule Explorer.Chain.Optimism.Deposit do use Explorer.Schema - import Explorer.Chain, only: [default_paging_options: 0, join_association: 3, select_repo: 1] + import Explorer.Chain, only: [default_paging_options: 0, select_repo: 1] alias Explorer.Chain.{Hash, Transaction} alias Explorer.PagingOptions @@ -82,18 +82,50 @@ defmodule Explorer.Chain.Optimism.Deposit do _ -> base_query = - from(d in __MODULE__, + from( + d in __MODULE__, + inner_join: t in Transaction, + on: t.hash == d.l2_transaction_hash and t.status == :ok, + select: %{ + l1_block_number: d.l1_block_number, + l1_block_timestamp: d.l1_block_timestamp, + l1_transaction_hash: d.l1_transaction_hash, + l1_transaction_origin: d.l1_transaction_origin, + l2_transaction_hash: d.l2_transaction_hash, + l2_transaction_gas_limit: t.gas + }, order_by: [desc: d.l1_block_number, desc: d.l2_transaction_hash] ) base_query - |> join_association(:l2_transaction, :required) |> page_deposits(paging_options) |> limit(^paging_options.page_size) |> select_repo(options).all(timeout: :infinity) end end + @doc """ + Returns total number of displayed deposits. + + ## Parameters + - `options`: A keyword list of options: + - `:api?` - Whether the function is being called from an API context. + + ## Returns + - A total number of deposits. + """ + @spec count(list()) :: non_neg_integer() + def count(options \\ []) do + query = + from( + d in __MODULE__, + inner_join: t in Transaction, + on: t.hash == d.l2_transaction_hash and t.status == :ok + ) + + select_repo(options).aggregate(query, :count) + end + defp page_deposits(query, %PagingOptions{key: nil}), do: query defp page_deposits(query, %PagingOptions{key: {block_number, l2_transaction_hash}}) do diff --git a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex index b0d313add873..c7ea7274d1f3 100644 --- a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex +++ b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex @@ -15,6 +15,8 @@ defmodule Explorer.Chain.Optimism.DisputeGame do @chain_id_bob_mainnet 60_808 @chain_id_bob_sepolia 808_813 + @chain_id_megaeth_mainnet 4326 + @chain_id_megaeth_testnet_v2 6343 @typedoc """ * `index` - A unique index of the dispute game. @@ -106,7 +108,12 @@ defmodule Explorer.Chain.Optimism.DisputeGame do end first_bits = - if current_chain_id in [@chain_id_bob_mainnet, @chain_id_bob_sepolia] do + if current_chain_id in [ + @chain_id_bob_mainnet, + @chain_id_bob_sepolia, + @chain_id_megaeth_mainnet, + @chain_id_megaeth_testnet_v2 + ] do 64 else 256 diff --git a/apps/explorer/lib/explorer/chain/optimism/eip1559_config_update.ex b/apps/explorer/lib/explorer/chain/optimism/eip1559_config_update.ex index 061273e6b808..f52e5e3b4cc2 100644 --- a/apps/explorer/lib/explorer/chain/optimism/eip1559_config_update.ex +++ b/apps/explorer/lib/explorer/chain/optimism/eip1559_config_update.ex @@ -7,12 +7,14 @@ defmodule Explorer.Chain.Optimism.EIP1559ConfigUpdate do alias Explorer.Repo @required_attrs ~w(l2_block_number l2_block_hash base_fee_max_change_denominator elasticity_multiplier)a + @optional_attrs ~w(min_base_fee)a @typedoc """ * `l2_block_number` - An L2 block number where the config update was registered. * `l2_block_hash` - An L2 block hash where the config update was registered. * `base_fee_max_change_denominator` - A new value of the denominator. * `elasticity_multiplier` - A new value of the multiplier. + * `min_base_fee` - A minimum base fee introduced in OP Jovian upgrade. """ @primary_key false typed_schema "op_eip1559_config_updates" do @@ -20,6 +22,7 @@ defmodule Explorer.Chain.Optimism.EIP1559ConfigUpdate do field(:l2_block_hash, Hash.Full) field(:base_fee_max_change_denominator, :integer) field(:elasticity_multiplier, :integer) + field(:min_base_fee, :integer) timestamps() end @@ -29,7 +32,7 @@ defmodule Explorer.Chain.Optimism.EIP1559ConfigUpdate do """ def changeset(%__MODULE__{} = updates, attrs \\ %{}) do updates - |> cast(attrs, @required_attrs) + |> cast(attrs, @required_attrs ++ @optional_attrs) |> validate_required(@required_attrs) end @@ -40,14 +43,14 @@ defmodule Explorer.Chain.Optimism.EIP1559ConfigUpdate do - `block_number`: The block number for which we need to read the actual config. ## Returns - - `{denominator, multiplier}` tuple in case the config exists. + - `{denominator, multiplier, min_base_fee}` tuple in case the config exists. - `nil` if the config is unknown. """ - @spec actual_config_for_block(non_neg_integer()) :: {non_neg_integer(), non_neg_integer()} | nil + @spec actual_config_for_block(non_neg_integer()) :: {non_neg_integer(), non_neg_integer(), non_neg_integer()} | nil def actual_config_for_block(block_number) do query = from(u in __MODULE__, - select: {u.base_fee_max_change_denominator, u.elasticity_multiplier}, + select: {u.base_fee_max_change_denominator, u.elasticity_multiplier, u.min_base_fee}, where: u.l2_block_number < ^block_number, order_by: [desc: u.l2_block_number], limit: 1 diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index d3eb9ed23d65..d95041cf2404 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -53,6 +53,7 @@ defmodule Explorer.Chain.Transaction.Schema do field(:l1_block_number, :integer) field(:operator_fee_scalar, :decimal) field(:operator_fee_constant, :decimal) + field(:da_footprint_gas_scalar, :decimal) end, 2 ) @@ -315,6 +316,9 @@ defmodule Explorer.Chain.Transaction do chain_identity: [:explorer, :chain_identity], decode_not_a_contract_calls: [:explorer, :decode_not_a_contract_calls] + use Utils.RuntimeEnvHelper, + op_jovian_timestamp: [:indexer, [Indexer.Fetcher.Optimism.EIP1559ConfigUpdate, :jovian_timestamp_l2]] + require Logger require Explorer.Chain.Transaction.Schema @@ -352,7 +356,7 @@ defmodule Explorer.Chain.Transaction do @chain_type_optional_attrs (case @chain_type do :optimism -> - ~w(l1_fee l1_fee_scalar l1_gas_price l1_gas_used l1_transaction_origin l1_block_number operator_fee_scalar operator_fee_constant)a + ~w(l1_fee l1_fee_scalar l1_gas_price l1_gas_used l1_transaction_origin l1_block_number operator_fee_scalar operator_fee_constant da_footprint_gas_scalar)a :scroll -> ~w(l1_fee queue_index)a @@ -540,6 +544,7 @@ defmodule Explorer.Chain.Transaction do * `wrapped_hash` - hash from the `wrapped` field (used by Suave) * `operator_fee_scalar` - operatorFeeScalar is a uint32 scalar set by a chain operator (used by some OP chains) * `operator_fee_constant` - operatorFeeConstant is a uint64 constant set by a chain operator (used by some OP chains) + * `da_footprint_gas_scalar` - daFootprintGasScalar is a uint16 scalar used to calculate daFootprint introduced in Jovian OP upgrade """ Explorer.Chain.Transaction.Schema.generate() @@ -2091,6 +2096,8 @@ defmodule Explorer.Chain.Transaction do @doc """ The operator fee is calculated for OP chains starting from the Isthmus upgrade as described in https://specs.optimism.io/protocol/isthmus/exec-engine.html#operator-fee + The formula changed in Jovian upgrade as follows: + https://specs.optimism.io/protocol/jovian/exec-engine.html#fee-formula-update If the `operatorFeeScalar` or `operatorFeeConstant` is `nil`, it's treated as zero. @@ -2111,10 +2118,25 @@ defmodule Explorer.Chain.Transaction do operator_fee_scalar = Map.get(transaction, :operator_fee_scalar) || Decimal.new(0) operator_fee_constant = Map.get(transaction, :operator_fee_constant) || Decimal.new(0) - gas_used - |> Decimal.mult(operator_fee_scalar) - |> Decimal.div_int(1_000_000) - |> Decimal.add(operator_fee_constant) + jovian_timestamp = op_jovian_timestamp() + + block_timestamp = + Map.get(transaction, :block_timestamp) || (jovian_timestamp && DateTime.from_unix!(jovian_timestamp)) || + DateTime.from_unix!(0) + + if DateTime.to_unix(block_timestamp) >= jovian_timestamp do + # use the formula for Jovian + gas_used + |> Decimal.mult(operator_fee_scalar) + |> Decimal.mult(100) + |> Decimal.add(operator_fee_constant) + else + # use the formula for Isthmus + gas_used + |> Decimal.mult(operator_fee_scalar) + |> Decimal.div_int(1_000_000) + |> Decimal.add(operator_fee_constant) + end end @doc """ diff --git a/apps/explorer/priv/optimism/migrations/20251104063632_jovian_support.exs b/apps/explorer/priv/optimism/migrations/20251104063632_jovian_support.exs new file mode 100644 index 000000000000..91bddac9e24b --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20251104063632_jovian_support.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Optimism.Migrations.JovianSupport do + use Ecto.Migration + + def up do + alter table(:op_eip1559_config_updates) do + add(:min_base_fee, :bigint, null: true) + end + + alter table(:transactions) do + add(:da_footprint_gas_scalar, :decimal, null: true, default: nil) + end + end + + def down do + alter table(:transactions) do + remove(:da_footprint_gas_scalar) + end + + alter table(:op_eip1559_config_updates) do + remove(:min_base_fee) + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex b/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex index 2d3c675bb28b..23f49d75612e 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex @@ -2,23 +2,29 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do @moduledoc """ Fills op_eip1559_config_updates DB table. - The table stores points when EIP-1559 denominator and multiplier were changed, - and the updated values of these parameters. The point is the L2 block number + The table stores points when EIP-1559 denominator and multiplier (and Jovian's `min_base_fee`) + were changed, and the updated values of these parameters. The point is the L2 block number and its hash. The block hash is needed to detect a possible past reorg when starting this fetcher. If the past reorg is detected, the module tries to start from the previous block and so on until a consensus block is found. The parameter values are taken from the `extraData` field of each block. They - are stored in a block starting from the block of Holocene upgrade. Each block + are stored in a block starting from the block of Holocene upgrade. The `min_base_fee` + parameter is stored there starting from the Jovian upgrade. Each block contains the parameters actual for the next blocks (until they are changed again). The `extraData` field has a format described on the page https://specs.optimism.io/protocol/holocene/exec-engine.html#dynamic-eip-1559-parameters + and + https://specs.optimism.io/protocol/jovian/exec-engine.html#minimum-base-fee-in-block-header The Holocene activation block is defined with INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP env variable setting the block timestamp. If this env is not defined, the module won't work. In this case EIP_1559_BASE_FEE_MAX_CHANGE_DENOMINATOR and EIP_1559_ELASTICITY_MULTIPLIER env variables will be used as fallback static values. The timestamp can be defined as `0` meaning the Holocene is activated from genesis block. + + The Jovian activation block is defined with INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP env variable + setting the block timestamp. """ use GenServer @@ -32,14 +38,17 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do alias EthereumJSONRPC.Blocks alias Explorer.Chain alias Explorer.Chain.Block.Reader.General, as: BlockGeneralReader + alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Chain.Events.Subscriber + alias Explorer.Chain.Hash alias Explorer.Chain.Optimism.EIP1559ConfigUpdate alias Indexer.Fetcher.Optimism alias Indexer.Helper @fetcher_name :optimism_eip1559_config_updates @latest_block_check_interval_seconds 60 - @counter_type "optimism_eip1559_config_updates_fetcher_last_l2_block_hash" + @counter_type_last_l2_block_hash "optimism_eip1559_config_updates_fetcher_last_l2_block_hash" + @counter_type_jovian_defined "optimism_eip1559_config_updates_fetcher_jovian_defined" @empty_hash "0x0000000000000000000000000000000000000000000000000000000000000000" def child_spec(start_link_arguments) do @@ -67,6 +76,7 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do # during initialization. It checks the value of INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP env variable, waits for the # Holocene block (if the module starts before Holocene activation), defines the block range which must be scanned # to handle `extraData` fields, and retrieves the dynamic EIP-1559 parameters (denominator and multiplier) for each block. + # If the `extraData` has version byte greater than 0, the `min_base_fee` value is additionally parsed. # The changed parameter values are then written to the `op_eip1559_config_updates` database table. # # The block range is split into chunks which max size is defined by INDEXER_OPTIMISM_L2_HOLOCENE_BLOCKS_CHUNK_SIZE @@ -96,16 +106,27 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do env = Application.get_all_env(:indexer)[__MODULE__] optimism_env = Application.get_all_env(:indexer)[Indexer.Fetcher.Optimism] - timestamp = env[:holocene_timestamp_l2] + timestamp_holocene = env[:holocene_timestamp_l2] + timestamp_jovian = env[:jovian_timestamp_l2] - with false <- is_nil(timestamp), - wait_for_holocene(timestamp, json_rpc_named_arguments), + with false <- is_nil(timestamp_holocene), + {:jovian_after_holocene, true} <- {:jovian_after_holocene, timestamp_jovian >= timestamp_holocene}, + timestamp_latest = wait_for_holocene(timestamp_holocene, json_rpc_named_arguments), Subscriber.to(:blocks, :realtime), {:ok, latest_block_number} = Helper.get_block_number_by_tag("latest", json_rpc_named_arguments, Helper.infinite_retries_number()), l2_block_number = - block_number_by_timestamp(timestamp, optimism_env[:block_duration], json_rpc_named_arguments), + block_number_by_timestamp(timestamp_holocene, optimism_env[:block_duration], json_rpc_named_arguments), EIP1559ConfigUpdate.remove_invalid_updates(l2_block_number, latest_block_number), + # if we set INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP for the first time after Jovian activation, + # we reset the last handled block hash to the activation block hash to continue + # the `extraData` handling from that block + (if jovian_first_time_defined?(timestamp_jovian) and timestamp_jovian <= timestamp_latest do + timestamp_jovian + |> block_number_by_timestamp(optimism_env[:block_duration], json_rpc_named_arguments) + |> block_hash_by_number(json_rpc_named_arguments) + |> Optimism.set_last_block_hash(@counter_type_last_l2_block_hash) + end), {:ok, last_l2_block_number} <- get_last_l2_block_number(json_rpc_named_arguments) do Logger.info("l2_block_number = #{l2_block_number}") Logger.info("last_l2_block_number = #{last_l2_block_number}") @@ -118,7 +139,7 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do start_block_number: max(l2_block_number, last_l2_block_number), end_block_number: latest_block_number, chunk_size: env[:chunk_size], - timestamp: timestamp, + timestamp: timestamp_holocene, mode: :catchup, realtime_range: nil, last_realtime_block_number: nil, @@ -129,6 +150,13 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do # Holocene timestamp is not defined, so we don't start this module {:stop, :normal, %{}} + {:jovian_after_holocene, false} -> + Logger.error( + "Jovian upgrade timestamp must be greater or equal to Holocene timestamp. Please, check INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP and INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP env variables." + ) + + {:stop, :normal, %{}} + {:error, error_data} -> Logger.error("Cannot get last L2 block from RPC by its hash due to RPC error: #{inspect(error_data)}") {:stop, :normal, %{}} @@ -266,14 +294,16 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do ) end - Optimism.set_last_block_hash(@empty_hash, @counter_type) + Optimism.set_last_block_hash(@empty_hash, @counter_type_last_l2_block_hash) end defp handle_reorg(_reorg_block_number), do: :ok # Retrieves updated config parameters from the specified blocks and saves them to the database. - # The parameters are read from the `extraData` field which format is as follows: - # 1-byte version ++ 4-byte denominator ++ 4-byte elasticity + # The parameters are read from the `extraData` field which format is as follows for pre-Jovian period: + # 1-byte version ++ 4-byte denominator ++ 4-byte elasticity + # or for post-Jovian period: + # 1-byte version ++ 4-byte denominator ++ 4-byte elasticity ++ 8-byte min_base_fee # # The last handled block hash is kept in the `last_fetched_counters` table to start from that after # instance restart. @@ -308,12 +338,18 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do return = with {:valid_format, true} <- {:valid_format, byte_size(extra_data) >= 9}, - <> = extra_data, - {:valid_version, _version, true} <- {:valid_version, version, version == 0}, + <> = + extra_data, prev_config = EIP1559ConfigUpdate.actual_config_for_block(block.number), - new_config = {denominator, elasticity}, + new_config = + (if version == 0 do + {denominator, elasticity, nil} + else + <> = rest_extra_data + {denominator, elasticity, min_base_fee} + end), {:updated_config, true} <- {:updated_config, prev_config != new_config} do - update_config(block.number, block.hash, denominator, elasticity) + update_config(block.number, block.hash, new_config) Logger.info( "Config was updated at block #{block.number}. Previous one: #{inspect(prev_config)}. New one: #{inspect(new_config)}." @@ -325,17 +361,13 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do Logger.warning("extraData of the block ##{block_number} has invalid format. Ignoring it.") acc - {:valid_version, version, false} -> - Logger.warning("extraData of the block ##{block_number} has invalid version #{version}. Ignoring it.") - acc - {:updated_config, false} -> acc end # credo:disable-for-next-line Credo.Check.Refactor.Nesting if block.number == last_block_number do - Optimism.set_last_block_hash(block.hash, @counter_type) + Optimism.set_last_block_hash(block.hash, @counter_type_last_l2_block_hash) end return @@ -364,16 +396,24 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do # ## Parameters # - `l2_block_number`: L2 block number of the config update. # - `l2_block_hash`: L2 block hash of the config update. - # - `base_fee_max_change_denominator`: A new value for EIP-1559 denominator. - # - `elasticity_multiplier`: A new value for EIP-1559 multiplier. - @spec update_config(non_neg_integer(), binary(), non_neg_integer(), non_neg_integer()) :: no_return() - defp update_config(l2_block_number, l2_block_hash, base_fee_max_change_denominator, elasticity_multiplier) do + # - `new_config`: A tuple containing the new config values: + # `base_fee_max_change_denominator`: A new value for EIP-1559 denominator. + # `elasticity_multiplier`: A new value for EIP-1559 multiplier. + # `min_base_fee`: A new value for the Jovian's min_base_fee. + @spec update_config(non_neg_integer(), binary(), {non_neg_integer(), non_neg_integer(), non_neg_integer() | nil}) :: + no_return() + defp update_config( + l2_block_number, + l2_block_hash, + {base_fee_max_change_denominator, elasticity_multiplier, min_base_fee} + ) do updates = [ %{ l2_block_number: l2_block_number, l2_block_hash: l2_block_hash, base_fee_max_change_denominator: base_fee_max_change_denominator, - elasticity_multiplier: elasticity_multiplier + elasticity_multiplier: elasticity_multiplier, + min_base_fee: min_base_fee } ] @@ -435,7 +475,10 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do {:ok, non_neg_integer()} | {:error, any()} defp get_last_l2_block_number(json_rpc_named_arguments) do last_l2_block_number = - Optimism.get_last_block_number_from_last_fetched_counter(json_rpc_named_arguments, @counter_type) + Optimism.get_last_block_number_from_last_fetched_counter( + json_rpc_named_arguments, + @counter_type_last_l2_block_hash + ) if is_nil(last_l2_block_number) do {last_l2_block_number, last_l2_block_hash} = EIP1559ConfigUpdate.get_last_item() @@ -580,7 +623,10 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do # ## Parameters # - `timestamp`: The timestamp of the Holocene. # - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. - @spec wait_for_holocene(non_neg_integer(), EthereumJSONRPC.json_rpc_named_arguments()) :: any() + # + # ## Returns + # - Timestamp of the latest block. + @spec wait_for_holocene(non_neg_integer(), EthereumJSONRPC.json_rpc_named_arguments()) :: non_neg_integer() defp wait_for_holocene(timestamp, json_rpc_named_arguments) do {:ok, latest_timestamp} = Helper.get_block_timestamp_by_number_or_tag(:latest, json_rpc_named_arguments, Helper.infinite_retries_number()) @@ -591,8 +637,72 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do wait_for_holocene(timestamp, json_rpc_named_arguments) else Logger.info("Holocene activation detected") + latest_timestamp end end + # Gets block hash by its number. + # + # First, the function tries to get the block from database. If the block is not indexed, it's got from RPC. + # + # ## Parameters + # - `block_number`: The block number we need to get hash for. + # - `json_rpc_named_arguments`: Configuration for JSON RPC connection. + # + # ## Returns + # - The block hash in form of 0x-prefixed string. Returns 0x00..00 if the hash cannot be determined. + @spec block_hash_by_number(non_neg_integer(), EthereumJSONRPC.json_rpc_named_arguments()) :: String.t() + defp block_hash_by_number(block_number, json_rpc_named_arguments) do + hash_from_db = + [block_number] + |> Chain.block_hash_by_number() + |> Map.get(block_number) + + if is_nil(hash_from_db) do + [block_number] + |> Optimism.get_blocks_by_numbers(json_rpc_named_arguments, Helper.infinite_retries_number()) + |> Enum.at(0, %{}) + |> Map.get("hash", @empty_hash) + else + Hash.to_string(hash_from_db) + end + end + + # Returns `true` if Jovian activation timestamp is defined for the first time. Otherwise, returns `false`. + # Also, remembers the fact of the Jovian timestamp definition in the database. + # + # ## Parameters + # - `timestamp`: The currently defined Jovian activation timestamp. `nil` if not defined. + # + # ## Returns + # - The boolean result. + @spec jovian_first_time_defined?(non_neg_integer() | nil) :: boolean() + defp jovian_first_time_defined?(timestamp) do + jovian_defined_now = !is_nil(timestamp) + jovian_defined_before = !is_nil(LastFetchedCounter.get(@counter_type_jovian_defined, nullable: true)) + + result = + cond do + is_nil(LastFetchedCounter.get(@counter_type_last_l2_block_hash, nullable: true)) -> + # Holocene handler never worked before, so we don't need to reset the last handled block hash + false + + !jovian_defined_before -> + # Jovian never defined before. We return `true` if the Jovian is defined now. Otherwise, return `false` + jovian_defined_now + + true -> + # Holocene handler worked before and Jovian was defined before + false + end + + if jovian_defined_now and !jovian_defined_before do + # if Jovian activation timestamp is defined for the first time, we record this in the database + LastFetchedCounter.upsert(%{counter_type: @counter_type_jovian_defined, value: 1}) + end + + result + end + def fetcher_name, do: @fetcher_name end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex index 03c767b61798..1ce8131d323e 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/withdrawal_event.ex @@ -251,6 +251,8 @@ defmodule Indexer.Fetcher.Optimism.WithdrawalEvent do game_address_hash: game_address_hash } end) + |> MapSet.new() + |> MapSet.to_list() end @doc """ diff --git a/config/runtime.exs b/config/runtime.exs index 6f7d2288be09..728971c31c55 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1303,7 +1303,8 @@ config :indexer, Indexer.Fetcher.Optimism.TransactionBatch, config :indexer, Indexer.Fetcher.Optimism.EIP1559ConfigUpdate, chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_OPTIMISM_L2_HOLOCENE_BLOCKS_CHUNK_SIZE", 25), - holocene_timestamp_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP") + holocene_timestamp_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP"), + jovian_timestamp_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP") config :indexer, Indexer.Fetcher.Optimism.Interop.Message, start_block: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_INTEROP_START_BLOCK"), diff --git a/cspell.json b/cspell.json index 1fe89931751c..8b1ba427abe6 100644 --- a/cspell.json +++ b/cspell.json @@ -391,6 +391,7 @@ "MDWW", "meck", "meer", + "megaeth", "memsup", "Mendonça", "Menlo", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 453beb805e6b..6f9ebd8dceff 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -327,6 +327,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_OPTIMISM_L2_HOLOCENE_TIMESTAMP= # INDEXER_OPTIMISM_L2_HOLOCENE_BLOCKS_CHUNK_SIZE= # INDEXER_OPTIMISM_L2_ISTHMUS_TIMESTAMP= +# INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP= # INDEXER_OPTIMISM_L2_INTEROP_START_BLOCK= # INDEXER_OPTIMISM_L2_INTEROP_BLOCKS_CHUNK_SIZE= # INDEXER_OPTIMISM_CHAINSCOUT_API_URL= From 5e9ef0ea57835a4a33ef83ba5005c3117b658636 Mon Sep 17 00:00:00 2001 From: GimluCom <79271880+GimluCom@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:13:40 +0100 Subject: [PATCH 0473/1006] fix: Fix logic of disable token exchange rate (#13414) --- config/runtime.exs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 728971c31c55..09d8533c3ee9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -502,13 +502,11 @@ config :explorer, Explorer.Market.Fetcher.Coin, enable_consolidation: true, cache_period: ConfigHelper.parse_time_env_var("MARKET_COIN_CACHE_PERIOD", "10m") +disable_token_exchange_rates? = ConfigHelper.parse_bool_env_var("DISABLE_TOKEN_EXCHANGE_RATE") +market_tokens_fetcher_enabled? = ConfigHelper.parse_bool_env_var("MARKET_TOKENS_FETCHER_ENABLED", "true") + config :explorer, Explorer.Market.Fetcher.Token, - enabled: - !disable_exchange_rates? && - ConfigHelper.parse_bool_env_var( - "MARKET_TOKENS_FETCHER_ENABLED", - ConfigHelper.safe_get_env("DISABLE_TOKEN_EXCHANGE_RATE", "true") - ), + enabled: !disable_exchange_rates? && !disable_token_exchange_rates? && market_tokens_fetcher_enabled?, interval: ConfigHelper.parse_time_env_var( "MARKET_TOKENS_INTERVAL", @@ -587,10 +585,6 @@ config :explorer, Explorer.Chain.Cache.Uncles, ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) -config :explorer, Explorer.Chain.Cache.Uncles, - ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), - global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) - celo_l2_migration_block = ConfigHelper.parse_integer_or_nil_env_var("CELO_L2_MIGRATION_BLOCK") celo_epoch_manager_contract_address = System.get_env("CELO_EPOCH_MANAGER_CONTRACT") From 30cab41675cbc4ae6ac612957ec28675d9194d48 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 13 Nov 2025 18:12:37 +0300 Subject: [PATCH 0474/1006] fix: beacon deposits: fallback to node (#13425) Co-authored-by: Victor Baranov --- .../lib/explorer/chain/beacon/deposit.ex | 3 + ...029090030_deposits_remove_foreign_keys.exs | 9 + .../lib/indexer/fetcher/beacon/deposit.ex | 329 ++++++++-- apps/indexer/lib/indexer/helper.ex | 1 + apps/indexer/lib/indexer/supervisor.ex | 2 +- .../indexer/fetcher/beacon/deposit_test.exs | 621 ++++++++++++++---- 6 files changed, 778 insertions(+), 187 deletions(-) create mode 100644 apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs diff --git a/apps/explorer/lib/explorer/chain/beacon/deposit.ex b/apps/explorer/lib/explorer/chain/beacon/deposit.ex index a3a69c44e46d..353b38c54704 100644 --- a/apps/explorer/lib/explorer/chain/beacon/deposit.ex +++ b/apps/explorer/lib/explorer/chain/beacon/deposit.ex @@ -69,6 +69,9 @@ defmodule Explorer.Chain.Beacon.Deposit do @spec statuses :: [atom()] def statuses, do: @statuses_enum + @spec event_signature :: String.t() + def event_signature, do: @deposit_event_signature + @sorting [desc: :index] @doc """ diff --git a/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs b/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs new file mode 100644 index 000000000000..eca7de796530 --- /dev/null +++ b/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Beacon.Migrations.DepositsRemoveForeignKeys do + use Ecto.Migration + + def change do + drop_if_exists(constraint(:beacon_deposits, :beacon_deposits_block_hash_fkey)) + drop_if_exists(constraint(:beacon_deposits, :beacon_deposits_from_address_hash_fkey)) + drop_if_exists(constraint(:beacon_deposits, :beacon_deposits_transaction_hash_fkey)) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index 43d12b5264ca..9854e18feda0 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -11,10 +11,13 @@ defmodule Indexer.Fetcher.Beacon.Deposit do import Ecto.Query alias ABI.Event + alias Ecto.Changeset + alias EthereumJSONRPC.Block, as: EthereumJSONRPCBlock alias Explorer.Chain.Beacon.Deposit - alias Explorer.Chain.{Data, Wei} + alias Explorer.Chain.{Block, Data, Hash, Wei} alias Explorer.Repo alias Indexer.Fetcher.Beacon.Client + alias Indexer.Helper defstruct [ :interval, @@ -24,7 +27,8 @@ defmodule Indexer.Fetcher.Beacon.Deposit do :genesis_fork_version, :deposit_index, :last_processed_log_block_number, - :last_processed_log_index + :last_processed_log_index, + :json_rpc_named_arguments ] def start_link([init_opts, server_opts]) do @@ -32,14 +36,21 @@ defmodule Indexer.Fetcher.Beacon.Deposit do end @impl GenServer - def init(_opts) do + def init(opts) do Logger.metadata(fetcher: :beacon_deposit) - {:ok, nil, {:continue, nil}} + json_rpc_named_arguments = opts[:json_rpc_named_arguments] + + unless json_rpc_named_arguments do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." + end + + {:ok, nil, {:continue, json_rpc_named_arguments}} end @impl GenServer - def handle_continue(nil, _state) do + def handle_continue(json_rpc_named_arguments, _state) do chain_id = Application.get_env(:indexer, :chain_id) case Client.get_spec() do @@ -62,7 +73,8 @@ defmodule Indexer.Fetcher.Beacon.Deposit do genesis_fork_version: Base.decode16!(genesis_fork_version_hex, case: :mixed), deposit_index: last_processed_deposit.index, last_processed_log_block_number: last_processed_deposit.block_number, - last_processed_log_index: last_processed_deposit.log_index + last_processed_log_index: last_processed_deposit.log_index, + json_rpc_named_arguments: json_rpc_named_arguments } Process.send_after(self(), :process_logs, state.interval) @@ -133,7 +145,8 @@ defmodule Indexer.Fetcher.Beacon.Deposit do genesis_fork_version: genesis_fork_version, deposit_index: deposit_index, last_processed_log_block_number: last_processed_log_block_number, - last_processed_log_index: last_processed_log_index + last_processed_log_index: last_processed_log_index, + json_rpc_named_arguments: json_rpc_named_arguments } = state ) do deposits = @@ -143,47 +156,191 @@ defmodule Indexer.Fetcher.Beacon.Deposit do last_processed_log_index, batch_size ) - |> Enum.map(&log_to_deposit/1) + |> Enum.map(&db_log_to_deposit/1) + + result = + case find_missing_ranges(deposit_index, deposits) do + [_ | _] = missing_ranges -> + Logger.error( + "Non-sequential deposits detected, missing ranges are: #{inspect(missing_ranges)}, trying to fetch from the node" + ) + + case fetch_and_process_logs_from_node( + deposit_index, + last_processed_log_block_number, + missing_ranges, + deposits, + deposit_contract_address_hash, + json_rpc_named_arguments + ) do + {:ok, deposits} -> + {:ok, deposits} + + {:error, reason} -> + Logger.error("Failed to fetch deposit logs from node: #{inspect(reason)}") + Process.send_after(self(), :process_logs, interval * 30) + :error + end + + _ -> + {:ok, deposits} + end + + case result do + {:ok, deposits} -> + {deposits_count, _} = + Repo.insert_all(Deposit, set_status(deposits, domain_deposit, genesis_fork_version), + on_conflict: :replace_all, + conflict_target: [:index] + ) + + if deposits_count < batch_size do + Process.send_after(self(), :process_logs, interval) + else + Process.send(self(), :process_logs, []) + end - # todo: sequential? check is removed as a hard requirement for deposits indexing - # since block ranges are found where node doesn't return deposits - # thus making the deposit index sequence non-sequential. - # We need a separate process which will monitor missed deposits - # after we check the nature of those missing deposit indexes. - case sequential?(deposit_index, deposits) do - {:error, prev, curr} -> - Logger.error("Non-sequential deposits detected: #{inspect(prev)} followed by #{inspect(curr)}") + last_deposit = + List.last(deposits, %{ + index: state.deposit_index, + block_number: state.last_processed_log_block_number, + log_index: state.last_processed_log_index + }) + + {:noreply, + %__MODULE__{ + state + | deposit_index: last_deposit.index, + last_processed_log_block_number: last_deposit.block_number, + last_processed_log_index: last_deposit.log_index + }} _ -> - :ok + {:noreply, state} end + end - {deposits_count, _} = - Repo.insert_all(Deposit, set_status(deposits, domain_deposit, genesis_fork_version), - on_conflict: :replace_all, - conflict_target: [:index] - ) - - if deposits_count < batch_size do - Process.send_after(self(), :process_logs, interval) + defp fetch_and_process_logs_from_node( + last_processed_deposit_index, + last_processed_deposit_block_number, + missing_ranges, + deposits, + deposit_contract_address_hash, + json_rpc_named_arguments + ) do + with {:ok, deposits_from_node} <- + missing_ranges + |> Task.async_stream( + fn {from_deposit_index, to_deposit_index} -> + do_fetch_and_process_logs_from_node( + last_processed_deposit_index, + last_processed_deposit_block_number, + from_deposit_index, + to_deposit_index, + deposits, + deposit_contract_address_hash, + json_rpc_named_arguments + ) + end, + max_concurrency: 5, + timeout: :infinity + ) + |> Enum.reduce_while({:ok, []}, fn + {:ok, {:ok, deposits_from_node}}, {:ok, acc} -> + {:cont, {:ok, deposits_from_node ++ acc}} + + {:ok, {:error, reason}}, _ -> + {:halt, {:error, reason}} + + {:exit, reason}, _ -> + {:halt, {:error, reason}} + end), + merged_deposits = + deposits + |> Map.new(fn d -> {d.index, d} end) + |> Map.merge(deposits_from_node |> Map.new(fn d -> {d.index, d} end)) + |> Map.values() + |> Enum.sort_by(& &1.index), + [] <- find_missing_ranges(last_processed_deposit_index, merged_deposits) do + {:ok, merged_deposits} else - Process.send(self(), :process_logs, []) + [_ | _] = missing_ranges -> + Logger.error("Still missing deposit ranges after fetching from the node: #{inspect(missing_ranges)}") + {:error, :missing_ranges_after_node_fetch} + + err -> + err end + end - last_deposit = - List.last(deposits, %{ - index: state.deposit_index, - block_number: state.last_processed_log_block_number, - log_index: state.last_processed_log_index - }) + defp do_fetch_and_process_logs_from_node( + last_processed_deposit_index, + last_processed_deposit_block_number, + from_deposit_index, + to_deposit_index, + deposits, + deposit_contract_address_hash, + json_rpc_named_arguments + ) do + from_deposit_block_number = + if last_processed_deposit_index == from_deposit_index do + last_processed_deposit_block_number + else + Enum.find(deposits, fn %{index: i} -> i == from_deposit_index end).block_number + end + + to_deposit_block_number = Enum.find(deposits, fn %{index: i} -> i == to_deposit_index end).block_number + + with {:ok, logs} <- + Helper.get_logs( + max(0, from_deposit_block_number), + max(0, to_deposit_block_number), + to_string(deposit_contract_address_hash), + [Deposit.event_signature()], + json_rpc_named_arguments + ) do + node_logs_to_deposits(logs, json_rpc_named_arguments) + end + end - {:noreply, - %__MODULE__{ - state - | deposit_index: last_deposit.index, - last_processed_log_block_number: last_deposit.block_number, - last_processed_log_index: last_deposit.log_index - }} + defp node_logs_to_deposits(logs, json_rpc_named_arguments) do + blocks_from_db = + logs + |> Enum.map(fn l -> l["blockHash"] end) + |> Enum.uniq() + |> Block.by_hashes_query() + |> Repo.all() + |> Map.new(fn b -> {b.hash, b} end) + + logs_with_missing_blocks = + logs + |> Enum.reject(fn l -> + {:ok, l_block_hash} = Hash.Full.cast(l["blockHash"]) + Map.has_key?(blocks_from_db, l_block_hash) + end) + + blocks_from_node = + logs_with_missing_blocks + |> Helper.get_blocks_by_events(json_rpc_named_arguments, 3) + |> Map.new(fn b -> + b = b |> EthereumJSONRPCBlock.to_elixir() |> EthereumJSONRPCBlock.elixir_to_params() + b = %Block{} |> Changeset.cast(b, ~w(hash number timestamp)a) |> Changeset.apply_changes() + {b.hash, b} + end) + + blocks = Map.merge(blocks_from_db, blocks_from_node) + + logs + |> Enum.reduce_while({:ok, []}, fn l, {:ok, acc} -> + case node_log_to_deposit(l, blocks) do + {:ok, deposit} -> + {:cont, {:ok, [deposit | acc]}} + + {:error, reason} -> + Logger.error("Failed to decode deposit log from node: #{inspect(reason)}") + {:halt, {:error, :missing_block}} + end + end) end @abi ABI.parse_specification( @@ -229,7 +386,66 @@ defmodule Indexer.Fetcher.Beacon.Deposit do include_events?: true ) - defp log_to_deposit(log) do + defp db_log_to_deposit(log) do + do_log_to_deposit( + log.first_topic, + log.data, + log.from_address_hash, + log.transaction_hash, + log.block_hash, + log.block_number, + log.block_timestamp, + log.index + ) + end + + defp node_log_to_deposit( + %{ + "topics" => [str_first_topic], + "data" => str_data, + "address" => str_from_address_hash, + "transactionHash" => str_transaction_hash, + "blockHash" => str_block_hash, + "logIndex" => str_log_index + }, + blocks + ) do + {:ok, block_hash} = Hash.Full.cast(str_block_hash) + + case blocks[block_hash] do + nil -> + {:error, :missing_block} + + block -> + {:ok, first_topic} = Data.cast(str_first_topic) + {:ok, data} = Data.cast(str_data) + {:ok, from_address_hash} = Hash.Address.cast(str_from_address_hash) + {:ok, transaction_hash} = Hash.Full.cast(str_transaction_hash) + + {:ok, + do_log_to_deposit( + first_topic, + data, + from_address_hash, + transaction_hash, + block.hash, + block.number, + block.timestamp, + EthereumJSONRPC.quantity_to_integer(str_log_index) + )} + end + end + + defp do_log_to_deposit( + first_topic, + data, + from_address_hash, + transaction_hash, + block_hash, + block_number, + block_timestamp, + log_index + ) do {_, [ {"pubkey", "bytes", false, pubkey}, @@ -240,11 +456,11 @@ defmodule Indexer.Fetcher.Beacon.Deposit do ]} = Event.find_and_decode( @abi, - log.first_topic && log.first_topic.bytes, + first_topic.bytes, nil, nil, nil, - log.data.bytes + data.bytes ) %{ @@ -253,25 +469,28 @@ defmodule Indexer.Fetcher.Beacon.Deposit do amount: amount |> Decimal.new() |> Wei.from(:gwei), signature: %Data{bytes: signature}, index: index, - from_address_hash: log.from_address_hash, - transaction_hash: log.transaction_hash, - block_hash: log.block_hash, - block_number: log.block_number, - block_timestamp: log.block_timestamp, - log_index: log.index, + from_address_hash: from_address_hash, + transaction_hash: transaction_hash, + block_hash: block_hash, + block_number: block_number, + block_timestamp: block_timestamp, + log_index: log_index, inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now() } end - defp sequential?(last_processed_deposit_index, deposits) do - Enum.reduce_while(deposits, %{index: last_processed_deposit_index}, fn - %{index: i}, %{index: prev} when i == prev + 1 -> - {:cont, %{index: i}} + defp find_missing_ranges(last_processed_deposit_index, deposits) do + result = + Enum.reduce(deposits, %{index: last_processed_deposit_index, gaps: []}, fn + %{index: i}, %{index: prev, gaps: gaps} when i - prev <= 1 -> + %{index: i, gaps: gaps} - %{index: i}, %{index: prev} -> - {:halt, {:error, prev, i}} - end) + %{index: i}, %{index: prev, gaps: gaps} -> + %{index: i, gaps: [{prev, i} | gaps]} + end) + + Enum.reverse(result.gaps) end defp set_status(deposits, domain_deposit, genesis_fork_version) do diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 159f6aeebf45..4a2cefa94283 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -264,6 +264,7 @@ defmodule Indexer.Helper do end) end + # todo: refactor: reuse existing code from EthereumJSONRPC app @doc """ Retrieves event logs from Ethereum-like blockchains within a specified block range for a given address and set of topics using JSON-RPC. diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index e19e6d9a2d99..90c6074a3176 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -263,7 +263,7 @@ defmodule Indexer.Supervisor do ]}, {Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.Beacon.Blob.Supervisor, [[memory_monitor: memory_monitor]]}, - {Indexer.Fetcher.Beacon.Deposit.Supervisor, []}, + {Indexer.Fetcher.Beacon.Deposit.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, {Indexer.Fetcher.Beacon.Deposit.Status.Supervisor, []}, {Indexer.Fetcher.SignedAuthorizationStatus.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, diff --git a/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs index 73b941b2d60e..98fc0eb92ff1 100644 --- a/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs +++ b/apps/indexer/test/indexer/fetcher/beacon/deposit_test.exs @@ -1,4 +1,5 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do + use EthereumJSONRPC.Case use Explorer.DataCase, async: false import ExUnit.CaptureLog, only: [capture_log: 1] @@ -188,7 +189,9 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do """ describe "init/1" do - test "fetches config and initializes state without deposits in the database" do + test "fetches config and initializes state without deposits in the database", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do Tesla.Test.expect_tesla_call( times: 1, returns: fn %{url: "http://localhost:5052/eth/v1/config/spec"}, _opts -> @@ -196,7 +199,7 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do end ) - DepositSupervisor.Case.start_supervised!() + DepositSupervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) {_, pid, _, _} = Supervisor.which_children(DepositSupervisor) |> Enum.find(fn {name, _, _, _} -> name == DepositFetcher end) @@ -209,11 +212,14 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do genesis_fork_version: <<0, 0, 0, 0>>, deposit_index: -1, last_processed_log_block_number: -1, - last_processed_log_index: -1 + last_processed_log_index: -1, + json_rpc_named_arguments: json_rpc_named_arguments } end - test "fetches config and initializes state with deposits in the database" do + test "fetches config and initializes state with deposits in the database", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do Tesla.Test.expect_tesla_call( times: 1, returns: fn %{url: "http://localhost:5052/eth/v1/config/spec"}, _opts -> @@ -223,7 +229,7 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do deposit = insert(:beacon_deposit) - DepositSupervisor.Case.start_supervised!() + DepositSupervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) {_, pid, _, _} = Supervisor.which_children(DepositSupervisor) |> Enum.find(fn {name, _, _, _} -> name == DepositFetcher end) @@ -236,7 +242,8 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do genesis_fork_version: <<0, 0, 0, 0>>, deposit_index: deposit.index, last_processed_log_block_number: deposit.block_number, - last_processed_log_index: deposit.log_index + last_processed_log_index: deposit.log_index, + json_rpc_named_arguments: json_rpc_named_arguments } end end @@ -335,173 +342,525 @@ defmodule Indexer.Fetcher.Beacon.DepositTest do Repo.all(from(d in Deposit, order_by: [asc: :index])) end - # todo: return these tests when sequentiality requirement is added back - # test "fails to process non-sequential logs (logs starts not from 0, between batches)" do - # deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + test "fallbacks to fetch data from the node when non-sequential logs are detected (logs starts not from 0, between batches)", + %{json_rpc_named_arguments: json_rpc_named_arguments} do + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") - # transaction_a_to_be_ignored = insert(:transaction) |> with_block() + log_from_node = + build(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0 + ) - # _log_to_be_ignored_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 1, - # transaction: transaction_a_to_be_ignored, - # block: transaction_a_to_be_ignored.block - # ) + deposit_signature = log_from_node.first_topic + log_from_node_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_from_node.block_number) + log_from_node_transaction_hash = log_from_node.transaction.hash - # transaction_a = insert(:transaction) |> with_block() + transaction_a = insert(:transaction) |> with_block() - # _log_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 2, - # transaction: transaction_a, - # block: transaction_a.block - # ) + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_a, + block: transaction_a.block + ) - # transaction_b = insert(:transaction) |> with_block() + log_a_transaction_hash = log_a.transaction_hash + log_a_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_a.block_number) - # _log_b = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 3, - # transaction: transaction_b, - # block: transaction_b.block - # ) + transaction_b = insert(:transaction) |> with_block() - # log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, @state) end) + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_b, + block: transaction_b.block + ) - # assert log =~ "Non-sequential deposits detected:" + log_b_transaction_hash = log_b.transaction_hash - # assert [] == Repo.all(Deposit) - # end + EthereumJSONRPC.Mox + |> expect(:json_rpc, 2, fn + %{ + id: _id, + method: "eth_getLogs", + params: [ + %{ + address: "0x00000000219ab540356cbb839cbe05303d7705fa", + topics: [^deposit_signature], + fromBlock: "0x0", + toBlock: ^log_a_block_number_quantity + } + ] + }, + _options -> + {:ok, + [ + %{ + "address" => "0x7f02c3e3c98b133055b8b348b2ac625669ed295d", + "blockHash" => to_string(log_from_node.block.hash), + "blockNumber" => log_from_node_block_number_quantity, + "data" => to_string(log_from_node.data), + "logIndex" => "0x1", + "removed" => false, + "topics" => [ + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" + ], + "transactionHash" => to_string(log_from_node_transaction_hash), + "transactionIndex" => "0x4" + } + ]} + + [ + %{ + id: id, + method: "eth_getBlockByNumber", + params: [^log_from_node_block_number_quantity, false] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + result: %{ + "difficulty" => "0xa3ff9e", + "extraData" => "0x", + "gasLimit" => "0x1c9c380", + "gasUsed" => "0x0", + "hash" => to_string(log_from_node.block.hash), + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x2f14582947e292a2ecd20c430b46f2d27cfe213c", + "mixHash" => "0xa85d26c4efa1d9fdbb095935b49c93a1ddcc967634d899826168abe486f095d8", + "nonce" => "0xc7faaf72b6690188", + "number" => log_from_node_block_number_quantity, + "parentHash" => "0x8ac578a998c3af00a0eab91f7fa209aeed0251c61c37f3b1d43d4253a5e2fa7a", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size" => "0x203", + "stateRoot" => "0xd432683a9704dcabff1daacf8c40742301248faf3d9f05ea738daa2dffc19043", + "totalDifficulty" => "0x5113296ac", + "timestamp" => "0x617383e7", + "baseFeePerGas" => "0x7", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + end) + + log = + capture_log(fn -> + DepositFetcher.handle_info( + :process_logs, + Map.put(@state, :json_rpc_named_arguments, json_rpc_named_arguments) + ) + |> then(fn {:noreply, new_state} -> + DepositFetcher.handle_info( + :process_logs, + new_state + ) + end) + |> then(fn {:noreply, new_state} -> + DepositFetcher.handle_info( + :process_logs, + new_state + ) + end) + end) + + assert log =~ "Non-sequential deposits detected" - # test "fails to process non-sequential logs (non-sequential between, between batches)" do - # deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + assert [ + %Deposit{transaction_hash: ^log_from_node_transaction_hash, index: 0}, + %Deposit{transaction_hash: ^log_a_transaction_hash, index: 1}, + %Deposit{transaction_hash: ^log_b_transaction_hash, index: 2} + ] = + Repo.all(from(d in Deposit, order_by: [asc: :index])) + end - # transaction_a = insert(:transaction) |> with_block() + test "fallbacks to fetch data from the node when non-sequential logs are detected (non-sequential between, between batches)", + %{json_rpc_named_arguments: json_rpc_named_arguments} do + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") - # log_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 0, - # transaction: transaction_a, - # block: transaction_a.block - # ) + transaction_a = insert(:transaction) |> with_block() - # log_a_transaction_hash = log_a.transaction_hash - # transaction_a_to_be_ignored = insert(:transaction) |> with_block() + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) - # _log_to_be_ignored_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 2, - # transaction: transaction_a_to_be_ignored, - # block: transaction_a_to_be_ignored.block - # ) + log_a_transaction_hash = log_a.transaction_hash + log_a_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_a.block_number) - # transaction_b = insert(:transaction) |> with_block() + transaction_with_missing_logs = insert(:transaction) |> with_block() - # _log_b = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 3, - # transaction: transaction_b, - # block: transaction_b.block - # ) + log_from_node = + build(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_with_missing_logs, + block: transaction_with_missing_logs.block + ) - # {:noreply, new_state} = DepositFetcher.handle_info(:process_logs, @state) + deposit_signature = log_from_node.first_topic + log_from_node_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_from_node.block_number) + log_from_node_transaction_hash = log_from_node.transaction.hash - # log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, new_state) end) + transaction_b = insert(:transaction) |> with_block() - # assert log =~ "Non-sequential deposits detected:" + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_b, + block: transaction_b.block + ) - # assert [%Deposit{transaction_hash: ^log_a_transaction_hash}] = Repo.all(Deposit) - # end + log_b_transaction_hash = log_b.transaction_hash + log_b_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_b.block_number) - # test "fails to process non-sequential logs (logs starts not from 0, inside batch)" do - # state = Map.put(@state, :batch_size, 5) + transaction_c = insert(:transaction) |> with_block() - # deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + _log_c = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_c, + block: transaction_c.block + ) - # transaction_a_to_be_ignored = insert(:transaction) |> with_block() + EthereumJSONRPC.Mox + |> expect(:json_rpc, 1, fn + %{ + id: _id, + method: "eth_getLogs", + params: [ + %{ + address: "0x00000000219ab540356cbb839cbe05303d7705fa", + topics: [^deposit_signature], + fromBlock: ^log_a_block_number_quantity, + toBlock: ^log_b_block_number_quantity + } + ] + }, + _options -> + {:ok, + [ + %{ + "address" => "0x7f02c3e3c98b133055b8b348b2ac625669ed295d", + "blockHash" => to_string(log_from_node.block.hash), + "blockNumber" => log_from_node_block_number_quantity, + "data" => to_string(log_from_node.data), + "logIndex" => "0x1", + "removed" => false, + "topics" => [ + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" + ], + "transactionHash" => to_string(log_from_node_transaction_hash), + "transactionIndex" => "0x4" + } + ]} + end) - # _log_to_be_ignored_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 1, - # transaction: transaction_a_to_be_ignored, - # block: transaction_a_to_be_ignored.block - # ) + {:noreply, new_state} = + DepositFetcher.handle_info( + :process_logs, + Map.put(@state, :json_rpc_named_arguments, json_rpc_named_arguments) + ) - # transaction_a = insert(:transaction) |> with_block() + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, new_state) end) - # _log_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 2, - # transaction: transaction_a, - # block: transaction_a.block - # ) + assert log =~ "Non-sequential deposits detected" - # transaction_b = insert(:transaction) |> with_block() + assert [ + %Deposit{transaction_hash: ^log_a_transaction_hash}, + %Deposit{transaction_hash: ^log_from_node_transaction_hash}, + %Deposit{transaction_hash: ^log_b_transaction_hash} + ] = Repo.all(from(d in Deposit, order_by: [asc: :index])) + end - # _log_b = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 3, - # transaction: transaction_b, - # block: transaction_b.block - # ) + test "fallbacks to fetch data from the node when non-sequential logs are detected (logs starts not from 0, inside batch)", + %{json_rpc_named_arguments: json_rpc_named_arguments} do + state = @state |> Map.put(:batch_size, 5) |> Map.put(:json_rpc_named_arguments, json_rpc_named_arguments) - # log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") - # assert log =~ "Non-sequential deposits detected:" + log_from_node = + build(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0 + ) - # assert [] == Repo.all(Deposit) - # end + deposit_signature = log_from_node.first_topic + log_from_node_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_from_node.block_number) + log_from_node_transaction_hash = log_from_node.transaction.hash - # test "fails to process non-sequential logs (non-sequential between, inside batch)" do - # state = Map.put(@state, :batch_size, 5) + transaction_a = insert(:transaction) |> with_block() - # deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1, + transaction: transaction_a, + block: transaction_a.block + ) + + log_a_transaction_hash = log_a.transaction_hash + log_a_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_a.block_number) + + transaction_b = insert(:transaction) |> with_block() + + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_b, + block: transaction_b.block + ) + + log_b_transaction_hash = log_b.transaction_hash + + transaction_c = insert(:transaction) |> with_block() + + log_c = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_c, + block: transaction_c.block + ) + + log_c_transaction_hash = log_c.transaction_hash + + EthereumJSONRPC.Mox + |> expect(:json_rpc, 2, fn + %{ + id: _id, + method: "eth_getLogs", + params: [ + %{ + address: "0x00000000219ab540356cbb839cbe05303d7705fa", + topics: [^deposit_signature], + fromBlock: "0x0", + toBlock: ^log_a_block_number_quantity + } + ] + }, + _options -> + {:ok, + [ + %{ + "address" => "0x7f02c3e3c98b133055b8b348b2ac625669ed295d", + "blockHash" => to_string(log_from_node.block.hash), + "blockNumber" => log_from_node_block_number_quantity, + "data" => to_string(log_from_node.data), + "logIndex" => "0x1", + "removed" => false, + "topics" => [ + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" + ], + "transactionHash" => to_string(log_from_node_transaction_hash), + "transactionIndex" => "0x4" + } + ]} + + [ + %{ + id: id, + method: "eth_getBlockByNumber", + params: [^log_from_node_block_number_quantity, false] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + result: %{ + "difficulty" => "0xa3ff9e", + "extraData" => "0x", + "gasLimit" => "0x1c9c380", + "gasUsed" => "0x0", + "hash" => to_string(log_from_node.block.hash), + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x2f14582947e292a2ecd20c430b46f2d27cfe213c", + "mixHash" => "0xa85d26c4efa1d9fdbb095935b49c93a1ddcc967634d899826168abe486f095d8", + "nonce" => "0xc7faaf72b6690188", + "number" => log_from_node_block_number_quantity, + "parentHash" => "0x8ac578a998c3af00a0eab91f7fa209aeed0251c61c37f3b1d43d4253a5e2fa7a", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size" => "0x203", + "stateRoot" => "0xd432683a9704dcabff1daacf8c40742301248faf3d9f05ea738daa2dffc19043", + "totalDifficulty" => "0x5113296ac", + "timestamp" => "0x617383e7", + "baseFeePerGas" => "0x7", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + end) + + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) + + assert log =~ "Non-sequential deposits detected" + + assert [ + %Deposit{transaction_hash: ^log_from_node_transaction_hash}, + %Deposit{transaction_hash: ^log_a_transaction_hash}, + %Deposit{transaction_hash: ^log_b_transaction_hash}, + %Deposit{transaction_hash: ^log_c_transaction_hash} + ] = Repo.all(from(d in Deposit, order_by: [asc: :index])) + end + + test "fallbacks to fetch data from the node when non-sequential logs are detected (non-sequential between, inside batch)", + %{json_rpc_named_arguments: json_rpc_named_arguments} do + state = @state |> Map.put(:batch_size, 5) |> Map.put(:json_rpc_named_arguments, json_rpc_named_arguments) + + deposit_contract_address = insert(:address, hash: "0x00000000219ab540356cbb839cbe05303d7705fa") - # transaction_a = insert(:transaction) |> with_block() + transaction_a = insert(:transaction) |> with_block() + + log_a = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 0, + transaction: transaction_a, + block: transaction_a.block + ) + + log_a_transaction_hash = log_a.transaction_hash + log_a_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_a.block_number) + + log_from_node = + build(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 1 + ) + + deposit_signature = log_from_node.first_topic + log_from_node_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_from_node.block_number) + log_from_node_transaction_hash = log_from_node.transaction.hash + + transaction_b = insert(:transaction) |> with_block() - # _log_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 0, - # transaction: transaction_a, - # block: transaction_a.block - # ) + log_b = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 2, + transaction: transaction_b, + block: transaction_b.block + ) - # transaction_a_to_be_ignored = insert(:transaction) |> with_block() + log_b_transaction_hash = log_b.transaction_hash + log_b_block_number_quantity = EthereumJSONRPC.integer_to_quantity(log_b.block_number) - # _log_to_be_ignored_a = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 2, - # transaction: transaction_a_to_be_ignored, - # block: transaction_a_to_be_ignored.block - # ) + transaction_c = insert(:transaction) |> with_block() - # transaction_b = insert(:transaction) |> with_block() + log_c = + insert(:beacon_deposit_log, + address: deposit_contract_address, + deposit_index: 3, + transaction: transaction_c, + block: transaction_c.block + ) - # _log_b = - # insert(:beacon_deposit_log, - # address: deposit_contract_address, - # deposit_index: 3, - # transaction: transaction_b, - # block: transaction_b.block - # ) + log_c_transaction_hash = log_c.transaction_hash + + EthereumJSONRPC.Mox + |> expect(:json_rpc, 2, fn + %{ + id: _id, + method: "eth_getLogs", + params: [ + %{ + address: "0x00000000219ab540356cbb839cbe05303d7705fa", + topics: [^deposit_signature], + fromBlock: ^log_a_block_number_quantity, + toBlock: ^log_b_block_number_quantity + } + ] + }, + _options -> + {:ok, + [ + %{ + "address" => "0x7f02c3e3c98b133055b8b348b2ac625669ed295d", + "blockHash" => to_string(log_from_node.block.hash), + "blockNumber" => log_from_node_block_number_quantity, + "data" => to_string(log_from_node.data), + "logIndex" => "0x1", + "removed" => false, + "topics" => [ + "0x649bbc62d0e31342afea4e5cd82d4049e7e1ee912fc0889aa790803be39038c5" + ], + "transactionHash" => to_string(log_from_node_transaction_hash), + "transactionIndex" => "0x4" + } + ]} + + [ + %{ + id: id, + method: "eth_getBlockByNumber", + params: [^log_from_node_block_number_quantity, false] + } + ], + _options -> + {:ok, + [ + %{ + id: id, + result: %{ + "difficulty" => "0xa3ff9e", + "extraData" => "0x", + "gasLimit" => "0x1c9c380", + "gasUsed" => "0x0", + "hash" => to_string(log_from_node.block.hash), + "logsBloom" => + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner" => "0x2f14582947e292a2ecd20c430b46f2d27cfe213c", + "mixHash" => "0xa85d26c4efa1d9fdbb095935b49c93a1ddcc967634d899826168abe486f095d8", + "nonce" => "0xc7faaf72b6690188", + "number" => log_from_node_block_number_quantity, + "parentHash" => "0x8ac578a998c3af00a0eab91f7fa209aeed0251c61c37f3b1d43d4253a5e2fa7a", + "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size" => "0x203", + "stateRoot" => "0xd432683a9704dcabff1daacf8c40742301248faf3d9f05ea738daa2dffc19043", + "totalDifficulty" => "0x5113296ac", + "timestamp" => "0x617383e7", + "baseFeePerGas" => "0x7", + "transactions" => [], + "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + "uncles" => [] + } + } + ]} + end) - # log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) + log = capture_log(fn -> DepositFetcher.handle_info(:process_logs, state) end) - # assert log =~ "Non-sequential deposits detected:" + assert log =~ "Non-sequential deposits detected" - # assert [] = Repo.all(Deposit) - # end + assert [ + %Deposit{transaction_hash: ^log_a_transaction_hash}, + %Deposit{transaction_hash: ^log_from_node_transaction_hash}, + %Deposit{transaction_hash: ^log_b_transaction_hash}, + %Deposit{transaction_hash: ^log_c_transaction_hash} + ] = Repo.all(from(d in Deposit, order_by: [asc: :index])) + end test "signature verification" do state = Map.put(@state, :batch_size, 5) From 3764295d529cd68bba677373829f2e2262aa12ad Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 19 Nov 2025 13:45:44 +0300 Subject: [PATCH 0475/1006] chore: Remove address-related props from sending to multichain service (#13584) --- .../multichain_search.ex | 23 ------------------- .../multichain_search_test.exs | 22 +++--------------- .../main_export_queue_test.exs | 8 ------- 3 files changed, 3 insertions(+), 50 deletions(-) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 91d625af435b..4ac4527cb2c4 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -23,7 +23,6 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do @max_concurrency 5 @post_timeout :timer.minutes(5) - @unspecified "UNSPECIFIED" @doc """ Processes a batch import of data by splitting the input parameters into chunks and sending each chunk as an HTTP POST request to a configured microservice endpoint. @@ -1131,10 +1130,6 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do hash: Hash.to_string(address.hash), is_contract: !is_nil(address.contract_code), is_verified_contract: address.verified, - is_token: token?(address.token), - ens_name: address.ens_domain_name, - token_name: get_token_name(address.token), - token_type: get_token_type(address.token), contract_name: get_smart_contract_name(address.smart_contract) } end @@ -1201,30 +1196,12 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do } end - defp token?(nil), do: false - - defp token?(%NotLoaded{}), do: false - - defp token?(_), do: true - - defp get_token_name(nil), do: nil - - defp get_token_name(%NotLoaded{}), do: nil - - defp get_token_name(token), do: token.name - defp get_smart_contract_name(nil), do: nil defp get_smart_contract_name(%NotLoaded{}), do: nil defp get_smart_contract_name(smart_contract), do: smart_contract.name - defp get_token_type(nil), do: @unspecified - - defp get_token_type(%NotLoaded{}), do: @unspecified - - defp get_token_type(token), do: token.type - defp get_block_ranges([]), do: [] defp get_block_ranges(blocks) do diff --git a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs index 94460e13d99e..e7ad203598d4 100644 --- a/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs +++ b/apps/explorer/test/explorer/microservice_interfaces/multichain_search_test.exs @@ -1321,31 +1321,19 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do hash: "0x" <> Base.encode16(address_1.hash.bytes, case: :lower), is_contract: false, is_verified_contract: false, - contract_name: nil, - token_name: nil, - token_type: "UNSPECIFIED", - is_token: false, - ens_name: "te.eth" + contract_name: nil }, %{ hash: "0x" <> Base.encode16(address_2.hash.bytes, case: :lower), is_contract: true, is_verified_contract: false, - contract_name: nil, - token_name: nil, - token_type: "UNSPECIFIED", - is_token: false, - ens_name: nil + contract_name: nil }, %{ hash: "0x" <> Base.encode16(address_3.hash.bytes, case: :lower), is_contract: true, is_verified_contract: true, - contract_name: "SimpleStorage", - token_name: "Main Token", - token_type: "ERC-721", - is_token: true, - ens_name: nil + contract_name: "SimpleStorage" } ] @@ -1371,12 +1359,8 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearchTest do defp address_export_data(address) do %{ hash: "0x" <> Base.encode16(address.hash.bytes, case: :lower), - token_type: "UNSPECIFIED", is_contract: false, - token_name: nil, contract_name: nil, - ens_name: nil, - is_token: false, is_verified_contract: false } end diff --git a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs index 41d4600c89de..7747e1d358fc 100644 --- a/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/multichain_search_db/main_export_queue_test.exs @@ -178,22 +178,14 @@ defmodule Indexer.Fetcher.MultichainSearchDb.MainExportQueueTest do addresses: [ %{ hash: ^address_2_hash_string, - token_type: "UNSPECIFIED", is_contract: false, - token_name: nil, contract_name: nil, - ens_name: nil, - is_token: false, is_verified_contract: false }, %{ hash: ^address_1_hash_string, - token_type: "UNSPECIFIED", is_contract: false, - token_name: nil, contract_name: nil, - ens_name: nil, - is_token: false, is_verified_contract: false } ], From 74c22d9cc21c6fd6d20293e792724271fa39fe54 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:31:24 +0400 Subject: [PATCH 0476/1006] chore: Improve DeleteZeroValueInternalTransactions migration (#13569) --- ...delete_zero_value_internal_transactions.ex | 136 ++++++++++++------ .../utility/address_id_to_address_hash.ex | 25 ++++ ...ternal_transactions_address_placeholder.ex | 21 +++ ...3_create_address_ids_to_address_hashes.exs | 12 ++ ...rnal_transactions_address_placeholders.exs | 15 ++ ..._zero_value_internal_transactions_test.exs | 73 +++++++++- config/runtime.exs | 4 +- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 2 +- 9 files changed, 241 insertions(+), 48 deletions(-) create mode 100644 apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex create mode 100644 apps/explorer/lib/explorer/utility/internal_transactions_address_placeholder.ex create mode 100644 apps/explorer/priv/repo/migrations/20251104134603_create_address_ids_to_address_hashes.exs create mode 100644 apps/explorer/priv/repo/migrations/20251104135728_create_internal_transactions_address_placeholders.exs diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index a711309b2abf..1d6ac78a8af3 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -13,11 +13,12 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Migrator.MigrationStatus alias Explorer.Repo + alias Explorer.Utility.{AddressIdToAddressHash, InternalTransactionsAddressPlaceholder} alias Timex.Duration @migration_name "delete_zero_value_internal_transactions" - @past_check_interval 10 - @default_future_check_interval :timer.minutes(1) + @not_completed_check_interval 10 + @default_check_interval :timer.minutes(1) @default_batch_size 100 @default_storage_period_days 30 @@ -36,8 +37,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do state = case MigrationStatus.fetch(@migration_name) do nil -> - border_number = get_border_number() - state = %{"min_block_number" => border_number, "max_block_number" => border_number} + state = %{"max_block_number" => -1} MigrationStatus.set_status(@migration_name, "started") MigrationStatus.update_meta(@migration_name, state) state @@ -46,45 +46,35 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do meta end - schedule_future_check() - schedule_past_check() + schedule_check() {:noreply, state} end @impl true - def handle_info(:update_future, %{"max_block_number" => max_number} = state) do + def handle_info(:update, %{"max_block_number" => max_number} = state) do border_number = get_border_number() - to_number = min(max_number + batch_size(), border_number) + to_number = border_number && min(max_number + batch_size(), border_number) clear_internal_transactions(max_number, to_number) - new_state = %{state | "max_block_number" => to_number + 1} - MigrationStatus.update_meta(@migration_name, new_state) - schedule_future_check() - {:noreply, new_state} - end - - @impl true - def handle_info(:update_past, %{"min_block_number" => -1} = state) do - new_state = Map.delete(state, "min_block_number") - MigrationStatus.set_status(@migration_name, "completed") - MigrationStatus.update_meta(@migration_name, new_state) - {:noreply, new_state} - end + completed? = to_number == border_number + new_max_number = (to_number && to_number + 1) || max_number + + new_state = + if completed? and is_nil(state["completed"]) do + MigrationStatus.set_status(@migration_name, "completed") + Map.merge(state, %{"max_block_number" => new_max_number, "completed" => true}) + else + %{state | "max_block_number" => new_max_number} + end - def handle_info(:update_past, %{"min_block_number" => min_number} = state) do - from_number = max(min_number - batch_size(), 0) - clear_internal_transactions(from_number, min_number) - new_state = %{state | "min_block_number" => from_number - 1} MigrationStatus.update_meta(@migration_name, new_state) - schedule_past_check() + schedule_check(completed?) {:noreply, new_state} end - def handle_info(:update_past, state) do - {:noreply, state} - end - - defp clear_internal_transactions(from_number, to_number) when from_number < to_number do + @smallint_max_value 32767 + defp clear_internal_transactions(from_number, to_number) + when is_integer(from_number) and is_integer(to_number) and from_number < to_number do Repo.transaction(fn -> locked_internal_transactions_to_delete_query = from( @@ -102,10 +92,75 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do from( it in InternalTransaction, inner_join: locked_it in subquery(locked_internal_transactions_to_delete_query), - on: join_on_ctid(it, locked_it) + on: join_on_ctid(it, locked_it), + select: %{ + from_address_hash: it.from_address_hash, + to_address_hash: it.to_address_hash, + block_number: it.block_number + } ) - Repo.delete_all(delete_query, timeout: :infinity) + {_count, deleted_internal_transactions} = Repo.delete_all(delete_query, timeout: :infinity) + + address_hashes = + deleted_internal_transactions + |> Enum.flat_map(&[&1.from_address_hash, &1.to_address_hash]) + |> Enum.uniq() + |> Enum.reject(&is_nil/1) + + id_to_address_params = Enum.map(address_hashes, &%{address_hash: &1}) + + Repo.insert_all(AddressIdToAddressHash, id_to_address_params, on_conflict: :nothing) + + address_to_id_map = + AddressIdToAddressHash + |> where([a], a.address_hash in ^address_hashes) + |> select([a], {a.address_hash, a.address_id}) + |> Repo.all() + |> Map.new() + + placeholders_params = + deleted_internal_transactions + |> Enum.group_by(& &1.block_number) + |> Enum.flat_map(fn {block_number, internal_transactions} -> + internal_transactions + |> Enum.reduce(%{}, fn internal_transaction, inner_acc -> + from_address_hash = internal_transaction.from_address_hash + to_address_hash = internal_transaction.to_address_hash + + inner_acc + |> Map.update( + from_address_hash, + %{ + address_id: address_to_id_map[from_address_hash], + block_number: block_number, + count_tos: 0, + count_froms: 1 + }, + fn existing_params -> + %{existing_params | count_froms: min(existing_params.count_froms + 1, @smallint_max_value)} + end + ) + |> Map.update( + to_address_hash, + %{ + address_id: address_to_id_map[to_address_hash], + block_number: block_number, + count_tos: 1, + count_froms: 0 + }, + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + fn existing_params -> + %{existing_params | count_tos: min(existing_params.count_tos + 1, @smallint_max_value)} + end + ) + end) + |> Map.values() + |> Enum.reject(&is_nil(&1.address_id)) + end) + |> Enum.sort_by(&{&1.address_id, &1.block_number}) + + Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_params) end) end @@ -121,21 +176,16 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do |> limit(1) |> select([b], b.number) |> Repo.one() - |> Kernel.||(0) - end - - defp schedule_past_check do - Process.send_after(self(), :update_past, @past_check_interval) end - defp schedule_future_check do - Process.send_after(self(), :update_future, future_check_interval()) + defp schedule_check(completed? \\ false) do + Process.send_after(self(), :update, (completed? && completed_check_interval()) || @not_completed_check_interval) end - defp future_check_interval do - with nil <- Application.get_env(:explorer, __MODULE__)[:future_check_interval], + defp completed_check_interval do + with nil <- Application.get_env(:explorer, __MODULE__)[:check_interval], nil <- get_average_block_time() do - @default_future_check_interval + @default_check_interval else interval -> interval end diff --git a/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex b/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex new file mode 100644 index 000000000000..b363d3f220a8 --- /dev/null +++ b/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex @@ -0,0 +1,25 @@ +defmodule Explorer.Utility.AddressIdToAddressHash do + @moduledoc """ + Module is responsible for keeping the address_id to address_hash correspondence. + """ + + use Explorer.Schema + + alias Explorer.Chain.{Address, Hash} + + @primary_key false + typed_schema "address_ids_to_address_hashes" do + field(:address_id, :integer, primary_key: true) + + belongs_to(:address, Address, + foreign_key: :address_hash, + references: :hash, + type: Hash.Address + ) + end + + @doc false + def changeset(address_id_to_address_hash \\ %__MODULE__{}, params) do + cast(address_id_to_address_hash, params, [:address_id, :address_hash]) + end +end diff --git a/apps/explorer/lib/explorer/utility/internal_transactions_address_placeholder.ex b/apps/explorer/lib/explorer/utility/internal_transactions_address_placeholder.ex new file mode 100644 index 000000000000..7eb78ebc3ad9 --- /dev/null +++ b/apps/explorer/lib/explorer/utility/internal_transactions_address_placeholder.ex @@ -0,0 +1,21 @@ +defmodule Explorer.Utility.InternalTransactionsAddressPlaceholder do + @moduledoc """ + Module is responsible for keeping the information about the presence of internal transactions + on a particular address inside specific block + """ + + use Explorer.Schema + + @primary_key false + typed_schema "deleted_internal_transactions_address_placeholders" do + field(:address_id, :integer, primary_key: true) + field(:block_number, :integer, primary_key: true) + field(:count_tos, :integer) + field(:count_froms, :integer) + end + + @doc false + def changeset(placeholder \\ %__MODULE__{}, params) do + cast(placeholder, params, [:address_id, :block_number, :count_tos, :count_froms]) + end +end diff --git a/apps/explorer/priv/repo/migrations/20251104134603_create_address_ids_to_address_hashes.exs b/apps/explorer/priv/repo/migrations/20251104134603_create_address_ids_to_address_hashes.exs new file mode 100644 index 000000000000..34e4506c2fc1 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251104134603_create_address_ids_to_address_hashes.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.CreateAddressIdsToAddressHashes do + use Ecto.Migration + + def change do + create table(:address_ids_to_address_hashes, primary_key: false) do + add(:address_id, :bigserial, primary_key: true) + add(:address_hash, :bytea, null: false) + end + + create(unique_index(:address_ids_to_address_hashes, [:address_hash])) + end +end diff --git a/apps/explorer/priv/repo/migrations/20251104135728_create_internal_transactions_address_placeholders.exs b/apps/explorer/priv/repo/migrations/20251104135728_create_internal_transactions_address_placeholders.exs new file mode 100644 index 000000000000..9123aba577f0 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251104135728_create_internal_transactions_address_placeholders.exs @@ -0,0 +1,15 @@ +defmodule Explorer.Repo.Migrations.CreateInternalTransactionsAddressPlaceholders do + use Ecto.Migration + + def change do + create table(:deleted_internal_transactions_address_placeholders, primary_key: false) do + add(:address_id, references(:address_ids_to_address_hashes, column: :address_id, type: :bigint), + primary_key: true + ) + + add(:block_number, :bigint, primary_key: true) + add(:count_tos, :smallint, null: false) + add(:count_froms, :smallint, null: false) + end + end +end diff --git a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs index a56cd055cd3c..fc6c7d4587b0 100644 --- a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs +++ b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs @@ -4,11 +4,16 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do alias Explorer.Chain.InternalTransaction alias Explorer.Migrator.{DeleteZeroValueInternalTransactions, MigrationStatus} alias Explorer.Repo + alias Explorer.Utility.{AddressIdToAddressHash, InternalTransactionsAddressPlaceholder} test "Deletes zero value calls" do - Enum.map(1..3, fn i -> - block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -40)) + address_1 = insert(:address) + address_2 = insert(:address) + address_3 = insert(:address) + + block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -40)) + Enum.map(1..3, fn i -> transaction = :transaction |> insert() @@ -19,12 +24,52 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, + from_address: address_1, + to_address: address_2, block_index: i, type: :call, value: 0 ) end) + Enum.map(1..4, fn i -> + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + from_address: address_2, + to_address: address_3, + block_index: i + 3, + type: :call, + value: 0 + ) + end) + + Enum.map(1..5, fn i -> + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + from_address: address_3, + to_address: address_1, + block_index: i + 7, + type: :call, + value: 0 + ) + end) + Enum.map(1..4, fn i -> transaction = :transaction @@ -105,5 +150,29 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do assert Enum.count(non_calls) == 5 assert Enum.count(recent_zero_value_calls) == 6 + + id_to_hashes = Repo.all(AddressIdToAddressHash) + + assert length(id_to_hashes) == 3 + + assert %{address_id: address_1_id} = Enum.find(id_to_hashes, &(&1.address_hash == address_1.hash)) + assert %{address_id: address_2_id} = Enum.find(id_to_hashes, &(&1.address_hash == address_2.hash)) + assert %{address_id: address_3_id} = Enum.find(id_to_hashes, &(&1.address_hash == address_3.hash)) + + placeholders = Repo.all(InternalTransactionsAddressPlaceholder) + + assert length(placeholders) == 3 + + assert Enum.any?(placeholders, fn p -> + p.address_id == address_1_id and p.block_number == block.number and p.count_tos == 5 and p.count_froms == 3 + end) + + assert Enum.any?(placeholders, fn p -> + p.address_id == address_2_id and p.block_number == block.number and p.count_tos == 3 and p.count_froms == 4 + end) + + assert Enum.any?(placeholders, fn p -> + p.address_id == address_3_id and p.block_number == block.number and p.count_tos == 4 and p.count_froms == 5 + end) end end diff --git a/config/runtime.exs b/config/runtime.exs index 09d8533c3ee9..74313d8d4e38 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -839,8 +839,8 @@ config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE", 100), storage_period_days: ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS", 30), - future_check_interval: - ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL", "1m") + check_interval: + ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL", "1m") config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), diff --git a/cspell.json b/cspell.json index 8b1ba427abe6..0a175a2cc970 100644 --- a/cspell.json +++ b/cspell.json @@ -268,6 +268,7 @@ "Floki", "fontawesome", "fortawesome", + "froms", "fsym", "fullwidth", "FULU", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 6f9ebd8dceff..2ef005b42431 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -562,7 +562,7 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS= -# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_FUTURE_CHECK_INTERVAL= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL= # HEALTH_MONITOR_CHECK_INTERVAL=1m # HEALTH_MONITOR_BLOCKS_PERIOD=5m # HEALTH_MONITOR_BATCHES_PERIOD=4h From abf7d768c8d6d364b15b6651206d21ad5adc5079 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 19 Nov 2025 15:03:13 +0300 Subject: [PATCH 0477/1006] fix: handle deposit status statement too complex (#13588) --- .../explorer/chain/beacon/deposit/pending.ex | 28 + .../indexer/fetcher/beacon/deposit/status.ex | 89 +- .../fetcher/beacon/deposit/status_test.exs | 55 +- .../pending_beacon_deposits_response.json | 4816 +++++++++++++++++ 4 files changed, 4938 insertions(+), 50 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/beacon/deposit/pending.ex create mode 100644 apps/indexer/test/support/fixture/pending_beacon_deposits_response.json diff --git a/apps/explorer/lib/explorer/chain/beacon/deposit/pending.ex b/apps/explorer/lib/explorer/chain/beacon/deposit/pending.ex new file mode 100644 index 000000000000..781241424381 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/beacon/deposit/pending.ex @@ -0,0 +1,28 @@ +defmodule Explorer.Chain.Beacon.Deposit.Pending do + @moduledoc """ + Models a pending deposit in the beacon chain. + """ + + use Explorer.Schema + + alias Explorer.Chain.{Data, Wei} + + @primary_key false + typed_schema "temp_beacon_pending_deposits" do + field(:pubkey, Data, null: false) + field(:withdrawal_credentials, Data, null: false) + field(:amount, Wei, null: false) + field(:signature, Data, null: false) + field(:block_timestamp, :utc_datetime_usec, null: false) + end + + @doc """ + Cast and validates that the `attrs` are valid. + """ + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() + def changeset(deposit, attrs) do + deposit + |> cast(attrs, [:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp]) + |> validate_required([:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp]) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex index abac57bca015..53774b379a75 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit/status.ex @@ -10,9 +10,11 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do import Ecto.Query + alias Ecto.Multi alias Explorer.Chain.Beacon.Deposit - alias Explorer.Chain.{Data, Wei} - alias Explorer.{QueryHelper, Repo} + alias Explorer.Chain.Beacon.Deposit.Pending, as: PendingDeposit + alias Explorer.Chain.Wei + alias Explorer.Repo alias Indexer.Fetcher.Beacon.{Blob, Client} def start_link(arguments, gen_server_options \\ []) do @@ -54,46 +56,61 @@ defmodule Indexer.Fetcher.Beacon.Deposit.Status do end defp mark_completed_deposits(pending_deposits) do - ids = + {changes_list, max_block_timestamp} = pending_deposits - |> Enum.map(fn deposit -> - {:ok, pubkey} = Data.cast(deposit["pubkey"]) - {:ok, withdrawal_credentials} = Data.cast(deposit["withdrawal_credentials"]) - {amount, ""} = Integer.parse(deposit["amount"]) - {:ok, signature} = Data.cast(deposit["signature"]) + |> Enum.reduce({[], DateTime.from_unix!(0)}, fn deposit, {acc, max_block_timestamp} -> {slot, ""} = Integer.parse(deposit["slot"]) + block_timestamp = slot |> slot_to_timestamp() |> DateTime.from_unix!() + {amount, ""} = Integer.parse(deposit["amount"]) - {pubkey.bytes, withdrawal_credentials.bytes, amount |> Decimal.new() |> Wei.from(:gwei) |> Wei.to(:wei), - signature.bytes, slot |> slot_to_timestamp() |> DateTime.from_unix!()} + {[ + PendingDeposit.changeset(%PendingDeposit{}, %{ + pubkey: deposit["pubkey"], + withdrawal_credentials: deposit["withdrawal_credentials"], + amount: amount |> Decimal.new() |> Wei.from(:gwei) |> Wei.to(:wei), + signature: deposit["signature"], + block_timestamp: block_timestamp + }).changes + | acc + ], Enum.max([block_timestamp, max_block_timestamp], &DateTime.after?/2)} end) - tuple_not_in = - dynamic( - not (^QueryHelper.tuple_in([:pubkey, :withdrawal_credentials, :amount, :signature, :block_timestamp], ids)) - ) - - batch_size = 100 - - query = - from( - deposit in Deposit, - where: deposit.status == :pending, - where: ^tuple_not_in, - select: deposit.index, - order_by: [asc: deposit.index] - ) - - all_batch_ids = - query - |> Repo.all(timeout: :infinity) - - all_batch_ids - |> Enum.chunk_every(batch_size) - |> Enum.each(fn batch_ids -> - Deposit - |> where([deposit], deposit.index in ^batch_ids) - |> Repo.update_all(set: [status: "completed", updated_at: DateTime.utc_now()]) + Multi.new() + |> Multi.run(:create_temp_beacon_pending_deposits_table, fn repo, _changes -> + repo.query(""" + CREATE TEMP TABLE temp_beacon_pending_deposits ( + pubkey bytea, + withdrawal_credentials bytea, + amount numeric, + signature bytea, + block_timestamp timestamp + ) ON COMMIT DROP + """) + end) + |> Multi.run(:insert_pending_deposits, fn repo, _changes -> + {:ok, repo.safe_insert_all(PendingDeposit, changes_list, [])} end) + |> Multi.update_all( + :mark_completed_deposits, + from(d in Deposit, + as: :deposit, + where: d.status == :pending, + where: d.block_timestamp <= ^max_block_timestamp, + where: + not exists( + from(pd in PendingDeposit, + where: pd.pubkey == parent_as(:deposit).pubkey, + where: pd.withdrawal_credentials == parent_as(:deposit).withdrawal_credentials, + where: pd.amount == parent_as(:deposit).amount, + where: pd.signature == parent_as(:deposit).signature, + where: pd.block_timestamp == parent_as(:deposit).block_timestamp + ) + ) + ), + [set: [status: :completed, updated_at: DateTime.utc_now()]], + timeout: :infinity + ) + |> Repo.transaction() end defp slot_to_timestamp(slot) do diff --git a/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs b/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs index e6b1ff3113d2..e127e660e616 100644 --- a/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs +++ b/apps/indexer/test/indexer/fetcher/beacon/deposit/status_test.exs @@ -44,7 +44,7 @@ defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do ) # 12405631 - deposit_b = + _deposit_b = insert(:beacon_deposit, status: :pending, amount: 2 |> Decimal.new() |> Wei.from(:gwei), @@ -60,19 +60,34 @@ defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do ) # 12405634 - _deposit_d = + deposit_d = insert(:beacon_deposit, status: :pending, amount: 4 |> Decimal.new() |> Wei.from(:gwei), block_timestamp: DateTime.from_unix!(1_755_691_631) ) + # 12405635 + _deposit_e = + insert(:beacon_deposit, + status: :pending, + amount: 5 |> Decimal.new() |> Wei.from(:gwei), + block_timestamp: DateTime.from_unix!(1_755_691_643) + ) + pending_deposits_result = """ { "execution_optimistic": false, "finalized": false, "data": [ + { + "pubkey": "0xb257656f0a024a5c3be175a3bafd96cfcc452544b0fc6a23bbc39381028a28c10e8bafe6119c34771b5d86c9cae559e5", + "withdrawal_credentials": "0x01000000000000000000000082ce3e15a02e6a2e5a677d9700fe1390efead8eb", + "amount": "32000000000", + "signature": "0xb12eb9bddaf201aac73fb5ba9972ed06093c95a8baa1b5adcf1936ae7f33b0033c34d9e4756567c0424529e2de6774230274b015dfbe848e24a962a8748ea6229a8a87825f979e17d5e66f0246fb5eeb180e318c6574c22339f1cbcf5408a8dd", + "slot": "12405629" + }, { "pubkey": "#{deposit_a.pubkey}", "withdrawal_credentials": "#{deposit_a.withdrawal_credentials}", @@ -80,13 +95,6 @@ defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do "signature": "#{deposit_a.signature}", "slot": "12405630" }, - { - "pubkey": "#{deposit_b.pubkey}", - "withdrawal_credentials": "#{deposit_b.withdrawal_credentials}", - "amount": "#{deposit_b.amount |> Wei.to(:gwei)}", - "signature": "#{deposit_b.signature}", - "slot": "12405631" - }, { "pubkey": "#{deposit_c.pubkey}", "withdrawal_credentials": "#{deposit_c.withdrawal_credentials}", @@ -95,10 +103,10 @@ defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do "slot": "12405633" }, { - "pubkey": "0xb257656f0a024a5c3be175a3bafd96cfcc452544b0fc6a23bbc39381028a28c10e8bafe6119c34771b5d86c9cae559e5", - "withdrawal_credentials": "0x01000000000000000000000082ce3e15a02e6a2e5a677d9700fe1390efead8eb", - "amount": "32000000000", - "signature": "0xb12eb9bddaf201aac73fb5ba9972ed06093c95a8baa1b5adcf1936ae7f33b0033c34d9e4756567c0424529e2de6774230274b015dfbe848e24a962a8748ea6229a8a87825f979e17d5e66f0246fb5eeb180e318c6574c22339f1cbcf5408a8dd", + "pubkey": "#{deposit_d.pubkey}", + "withdrawal_credentials": "#{deposit_d.withdrawal_credentials}", + "amount": "#{deposit_d.amount |> Wei.to(:gwei)}", + "signature": "#{deposit_d.signature}", "slot": "12405634" } ] @@ -119,11 +127,30 @@ defmodule Indexer.Fetcher.Beacon.Deposit.StatusTest do assert [ %Deposit{status: :invalid}, + %Deposit{status: :completed}, %Deposit{status: :pending}, %Deposit{status: :pending}, - %Deposit{status: :completed} + # deposit that appeared after the oldest one in pending deposits + # response is not yet reflected on the beacon node and should + # not be marked as completed + %Deposit{status: :pending} ] = Repo.all(from(d in Deposit, order_by: [asc: d.index])) end + + test "handles huge amount of data" do + Repo.query!("SET LOCAL max_stack_depth = '128kB'") + + pending_deposits_response = File.read!("./test/support/fixture/pending_beacon_deposits_response.json") + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "http://localhost:5052/eth/v1/beacon/states/head/pending_deposits"}, _opts -> + {:ok, %Tesla.Env{status: 200, body: pending_deposits_response}} + end + ) + + {:noreply, _timer} = StatusFetcher.handle_info(:fetch_queued_deposits, nil) + end end end end diff --git a/apps/indexer/test/support/fixture/pending_beacon_deposits_response.json b/apps/indexer/test/support/fixture/pending_beacon_deposits_response.json new file mode 100644 index 000000000000..84871c152476 --- /dev/null +++ b/apps/indexer/test/support/fixture/pending_beacon_deposits_response.json @@ -0,0 +1,4816 @@ +{ + "version": "electra", + "execution_optimistic": false, + "finalized": false, + "data": [ + { + "pubkey": "0xad000ed585fcad3cf95888f6c22319c5422cdff058ef8ddc1562cef075b270f5036ccd9fd2f6124421e67f104acb42cb", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb39ed0adb094d7ac3fa561c954eead6cdc1fab89aa3eb010e9d0325425ec8b5d3d8577d6a26a37f2cd0421d467ff62fa0f533493f929e36304c9675f264db9678c2292cf29c488357f96cca0b76cf211d319c2b6a8ce839e591feeceb80b0e3e", + "slot": "12856210" + }, + { + "pubkey": "0xa5235a4c09d3ca42e202f3dc5ecba4004311844f392eadd1669a76c47f5974eacd65267ae71e703733d9be63d5804a90", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x85c3697b258ec79b535564c48a09b4f72dad773ed9dd148b116fb50014a9be8ee59c2296f93119a16c76555dfa8f8b1203aa03f9d4413026e7528ceb2f4b500af4e249e30ddf20ee29484ef9dc3b8744bb497543d7aa62440980b0ad5f8f9991", + "slot": "12856210" + }, + { + "pubkey": "0x963f658cedb572cbcf8f1ff28184a80336538146ab87aa70e5093b1c57cdd83a1fb167fce83c85d0c27e9054b3b775b9", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8897f23c44c7b3664437277676ad9973b94709aaf2542b7f25198f0731e9588c023b0e98b0c095b36b38e66316a5038e0f003d986a87088797ccd870322a9975da9a9a7c163bfa3ec0d841fa0e3f10bab13d5f7efc7aba57f17dc998962ff203", + "slot": "12856210" + }, + { + "pubkey": "0xae6ee3dda3f767786329163d4c41bb5467b6f55d7b5dce238309a3b346984f6014a9ca82a2255896b02ddca53c81e029", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xaa30e77781cd4a5b29f7f3bc537d210b45f740d25e7969bc2ccda311f4fa35afac3bc6c0f7fd0ba381f982222e0ceb2a188f81b23193ea7882a4998a97a61056854df9808ec04be239be8720f70d15c902f45babe484aba5275eedd6a7f7020f", + "slot": "12856210" + }, + { + "pubkey": "0xad84264c144c6dcf43d380ec18b84d390e8311c3ea530b80a7cfbc2d8f7a19730ddf5745c5b505765dbdb34746a9571c", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8e499de53c29f671375d25f94108b22e1ba9fb3b13014d9787c7d1b3e6eba2aa5e8594f8bbdc76f42299825247420f6710a86b84bedc80ff89abbb930042085c5cedb394f0be3a21ec80f25a059d7661b22f55338f170f759f70692ad28112f4", + "slot": "12856210" + }, + { + "pubkey": "0xa1d880a979b98fe28bb16312f433c36c065552d1dd44b37e49b64cc8af85d7eb523227f05b9c0dcb2e71eec28265bbf1", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xaaeaf10bb0ac85c658ec247b4a82af9d679033a63dab2c58e467329f86251f275563f5430b0770e393373bff5cb2e5fd030c6f57a8a3c4dfcbfb147aa9f20e2543aca3cf0a2a97a9f4e659fd67b389e764c4cf47c0927f88181125dd7f0720c3", + "slot": "12856210" + }, + { + "pubkey": "0xa4c056ba3bd791bdcd0e029d99701cf0d30c0da495ba6a258378efdfbe5e719501aecd8f55b348c02278b20b66e78760", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x80c63e12246ee6d61dd716d429ef95c86f83c3ed5158dad89c5c5843fdad24f04b536cf47496580d8c0dc589f93a5299024b37e346119a1d3e22fc6f314d0c4f227a22cb82d0b914bac2496ab6129451412322e6fab8baf4c74482f40d248b23", + "slot": "12856210" + }, + { + "pubkey": "0xa98befdb066a07bb867684bbd5f0bb3dcb20554c115667ce376155dcf4f1bb05b95c5c62e144ff4bd4e13753e46b60d7", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x86bdd0121fa0ae43f46da332491d0db528490f1240a09175cede0436dd261c1b6fdc5a67b35e5608c294f066101ef86f0e917e0f59601e72f07a5d3396581b13d5e5061e9d48cae9f946bc31b8428cb896bd72b11eb0a3663edb264cf6694ac2", + "slot": "12856210" + }, + { + "pubkey": "0x87e69a330548b427ae5e03676e5b580a2cb1032d179d8dc895752f992c23fb544c0ac46dabe3c6bf77568edf9a1bbd5b", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa3603bff791b8a11b1eef2ad8143490e2386139a1d7d6cf518cfc0f30d9dd67649fc365a0f0bf83f1821ac0cc7802e1a084467b38d396483e6d3cd94b3fe49cd5da39e0c14e9bbce9e0c2f4f1fd6e50bd8ac7a054e797d99a72f5b1295235c17", + "slot": "12856210" + }, + { + "pubkey": "0xafd0a2e561e287e391f83eba5e89a76bdb0f59559fe0c938195f34810f495b910885222cd467780032b0dc07ff86be11", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x80e31ad4f1d755a9261a68588d2e85d654d3768e5739112feae7ea1b8fd73310f09e84f5e1c588cda7aca7acdab91c440081d68fcf1667d4b95d838d3ea21f20fbd7cbf562285decf76269421f8f9f13aadf0c85646c879daf8f39cac0dc2841", + "slot": "12856210" + }, + { + "pubkey": "0xa7b4cf31911b0a8ff97906b80949346ce75b667ae8ab6d08ed21ef3cf7532c5a346086152c9b4c266eac983a7580d807", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x84d8d988d0090353c52a48a0cb520945ac5246947adb87693506e42ed05a298bac674286d2f6affa4114bc03536b10fe10e1a16bdbc52b5d3344bb7fb428430c5b2f71e9ca80e0d0e8a7868491b308ec32b6119cb463ca035dcfb37945385fc0", + "slot": "12856210" + }, + { + "pubkey": "0xa16da7696868eef5ed4238f88d38744fbc6002184d3191247f59dec9fe5657f28250a90bab46553aca0dcef4eb49d37b", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8fcbdb0e48c56183b16dbb58be90bbab2bf0cf37a0c46344c4793cf1a5765f8c6440a3255355a3c763678565c51e61b806ac084c20f68a09cc3f8812f18ef1be5f340f9906f92771fe0e8cd56e09af175b388cf19186e34c6c0ffbc2435064ec", + "slot": "12856210" + }, + { + "pubkey": "0x95478c14d3e838f4c5823288ca7b34776b93de2b7754eeb41a2f418834bd1213b56d4fbf3f69edb088c7af03fd3af788", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xae92240a2c3d9ea8e246ce3c40430d14afdeb9d496fbcf8be5d5786537db850bfe36504cf1d51c4e5e3f8d59f330c2460a1abdc76d3c52982322f38fbe472f7ef284cb0c04d1c9261dce432844f03b53d6f487d4d8b5b876436f4307842ec7a1", + "slot": "12856210" + }, + { + "pubkey": "0xa364bc99934e2f07f1be3ef13c9f2109816fcd1c05b76287e4888237312f5c97794d6b75045bc5532e02b64287925ed0", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x99e0f7b0b5ad661ae464c8bd1dd3d5f850518df3ae573e67b980053c27d6d2eb42ce2bd22d612fd6f9d9451b3d34ffba0e3e02021a080d2579160ca7d857f1e6d0935e3ca7760c55db092970e27bbbd1eb1ed672c33ca6ce3f5ede0566e3635d", + "slot": "12856210" + }, + { + "pubkey": "0x8a41740c4402c34511d3ed2cbefb6723985e77eec988f919d356acffefc7a664227e712f09f0d82941fee114aed370f3", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa0c83955fac2ffcd1f9ce20fcdd1361484ded55dba23b59d63427ea4396fc5acc0a87dbd72743b1ac3de40a861e3841c0431f2e295c33dd6dfc5f37838fef591d7877efdf7a0ff95882bc9a06ec046e1d510746cbe32fff9368d952c40e03fe3", + "slot": "12856210" + }, + { + "pubkey": "0xb51839b108e2d87ba7dd537d79264323249804c39fcbc289a6a308a74d1b8af5282eb8cb0926fcc35fd8ba2d96ecfc58", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8d7728cb878d6fd69bbeb0a25809cb70e6b5c4a24ce274cabfd8e57bc50a5ea1dc91a8928c1b84b9e5d3d948b1e1f50a0ab50fdc5e31cf2d04f5b02d1413dccfb54c59a8f43d63efed2dd07137e3670900be2f566458ab34fb480c0ff8d97b34", + "slot": "12856210" + }, + { + "pubkey": "0xa9e8e72e8e1ac8c041bfac604944622359d250f6ff095238f10fc1571950346938acb0a238e30dc47101e620ab7f5b3a", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb8ea7ccd0c923ea5bc1cd29a9dd32a32da5a307b256b8c6e6282adc149eeb8c249d803b82aa3774ea1fe19d8c18595f10f7727d29e8651520f0f6e9f854ee22f7ea75c4b257f8e157b8ce0aab4f702dd756a1c585d4b7c6c933339ca7d631989", + "slot": "12856210" + }, + { + "pubkey": "0xb83ccfe1a9a3727beb1f7fcd1fd6cc26b07abac3250ce91675419d232f59b7bde772136de6c447e0bb6ecd7d1745fda3", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x9217fd5d47401d8f6cc6177bba238c575c97264de03d589aec79ee2664a2cf7cc5dfef222c4d000baa519bf8e598115911fd8df9103fba9045a73ae6345c71d5fa5373882edfd3c41281e79f64f36f791af353eb72727055273116d06c047c7c", + "slot": "12856210" + }, + { + "pubkey": "0x96747ade9e515d2eadc53cdac41aa09c6913ea4e73a40b3746239672fa48811eb015e1a194e322d0389152c7ce3efc17", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8a2a3fd52de49f373dc136ea5959384117992a4f9fdbcfbd8f9dddea20195ad55b9ee1649aeaa75425e293704f54309512d862bc2f991e1dbef68edc3444e8ae24e5a9556790c540a35b20e6431812e38f31a2c25be50befee9a6cb949d21838", + "slot": "12856210" + }, + { + "pubkey": "0x97a6f2bba1687013d2f559dd451903d194601233dea377852266f6d8df1e675e8f68f83b48d60107d38cea1d31a2dbf0", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8c52ca4f82b05b5a68cd7d2b27153d8764db11a51d35f09121bb1eb2b1cae428d504b82520ba49196fe159a19722a949199787ee356466fc4d5af58338fdd031681c4c3f4e375f7fc2855dfcf08d4fcd61e6cf8281e0839261f6cf7b2a0ae7fc", + "slot": "12856210" + }, + { + "pubkey": "0xb3cef4ce62f34a40f48e678e995908eacc69da79ee7f88b506ac4a2b099f9756cde9f8532e1f0bdbe1b97b7fac052c03", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x86cf9d6a07bf1b3b699d7cb8bb8b8782ec1aa43d4a3a05d3949c0f34af4cb12aba7b73495d01951152ee529de2bec33a011457a4097524f9ccfbec3fabf1448caaf2c522d6ea1cdd339bdb370c815fb14aa72cb1265b82526df821b16993a172", + "slot": "12856210" + }, + { + "pubkey": "0xb8e8eb3914202a62197eaea71cc35aa6772225cf72b93ced7070a769a340e37a19a0f992a11b593b4d16b311cd2025b1", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x98fd97694df8f2a03ddabb9818e43942167638cefb3b61ea7d23f46363cc8dd4e6a9708dd8c0b0b0e4d118f9ae47cb190112bbdefc17584301bb902d6500dd5be3b578e2df7400ba33a573ae0206f6f8653c8e4f3e4cb3b9e01be75cebbc2589", + "slot": "12856210" + }, + { + "pubkey": "0xb738eeeabdb13c1c4fd30ca0ac288de9759eadbc50c9f54dff2a805b277cbd362079b5c535eb52b67575eb5b5b70e9c5", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa71e68f4f2dcd0edd261ae45b284d316f05047138e8a4a3485d4d61edb9230035ae8d8428da8a07d334592589b3987420b95757ac994625b0dc55c349a3a51522b707a7039c66d257202d3737461e2f99228ffce56689f4aeccefaadb82d158f", + "slot": "12856210" + }, + { + "pubkey": "0x80372df8d8dd4d0c2466d400854c421f26fe042966ef1d04540f97fc4b2c9d746f79a19cdd1b0eb2fa07944a8463ae44", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8fa7bcc95728e9ae45a3ac40a1329f2796281cc37b737e5af4100f1e65662876f304a35e6de3c5266e2f41123f424dbe021304837db5452d3e750167bae8fc11d9a730330f3fd3b9fc947ada477b8da0dbd8e22a182b7e0cc4ba5e95bb655750", + "slot": "12856210" + }, + { + "pubkey": "0x936e623d0dbf060d79f88e3632a82e280ccfef1329ebb9274e234293c6bff45b60fb6a3151c22d14c720a3dda9185e14", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x994f6c9f486544c5c12dc7389e98a24031c1b84c488e2777d9a4dc13bf79ec7cd5ae4c71eda964ae351b1e21b7c474b402da4b6e2dc6a00dbbaa4c9421e6b41466954746dec171e4d07509796c194f2ed7f8840b98ae243a71c1be01317b5d17", + "slot": "12856210" + }, + { + "pubkey": "0xab1a007b4bbed2babda52939de00e1e080bb75fab73ff49c3963b4391935d7914557c4362f39e688c4621f2ccbdeb020", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8f1aa1b654be3efd211faf72d8193d816c0222a1516d2ddd57cca011e51c9e15d65022e98ed1ede466e5579f4f2c834a133e1e2fbbbee1e6c81d99b886870e21d0beac069b7e040a8547098e599f5e2de7069f6c16aa681d154dace29abb971e", + "slot": "12856210" + }, + { + "pubkey": "0xacda7e7ed5bfb9f8dd5498bb0e1523bef26cad8a4b882a0bd64373de3b7b8d4a401db1a6b84e1a3b35e76f1a7d62e167", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb1959f5676d4e49dae92647eca7f274d4e5f6fcb1f83c125848d240aad86e27560f1ee5eb7eba331b6d7f109c9a4906b04644804b5dac5cc4b1a80cdaf5b56cc1dea5ef4bbaaf448cda39e00284b2d3f9d04bd23b7c718b9fec49c807a8d15f8", + "slot": "12856210" + }, + { + "pubkey": "0x8a122aa33472ce43b567b65db1df34f9096b78afae5c78674aa340ab7523900170b30d5fd46eb18a90e48c53e224dded", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb7835ef10b974cc043c5fea2bdf16d9dae7e4c260f1fc0f9ef6005691d6c668771c7b7553cbb1d670a5d53f63d473a2a16b0c9bdfb4ba46e863581b52d622b73934b8360e8562c70b743764d9949e7aa521eee6c86f3b87a631227e37860ef30", + "slot": "12856210" + }, + { + "pubkey": "0x93cf06514a01fd7958463594a660e9e8661a97bc7f5f32823351e128b7ff8cf4eccc2ab05a3b9c7d758e369e68a0c999", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8e3ea36be8eb58920207400f79ec3e3551a1a93da7bc3df423362f310063811fee98570e23f2157b750cae3dde67298f0aee19a34549636cd50093e296b6b416445124e55c51a65a7d42cd0e0c6611311c5c37c59f10d6083354609cecbb75bc", + "slot": "12856210" + }, + { + "pubkey": "0x9486505a2df9004c0b620e36f1cc77dac0cd2da175b65f1c581236ea80a1ea9623e8d17810031ddbedb36f662c10cafe", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x97c80a89025a94751a41ea84c7baf162e75d843f4ccd6214a421198b9a2859899d573cae76a5f291dadbeb7e1dbb34280884ee9b287599e7743c509b02d700fa6b85f042f96a5809d85506a54a6fcecf4f5eaef99d6166f1662b933ac323f1fe", + "slot": "12856210" + }, + { + "pubkey": "0x8036c19343b2dd718ae349c85d4a4099250c039e0b85817a02f130c34eb7e7f286d87d6f7a7cf3c032aecb2414f50471", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x99e7bec0d70fbdd3b7a110c9b59bf826dbe6524b18d775e37fc7103fba5769b2b699a4129930e41d48530d0a3c555dd209569c748f12a8b14b56a2e46b277613058e1e6df9450c8a6c37807b16a91ae7d94057a29758e1261f5c9e06b8fcdea1", + "slot": "12856210" + }, + { + "pubkey": "0x8645bb9fa48297c639f38e0bb962b56de597ec74b0f6d795bce001315bd45c26977ad7ba39a6ebac372755a0bffb7ba0", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x99b50c54ad04f1f5eec9de52b6f95451ab4a68e4400c9fe3563c6fe5de8e0f06741be0daa7c7819e690a32b98871391a0f2865e40604f8e356686a58d5546c42a1e89b6d2d36c3bb50f8fe73d342de3c48baf7c7400751701b2eaf503322d5f6", + "slot": "12856210" + }, + { + "pubkey": "0xa5847437aa4627437f71288afa75c1520126d3f7c726898355bc4596b216ca4f4a68e71dba5dd7b672c0467e91eaad33", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8e59b5d86b65dc0c80a83f33ca9d6e15b771538d691c98a2dcb02e4c1adc183449d9b5b9e8fe7cc83007c458cf1a13230b6d133c6d31d204f66a971785856fdc611e2e69ab7e499557fab13b3f7fc5dae83f52860ccdafeb4825df6773c9a23c", + "slot": "12856210" + }, + { + "pubkey": "0x85e485a742dd50ad3a04f1eee5f688cd5a80cc8b5415add20e32404e3e325e7d5ba9dd887b475a407572a493aeb7cd19", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb3e0cdc104438650ef9a38b4b329dd90d0eda69ccb9ba2504aef7a98b112f5ef98458aa526e163fafc717efa5ab2fca4154760432db371d305952b688821f9891a95d89bf1b98170c836e0bccb81ad547ad449b597cccb7229c73164fb01a25c", + "slot": "12856210" + }, + { + "pubkey": "0xa7d5acedb0f088041dadb1a811a9d77a4c0c14b5944887ee22b41107df8c3f3b0aa56a1035d0587b1506e42558b459ca", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb69836a695ac2001d3cd24aae2550b785ac869600c8bccfb860aff7b7884819ce177053bdc055a4dbb41fb8ffa344178170d7d54f477e039432e2783a7910f9b1b11053548c2499c9001949ac201d47bfcf16d8150f297b2f01d7a27f6260519", + "slot": "12856210" + }, + { + "pubkey": "0x930242a18d1d9ad02a3c1b099287152e18372fa9c9c101d19f35390c169c3788c9e2c60ded8ecccaeceab807bf56e822", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xabd115c7f9cde18ca4477ac82e9ec409e37015ceb042875a5e4d10e27d4f8b993a19a7d9aac319166dade05349c1d22f14f74099d9235ed652f01d1bbabfede7d1837249881b830969bbca7b1ec7f605aa5af6b069ea5302241a37418999844c", + "slot": "12856210" + }, + { + "pubkey": "0xb8efee1271569e89962e653e003746c2e3f7dfa236f35cd2c840f0f260d7b986cd01345c90b280551919b56a789b4016", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb3dc3f435830d3f14ddd8ec082d07737c05c7b73a5280cc8c14ba8feb4947c35ad55510496071f880b7d4549d7438acd0110d618eeb45cb094aac582ea16840c0aed08b5f227a6092ec30542976ad6f165d3f3c5b825f2a95f98fc75b90d9d8d", + "slot": "12856210" + }, + { + "pubkey": "0x87bf6cb13e9c43f5520ba8317dbe60b5b2f7b38ee026b4cfe9c411b1e8e93c3d78697291559293c445dea403e3b75fb5", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa3a0cfc64e56299b8efa1b3feedf06b458cdb1b84048dcf7b35761cf280fcbb0ea6fefded9d2535227106860aebf6b971716fb544945a398a4ef4d2acdfa6b3f136e968be6d16a8549ec0ed744cd7d052c9036b9113f0d759314307863556cf0", + "slot": "12856215" + }, + { + "pubkey": "0xb9a1c45f2ce5a8d994373b106f4c4a208c3a14ddfe1fe1f421c15c1bb3bc02c82c685093cba848d80ce302a4496d2103", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x93224d27e9a4f02c95826f73b40c8273595b786b52bef91e9ae5a2186a2fe7f2a739eb8bd5d07355d2c5a5ba3c5abf98000b358d5eb58f8a0b14195be576d0dc1e08344f967d8f3bcba75b0eb81ab7970a18103b6ffb184a130780ff84b18006", + "slot": "12856215" + }, + { + "pubkey": "0xb07f002690d88901475eac9da96321a2d8be0800d8e96ee299c1e109409fa224dd43cf0f50565ea1c498c0767953974c", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xaa4c6bcab4d480e4ff4efaaa7c464f8ef70e0f271c9ca31cf54000a46b30d36a6f6d35c8e2d00d2d30ab99f47c3a46100e39670e1e2e88729a3ddea944021381fc5dde56bc8d8176d237d8c9d7f725170d61b1f6110ebe425a92d56506790c5f", + "slot": "12856215" + }, + { + "pubkey": "0xb7615a6ec593353a58b8835aa1c4162fbca57301d109d428f51a07b5d57f55072923bc87f6bf59df92a5115f76275e28", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8c5ff41a7c6f5d2287851782459b99131d93cea80e13cf7c8605d581180a1d331e70ec74431dd7f7496c32e9e256098c14565958541d23fcc34a8e8b2885997edb4745a745cd6e4ded5985bc06852e27d2a8a206d743114caff86a230744771b", + "slot": "12856215" + }, + { + "pubkey": "0x8257833ebee325e77edbc9c4f395958ea85bee155f173b3190511e4d00cd9216e7b45005025eed2443da6c3890674710", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb585b93b001434b531d3d549964b4a08db9957a77d81549ef6885edf8fe6c73568cc8d4dbcbb96cc5d514eacb6e588e105baeea342af0084c1c8753d6266ac4356eaaa3318f21e0f84d1b64c8f8b65e70f04c699339dd1c7a4135f51e37d25de", + "slot": "12856215" + }, + { + "pubkey": "0x82261f6b4cb421539499ccc5d5549ccf33cdcd9a965cecc301b55f88a865c7c2c581e3c8263b12a1711de9998f497c92", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb74d8f34f30508e66032a403e2d15c874c2e9279a2a6366c9ba1a152f646bdd12cf54c9c9f193cd4c459f7945b7e62ae0cc4005fdbff00501681d26f54e450a2f92c099897bdf212313fca916401559908a845bea8ad1e8a9281e906956449a9", + "slot": "12856215" + }, + { + "pubkey": "0x809add83998be0abe104b0c28d1dd987864321c81004c1c60b91f1961e29fb3f3a3ecd11d7cf594f037cda5ba709196d", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8d1f16145a8d12951866273a8174e09ef14daa80972799b1fb84e3242fd64dd29f1fa885de063a74ace02d50665c559c063f07d15461be1762a331df5624534ffd02a6f2575a187ca9ed0a6a8d6e1a168886f3835823f76a8c3daf2f258fc476", + "slot": "12856215" + }, + { + "pubkey": "0xb0ce556bef2e74033273dc03cd1ea2547bc2c7faf3560601581f990a8743af38784bd7d3257ae0d0cdd2a42d4e8b215f", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x91129d101601f3a1dff99ac35f9888bb37813d1a43108262f55d25566e2352ce65341b0cad71536ee95eac5203ff2b4f0dbd0d76681ddf1da913112dc461a08950d284ef82f20d3ba15f3a26d22cee12054c3f1c73036dc605f04c989e385387", + "slot": "12856215" + }, + { + "pubkey": "0xb36a37f07baec803c9b5719d92b98f989d23eb15772fa7b2347dd5208fe472c2b053b71c6a7cc2f9c89969edd1784425", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x85cc75e70094608ea557fe265ca0e6658e8d392238fef1fa43062e63caf595769d12bef8b6c6cd0ca8200ea2d4f7fbe70d2b810a66fb322af7a5e1caf6f3da4166adc8d50a6f628aedd45b7fc5e5314f65d745d48727c2a54116fb7b8a314b4a", + "slot": "12856215" + }, + { + "pubkey": "0x8c4169e4d3979555f30f803d57f62770c95745fe41e581ced32e205a0707a4eb987d827f24370461e2eee10be1edd364", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa3f73bc17b192ef8904c8b9b81de9ebcfb186c3929dfb389bdcf8b9c2c1602e530c97d3bb521714ece2994de922b4e95122767d216c5e5400cf44c2e91ce0a65a5691fc3a13603509756f081acc482a7d379311cdc644f78b6a1e38c32fd3487", + "slot": "12856215" + }, + { + "pubkey": "0xb832f785db4646e637478c2b5e62fb4148d8aa91ac9084ec81184b9a255e2bca7ada8a27f6bf6ee48088a97cef620520", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa44a1c4521e85b689a950ba2c4bd82110703bc788bd087ccf4b3f53d0b04a64d437a5fce56a2a1d6ba42f30dc6548e0f0303f5fca2eb2223d3147203d8aff05f2fbcb377a283f60a097bfcc94c76a1f780e7ba098730119fe14d4ea2ce84b857", + "slot": "12856215" + }, + { + "pubkey": "0x975bd491cd25bd383c89c518d89485c9f8fdcf9c909770c183ba3dccce3f7e887a67b2efbd648fc11854625938eac7e4", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xa05dd53b8b0eb5452dcd30a25b8be703293e1146f7fb54f60747f814cd34f91fd8479cbeced29dfdaebb56fcfd52f13b0893f308bd4b0aef84c10c13b1eb7eda3c242521d956b577613d39636c9fc54ead50e1c8993eedce7e517acbe1460051", + "slot": "12856215" + }, + { + "pubkey": "0x95c27383e5e6fcffa31bca8e6046ae5d78a8c45ce792d2031728d07ca7d9a59c1d25e9d610b1a0d22f8ad0bbaca25360", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8613b76d17184e15dccec5e85ff731d96bceb05445a5605140fa3fd1878c47f4fd15a1b990d34abf777329ee269a3e2318a5752e933cb3493f0631793dbdeaedefc8727b08d0c07be348b0fd9599f6508f128789497815338ee039f93650a796", + "slot": "12856215" + }, + { + "pubkey": "0xadce47d94ec02e9b794acd00f8e0b7afe53174fd06918837e6a2f91ef306bb92222f30e3b583407e8c556279c094ba40", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x8fc701bbc42e4f306a5fa0a4f03e9d7ccd2c403d953ba6d23224ff7f1e12e67607641fdf90f99a1e0b4a52779478830f18122546f2dc56f30805fdcfdc83bc181eb4908cab212519d2ed5f5d5884ece10dd8c74a9b7f1e151e8a0d3d6f2d1518", + "slot": "12856215" + }, + { + "pubkey": "0xa51e1683d2163b2cce0e51d1f41c3e64f1a948aa0dbc0133dbd602a538bcd8354c2a3f39af5f75931966b393976236c1", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0x90d4ef01b07eeef1cae472b07222e1d9da53cf91e3534caba90c51b9240c77d7866913825f124230c5364d3ebbd03d330b8579030a27e6a4760c19d1de6afbae053a6ac2aebca9e50d4c129679987ff6d07b53a75f6ecbdb34aae8e39a9f2461", + "slot": "12856215" + }, + { + "pubkey": "0x849e5c60a1688cd42758ea9119c108e4c9e3063d43e8107206b347ee13bdf87c4f13c1604044a9ffed2110a68f6f28b8", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xab7d3209425afcfc7baafea06916f293d06a41884cee14e26595fd1bb84f814f617b2fa0b34cdfb990b64412f89f5cfc19ea51cdf267e88b500c45636603d5efeb29a697094ddcec84165dec4fa8457a3c4706c061e629a68cb2cb5c91b7c8b9", + "slot": "12856215" + }, + { + "pubkey": "0xb1e461e448c80cd4fd8647cb6e2e54c1350841dfa41a758b71a9da48b3c115a660707ac1fadb5ee262fee6ca95295a33", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xaccc3135d025d7bf7cb101f6323281b44dbd0271845b18642bb1941f544c42d62c78d7a4bfd34f97b36b3c6f8308aa4406538f3c890eb56e72232a47c5f667f0e753e5ea36417de510f4392ef60cb9aaf368b75b3b231dd633938b3e479c7eb4", + "slot": "12856215" + }, + { + "pubkey": "0xa54e6cd6b246be9563cc2bb934825ca6e63974c53e9eb2104d79c3829c52126272da15edbb4dff8c7f2f6cbe16cc4f67", + "withdrawal_credentials": "0x01000000000000000000000093dbf616676d22ebd0db51c5ebb6f69ea1f9bc8b", + "amount": "32000000000", + "signature": "0xb859e3a6785da7cec6f7a82e1efac7d754ec831ec1d3c5845bd7c9be5432544a2f02eec9eaa8220c5c85704ef45e33a50d3fee094007189e4d74f1bbba5584dbeb97c09bcba624e63a1a43818f16cdd3f6fb4647197cfdf57f76490c3cf36ff5", + "slot": "12856215" + }, + { + "pubkey": "0xb4850d68f469a941ae66ec2561bbe03d6cd659c7c2f1f2fb09bc6ae6976bf88a9bccf24ff297b07080c362c09a609420", + "withdrawal_credentials": "0x0100000000000000000000001e2ff9ae8b6a09c0f5c225a77df6c89dc1329ea9", + "amount": "32000000000", + "signature": "0x8420c2bce8de737f0bd5dfa2578fef822fdebffe0d1f5976aec07d26a4063d89858e79299c57b543317e2e509d8e58a716d084f33c935cf5b16ed0f83e64dd20855fd0f481d650282180ad460986939596dd949b34cb8ee0bfc6e4a8f30fc493", + "slot": "12856290" + }, + { + "pubkey": "0xafc52dadc0b1d7b1cfcf0da1a23a29ce963b61ef92d4c743e2c2118e56dbfc440a79eb743cf9daad9f7a4f6b102c75f0", + "withdrawal_credentials": "0x010000000000000000000000ce0ac4810b3f33598e51526d4c0e4cf5d05a3c7e", + "amount": "32000000000", + "signature": "0x97b76fe5871fe6789e9f1296af5e96812d1fd4414630be3fb3b357a86b5581ac983578ab8b54c0d5aacca14e46044eb21441ada661fead336f64ce060b73c756a6ee863f35f29d2b479ce4b6b42beb9cd102679e99fa363a337446cff20e386f", + "slot": "12856401" + }, + { + "pubkey": "0x82ca83229708e52fc3629246cf8e568f2ddb48be796d9207a3c229b35c5d7480bae2eee4ede61ee0198a8072d6915df9", + "withdrawal_credentials": "0x0100000000000000000000007a9875844666f18110880b67175d301b93ab8c14", + "amount": "32000000000", + "signature": "0x80835e6bcaf5f4a62c6411e02cbe99a3bd23a1ed0bcaaef49ad2f1b7744a77163104200880c4cf2cbc7aa4ffaca607c3169b57b78c0ff6a4df90d6b66f0099907d460cb7b54a6181da3f14389787d857023e4c2e7e405c65ac54ce79aa469661", + "slot": "12856429" + }, + { + "pubkey": "0x915e0746140031d412ca24ac081c067e940b4409986233c44b5550474eb594268e5c7b7d8f186aa66d75de49b67b3ac9", + "withdrawal_credentials": "0x0100000000000000000000007a9875844666f18110880b67175d301b93ab8c14", + "amount": "32000000000", + "signature": "0x94533611375705834518e64cd602ad116a64f1b5fe0c87eb83ede1323b436acf522a99e572ca40e01b693deb3f85e4f808f1da9eda4bc3f731db12714326538627c169a675ccef134818e60f2c935a6625d01ba05d5132d1fed8c873bd1d3454", + "slot": "12856429" + }, + { + "pubkey": "0xb539939eef426ff30675421e20be69bbb6dde2720e540f57208d2ae2e248bd7e584cc49b623f186f0ebdff9856be75bc", + "withdrawal_credentials": "0x0100000000000000000000007a9875844666f18110880b67175d301b93ab8c14", + "amount": "32000000000", + "signature": "0xa009e95ee7afd88f26a954a7f32a7aede9bd26035b0a83f1203a737e762b7a5d57b7d14f8fa4b8b5b13bb5cc590e0df215490f8e4e47d8ce9419866311288578396377f0803f9b75af574b2a6720f346f16e99d59bf80c08c3381822bfa11e5c", + "slot": "12856429" + }, + { + "pubkey": "0xb547fcd772a7010143d7e1febe6924de32972bd23bfe3ab0bc0f0230d865887c283641477c686925904de219f7076814", + "withdrawal_credentials": "0x0100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "32000000000", + "signature": "0xa74b1dc311264f4f5448a1a24f069632cdc2d9a8972564d3b1361a5fba2a2fd857176542493b3d138c2a57c1695aaa5919a070c208ac02da6e71db92a5200fd1090f75a2761df85a605f95f315a916301cbaa598272a96a8c92fb8f4e9141d5d", + "slot": "12856578" + }, + { + "pubkey": "0xa07ec9f54e3e9a88bf867c9e99dab32445018ec59aa5c3d381ee78d0dad122b0ee459316e02fc4d853be7e90d171966d", + "withdrawal_credentials": "0x010000000000000000000000f7c846a539c47fd36ee892fbf965c7951a80db88", + "amount": "32000000000", + "signature": "0xad65702fdd1cc9114dfc6ecabb307b5a1c24e1ed91203a4b7b099e529e5cea2771fcb7128d8f82639494b33daa0e45440b5bc85f115feef01b7106da4fe09a21cad7aa309dad0c4b728f09a6809441f5a7f9032126121fa5fdcbcd6148624394", + "slot": "12856742" + }, + { + "pubkey": "0x90147d9b0b8bf6f9258d45ba26e0a786abd2d1d402d8d27a0c60a1574dfef2e9f87a63027ecfe49f3b595014f98fb8d9", + "withdrawal_credentials": "0x010000000000000000000000f7c846a539c47fd36ee892fbf965c7951a80db88", + "amount": "32000000000", + "signature": "0xac5a59b91df6b9b07c4c9156e8dfb086070852f85cab5db5742648ce92e023cd03d2bc06349a0c775e44c3ffec180080090c43ae2d4435b1ff48710ba2c20b12c3e47cd4dd8e99424ed9b1e4dcb2c5ba4c3675e908a6d8d94178ad37cc5fdb38", + "slot": "12856742" + }, + { + "pubkey": "0x8cfa4211afa2cad3796c58c6964ad53562c70266070dd01663a52d5bcb3e843e677816411c1602e4b577e5237c001e2a", + "withdrawal_credentials": "0x010000000000000000000000f7c846a539c47fd36ee892fbf965c7951a80db88", + "amount": "32000000000", + "signature": "0xa5f5a14351fdcd689164f7935c209b67ff8dee377ecec816455dfe066b2039d224434305ceebde85d2126ef3b06ea2680592bb3a729b14e729281773bf089eb988c0978ae3536bb8f47742d18306f035be94ca6eec28bf9dd6bb131485bc0762", + "slot": "12856742" + }, + { + "pubkey": "0xa270a6b708515461e0ca0382823c9abc74e21ca68ab07d582e89787b5b47e1ef7e9f4cc27730646b4f035c7f07a336b1", + "withdrawal_credentials": "0x020000000000000000000000d5f8c864f21824daf3a4b22d9c33693642ed7634", + "amount": "790000000000", + "signature": "0x8e849744cab60ade3af57080f41c844e45a095328f90acb9a7cda25f352c28dede1adacb5726e116c87edee394f8b998145b4e934714e9d654e1ff624862d37d7d45d957ba3aa39aa5e5c4a0f889b96700f5d7d141ab85e8bc492ef6c4a253e7", + "slot": "12856760" + }, + { + "pubkey": "0xb2757152e50adc9ed7e462b2ff8b6035b313d8bea5f2397071f2eb19a0203e4babfcdfac4633f60b787579201568ce52", + "withdrawal_credentials": "0x0100000000000000000000003859a77228139a72d0a088f5d39144d558b01fd4", + "amount": "32000000000", + "signature": "0xb769df3e42227ae1610dfb78728051cfb90aaf6e13db33c4970f3b501459da64854cbc1eea2e63ac44cf5d5d9eeb8d0a07bccff98c4c05ae712e153fb5fa2e4a3622072e3eb99183541103ae6b03bacc39b49e41175017952bbe40fd0c2138fa", + "slot": "12857130" + }, + { + "pubkey": "0x82fd4c363fcdfd25fe6ab0e940877869787431898ee33155f8efac26a0ed987c2d9f82e8746a93b99532b9f44b1918c8", + "withdrawal_credentials": "0x0100000000000000000000003859a77228139a72d0a088f5d39144d558b01fd4", + "amount": "32000000000", + "signature": "0x8550790c526b4c35ed83692e0b22d1187ba07f761955181a96adb8f556239126d8a0b1a3a400a1e6f35899288b999e9a00454becbbe24e4b88c10439b60c28dc6d80fddc105fc6d27f0bd854e287c4ff88b8b18367b1f8c339975f7e062a2767", + "slot": "12857130" + }, + { + "pubkey": "0x9696cff857379b6def54041a0413a04dd1454560250ccf489c0b52d47474bd5f3b420240b7af010ef39b57c85c5f1eb9", + "withdrawal_credentials": "0x0100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "32000000000", + "signature": "0xb994e2029bab315350af2e4f2a257bb585f9c5b763a2f03e911d900ea6c92d670e7a0cb31654f21e649c907f1ce4719b0fa8bfbe3496782bae9bc6138cde6d09569e90a3af65da688642a26b2800179c48cf95ad38e93ebde30f0568ad59d581", + "slot": "12857178" + }, + { + "pubkey": "0xb4a21842ab89fd5c0a6000ec67c7388a438d3ab05c86963c383ad77c53771cf0ca9b103f37e94737594970564207ca72", + "withdrawal_credentials": "0x0100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "32000000000", + "signature": "0xa7039f6fe93242f425c5e386a858cd6f853e0cb41d1e314a5b614545f8a19a8cb495cb7856a4ff55af5f66062dd56c6800c623b3d0d8763bb335c620f8a7f715b020e3fca8f6f7f3892f3765405abff262ff336cd3dacc5c4ff21afaf55b04dd", + "slot": "12857180" + }, + { + "pubkey": "0x8404bb5e17cddcb283188a6ab8840a24fea1196464a8f27db1e48528de3d1a238fb39f723fbbb12135f44206c58775f3", + "withdrawal_credentials": "0x010000000000000000000000ae3fadd231c8fb2b2c756987ecb582963405d018", + "amount": "32000000000", + "signature": "0xb010c8177e75809ae5f965720673ad58d500500472d5cc6f6e1188c975ec35d48cb0d2da5bf2c6f621576e42075d978e0e3dfcc8eb4d00dc3d7d5bbe8d013659d1a157f70ca9af5ccc5d21a435d080ec6c86fa3f6a633aa7bb4c684c7d3af4fe", + "slot": "12857180" + }, + { + "pubkey": "0x8ce9f67daae228dd52084c91b9c23e29be0c1591315591e269fba4a54560c1d0ba1188f8d9ae47216e38694e9270c6a3", + "withdrawal_credentials": "0x010000000000000000000000ae3fadd231c8fb2b2c756987ecb582963405d018", + "amount": "32000000000", + "signature": "0x906f5f47bcf45d46d07d50a67fb7181471cbb3ccccd0b1974c67966bfbbf535fdc3b0c7a6003682dcff085b420a5f2590e82297046e442eae46646c049c67c3d4f14c9bd2df687ce37ec6cbfdaa1af81e604c85333468794a466753f7a8a0f6f", + "slot": "12857180" + }, + { + "pubkey": "0xb7ace6ee6ac3ae62a38a638fbb989027f7c6cb8c952ce2ad6e19a22ad40f1c1cdea9acc102bfaa7118f971aa508d0682", + "withdrawal_credentials": "0x0100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "32000000000", + "signature": "0xa894149a6d8ab56a76de998c28c6292af70826ad5e61d352e57556b5771bddc350050e647c9d2a8127ea090841a31a350b1577f8a21d977bbed024956d0ccdcea5546b685c9602041cad2a9755bef3eaa07a02c31ca51b7a650eebf31d69ecf8", + "slot": "12857181" + }, + { + "pubkey": "0xb9909f3a4ffbf21a2ff8306464087a330156669f25190c0428f60d1e3513fd8aad7dd4ed87d212c224123e5536a067b3", + "withdrawal_credentials": "0x01000000000000000000000008ec37e2eb451ab6fb29fc14d215b0aeef170040", + "amount": "32000000000", + "signature": "0xb200f186a4246299359e754d3ddbf006b37475f33af6174e8361dbac315e435926a298c322e82d4a05c9c0b4880ade8c0087fa69063371acfd5d0b2c87f89776246f36bc664c06cd7afdf6d32ca05300bd98a3517f5f215d7c29fa156a0644ea", + "slot": "12857459" + }, + { + "pubkey": "0xa2d13b6b7d531180d57a00d2028811a4900f84f929cdbf9b2393054d84714d3d0c67d0e6fa3d2045d76bfaf330927772", + "withdrawal_credentials": "0x010000000000000000000000973846d98a952745f2e2113a76c5d62ae04e1eaf", + "amount": "32000000000", + "signature": "0xac317b7ffcfcdd2c47ea66ab5ed6949bec8d581482428577956367e907e22a4c2ed842801fd52a682c0a701a6b9554110d52e016d909672770fb48146d0d853c4e48e625c3b3f90e4da0ea70e45dd18f5d7a86a381f6e4c44dcba952495ae10a", + "slot": "12857604" + }, + { + "pubkey": "0x97b8f02d9201fe34b3dc50f37ebdfe9fb7101c4228db92ee85e11d149b2098bd5916ff37e3432d68872dd79f34879745", + "withdrawal_credentials": "0x010000000000000000000000fdc6a627675a079f2ad446213d896ea6e60192a5", + "amount": "32000000000", + "signature": "0x8664ceb5d063a6410537fc861b32df0614163fb37b81b8ebe1d6b09b92f608068bf1140808d77807ca4f80d4aa2d12a1003867ae3c39b1288b30c67a3ae94ca7d3e307ad46d3d05c353a51c95b8fcadc480b5d51ea2ddb860b3fe6ba64a10dcd", + "slot": "12857730" + }, + { + "pubkey": "0xb4a5bb2ad13a539900708c4387cda96820acdcea1bd30cfe6364d77f48c556d4fe75b9f77376792a2ba42e3d42f3d2b1", + "withdrawal_credentials": "0x0100000000000000000000007e2a2fa2a064f693f0a55c5639476d913ff12d05", + "amount": "32000000000", + "signature": "0x83ff63e3c08c36c6b71b84b74e86fe95881a31d57e06daa314f6d832a4bb60e44125a46b8b02faf02249e4b397e2e97a09cd6ce8ff549b32270e715491a854e709fdaf2150f50d0c1f8f90e22e1373f29a16b30bee9111316739a387576df146", + "slot": "12857778" + }, + { + "pubkey": "0xa5d6e506ba2fbe1f8928f2348008bb50f6151c42d28ef079ad3f3a551c3b7ece2b28def0675855fd3123d7bd8da51604", + "withdrawal_credentials": "0x0200000000000000000000005625e24bad577664619eb2a4e06fb41c9594d181", + "amount": "33500000000", + "signature": "0xa12a97288e38f4f4b30decbfb6b1da043d924c28e80f8e4c48d4010c508d9ab39ee8e5fa051db72102f709a4f9b1c7ca087851cfcfe278a80b78f8a3753d3d9f72fc39020dc288774582df6ba3f609cecd6ac4f44ef48da3464f01497a39ca61", + "slot": "12857887" + }, + { + "pubkey": "0xa50741c4c79246391747f30a7676625576813604ad55d376439efc3da3b1b0e4591fc16edb5e11053dd65e2e937120d9", + "withdrawal_credentials": "0x020000000000000000000000a7b47c53a52b3bb7cf1cca8c07d715ed8de68744", + "amount": "1000000000000", + "signature": "0xad794d0286bebb7e38d04af38afadb0707c0b8ef4064e7ebc6f5391bef18f3dfb41440e9f5790ab61bb8b4e17b49303e1147574c3e101c6d7f5cdaaeabb06904098bfe141219c4818d7154bdee3ca45e3dd4dcc13bba9809de69ff45fc2a50e7", + "slot": "12858026" + }, + { + "pubkey": "0xb3e43495e7a5e2245cde62c1ab3e64e2f19d82f484ad015ad2737544fb34625582960159673d4f7a3a936711ecd7cb40", + "withdrawal_credentials": "0x020000000000000000000000a7b47c53a52b3bb7cf1cca8c07d715ed8de68744", + "amount": "1000000000000", + "signature": "0x822b9c15a0e343aadf2a4e22034a338282703791b6934e206ed5db3df234844dff305fe0b520d762302f6c2425cc823b199ea00091609ec3d4f8cb07564b48c6ebc822fa884cb2b2801f7086aeb7c40d8c028e5b3fe8b9e228af9cd17f844f54", + "slot": "12858042" + }, + { + "pubkey": "0x852220b5225f7b29115f7e4565cac1842a2a981d3d050f7e48a1e10d3286e4f868cb064403e328be9e94bc14e9c25bd1", + "withdrawal_credentials": "0x020000000000000000000000a7b47c53a52b3bb7cf1cca8c07d715ed8de68744", + "amount": "1000000000000", + "signature": "0xb0eb5c234a0274f9855c7ff7b6c319a11b2bbb9798d2b96c6f7ae01c3cb5338d4e2031359af4d8b0b4771009848d52710b4334a3b6c94278e320c8557a66da19d2b3662300016bfddb898803a55fabf235d01363faead05d0efa991092c0331a", + "slot": "12858062" + }, + { + "pubkey": "0x8fbb1e02ef434ee62613cb49307c7f3e529e4fa714d15e8035676d7e7e906b4096d29b259ee7223987dadfc930f8e3c6", + "withdrawal_credentials": "0x020000000000000000000000d19104593d0395080d901cc74822807c9108aafb", + "amount": "100000000000", + "signature": "0xa554398aadf91a18ace8f6744c349d778e2ca14f61b0b9058848bf0abc99ac20b852d7c90e7b422674f4fe88576ecc430ff6be9dbbe97537c57920a5541a0b41277f3a394f325347feb484f2c92176f464b343e48bb498a5743176a123ad56b0", + "slot": "12858080" + }, + { + "pubkey": "0xa7cb6e9281d3c6d10df7afdba97c2bb5be5570a5e27e4aabaf379a378202c863668976994b66643b57342fc3d4d9f1ef", + "withdrawal_credentials": "0x020000000000000000000000d19104593d0395080d901cc74822807c9108aafb", + "amount": "1000000000000", + "signature": "0xb12807ad567f27751af9af57d8d9051d603eab6f537381fe451860d054b9c08b5d3228e4cc52ed75744bc3d21b9f2a8608f00fc2da134afdd2a445c4dba178e217b7c2817017a6303eed76eae0889a73fc241e6c6ab693e9c1067bc0398ac32e", + "slot": "12858088" + }, + { + "pubkey": "0x95325a9148b32f6865d8978487017c56877add04978377c8c5031de18de7e3ec431ce65fa21c2f13fb613b7c906db917", + "withdrawal_credentials": "0x0100000000000000000000002b78035514401ed1592eb691b8673a93edf97470", + "amount": "32000000000", + "signature": "0x8864a477ddc9fc9f3a973b5b4a0924f767f34dc071de8b62b16e061d01a41948b6c58a871674c5592e77a3bd1cceba07115cde2fe0781506a32d194c72e3ecbe9e77d5349b2cbcdc8c0520645138c32eb3adc04f0325c00a22e08cdbd1ff44e1", + "slot": "12858114" + }, + { + "pubkey": "0xa96978e7e6d31f7f6d1ea665219180cd690c10701845867cd078ba936eb6e7a0a00ed6c23ee3ca79858332f97ac4c052", + "withdrawal_credentials": "0x0100000000000000000000003add198d8df24c62e8aad760d51a020bc2d6dca5", + "amount": "32000000000", + "signature": "0x92a06005a12adb2b8d368847ecd7e143d3dffd30096c797ce131ac5363d2f372ca0a49fabaac9ca437064f8258769b270b21b7d48d1b91865530298a0e67e7371557ce7c9229b85409d01f8ffeb380ab46a892a740737a78f4644ac831c913b1", + "slot": "12858154" + }, + { + "pubkey": "0xb108e36675d1a5f2ed68eeedb27c8f5a3cc472981c3fa4e943cf2a4c74e7eb2e323a8f81f384e39ba65e2cb9b24dffd2", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xb406d5a487bd0d9cf41e0cef1e1107b3e3fca65e1e84a33790e13214e6e85c4ca901447a6d11716a8456b0ade9b69852081356d6ce46e99dc23dc04b7e3750a74dc127e8778c761fdc9af97a2e739a64045db0cbf7465e8c79875d945c12ac7b", + "slot": "12858172" + }, + { + "pubkey": "0x8716e7c4a87bdd3616305b6d022983faa4817134f0df1cdbf240e162f1a3c92459f3cb9a23e443f0b6bd3a4df2b03ab0", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x9692e57f1b04e2690f6a32a051aad2b7c813c770d2322b5b4608ecb7ff236f25ef282ea19554349738a829e826af3e8919ded48d3777f55ac0e6219ca8c73a9e712c2addbb3a223e14312bdc6ef5ec803b63b94bf8a03200ba89146a820787eb", + "slot": "12858172" + }, + { + "pubkey": "0xa1f7e8bdc323d0a692d3396a0243987fa12a56e8031df3775be2451f1409cb720c16e4151dfa3a3d7f7dac3227bc62fe", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xa0109be9b40835ea2b24e712b8a97f1bede3630b5757a966e93854d4e2f922f411f111565a474c80dd6b9c1a0e6aaf561869096bc68f1c81a5bf995606634531afd419b33e5e70fba6dae9d4c442beeb5ae11ed33beff84cbe8459191296186f", + "slot": "12858172" + }, + { + "pubkey": "0xb2e222888b54cac44f513e5ea3a2779a85990dfdbbab004bef0bd629f7e10c571ee6d676786c33fb19abcea58657639b", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xb55ab1c02a910ff479c69d2cee6e583cb70cb6636d5baeda24cd3facdf4cc318abd524d7009f889d5cd2a137e79c2d9604ed3f8900a8af1b811363a66c508a08d3a78b599a1dead2bffd159d66958183ec75dc7e009372faeaca91c465a75e05", + "slot": "12858172" + }, + { + "pubkey": "0xb57e4eb39f878e1c9ea29652a1c389b7b946e7921feb6cd40eff709b54baf4853185f2c0f8f7d962c685e46a7aae2580", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x970d7a6cfe2066e6f84b660f316accd03e762f4dc64746f4e7176121259f2799626dfc023aaaa03861004c55feedf0cb044d5c632938ed5d512ec7b67533f09e5a9507669d279d287ba9d04df899d200708183beb6cd2c87ca3f18a4d0c5fb71", + "slot": "12858172" + }, + { + "pubkey": "0x87a41076c865bb81f890208d5dbdf074c32bf549879b83286a55ec2ca491a87ce264eae990e1a6342b9dfdf1ae5796df", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x8c584aab6ac750554d0966e0e299d3071c54f53b2a149f2140197126218d1b228743014b16a1a9da772a4415f370e65308a67e16a18fd8c088d0570ebc81e48d17463d9164f463693e1cf1c2a04c4f0c83dc8e71874e527b9a40ce9d467d177e", + "slot": "12858172" + }, + { + "pubkey": "0x90bbe3f76d51ff3ffb6bf0047b4405b448e907e5d5929f753a8985208fda5a931e18e6e95f6d6073c3c3baf7bcb245e2", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x86a0dd7a582f8eac9a9f92260800b075620ab187357291b82b3fce8d998ed20999345baa9a11ebecba991778d91eea7910df6e59593ea159575b267259edb5921d19365aa7f17bcee8d08006ab4eab9ec25e336d3d04b49f72d63c095d068726", + "slot": "12858172" + }, + { + "pubkey": "0xb505fb7f810e1659c324c0d62c59da91b08c4e6611aa89ce0ed5d90481b7e303ab17ea224aa2f002c0db56931257f03c", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x86c1086d1147060d549b9914fa56abb167ea5c32623a730809d8fb48492652fe2d3259662907821c2404f9ae4253b2cf08f7069571120659f883b756dea9279b58367362ab501bd6b0390139618ca4486b68c0340cad263a8288e355d4fa2676", + "slot": "12858172" + }, + { + "pubkey": "0x890c9e7a547c4b6daca756426b83dd9d37ae8f2c6b37ac348cd535949c60e9f94e2b2512756e516d24533cc2d9659faf", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x868a1d8561f5cc92d87f9db16a912252fc2eb4de78c1a697e17994d555620cf1d49474a8ee967015687dfbe2369176230b0107fb992004cb08c546b644d9c1cc010cba59f2b89bc81c5bd7056c320d3d1739372aa2d69798323fd5f59f374905", + "slot": "12858172" + }, + { + "pubkey": "0xb3879c49c3817dd2f286ddd3f7e65719db81e14055f04e64867971ca3d4e0d8ba57796234eda74b3d71b04542ba32695", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x88ce91f50a52a623efbc33fd570c5ba3738d6f6077d18b0eaea7810419c657053ffe69550cfaec92a65840801c6855890dfd1090b3446b0327ea8b99b201cc73592cf1a00151163969de1fd8cbc037c540f6fa29f8afdf3a4d204a1528990473", + "slot": "12858172" + }, + { + "pubkey": "0xae1890d23fb14d15d77146de9d1b516f2df407e679508fe1c4a64d4c600dba9074b1d6879d1f4859516fc6c8fb672b3b", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x8da9515b06ce6795c22479a94beba346d0436fb4d90c37f72022af06cb2b618aba72deb90ed6a9d98d3bdc7e282a8f4415eb46a0a39da1b23eaea225bfa94db83451c909beae3dc67418619c0d643c7a473220227ea9316c7a41a27b9d16f7bb", + "slot": "12858172" + }, + { + "pubkey": "0x94bd21e50afc6d7f31c97f7a4469ef3b9d000f036fbdb37f09d93b71c2fb74e222a8103fc32a4cd25e65d7deb6de5769", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xb50341c492d700eb6d92dd8dfaa31988ceea3df6dd3ef37e10e1c5baec9d09d334205d07125461be2fa762c81ae81a020b369d09dfc276368db1bd70cdb1d11a789bb4c49ce778b18db648396638ebcdefb642ffacef12f983f5db99fad5296a", + "slot": "12858172" + }, + { + "pubkey": "0xa4a8b99c338d9f40e9d1f87c68ff32c7801260a7e5fe8095a8f2e13a6053b529659cfe8f7d127901b3ee217b4474e6da", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x90b22512eae7fb7cb5991d89f692aaaec8b4e2f466a8fa1193dc1462d0e29250d2f149cd8f343a907c90b9f5ee89d4fe1567a0ff194b6618af3dc29a6078b5e7c1aadc2617701038f6e6b70403ab8d7fb747a9d2d0ecf7d230cbc5b0de26bc2c", + "slot": "12858172" + }, + { + "pubkey": "0x99cffd44d56e15284577955fa22fdb2ab1122ea689a9935e4ca8f2f0cf18c0bca6d112fa40ba263b727d4c5a11721caf", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x86db09d98491de9344b362a8bbd26fb150f075941b758a792bd61ad7bf4d56d3d835777f01971d602ba363254961293e0c5a788eb908a708560810081f843c72431a20ed48aacdbfcc93771d0402abee54878adee8b2e405c1a161b44dca1353", + "slot": "12858172" + }, + { + "pubkey": "0x959cb37826a69e9be39d0888bcbd71ca55763190b7372a86c1528f60987066f051989858d13bd7c6a8d37170c537b11c", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xa0878d5a4e64fcedb8227ca87fef0417f74ad4cb440f6c4e9739fb6963981451e8f474fe9eb0359630f1b05623ca98630aa17492b4206786d07a77df233037023a11b1b7a36b2353cedda14937d587c79d839ec830f78d2db17f79c172f6f0ce", + "slot": "12858172" + }, + { + "pubkey": "0x9831d17c40c8a8750ce7d8434ecdc57c986d538ffeb7434b2501c23b012add50cbd35f1b6c2002ddb051095e1737449b", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x832329e0a5838164b04c91333f04c501149edcbf9911f6de88b7995c1a5897f94ecb6b6aa52768e6a20ee234d23996f5050cc470e6dc4df475013c6db9627f1edba1efd1cbfd5aab83ca3716d0f292a8166de2e1c3eefcb239a7fe606a3ff7bc", + "slot": "12858172" + }, + { + "pubkey": "0x912dd026e00b7772025defa424619e9f10ca93476ee0b606aa04bc7dba33216f5f08ee4721383539cd37bf459a604f87", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x858109c1f7307be5d8d61749f28954dffb1a7834d9d20223dc735142f1c2fd62b328fc9eb87a6438952330943b541cee15bfc131f621eb71fca4310cb21e6a1cd6b145f46646c7f675c8018654a6103553bea23e98dc814823e54d97826f3f73", + "slot": "12858172" + }, + { + "pubkey": "0xa0e7aa1f91ea63b71a8aee17d214f972b6bcfdea481e9d7031bae1bc1e7ec254a1cb63912d430d25bdb680d9758c2a9d", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x92cf3c4d1741ffa45e01514a88d216b3f643f2223f678bc3bc2f0856a64d0174a89b09865c9cc1c8e2c0aa73c3c995e60c4bed42adb28dbdfd822ac2ac505f547d8c2786926e739bc807ad2f1b3bafd41f782d5b3f7607bdd1fef81cdce600f4", + "slot": "12858172" + }, + { + "pubkey": "0xa45f2ca6be75817ce01ba33ffdada3683e5e124d9008e3ab23ebaaddef471f7903054797d52c60979568cc2e63b36dac", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xb3444fa381009ec2f115925577f634ef3e7d5f84bd0948a033bada98bbd7f3a3ad9e0787d86d82c736a97de00115f08f16a63f52c6153170e57335f75315da5eb227cc713e0d7dd43894585e4117604b7c9713625c432c214eb69cf6ab98a57b", + "slot": "12858172" + }, + { + "pubkey": "0x992b4524d572de687a90d9436439367b3ba13c525874e0abd1dac2aacdbb5a8f6bc89a3a1388cdb79158309841860862", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x970e29d44b351ad684a8987d85ef168f77b20a46a1bb864df486d483cf268cd636ede0cbaf80db6e6c8af9a2d635f4f700d240ee107625864fe0090f7f1e296a4ddb84befc532d16bb59598b6399808a78d82ed6e7f8ed982ac957ac8d8d2581", + "slot": "12858172" + }, + { + "pubkey": "0x94ee7be220d1afe707276c0f7234a9b07e60e07471d16858b27c0d96b2160a85470d17cc7a020db7226a75f0dba48e5e", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x892e9d015476f268005916321dfcb24d72d70825b208b4b7d184d05fcc8d64fbd78aebd63724550dd97bf000e9e808800a0623f5d70080a175d16fcf19d58ee2ad20f7469e7867df8ebe1ca5004409bcb52bb105c724b0f2297e646e1aa079d7", + "slot": "12858172" + }, + { + "pubkey": "0xa17729622a23ca8106f45ee50ba0bd1b6b53c2647f0e93311cfaac9f83084ce50e53cf363d9832afcd40efcb54f4c72c", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0xa9acbca0cc001a85cba8503782b82a19a220e5ebeb36992ab54b3534083bb057514056df441c1ec4664fb7597a3f4ec702f33ed25d0d742b24d7fd5eba8f9f0e024d47a616b148c4a35a52dda2753633b0ceb6baaed79507349998b9a3306b36", + "slot": "12858172" + }, + { + "pubkey": "0xb29ef61909a1a202819eac204eee1c5a0fc8f8c7c96b87f989ba36ee62c89ffffccf553fba9c7e9346b3930345958700", + "withdrawal_credentials": "0x0100000000000000000000005396a49ce15a1b1a281816b577a58fe4f87f1868", + "amount": "32000000000", + "signature": "0x8738843a869c771d0275ee5c860d29e809ceffc3f62733ee8a3769d91548f4704ffc4b30ec0fd479a9c64299aeca753b129228aa43d2aba154873579595ae62d5c53e0fcbad2b042f3f81c8d6b1a4f87705571faed5d0108fd672434e9752f1e", + "slot": "12858172" + }, + { + "pubkey": "0x86f168e0af480db07a9227d815f4b674e3adf8b76c68f978a672289de973de726c23875adb3856edac6744c2ae1a60bb", + "withdrawal_credentials": "0x0100000000000000000000003add198d8df24c62e8aad760d51a020bc2d6dca5", + "amount": "32000000000", + "signature": "0xa9c9c27520087a8e033ca797b05715a3272189fdd3e31cd91d507a0e7387a974f5392e97d7ea1c872d16da481b588f1c1696e81928145ba67f3c08703a98f89db48d72fbb04428486982c6c516faa8bf4670275d12fdf866729d5db7c8ae787c", + "slot": "12858256" + }, + { + "pubkey": "0x92e19c36be292f5f4a9c27080bdf31d0d7d796eb55e191d2d7194bf4e236923797efc8335bb770aedb73bdc6dda2a22e", + "withdrawal_credentials": "0x010000000000000000000000f9ec7bba855536be67366119742d6f3501b0db54", + "amount": "32000000000", + "signature": "0x85563e71f0b677ec48fad155aecc31dce713837b8dd8c0a5cdcfe76f62001eaa19049e92503ea2b6c220e15ec0da66eb04ef91f0ed08b78f0a5a09711aafb2190000915dfb4682b6b75f27ae112f2a81acabf5c6c09d49174d9ef972725e325f", + "slot": "12858386" + }, + { + "pubkey": "0xac17f24fe06909206015be5203268101f1e9b20ffcf793b84fe859a594d430b443608918d4c4a8ef2bbd4cfe5a6745fe", + "withdrawal_credentials": "0x010000000000000000000000f9ec7bba855536be67366119742d6f3501b0db54", + "amount": "32000000000", + "signature": "0xb9f9bd0a1875e335f5f59dcca6d994b5e4a4802d6e2c2d08f71019714f58f04ade1860b9ff5ae853f2cf3845cc605eb510316f1640eae218777b8549d4c1276ae557328f10af19cba29ef568c88d29402db032763308984aa40833f8b2e46e35", + "slot": "12858386" + }, + { + "pubkey": "0xa6b52a2c3fd063f398a9de6da1ff8d3587967c3f5f4ff30c2f53d5418cbeb087f7674b3be38109713ec31a766a476b1b", + "withdrawal_credentials": "0x010000000000000000000000f9ec7bba855536be67366119742d6f3501b0db54", + "amount": "32000000000", + "signature": "0x8a1bc453623a83e9e5c3bd6b6aafeefb65603a173accc3e8319a8a5a70a141a862ff9afcce21b27dc5327ec7bd7176ea0501e3d6351e7321b1fb0873a161a02a65898d813f7dc7126f9c707630c186724a006e4f4c4626858e97a3b04f34f1ed", + "slot": "12858386" + }, + { + "pubkey": "0xb2e9f287d9987bdc6e0a267292e1059986ca5a92c31dd1d5f4351972c95eb9390dcb7de0cf0d0c3b25bb85e588b78054", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xb2e10390ac35ecef4abbb263b25516ece7c16c4d1d39ed1d398a5f21b7e2492c9fa7c9e74bd7114c9a061442f4d52b05097bb9f9fdff5e5d1565e2e118610393c723014b4b1eb4bb17f3921bb1b26317f2715ac2757c708e98faa504aa6b9d33", + "slot": "12858393" + }, + { + "pubkey": "0xac0f0e5b12b8089b0ce44c7d76feb3c6facfea43a9faa46ddcc8b871f16f789a98dc13eb63ff3a609873d9da8cabcc10", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x95c00a812d005b34e2616214676335163c955910cdd7c81f42d8ead8c38d153f8ee813af540c3ec302dc9dad68184cbd09df9915e0f7e83f2e416b99d660aae9f72c295c48f72121f492c870e2516446640ad7849442ea37fa84c9913ee883dd", + "slot": "12858393" + }, + { + "pubkey": "0x9797b72229b0fc2cebe76e20358bf72f942dbddf2ba0ca3d969b1c44d83c4533560cab785273fda80a457e68184c2f3e", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa443c8a272cc23a05fb11c01033173543ad4ea870740f2593a5c0bd61f7ecdb1353c1f1471cbabcc8c88f0e430d5b44f07e9739fa2a71237c05455db11fcffe6457a721c037d921b02bb8486a737195d9a9b717b1cc03e0fc7337302e3bf9970", + "slot": "12858393" + }, + { + "pubkey": "0xaa08741403cc2a39d082d7036751954c38bafa049871d10678fddaf0536cef4f07dece6abf8c053a3fee4e73782a5562", + "withdrawal_credentials": "0x010000000000000000000000a658a44cc2592410b58b80cb4259a898a891304b", + "amount": "32000000000", + "signature": "0xb5a5794e1168e70db5fd9aa18ddb5c81867898b91bb5f95415be7b3074856cb1e8bebd0deefd6924ef0ff903cc6fa97b1902ccd846341d562e126cf44a7c544d010c5265ebd86ec4fe7cfeba35ee702570505c30b766e56955cf0dd1d41c539e", + "slot": "12858440" + }, + { + "pubkey": "0xb8552395e8395353c39119209f813f2d860e2d0cd5402fc72b67efb1c918ff4446fc6fe5cfe83c2ebfa7c015db6f2ebe", + "withdrawal_credentials": "0x010000000000000000000000a658a44cc2592410b58b80cb4259a898a891304b", + "amount": "32000000000", + "signature": "0xa051a4c5dc9f83291d0aa55af0cf8a2464cbdc33c6895367e3000ab0eceaac6a4012e6830aaff64e7eab3919d92a9ef303095a5f2dc9aeb0908de1bd616ebe39695a008311215ee811e0f80eedbeb271016d0a76fbcf5fcff561180ec600b26b", + "slot": "12858586" + }, + { + "pubkey": "0xb5f838bc430eb9322832a2f6c834438d4911ef71b638adfa209ef90aee090bd23ace62c39f2cc633e806027b0799e448", + "withdrawal_credentials": "0x010000000000000000000000a658a44cc2592410b58b80cb4259a898a891304b", + "amount": "32000000000", + "signature": "0xb364359758f33edd2016d64f657ff06d8b5ec49db63c1d50d77e636353da5d44d7533c4d1a6a9cc8e4fd089b75f2e83b00ce9e8d77795178a49726b5c4c9e9712dd9e18ac9fe2ce4851407d988b0f68c7f3ef1d1fed1ddab1d7159b18a0d5b0e", + "slot": "12858586" + }, + { + "pubkey": "0xa16e25a1ae74402aa34851d73bab3c2998b485808afb38351cdd9b4e804153c35c17a8448500fe356b08cc67c33db9fc", + "withdrawal_credentials": "0x010000000000000000000000a658a44cc2592410b58b80cb4259a898a891304b", + "amount": "32000000000", + "signature": "0xaada985e4131629b4c49447112755c95368f63ab1d6634dd61f7d7bcd8be6e18b6bcd694f9a583b1dd278e87f3a5be6901296831e13a52debd9863fc3d7c6bb25211f74162a3773abf488a7eddf40cbe4a7ea92e4e711f60d589aa8f0cbd5493", + "slot": "12858586" + }, + { + "pubkey": "0xafd74b8ebd8c7c4a2c5bd5866602135ab42cb5df3971403a51a37197e77f9b15244ecc195bf4987752396312043d14f6", + "withdrawal_credentials": "0x020000000000000000000000c0c9af953317cac889bb312c04b8357232f0a46e", + "amount": "32000000000", + "signature": "0xab6045a263a11c9194194e386a398b9fec2e92d66518859742bf23b94f1733f831e1800c2ba586a1bcc06baa14cb7342135b0c26c330cfbee58a0e3b5d8da3726abb6b0654a877826075a9593c3f5d5064a5b64a42fbee8d80054bb8a585fcfb", + "slot": "12858825" + }, + { + "pubkey": "0xafd74b8ebd8c7c4a2c5bd5866602135ab42cb5df3971403a51a37197e77f9b15244ecc195bf4987752396312043d14f6", + "withdrawal_credentials": "0x020000000000000000000000c0c9af953317cac889bb312c04b8357232f0a46e", + "amount": "1856000000000", + "signature": "0xae60f56dd5f79efbe36cc183661b83f3918f1627d57a97c89792e5e687eea6e83c2666566adba6171d65b04868867465148da1773734541c93342211cf54d9c9e999ec70b933a59420efabc1ea80f27b0ec51c838b16cdce9932f1f2d815843f", + "slot": "12858966" + }, + { + "pubkey": "0x9482ac00cd61eb0504efe8e964059ca4993058f4781f3ddeceb8262c9f1bb62c6d470cee4b855ab8c497e7c44e0d0ffb", + "withdrawal_credentials": "0x020000000000000000000000c0c9af953317cac889bb312c04b8357232f0a46e", + "amount": "244000000000", + "signature": "0x8ed1a50999c6cbfc3a82f4eff46cf2cbf0686975ed946669b7871f5319b89f6693e4195d33f5a789fec40f699607b167147d322f1d04f1fc52639eecf3621f0ae86855623d72d366e0811a16cbd027f826d9f631e025afbe00aff6dc8e653c79", + "slot": "12858966" + }, + { + "pubkey": "0xb690fd0ecede3a22b424cc31d1a5b25276706ea9d9b9cc4b322ee733bd3e5fc5ed5b572b2e903cabaafdd2ed9184affe", + "withdrawal_credentials": "0x0100000000000000000000002b78035514401ed1592eb691b8673a93edf97470", + "amount": "32000000000", + "signature": "0x9299ba8aef61160d0a1590383af685aba54dfefa593a3db8a094adc19e92da243526aefb02c156e595a5cf65fcdafd30074940cd37a8b4fd01986221572fa5f78360262bc2dfb1dfcdcdd62c51b76aa25b9e3ac6a07bb6bc2f52dcecca5a9c90", + "slot": "12858976" + }, + { + "pubkey": "0x9482ac00cd61eb0504efe8e964059ca4993058f4781f3ddeceb8262c9f1bb62c6d470cee4b855ab8c497e7c44e0d0ffb", + "withdrawal_credentials": "0x020000000000000000000000c0c9af953317cac889bb312c04b8357232f0a46e", + "amount": "897000000000", + "signature": "0xb3cfdf464d58c082e17a652ce59718d042ba3affc67c830f60ea90c3cdf943aee11267539494d9bd95fd58971d4c215f0a96cac43ac6d261e05db71a0a954c7b6b542c2cddefa25168c739dce02191ad0d63f7b961021fb26daa202202143816", + "slot": "12858986" + }, + { + "pubkey": "0x823fcc043dde3a00cebf050d029844d5df5f30544e7f707eeb837b2bd6615ef5ec3cbc41f89bdd7f6e8b88ca660dafb3", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x848818e861147d615133c471e0022e4d9e133852b23df88558b201b6ce70e72ea20f2377550591822010b956fab71b5d11eda8ef51e84d7bed44528b1bd418c2c525ad6c04cfe0c5505bf550646874d6ad62a8ae07a5c1fe571110e57426f777", + "slot": "12858996" + }, + { + "pubkey": "0x8b2cbfb115eccf502c362f6295b11cb809d17981edd47f7359ed69c7195a5d2a67522ea5b49b6c4600881183644ae0a7", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x8857b0ec5cf2656daf11f29183126fe64f3c9fc4b063091b4b9d1634d7033b50a15cdebc5a38a8be009623890d7a989f17fb52a003044b29b9fcd18be2e466062c993beeb354856c60605332612fe1e1eb8c54a156890ab796505cccbb1f17b3", + "slot": "12858996" + }, + { + "pubkey": "0xaf1b8dff0988d924d90af2256f91cc832bcb1055f0deca2d14cf12e291981a75a5f9856de4489195e1b6ad33a3c81404", + "withdrawal_credentials": "0x020000000000000000000000c0c9af953317cac889bb312c04b8357232f0a46e", + "amount": "51000000000", + "signature": "0xa6b9cf42f41f00065be67f6a4e5b23aeaf0c0b15418ef71af862b11af556d06d7007e1d43294914754fec62cd92cc9f417a42b9415cec7f241b112576d4f2a5f7747891ebc6cd1b3ab92b8526c0693b0fd64cc6c85a660ac281c00ddedd50bc5", + "slot": "12859135" + }, + { + "pubkey": "0x8e744c35e2a12586e4c81fe5c814da2acdf81fe1a33bafc28653f7f3aaab51a107c0c792f6244bbad64472ae4d97a7ad", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb77b19cc65cb38ec49113f4b02642a1f079e1cd83366e7b54eeb862c3e01921db9ff739a638f07c23151b756ece015cb02215f1684162d19c64ea3a9c19b9d0f3b66a1dc804d46e2a4890de15d40640007fec8d9998bcb706be0dfdd0b2ec76b", + "slot": "12859142" + }, + { + "pubkey": "0xaf2b1175e3c158ae5d145286f757aedb741bc608d40772821e2b300a574a6a7f436799238d1b3d96dc4327745312ab51", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x85fda2459bb7f9116507fc8fa90b04ee8427070f1db97bb273f2e152e3d8fc64e643541ea357fc3f6e6fe20c085d57de0646293785193106051d8c2c545bfdb8156ae20c799d3310ee77738b911b8475afd46b44869aeb108d942b9124d44b3d", + "slot": "12859142" + }, + { + "pubkey": "0xa1eab6284aedd058077de08467dcb307fa52070c09a026721265df986094e2122b218caa12264a2b43265909cc2306e2", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb4e02b5afbcc5bd67401be342831a4e3c4d5fc3ba8e5d197c519c2b293326e325dcbc3cdae911df4f5cd5611007561f906f77b38e669c65d94739946fc26d0c35fd187f9bea3d4d39961f4b6cfc71e3d30a29dc9866117839068381ed0864b58", + "slot": "12859142" + }, + { + "pubkey": "0x87b023047f52087e2d801d3d23fd246887e960ac3f48ffc0c93de01f97c8aeca544541120dbb66fb85e43371abb3ba42", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa1c03fc7aedfc4788f75d677a2654178fabc3fa5f2e1273607877262ef9a44e31a56d906cc450f65d992e64241d30a0710d22027cf98998039af2e5607dce81fd28c3c3b115deae8226a238eb8bd8ac32a2115cccfa4ea66d654bb5380a0a2c5", + "slot": "12859142" + }, + { + "pubkey": "0x901d08b53928e7f18f4eb6d7bdb864cf4e469d1ad0f28d8b9a7d09a97c8244695c2b18bfc27533bced7196132202e601", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x83348bc91d2a73d1ed6d0e04f2ce939f05438e4fff8c597a302e68b9590dfd8f77056cdd0f3003e86f6f5b9991a5b5630bd20c25c0833c6264aa958cbf36fe1ef6694d6e5fde90af2ece8e79fe67a4277f41252d18ceeeacfb89eff1b14981b2", + "slot": "12859142" + }, + { + "pubkey": "0xa7b5cdb76575fb16b5bd37b3d3f3f7803bf46d795fc15aab44c512d0c91419d31a79a0c7f5ab0cb11bb56b006176de36", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb39aeef1c09085fb680630c0521c654c2afc67221b8bd46f05f57a695ae9800df2208553485687d2265f5879ceca06350d283a95d577ea2c705c04357a56b7a7e35ffc9dc7893d4070935303965b98e4cd7604e1ae19dfb2bbcb5a109ef16b94", + "slot": "12859142" + }, + { + "pubkey": "0xb26af9cb7d76181dde1f63a03a6c8ebd7a64b5d2bfb6499a6e7292eb1c7287fb631d6d8990929fb5daa8f4a4c6cd3b76", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x81045f04cbba62153187d628b6c6c62ef5c7e19a3dfc9eb0cfd5e307d515b762cd9434313a5cd53c02507b0a3a5ef0ad19110d5ae5cd2845fc1eade3a89d7c3e5d43a006c8880e187b6d907a91366a0b5c74f334b51b675074edf1f9bd6ae1ba", + "slot": "12859142" + }, + { + "pubkey": "0xa354c3d924eea692fe90048b2863076e636909304f1cc05e2212f5f9ad18043f126d25e98a7057da580eca3168ba90aa", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb5a88f2f8d6b247354bdc9a33dda0611a154d1e78fd22ba218bd7e878e411387bc87a838e9d4a8df6a3425e899eb67f8165072e8e2e89dcaa3ad656006c32ca7de1bb9fd06cdbe56343575e356559ecc4b9d474dd350d03d2a407dd962f6a613", + "slot": "12859142" + }, + { + "pubkey": "0xa98d3741e3261536402f1f23064d6622507da1afd50b74c9ae12c171a760104eac607fb6cab945d3c29c15c7a3037529", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb820a6d9d149b6dad4e3c418f4658bf7d6b48a2ebfb5ead59488f83fd835b66405eda2365305f2a4769099e432ca51d80d72202eb16140ff5b2a4201a36445de649788920e402d3dd0081d33772206d8ad3ee2429bd394a81c70566a96f29de2", + "slot": "12859142" + }, + { + "pubkey": "0xb173ef6572fa9010224cfb0964617b44e4fedb1bcf5fafdd6e792d699026f099c6c6c40babfb27b9eceae06af213e4a0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb54b527becf7c95c7394ce179001da8fb95e627387ef529b523d21eb49fb537824f7667d2979ad1613f3d3683d5a4241170d3337b6c160474df392aba4348c9a79843f7f9133cfb545cf882f576df3455d1167d21ea87f2cdb77bd1c6603fd96", + "slot": "12859142" + }, + { + "pubkey": "0x8add52730b108494df2cab3debe8e55ea734c1ba7e0de8c1c77bc162d1311d9f43db19944a10af9968260f8b48ef4b13", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xae9ab489a7e5d85b39d04d67c0d905e9a341493a64a1175c3d4ebf10fb553b15e1f5a1478e5f4119a975eaa1846e2b0300f9afe152c562a857f9c8f95508b5e8205583fd17b8e18f4c24d9ce6cc034c090c94470789400977cd9cba1b94d4516", + "slot": "12859142" + }, + { + "pubkey": "0xb2bca709126c6c0408cfe57018ee927db5378f1d55f5e8418c5f320388086e3f8a54946ade9e484c6ff8ab6f4f8c9d43", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x872dc6299331d650672d1bdf3ac62cb152844f5ea21598925bbeb70156a35793314714b10491f83b1ea7859e056cdd0d0a9930968159244f6b112bf0961f576e54564ed8040015999daafcd3c8748a511bac423ae95ab28731df3ba44d90a2c8", + "slot": "12859142" + }, + { + "pubkey": "0xa49e6b2269b800f04ca607ed67ce8e81fb6e9e97d4d487cc8ae1247129d094276d60be5bce3334083becf1c83a6640bf", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x926ed5e0e9575bc3431dd1fa86d6766601e493e09a473e376a8429ace4f33902a8d5289f61ac34777e775db3a12b22161615345c56ee459add698fbff8804dd1dfe409a34c05ef4f6614ddc91df32c958bff21f067be89cebb797655d1f9f8b1", + "slot": "12859142" + }, + { + "pubkey": "0xa1bfb7074918525639f85673d816efbbc6d41bdba52dc4134bbb49495221551001b1ed1d66b80aa5b2d97409a77ec164", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8226399a5d1adfec5a08b0503ec763006a2f129bc7a8ff1963b6708eaf240e08d8c2f7a5591ae8f3c3ba290542ba5321093f39831d53c2ef3bb019ab024b4230ad3dce7e04b1a2a4202942e845b5e79772991c6ffa27312f9cfeb9039a1ded67", + "slot": "12859142" + }, + { + "pubkey": "0x93c25ce7df51c0937efc7cc906a3a12bc732c4827751797d84ad400bab91b3304c5d2883aa8cf1e4606e1fd4fecd964d", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb28994d712563dcfed0204fa95cd4ce8b5baa7fc9d2f9d70ca466211c66bfbeec69d7dae44dc719ac6d53c0f58419a4602792ab7c848820393aaa61ebc474d527a76d39398f24a4a1706152a799ec2fed3d3c0fcf8aa8613686df4b5c8a0ceb3", + "slot": "12859142" + }, + { + "pubkey": "0xadcd062f11d0ed61d581986b1ef25144b033994b8b29e6b49dd866a19efc8967291e2a96b76bb09555834ec5afe9f999", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x97ee6a527d15a224442b4612f77f2d49f2e158e8469daa3bc4f0dbf0d4339bac9c8d90bf55f6a8683df7c286d7b8c62a013a9b8a4c69a96525cfdc629959c07158a9ec4b1365adc42597fcae0d86577eeddb7670d0f9c2a3858120a0cc3269f8", + "slot": "12859142" + }, + { + "pubkey": "0x8c9fa18a04d008f742a4664f1ad5624bf39b18ea0546f41a6cec33167c156c939ac3b95bb6c09f52ab36e15636dc648c", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8b643cc7d00de6ecdf7311e1e89259928147878faa634f1b96bb02d637ec3e001e9ef7a5d4041a284d2692943120e0aa11758b788890500dbf7392a8582a47c0124f4d201b49e78754dab4b598f22c74880b6c9e2159913f60e4823a53611b7f", + "slot": "12859142" + }, + { + "pubkey": "0x870c5047692b82dba73314dbf1187b99dc34220629298d8cc175eaeeb50ace1edea41004a9231c157f9cef9d6046bd5b", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x988503af3f042af29eeae630588e17317985d7369c0a0d9acbeb83c233b432f6c46fa440b31ff151af4ea15295ff2b9d04cd42dbdd0d358f24434a4948b1d64a9b51c74459f12bf3f36984d2140bdce1f22363d50abbc93e0529ecdcc4a1d623", + "slot": "12859142" + }, + { + "pubkey": "0xa482ab057c79f3dd3f18e01318afc97b7e586f892393b93df2bab596c97192065196fb86bce1a4c9dd251fc03a84ecd0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xaaced8fae9d4c300e8460942b97697276366e8b666bded67614077af7f99a8f0ab031f48d5d69ebc09774c4f595073490225c03caa3821815f70d942888ed1a639efdaf2a1802aad0105cac68abb263c063c3ef08a8713f50b6e5d7a68dcd887", + "slot": "12859142" + }, + { + "pubkey": "0xa87eea8eb3f10ae7794cefc3836b3bb165a591317e88ec1d338cc1930d20a6115c423fdde659acdcfa0bd8062389613f", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa1d11a6c77b672d3da4af09724659e4e646c6e927a8860f741a4d22df603b91c3b30729e399d5b5b667f197565a13f1009a45ee98e39cfcfbfd9b7af6fd5bcd81c33aafb3fd7453d797fe67c107f07a4e0256b67a6ce09eb95f8dfbfca5264f8", + "slot": "12859142" + }, + { + "pubkey": "0x927ebd483fe333b7e47e42b7624522567240e2ffd6d2809843ce353927b4ada547b980d2804775bcd295695333ac18ab", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x95644fde7ca4c7b42af9880becffed907cd937eb18f4bbec1c27ac9f2f42778318df401cba8058070daf6dbb2afa8cb7125b06118e81e762698a4c192febe0c2d97bd37fc592926ae5e4f7b8ece54165826224f6b347e76ad551b7e062458aba", + "slot": "12859142" + }, + { + "pubkey": "0x902ad5b6b584c53bd673f3228dee59df5c1f67e3fe6f55a7d1c640390e894f9bdf6b583a1395ddfc7926d9148d33e9bf", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x81b7e8fa41ebf4d8e4ca6978d146cc4c4d68853a13e3fd0b9c36477f8b2f6a99a6499bc9c1849d60bcf2114ad927cabc020bac3f2879c17ad11a68ce236f3f6c5666643c705790a0190d42a80bb0d29d71a83bfe4304bc919f64b092355e7db8", + "slot": "12859142" + }, + { + "pubkey": "0xb18b938d36ac564d11a4913127e8af4423ba5deca15eb82e2571c2ebf589192327c47855534ab7d073b4473cd67e51a9", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8b4889873104cb031580dbdfa5d76e6954c298bd1dd26248a3d2a52243c422aa1e35e41e8986a891a145b87a07fc91b0055cd3119f00d22763589c1fdac112fa5ea2815a411cd01277859a024bea66a4e1fc13eb54251e339ab4cd3b0e04457e", + "slot": "12859142" + }, + { + "pubkey": "0x8a5a2bfdd510fcda9dd1d16fd9628f4f96b84c3705dd5e1732845690e5a9344cdfc4dee28a17426ee5ecef136a7acc43", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x83d9c8221ebfad458cac180f21e9052cf8a8ae48dea431a60806e7d4cae684805d7f3f8311c055856b71c79db0691c69165ed2fdb28f81e77e73c35c178e6bc1a2084a1c245097ca87deab8faca1a2559ee4cd193af9cdc409e4551394d8357c", + "slot": "12859142" + }, + { + "pubkey": "0xb800e34eb2aff3f00de5ca3fe56a19a697cf61a3dd99247fd49dc962460d728a7def82e3a567c9c4e539d76b33746c1a", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8687dc47f12ea7a803303cdd4a8a3fb28ab9bc44844a4ae42ab5bcffb8886a4a1dcac6aca551488d967007813c3b8f7816077b5730e1cc2fb2f3801422f4c4c76f50b88688b67a8b26014d3c77353c154d7b7fdc991635857ca32e0aa1a71360", + "slot": "12859142" + }, + { + "pubkey": "0xa2278fc40bb513b93503182281fcd99f0d12ec308b489b3d36df8b2c5d8d36f4baf5d460ee4318ac7e0f70a74c76c0f5", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x95b7947e138b5222c7d0de7ad398e06bf09c4d7ac90fd370353d70d1e585b75c221f3487237a8fa94b974f1e8f3247d4022d1678c6268fa48e3ad4956571027980d781f778203fd8346dcb6d1889e8aa02acecc27835f2eb671c2824f1522393", + "slot": "12859142" + }, + { + "pubkey": "0x99af060fef8196827aaf8f84467c5b6ce5957204b9abd4b04d788f5431179ed81bced3f6ba542b83c67530361445d3ec", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xab9ac3eaf3f61dc6a199d7ad71a813e454eefd89dd599dbd3dfb6112c6865ded83ab5b401e3a7dae345d566e31d0961f1000b0ffeb85181f8148291e3bf68240f47cde653583aa8a43466d612b23eab811c2c411c4529bacec63e7064d8e7fdd", + "slot": "12859142" + }, + { + "pubkey": "0x849d3545c7a146bbbcaab44b7a2810b6489afcb4e9430aaa95fca4fbce22f35b914e8151a78c14269d9a9f4594e52d4e", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb337a7e727917fe530be370cfc2cabf1f4ae2a77ecece7b17df9c3396f0e3b132f5506128d16dcdb522a600fcaea4f1014818d5c80d924af9080696122774ae88ce903e29914990a399cdb1b7255f6e503f64841a26106fd3583b79fc17c4828", + "slot": "12859142" + }, + { + "pubkey": "0x87031a607039652e771be62fccbdae8959982f3717fe03cf382ae64231a4f8c507a8d31a7649c98c7bb65f82a64fa149", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x93cdfd13f22edd45f0ccc052f35883c5f174d98b72b931320e9e184d4327c395e2b84eacfef7bd09898308be5657060d134eeba2544e22aac36b778339273c24e87ce2c828c78cbe884b83eec09de88a7fd840214ca619c5c6fabd383ac467c8", + "slot": "12859142" + }, + { + "pubkey": "0x9440cdb251668faf324ab8f9a60b8a67c2a25d1b575138f88e5bb6c24e9743f8512b1fb3e88562d21cb3a7ebc57c4535", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa41bd60720921be82464603daf8560f7c0a65bbb7451ef06e529bf92a2eac94af4d2c6c7a1e9e399f46dfc143f0f5b021815e3b4475b5a01873ce4344be33f31645fed9a1251c8df65265c1cfcc5f13748d1b750ef6d7490d2da2f8893113b55", + "slot": "12859142" + }, + { + "pubkey": "0xaa92a56dbbf5e49e040b77d963313fb6b63972cb1c0a48b2c175a912e1d3d2d80e1b30a04d2a25b8cea41b731fa82a22", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x9666df4bd9421481c82266f6ec301c69f26d420c8e1e129a414c5962e4661f348dede0878287c9c81c5bc17726b7d6cb1257dde4f766b18e042b759b9e8f52d9463385ab6b9aa16de495ef9e6ad337f63e1caa05df52ab1c8bb5c525061982d4", + "slot": "12859142" + }, + { + "pubkey": "0x95d7ca6945ab63cd6d3a7311dfc55cb588c70204653aecec115a4ea0e4105cf77da2e3bffe817761245e054fc7db45e0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8d2d81b56e2e73372169d6159b602b97602902ff7aa3025a038a902415f04b429b6dc15b973123251927a01e8836b05608fd7f17ea15677a4b532587c466d6e73a9f4308b1032e912433a4418bc714e13860e0584835efae97f5cc60d1663445", + "slot": "12859142" + }, + { + "pubkey": "0xa997123532c25af80152568c91a18623f1bfc2578813623f642b6a2768e30a24883eb16b51278071f6a775a36269e4f1", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8b06b7ddd66a36de8de1b395fc8ad43e7a5933a478ecc3c38397e3705fea5852ab1c3ea66a9a557ed96a5559d672948112347211988e257d35d1b81ca42f440015b8ff8c0dd9dc2682928b6bab5ed5ea6e156bc5aa4f614e5d02a90d31c8d36d", + "slot": "12859142" + }, + { + "pubkey": "0xa0e2cbf994f6573bc032c00144fc9da66dfb676ab7bde4029dd9c22b6cf274e1c50a90324bc47ca23e79af2f926c90a0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xab057ce488e94630577819708f9343a991205ac161dd96d72b335097b2d68bc4fe3b699b8d67f826dd3d84d345206bac07744c264dc96f2499f163e2825e5b548efdddaef49e36d318c6f3d77f7b0a9e2728514bfa99aeccd64d4db7f9b22c41", + "slot": "12859142" + }, + { + "pubkey": "0xb9d3c1aaecb0ec8329ee61e7ab425e5821b4a884c3f06229172904861835dda3f3930f45290cb6abd077b7291c6caf1e", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x89df34394921136d9495784171e00fb5884f900ae6b80a05c6c991c9195b34c0f7850084199abb95bcbc9a6f305661ea0239c131092ec1e56a038cac1c50332be421d679f9f7007f5b214370b29b331abccee801431e3639bc8cb677301e9b8e", + "slot": "12859142" + }, + { + "pubkey": "0xb16d1b58f16ab1835c74836bd6821a59bca85f8990d931f56af152f564ec123bb4b9f18f9c85a3b5856131b0ed9e382f", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa19ced7b5018abf7269422738ce6fd911abea3333bf84092a79e352cdad04e6f9af3698008f447fd693c1f7dc90723d20885b5e60b415d702544ff5faabaebd9b41c863fe1d8651d67e9398f12bcc998c2211a7a9dec887826195b2e28951e24", + "slot": "12859142" + }, + { + "pubkey": "0xad0854e224272f6c982fc1a205d242295ab89c966b8cbc0cfa2b5d4667163b4535f951a010193e2a87f3538244799a2c", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x99edcac42e57959ef76ac61e2a52e7a902f81bc74493435a0982af62c8abbe08bd563c7a3cf87135078e75a3d5ced39e12c9cd43e0a4e8422d7b36115bcc5caa9f466b033c4af214dfa9b30d206f76a6fee5249428238861b8d11cd40f35f6bc", + "slot": "12859142" + }, + { + "pubkey": "0x8e5331dd3e99b11b779965c500a6f0f7174cf34641701fe6c51fdeb5a6a82dd3e0da53f6dd311980752bff4e8c9a9d09", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8d79bca9c59413193b67ab932c5cc3534bc9dd012a495f0b3ab702d51bb1f49817859052b47100ae9e8ab06471eac65a05e5e6f7eb0a8b820dee60873cf22b01eeea71e7b6884794a22d1e9278f202641bfa0846f0e2218906b92f840a94c76c", + "slot": "12859142" + }, + { + "pubkey": "0x8d5ab2fcf1fb3ae49f0dbdb6047095dde4b152a283fa07fdb1bdc56abaf031c7bad47848deb2e1da1b3bdc869df59364", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8c50460607ed8978dbeeab010a7a3b269eed83f4e20348f38254749478941d1b08b62303abf1c90a3f77dc55171178ac0deaa193eb1b7e9987753e2e0a030a2826454203ab2aa73469aaedd6279b577f0b557c4ce04a84f0dd83652ea2098651", + "slot": "12859142" + }, + { + "pubkey": "0xadcd8deb171c9f16cffb33659796fca7370e2bcdd993aa64a21f913eabd57ecaac7307ae30a639dd20c0e07097819373", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xaff00c032f50963075361b647afe6b4c2bc5e04bc8f9c43de9854331ed4315b25af2e7775f22d4165a6c684d7e80579506b0ee36e9ed279d6d47d91eabacdeab12ce282f160ca3a1576a130985fb98d941073a4c713244b1b1fbaf927f3d542a", + "slot": "12859142" + }, + { + "pubkey": "0xaf63dfe6b000bc94d1c63d18039bed28a3fd5f582dbafeb6ccd9a1a8ae885d99133395be7484d2dbef9410f425dba0c0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb8f3964a48034e406e05a8fe4f9e58568dbe5ff1a0bfd8cedc97ef28efa2faa1337745781053e03b1ba88f942461d67b0be51d0f21a12c4c686f6cb6e505709bc1e64512840ece1683a0e7183002d7c59cb58b95f23444031424872a2659f5ba", + "slot": "12859142" + }, + { + "pubkey": "0x8749dec521df6679aa8156d9b49c733f4f99794f5dce1cdc6e1c92716c3357f34a05afc05c2eb884adaacbcb7f06d433", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa0e57812db3ddc264cf2c34bb2e8d8f8a3e02d3e076bf229bc92ca0952e5c8c98f7216af9ae6742f21ff6ad017d8477f07293a50963103dda204ff62cd9c91120e287afaa064a67ba81325bca47a87e12def69879ff694fe734124fb30c2019b", + "slot": "12859142" + }, + { + "pubkey": "0xa7ac93187e33702dcc13f61f426e012a08129d78a13a59b946cd0a6ddf294e43b8b55774e5cdd92d6368bc45fea1b168", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x869a0d384f37f18ee33b2438f4ffcf12c78f51eec4e5d643ca40cce2f51c782b88cc29e44e795fd9bf548a3a169317750c012dac55b1ddc6be8f0ab161a9d28f41d0e13d60a7afc64d9bef820914439bad3f2302077e3ede1dd13df88a30cbd0", + "slot": "12859142" + }, + { + "pubkey": "0x8e5c53d830bc792223ece963ba2afab459c6cc2d98731151a1a83ea754aa10464c31d4182a8c2154a0b16b5e34af87fb", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb868520c304d8e9f8f0a8ec46f42998cd26d361c633ee83cb36c8aca4db480d88a33fe5089317a6c26dd491eabf0db3205f3d5375698a8462b8b79d42e22c9b3eb5f352ac69d9dbe77fe803e39f87d4935e5f5f60efcda497c076241e76bcae1", + "slot": "12859142" + }, + { + "pubkey": "0x9902924b8afcae7e6674f46677611a5477f0255030299da3b6a7d8d23eab346bb1f34bf5ffcecae7ecd4392ea15eb019", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb452ca306722ce938aaf354af790e00a404aad8910b16a071b75a77bcbf94faaf18e4af214ca2b9506df046df29560c7139a614c67e5e7d65b6d9ece60d03b37d883abaa46b7fade38fc4a02e3a048525cff0cbbd9215766a86665465fc50a4d", + "slot": "12859142" + }, + { + "pubkey": "0xac9474c6193b6a6cfabe49f555dcc19fa427cd69ee615ac4d87ffc36102a41adaff3c190396dfddaaeea9dddc5662745", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa165e45f7e58b3065da8422d91c58052b3f055eec34cf43c0dee5353b24960d432025e638351eca0333524e0cb367b0b053235a22f9f83c8af24fd93e69b8bc7f87c08cc7ab21a5a72e6877b058feb5f9fc007d0c3ebb438039a4aab2eee6605", + "slot": "12859142" + }, + { + "pubkey": "0x8588716a1387dc1b81dd82c4875efe977459dc68eeb6ad0e9d63bb3a98dcd8e193e30ba036e30e08759d1505960a562f", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8255f54f15c05e25cfe79da0c253d8a537f9f409af922a20624c6e60165e2ea7d8a3a9bbdd8af5ef70cb1b5506b4d88d0bc69ec4827f309b171254cb0f899a20dbc2e55d86ad9647dcddeade4b11ad7eadbe174312fdb7b4bc48843a459a1cf5", + "slot": "12859142" + }, + { + "pubkey": "0xaa161f1435145c710f83bf9517c41517c5a9067dc9dccad51ce948e8a69762c395a2e3fa6696f01290e5e1e8f9ec14f3", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8f901d961ff569db5467eb0defccca5fa8b8628020479d5e52109cd3c944bf32ef580215e950d6b4dcfe50ed5b98f90a143a64c78ee1eb8455104965865d7f1647d782cf224fccd53d432d2f12030f94947f0c61bca355cd9bf7df411c331ea5", + "slot": "12859142" + }, + { + "pubkey": "0xb79c9e8aaed9fd5f93b0ebb0361bb3d47cb31737666b5454b23800cad292eed64c4174d6afcbad3ed8a7b13474974eb6", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x909d7d9c8929cef17188fd1336265830fdc9a3f47502a062107968b1149b31afc8c3d2b84ae261d3daf96b4ffb3517cb12abc6f1329197dc2108765fbc27dde9635c955d48223941645fb8f40237666d82622ba2b29356fdc86718705cdd695a", + "slot": "12859142" + }, + { + "pubkey": "0xadda56aa26c846ea68fa91b9bb96cfde475b9926a6635eeade4067e7e3a4d19dedd28b0ceaf3f664cab282908dd6a30e", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x88db86e51bbdb63d19f7e75629e202b6eea51a1118b5e0ee785c327ff2bf931d3a3b0b6e5a084d8dfcb2dfbf9b84bf7c0e6f3be32f7c6f646304affcf52520d2a96b280e88bd55aac82fe939af9cf4988e7be2943f0848344f7d8caf2077bb7c", + "slot": "12859142" + }, + { + "pubkey": "0x801fa0ec179d74c558dfc9025eb541129637f503ccbde1f74ada179abc40b0886bccbc0da1433c17c3eb499ac8d3ab0c", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xaaa16498bf3a5c7bdce646632f0fa8f00d2c3f5088f41ac161f76bc1bea6d5f7dbf84044fad567572ae15c483c8ae96a0351143dc6a7b61cdff3142e29e208c7b2e337f835b06c51adcde4f8f89edcf4f8f198abaf531ffea0f23ce5d9fbadb2", + "slot": "12859142" + }, + { + "pubkey": "0x8ecadc7ff0f0339d9e8e42c7ec4258b438a2c45d8b387d80136bb1c120b4929c7f732500ec8d3b3391defa5bc3ac5a9a", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa5ef5e92f51c590b61470e8e93a4c31296ee6d5c793b7be49071c01cad139294338b17f005f1f67423eafd39e4ea5f3516f6d0f34dbf8a422e36255439d0a9ea2d5ac50013186aabb03b09530c3efc9a2a362a948162a011901b1bb67608838d", + "slot": "12859142" + }, + { + "pubkey": "0x93b641c87d9171b376dd38787f0c9534ef2f01dbed5d057d189f942ea9e190003e440a663db23922c136f19729a744a9", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x930d549079ca55877cb2dc10cca308a983615c95a45bdeedde1015421ddf7a60f1d88f90b407331ce2457fad089dd23316a198f2a0c7020680582198d9b95b47afc40ab7dc098e594d2fbe43dec6a81b78408955b7d8f6ecdcbf6cfb223bcafb", + "slot": "12859142" + }, + { + "pubkey": "0xb55bfacd7c4635176ee3f2c45e617c7a68b33b738b96a24f49538179db5529596556ac5a60b0765d7206e423c807c985", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb26b7cce680ba2d447a07a30df40ea81f5348d4b8a6c010ce97c611409682a38a2855e3f14d877b7a6d1723866c975ee084db3f9073445514070da1563aa682d2d540be08a7ca51beeb45d6ebe65aefbea29b462d7e6a7c70d9cac381ee482eb", + "slot": "12859142" + }, + { + "pubkey": "0x8ea72cf22d102d1ec349c96b631d5f03955d798bc02af6c33af83a60457e867b8fdde376c65be14d0587cecfe2945d65", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x9651f0f97b212b528680981d15219f8afddb0d266e780fa4cd94667fe0290ecdfe9a7ea0464dc83108132ebb86809a3112e61f019d840454712a80f36d0d3936f1d8ba551b958b14232f195e61156c6c2284e9013f325c1405e97459e80fc9b7", + "slot": "12859142" + }, + { + "pubkey": "0xa7682154150b7f08e8d1a9af7f58bf1ea1290adf5a7553840859a1a1aac838b524d7d4a8a61a8332cb86b716b043ffa2", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa21234ff04942ecac837856a129d2b205c402eb8cb4b5cd57d8a03f4d5c86747e5eb01b8a492ea2ea0b94590abdda51c13ea3d136fbc5b9ad5b077eb5cfe0a703d22981c272dbfc8e10c3e7dffb2be416e97fb395ba47785b57e1264478b244f", + "slot": "12859142" + }, + { + "pubkey": "0x92eac287f1ca0a6ae379850d93dc8f87b4e840a77649f262d0bc3b3a5100a197300c7fec7f243ba3ba5b951d9469f2e9", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8a219de55ce6fc90bec3054fe4e3cbe0097d197d463139535760871fbc853925608b07da72af4c51c86c29b9670bcb4412a678c3264a328fe90e9f81c24adac7714c8f8926b21e39c44b21d19b2c017b049f957ab1920862ede42379bba5fbd7", + "slot": "12859142" + }, + { + "pubkey": "0x92ca85dc16bc1deabea2923b3f7bd42b133c5a155805df33403f0de5ed003bc2d18b3ca1fb0734be6aa2101918028e74", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x94e45d19664a5007256508fd5cda1f0b9e1bcf57a7dc4f62e979a4bd9bc7356048922b2313285d8d29dd6a5de9292960099cb594d56a3770a260e2b5bf5d9a509a70a2efe5e99b1ddec49f060cc40b0b09572dbab92a635994bdc28346e3c34d", + "slot": "12859142" + }, + { + "pubkey": "0x8da5a682b494c4d2f80fc1addfad1acba37185197d5a9d078d4d526928589f7d97a3cec2f3a342c13e787af656775faf", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb019955aa646e9191feb3f2912cbcce294396445c59ad8e12b74b244fd7865ae337e66c860cae00874e0d7f1d13ba6500378a7deba02b79e0e7477e83eef815f0687ef8b9feb87535402a62cafdb506b172471deed50972058cf16f0dec9ba73", + "slot": "12859142" + }, + { + "pubkey": "0xacf79ffc4271f8aefc6ea3ee5bde405b28e03a75c3f1b3e703e81454fdf4444843db5da6182a6c9f933f43f2eff1d9e1", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xadb8421efc39040263a1863d35ca7da1a2c97f6a756865c0fd46d7b06057472e5dcd84fec90c23f32c732a37fbe267651320e32052f0d97b914f65b182b6e2ad0d17ed3f7858ab9a2ab0bcab35cad057ba05a73d43fdf41002d6fa9f01b0904d", + "slot": "12859142" + }, + { + "pubkey": "0xb5ae38ffd1a588c35ed4f9796a64230caf552afc0583fcbd1d8d369817eb768be60bc6ce0bb9c690f062c6c4dd052fbd", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa1cff366c78c475e0d623af1865617ef202a979ec4ed71971d3fce891294aad54e1588c449c9ccee4c2e64aed3fb003c0b2c55951577c15465b322c1a47e8164e389494d677807cfa22a9095fe39e06ac28f39339ab52c611aa09ebf94b04c9c", + "slot": "12859142" + }, + { + "pubkey": "0xb5a0d603b9c2a992cfaf233796f9443c469031463bfd10ecc9f17cfe703ee237ea9ed28b1cb158c9314862b5c9a46ba0", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb013298faa78cfb2f4d026f3acf6897922b307d7128f598b5e9b22b8973e518d33a324635df4cde623592a0ed14786270dddb1e62ef4d8d80717de2ec26dd3b349c93e34c8c6dd5b4252d9b52fa1e0f095adce883294567ded209107891726bc", + "slot": "12859142" + }, + { + "pubkey": "0xb607789ecb07d68fff98a7a34cdcb870908163b450829952a96403726edb67089571e933e9f06590f91cf47e8db7fe49", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb55a83975db711760a7abafbd5b96fe6e511ce86be03bcc3a6cfd9dea71348604459d46208ef32b9db305341442cc49c09b7e5fef652a9bc59982ecd553a3f2c78a9efa7a0d959aafc24227449f891d62d65cab4d57e7a8c1b376b523d1df80a", + "slot": "12859142" + }, + { + "pubkey": "0x917d896de71a2f2f685818345cb7b3c157f7df6c3da14b8865b31fcc9f4738e49a3ec32caed77df458e675287816fe30", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x85cfea3a9ead20d25a10f847f30429156a2aeb04e874971a8c19743badebc9757493ec59d415c4dad24aae84fe6c936c10098253ea8f6dc55ba75b81569072cc49e63d4b0610ba73e5250439868f3f36b0105d9fed185bfc19cda159b66ee063", + "slot": "12859142" + }, + { + "pubkey": "0x921d0476c9b4427955ae346929334eb0e10fb047d56b2a50a43a286977a81fd897d0449873ce0ecb5145ce54cf137d71", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x989fdd83a1aa9b45bcfc48333df3493972f572a63ba8887bd4fecdf056168ef0f38606ab83384cc6e60b854de7631fda012292dc337c9afc63eeb53900f0af463cd0a6fa23c9fbffec3d955f41262c5f7877fc8c3541a6b4a8c22eef25d0145c", + "slot": "12859142" + }, + { + "pubkey": "0xa451df13194ac4b77675fb2ac2a2a3e717e3740fef9e7e76d1cce8d7954a1812831e4232f9e76026beb1aeefd5629f7e", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xae6a9e969d37dc4df5e7f69851c8a32a050963e250e7d15364f5e5645bf01bb77a137c53a96e42e7c8535ffe518598101154a1c4a3a2cbd91caa80d8dd85574c91bcde591e9cddf0beafe942ee93c6c4878698989af1c5771a9a4628f4fe053f", + "slot": "12859142" + }, + { + "pubkey": "0xb068d7b31728a8773ff6c16ab2ebb3ea30c2da98c6dd2ea4d675acd2c598e95c42c606d1e0b24fdcaba7b06f78950c2c", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xab68ff243eb8478a9b060231c529312fec777b5214ac647f29f8764e9456f55ee813a1ee8589bc0ed8f6ec22533df6c40902189491f5e3ecac7578dccc38e6206dfb9cbadef53a2e86e20aa9cd5ffa5eab1361abee02e9d3116a586e701f93d5", + "slot": "12859142" + }, + { + "pubkey": "0xb2dc98cee6251d9b351e3c6d5212f1c4bc2599d18c0d860f652907d5f71bfb6b04b85d37f90bedb7b0d6793ac22f6d3e", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8ccf0965072a70296c1c4b61d4a546dbdbd2d29c3e5083ffc2d70408f8e1c1f98335df4d9370807391dde9615bab79b7083a8fe1ce27b3960b0fb2798783c6b548551fc39825d40c5e3843398bc99380874662a874f3b0d3f6da948da01c54e3", + "slot": "12859142" + }, + { + "pubkey": "0x8e4e39140673da501bd6ef2a584c1cb04c07de89e1944f2f4113a86ca9afc1fb1b704a529bf0de857c15eac1a146c47c", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8def4bd1c4b8367cd5d22fcc45dcb920fef896c9162a3a71a65af0774f7e027fee5f9c13f37e6eea2c4e0c9a72d2a8cd0641931e6aa090e87c78fd536ba9da0d36764f4bca8c6265ac28e65d6e2183b4cd27b573841f9ffab1a7a6b76e9e3c63", + "slot": "12859142" + }, + { + "pubkey": "0x96c3598c08fae29983a0d50661265862978eb12fb213f4419fd460e6a911870b3e2af582bf871a341d4bb56a1bc5a38d", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xb168a6b50528b5f230aed4490963c2d216116072e8bec03c9efe2f80cff2bd6c79a629a1a4ced464fc0d8fe596e7e80518f415bd9847cfb344a81304b4071a18738c663c14dcd3791d0c2251152f5f8e58cd88ddda81d00551ddd9e54df0d8cc", + "slot": "12859142" + }, + { + "pubkey": "0xad5b21e88923607c08f4a5e2c26c5448ae30adb39159dd673d412d732034b76529b9d4bd13549bc79014f8b5b20d9627", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x83a028d1618357e086f149b5208c4a572440a66422b8b41d774165c3d67476d7d9ffc19da2a453a8871ad3798bf94306111b092601c1aabe47b6d059d55013e487d5d8bc8a1ef8a71deb1724928ac18c8a91bb6d5558e4bd7a6c227adc899af0", + "slot": "12859142" + }, + { + "pubkey": "0xb38dc305d5442072ba94bea97d3cd323c458e74fc9d9962089bbfb3f5ef64b21ac4b4422b80c29c1ff47470082a8586a", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x9110b2d7ffa0b0ae5344f8503091dc3a144b33f095ba9b59670b9da9b501cce679b2d3a4f5a6fb9210ad154f3d82c42d0d12f5fa5f121cc5c06c0e5f7a99bf30a2a1e223fd9410e7d09e6fd61dcc3a803b4d725a6c23a552e9b45914a0d03e39", + "slot": "12859142" + }, + { + "pubkey": "0x969ac7469d75c114c9620d29958b25476e3fbbeb7bc726f5ae7cae99dae36fd90f93f52b04520ad6b226a5b925e7d182", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa62196f6f71d6a56371204552b94dbaeeff35791617a65ecf35e56adb54561f7bec68e4ced730ab352ae75274b2f8dea1090393b37c5d56eaa569410997720bed58b31723d5c010287d5973f63afe9b85b15e115aede012bf99a66ad4425aba1", + "slot": "12859142" + }, + { + "pubkey": "0xb04b0ab2eae0ee43c5ca89a4d323a935ab9c80a0f0ad040a5e3250bdc84cf4f2fa549411e6666aba814dd4a5d77a9528", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x88c0d5450a37d84a868065e513d9df10c8d8e90a7df0edf53b7cfdec19d8010332f10c43043d23e65a9cba60255a20fa00a938ab50cb2a4450776837be82fd45cf13093fa486155e648d061ce7e6ac459d2b1a45b607b1fa9d64c2b21b0af73c", + "slot": "12859142" + }, + { + "pubkey": "0xb7a47f6c30346a3bdcfbcb6641e138438d4dc114fc71c39a04f1366a2106acbecc208d7ebc0cb511297c1990eae68731", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x824cb869dd22774dbe255421af08a1759d1f6474bf235795efd649de758f1ac94bdfd9dbe9eb5d861fb7cfb15b539fa80a3ee55996107fa53c3ec5b16582aceb310604c7887d092e8aec1534d4bad0a37ddc36dc428ec7b921cedd9eb15956cd", + "slot": "12859142" + }, + { + "pubkey": "0x90bacb33c7ba093b823d4290b6719d47c2851d19a86e478e4f32fccf75d388eafa56e30af317e1e845c0874365366b8f", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x8f2f396dc74ab02810a686d60c5370ff79aaf404479a3d188919fd6ac73b495758465b19051bef4708713231b1a69e84130b2b7b6f5d6c4532695049ca8bac4050be9e8704f4c6480c708056f0f9f4ef807ad446abe152d581b0cf8421a1a443", + "slot": "12859142" + }, + { + "pubkey": "0xa44f6144cd80f7bbd83945ebe54485dc61528471b4735fe30f0bf3cd9fa20317e75f33db525dce5ae7e3e2e0c2d4e0c9", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa62f0f1c3790ce98cd0fe4c27894ff09d9fbc095b8120e883c5cf8d03426c4197be18264eb4798cd3cd8531a7bc1f5ab1312d2fdfec84a4a2e5f24eda76fb6fef59d83d52665013c62baeb5ed416644d7da1e39f6dcdbc29e05203b8dead4253", + "slot": "12859142" + }, + { + "pubkey": "0x86784213af78815998069fb4403109dae479f6c4964b68a9f0198de1b314508a642069417a777fed48c55261c475e4a9", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0x81b07ff18a89d4d7e20f44cb64a66ed6e4524c745c3efb8ec30ecead3ee264cc5daee1a4f709cb5f7b755f673f25e50903fcb65e04c6cd02ff0d4c368fee46af4ca15e55d93d82382c50d43c71e8b97263e8d9ed7f90ce533fb07a35e819870f", + "slot": "12859142" + }, + { + "pubkey": "0xa1ee685bcd2516e194755d66e5599590c658d5f9961a7445e1b19e14d1b2c7fea1aa004edb3c452d6ab7b58e0b8f7879", + "withdrawal_credentials": "0x010000000000000000000000e1f4acc0affb36a805474e3b6ab786738c6900a2", + "amount": "32000000000", + "signature": "0xa6208f24fe76e1ae5ccd77f60e10808e41210eddfa629a28823ab4361757be58b7b4d2b3a23d9e5d0744d915093917c114af673af07fcbcb118da292926bd703ca100971612756192de96e06e98c1aeebbb4db1742b1fa420c7643f8c88014bf", + "slot": "12859142" + }, + { + "pubkey": "0xb4382274ebb76c55cac692092fba2af087f3a64f53cfee1614e450e25b119023c50749d4cb75c73d274626c5e00c1f3a", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x828c454eab5dacfd95a65f9c7b210f4a66df979181a6d6523d5e556e5beeced960416a99ef5b334a42d112406124a0570d52ef5bd984a528f54a0c5e9d582f697916800effc2f757f6cfc4899c553ef287ed09b15b3e4930d12ce35959e03c2f", + "slot": "12859147" + }, + { + "pubkey": "0xaeee8407e1f2127bfb1a776debef0fc50894c228d2d92257ee7c0b137bf68496fd7fd9f49669f9f01eb5d1bbcf743c79", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb0e2094abd165ff2251d9bd741ba728447e7291a54d006410583674b817fc95deae9e018d8ad4c933b667cb46139e70e1768152e654b1e48bab937e0746dbacaab1fc9f71e75349fc3169b7fe181bd67e0bc5df1ebb9dd7d4b21b9ce390b4b94", + "slot": "12859148" + }, + { + "pubkey": "0xb62e53e42e20078e5387ca6995fb14aa28a0bd72c9bb3f4b264a31a61ae4cd478d74707ddd9e21b6c5642863434f4280", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xa36635c348dff65466bf860bf5c1e7ad01a5ab79607c298c99763a933e37111eea11d222ef44d48d1c4c5e7eb6b1f206153cdf61bb0f60d48f0ce5883edf6da44d0efba8bc8e0ca2de389c6577fbca5c73323fb6f8ba89c48937646265352482", + "slot": "12859213" + }, + { + "pubkey": "0x80df62e16ad2a7af78722afc2fe2ee84bd5f3a6ad4b3d2d966bfc5007d9f3f6da6a57da835f02cad0c2c7244f4f250ae", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xaba8bb2ce86ef7d2579ae4d0a6d5b7ced83cf6b8c09d9e98236ff82e86881f4202a5b2d2404a3746e70badd633da32670d944d5de78ab8c4849396f7bb5154eae86193af023a9a112d180d5cb87cd9bd3fcf1c8c31a567db74b0f6288416f340", + "slot": "12859213" + }, + { + "pubkey": "0xad0de72360a14546f438d896d1518d5f781619fb750c462f2a267e239df24e55d0e98715c7338bda44cf1f4211afdf99", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x81888b541b6134be4a3a8b4c4d1e51e907159139f53a784dc4cf04b5b82f20074176cf616602a5da6f9bd1460e08f4640e8d3eb8efc0fb3a1de6190a9f007d1460c467b9198d91a6850e659168118b57cecd7697c3ea0ab2e779486c84c78714", + "slot": "12859213" + }, + { + "pubkey": "0xaa51227cfcb89beabb30d3705a48351be25ab00a9fdd5e5aab467406329cfcff0bf1f0c693c34a7b09fda29072ac08b2", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x966f17711354f449b417f5bbe4d996e3b48c8ec93eac1c3440e37fa29ec85a348185d077e456d6ff2de108d94b9df411067adbbebc7d182d27709d6f22d5f67fbcf3132ca7370c8b5aded9222e4985b5472c5f70515b8d37f826fe910f359c2d", + "slot": "12859213" + }, + { + "pubkey": "0xadeb30f5535f0105bdf6e9751251c504388dce74dcd568314609fce1a6c15114384f1b26567ddf01d9f8f0c615e6e262", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xb47d3850a10fbbd2e35790c8b27302f67bdee554ad1d976a346595f945414f2dae9a1f4a80aa9150fe9af8661e0fc78817957f97e57ca7a336b308559ab82020f67691ea54f6550f8d34682c34dd0811f2b3e40d69b47a9fc431334aabb9ec6c", + "slot": "12859213" + }, + { + "pubkey": "0x845dfd1e49b99a9f223cced61ec4588b97b8d2151795ee35d9dc9e1e67caaa89c7ba8bfd8461bd027ec1d728bde3ee9f", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xa6c07474c2432afaeca716b6a77cf2879b501d074e89023d8270086b7202fc72a4abbdc8e67f44cdf9ddeb59b4e6c91e0898c0885600f3b9656c64fa7b70a35ff485701812b986af0881e502e28dc5b1189715df756fcd200a59807fd7a83b1b", + "slot": "12859213" + }, + { + "pubkey": "0x8b60bb2b6d5ecd525ada8e22133b0af4e8871a3274d8abbca83e5d129cb1781e7e64f47e6d7d01b91fdd190ef761740b", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xafdf7a37e0f13fa77e55c8a17a5436d1874b7dd48c3fcf7e1bd0c07ccf712736a4b1da7de3bb736b0900c2f118c1dab0180ac13f67da9513d7bdaa906d0603f1155ac6af12f86affd3d8adc27fe905e840d6b645a74e90ad4a727213f17f5dba", + "slot": "12859213" + }, + { + "pubkey": "0xb20aa4e6d94e08ee06814bfe3e58bbdc41e2ce50d1ea7948b0ccf1cb9542f5c8a0e216f72559bf6eae3136cdb20ce236", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x93851d92691e061d5fdcb85fee2fb43a7f45c2bf8ee625d0ddcb4498fa51020a3c19eb6dad2113d3fbdde09447b78b73054988a31b782cc69091ae3de93c7d02f4b934a6fe57088a7611db8dc61ff45b0358d8eab9bab348f1ed2f3721760a7f", + "slot": "12859213" + }, + { + "pubkey": "0x93488959ae27cc39429f508df0f040849214b1262a6cc45aa9e5eb973b1ce11a3ec6d18c85e8dfe2a3de37034b7302bb", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xb0e8d61598ab056ac732b67fe25112d7cd092169ddf5a09235aaecc61c8ef777e3b5d937c4784ae208553894b04f795c18a007111e1f224df24016444963cf3a8bdc80ed5d99535a5237c13f08c9fc154313f3c4da7cc1b60dddcce678e7da47", + "slot": "12859213" + }, + { + "pubkey": "0xa9f23dbc6c9852578a65643fab5b0998c84f879992f29e8cf45ea62d60e81c7e8bc4886f0a5bb572e059344e52445025", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xb05a2d9099f62d3faee6a9ff8e6545bd42f9375557f8d8a08cab0713c3c760b4f21d15ab4727e9ceaa3cd08e1987b97b0df74411ecaee697880f12db55c534baae9200be90fbacb075f732d07101652c812b14db4bc7b2e5c1602d512e9f74d5", + "slot": "12859213" + }, + { + "pubkey": "0xb7b0bb466811880fecae1cec484d900c4aef9fea5de2ab0cc27010bf8c9fc1e8b9869f739678a546b53fd4512c77e151", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xb0691b5f1206e240a263130025b8666b1ea108a35232aa5618cf8b4c3bf127a149f0dda5931d6a8cc7d061c5c41f5f601431fd3a41822aebcd005c8b8588d98311800f20866cab0fe9c2fca31145beefc1dd0fa5d057347488994d32e98525e6", + "slot": "12859213" + }, + { + "pubkey": "0xa8a05498cc67411cbc50357e2339fcd9af957fece470ae47f582f12931db7f2ee0adbb7a07329027fc64f55c91ddb8c7", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x94536a7c671e8748d5bbbe3a8b9cb118811618e35911d0247f64f0dbf69b23e4a8eeed8035d6fbe660637d0d55f90b8e0dfcb22f0c7f6b08a1c4095b745067a821109a729912f56fb5de35f0f9ab8397fe7e500e8a3ba68b115ad05707a37c6c", + "slot": "12859213" + }, + { + "pubkey": "0xae1397b6409ba9736fa7f8e8b9cc985d51fa2f5729f76868d5748edefeaa5f58cfd8c3ae18fbfcbdf483993ce52a1671", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xa0ee87601541b052464cbca57d705c6b45e9f9f23bc27d495200b09389ddd34a7582eb507512579c7b359e98831568ba0240979e89445071905ee58cf067967407a0d32d1b021cd0be54c62aee09fb63fcebe33f39a02172aa2ca70b5d63aa1d", + "slot": "12859213" + }, + { + "pubkey": "0xac4b91deece91c41fa5d1b277f4a05356b3a663f0182599c68acffedc74d2660684a15a9a06af5db28080350033ca443", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0xb3f0ba5f37de3d7bfc135ddc78c3acd3236fe3f5434a29bfa90da051e7c1cfb9bbf403729ebd2ee410e7c08132c5a9140a8c28460a9114a359964de0df15157a1e78a1b5b29970c12bd1df2daa601ffcda35775c1edef1fd3961acfdfbd72b1b", + "slot": "12859213" + }, + { + "pubkey": "0x818ff5dd8ad0c6e6819f756b7c7a96a3a347125684b32869c59624085d4417a10e1d7c270551ecc73015bf80bf7a6b04", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x8e46a3f048aa48791fb5cf52880aec5a16cbba18ba1b333700f406b6e66481e5116f501d84f20c48a72647d4ea45e9cc04cbb84836cc31f8d0c70ad22550bcbff1a0685d4821966047fb75a60ec7bf74b2db6721e880f503def37e44fb6c5b87", + "slot": "12859213" + }, + { + "pubkey": "0x8e2a4eb81bff9d92e55c12f3767accd6df562c2b9caed946a29b85ad359fab8c5e2fac2395804c627a628e2f3cfd8fdb", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x862a1679b4a3b886e77ab1c6253eae188e15381d508190430a0c99466b4717968150b8a48f7529cc27e5ad5d7f5c23a9152c7f5ea6d9bc571470db30e589302d2820b7c94de3bc9d5780c09f2275af0ad6ec106dd31c252b7abcd1b94fd76892", + "slot": "12859213" + }, + { + "pubkey": "0x9007dc535c3a2b745a957612891ebb6bd56726d69acd373897fb85c35dc42fd9acb52c5046ba5cd4f589ec167f5b3d6d", + "withdrawal_credentials": "0x0100000000000000000000009cc337313a93e7029b7304f6f74ad99fd132a7ee", + "amount": "32000000000", + "signature": "0x87fd67797cd3308d92b94922eaa250ec36627b3800ec5129ace7f933de56dec5df2abf0a80c2520fc48d29037686e5e2177d01df1374d386b69327fe56221972d78f994b3aebed50701819a0714275dc58db37e81f6e4131ca5dedc8960c78dc", + "slot": "12859213" + }, + { + "pubkey": "0x8bcd955f5c0ff8327abdb23452a2796b92e9f5ebbfa2f28165d6e7473f2c4121e5780631824ad025d96136ad5905dbde", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xac349ee761f26f97c05652d08a99b4a846e46db782e261a86e8b0d120b4625b6c60b3469564976371edae59c8040aae004ad5ca0fcbdc25a2562d9e310d0936281c18058e821467e25c99e05514e055ecc4a3816000b34763ca344c4c67939bb", + "slot": "12859296" + }, + { + "pubkey": "0xb688fd0f48f1d034fdd535c5307512183d923d6def5daa0bf23edb1daa5f352ebfe207f8febb3850488bb4293976f073", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xaff0c8b8ba702c23dc3c39abb7f62d516af7ac9af892458cf9bfaeb4ee970954a73fc6f767f57ff56a4154127f922e2b1462b81aff13eb91db12a506ca8981883fadac85eab529a9bf39c56b1c6a540a3cfa7a7b0735d0e5939798c2d51d7008", + "slot": "12859296" + }, + { + "pubkey": "0x95f78c87107cdaed890d260916083c8fd5b1f29599de98b4a4c64d28981c024eeaf5c7991faed22781cc4bb7bce2f735", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x82026ee886dbc9f5cd79246167e668ff6b6967f5b52c62bf74c8c31b7fb726fd8404d36f6c5ee73a2c60e21faadf7b331153dd4af455e527f3a0dbbda9a1af59b86d41d6b4950c14d8c0ef47f84ebdbc328521a95584449cba1e611562d0534a", + "slot": "12859305" + }, + { + "pubkey": "0xa819477698ca1a73022756ec39e4f45bebfef4b8349a67546be7c5e556b1834a87e6de9d5ad6c95088fdd49022f2ae0f", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x952d99f233cfc31850152a30b9fcaea65174183409f791cd8b1f0d54d104fc878fdbccb08fc8beec068041ec5b8a3bef16b7f70e8892fc24c375d89c66d8b29a983fb456b502ccfbfbcbf32ac78e980f3d789eddd528775912dec05f2699d5bd", + "slot": "12859308" + }, + { + "pubkey": "0x8bbe12beca5067a786549e072afcbc80183d3f50f214b6e551cb5b1d1529c4f11d8ea5aba7a3a4deb4bd8aa2e8741573", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x819c7bb72696f177e1a94ddd21980a1a45b92990a28660f1610a2c234e76e168cef4ecc6a15bd60342e72d3b56e0af1215aa30f07379fede12352480576a2e94edd46e113e15830c8f95ad7bbe4a22541377b917cddcf8c8c205b24bece33ae8", + "slot": "12859313" + }, + { + "pubkey": "0x90654dfd2b89b0d87ce21d07ac22698722a3fd8af880ca06ca83fd4b2b2c5b7844d040967cd0deb832bdef0985f77302", + "withdrawal_credentials": "0x010000000000000000000000b36e9d2786005235ba9036585ecd4c40174555f3", + "amount": "32000000000", + "signature": "0x95c882e8d4d95626379f4a4c5e8f6c41cb9a8abfb1de51277d07ebd483f84ad26663ec04234b6bed657e22a3b5bd6cd4182003879b851e2dbddcb87fb3244a97ca2a394251a7fe212d33e2dae3284419459aa49af589edf7d4d90c1482aef046", + "slot": "12859314" + }, + { + "pubkey": "0x9179546a8fee354d94c0ae7ecd51d89ffb76f16135fd65f2d24cc11ed7ebe94dc8546bc199d55abbd328410bb4bfb5c8", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb4f5fba2305a944dcea3ec91ffd114200e4ced7e0e34705f93f29bd6b8cd7a0d71b53332a4f7aa06d97847477df4d3c6017640196420a49cca561aa8cde307413d0ff433ddc511f9c05f8b87184b09266f2da5eceeac1ad7558b992efad4c40c", + "slot": "12859320" + }, + { + "pubkey": "0xb38fb13c1da3517bd8fc10cf70b1be924bb3f95b02317f7bc7658f8f97c6a2d13a88e25a5229ac7fd6421ffde4f0f8b5", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x87b7e14e96434bd43c0cffced9098726fe6c7dc6f3d618212f5742428c9aa01f9bba5701c9eefa399949a8cfb643027c0b9d536913d6b79e2031a587d6f045ea20fae6aa53a4a84be3d7e44444adbdc8228f1e4a471348cdfad2f7bd2851bd06", + "slot": "12859322" + }, + { + "pubkey": "0x89da0b4edf2e06c05345e021fb7cbebd747517428fdc67a5f668d40016594dce2f53a329d494c9402721fa8ce3d232cd", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x9978671939bcd6f268c614d6d4d57956954691242d93e990cfe1196a599af710ba43491c660fe3c21221e9ca6da2de9706d7bb919413a14fad2c04bf647e44ba29637e0985a3e02e1ed4e20f367c2a8ddeb8bec82de3b8a04ccf00d0d8ee1e7e", + "slot": "12859328" + }, + { + "pubkey": "0xa1d2370ff590644b7541c1a2346405a6dd20f602f2bf512ef727bb923f550e49b69ab4cf208b81abbe1329fc2b179fab", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x9276993e79b6858d8024dccba2b9c6610540b998db85a9ab09d9a75fa5f0da1c3dfeab4760d72670d914d9647bd1fa72014a632cfd1ef99f8659e29aa7e2854bc599f76fd7e1b35b48f8389dd4747e85091e7f165e419a863527f5768a0c7fe3", + "slot": "12859330" + }, + { + "pubkey": "0x8a39a3e4afd2c7693d61600f6ee7cb09767c7fe4ec63dfabdfde9a4d04033d57aa056c98fda79259562e96191ae97c50", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x91509b4a2d42d04a7811a272cce3e53058a95991b50bfabb38c7df39d00731723b949f7037962ca12bd361b5b21c2678050a8f47f4cd4680f052393bb17c0b0e9844013964f93be711fe79305d751e524df7ceb54bedadd41598edecbb93c816", + "slot": "12859337" + }, + { + "pubkey": "0xb39d01a32c442869730fb056e6c34d76252bcb4ad67d6e334396a00a724e915c21992446a96db694289173a3c5cbe589", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x802ce58896950a8c5f7e4890ce9205517518ac568b1ae68d80ec5f8201c72941fd141f0f7247f4c411fd6d7b8a898acf1405a70ef6ac5b8bfa326b86defae08c69c22eed5e6c04d3a70d30f59f141fe04192c2b5e57f98a73e086d4408b5ebb0", + "slot": "12859341" + }, + { + "pubkey": "0x9387f0d888d0b85da725829851b6627a0e4e8f79a9f4cb2e54e7e1e41986c81c854157c0107d1ae5c2329d83fc66c214", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xa44db45e2036ed5ad8bd2192fda21b5c4c57a108152c4edf5540cb857414daa87855a78bcc90d8677a76253c775d071512fa25b70264f60b3c1ad2ad749cd3ec2e49d584efaf3e0bb51127b042b409a409e40ef1ada8e230e52ff901f72cabf0", + "slot": "12859345" + }, + { + "pubkey": "0x97a32fc7ee0c89ae1062b3cfcac89e6713bf3dea3a1cf0c702f004c5d9a3bb40ee4f9c6655653eab182d7e7b59cf6fd6", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x97c1e2f6aa6c4d157a169bfa404a0f5ea1a3a1cdd1ea320b23d09774103e1da4a0a3ad7e6a57e0129a3f8b4581e6390202d79f25fe2d1509dcd075c0dc7bf5f10d639296a6059e80e64d1adfe4f0a39e6c5adc1849d008e3a14d1eb2f8ddb241", + "slot": "12859352" + }, + { + "pubkey": "0x8c8498a55f3086cbc150d2643077050c1bdeec51a0d8ff7b9a6e430ba135b8675fc26babefbeed2d8dc575edf25b42b0", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x8cc73f7ba79d50f639be5341443f937cd3f1c61ae92b4d3c90e1210747b0ded334873c35ce395a51d842be63ef337d461051779f4d64f605202e66ab57c04cee47e9bfb5b291ad478315008e094d04eda947af581745bd03bbb127ff3ca0d1cd", + "slot": "12859354" + }, + { + "pubkey": "0x8788d4369b70d2b066ffaa54a8f830659010234a58076ccf95ae1bcf5a9e8787b85439061707fa49d93c91a894d9e33a", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x84b404d71d6b6c31a694dd3ca9d50cd07c6a4ae5c62c7576702244010379cc5802f1478820d3f847c1a2d2690d3493790a69ce38bc2ea39786157c9d93cd661fe5efd513b5b02db93822c14b47e342c5d63fa62a7d5a5dd425a145799b59445d", + "slot": "12859362" + }, + { + "pubkey": "0x99486b3009a73797c602ffb66b9466bc778d72c7c1c4086ba7278b398c88f441efddde9f2c64d11ab0b55fca216edabe", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xa7038dfd5569cad0c4f7e4944f7fc6fe371a7feb3d55e92e5d287a63a70e3bbd8645cf0ffbaa1184b11bb989fc3e5dc6100a3226b9825eb6cba88fe19cebfa28c22bb1eb694f1bbb59c668ea301058568f18a1b2ff649394945a8f282504c6e5", + "slot": "12859363" + }, + { + "pubkey": "0xaad8bd62f4cbd02cc19a72ff5db7b6df570f404f7d06c626c04e12e3ae293104bc60db55863e53b204a634529f20d0e1", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xad30026f4c27076b71fa721e937e7a6dbc704a4a7e6a98baa5ef9c0cb88cbf987df8e2194b727dc983fdfcc21525d5c113e1fc0b3a730690dffe3a28a74283a12f0089d6d49c1accf5403c206d933ab62d979b6721c0932ffeb8be2e7e6b9232", + "slot": "12859377" + }, + { + "pubkey": "0xa85ca5de77ff442c3fc6daec2ecedfa97092951cb6b8a9020e988494a3edbf9091a2b5edd9e8f7bd2ecafe03588c776d", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x84acd7040d299c1c682493f622c4332bfdff0dd6bf5f79f61d7c5ce5cb8be4d824c234d1315a62d1f72ce264411d2de10cc9d1c21df0e633a3212e3caa0e6ea44049a031467d0fa9d156d3bd203bf5e5be94550f943db7b0a9b9132f2bd12e49", + "slot": "12859388" + }, + { + "pubkey": "0xb9d44704956eec471555d827d9391f1bdc8813a2a9746dcb41349fc3b0b28373d983f43306756e13cbc928d05697843d", + "withdrawal_credentials": "0x010000000000000000000000973846d98a952745f2e2113a76c5d62ae04e1eaf", + "amount": "32000000000", + "signature": "0x8ead0899eb504211e8ab980fac6c464fab12529d5caade19bedc4495941acd500cbd77190c8e81adc4714d17e31544ed0f65b2a4f036f3e9a3da63faa3b5db3081753f4f8a749715fca5d660aeb71f239fa62504a6a6d4fc8cde2ac431daf024", + "slot": "12859404" + }, + { + "pubkey": "0x8ef3468f924a665540a2d731d7b68a6c55ea0c883542f8bf73cbae28fb67352b434f35f03e8bae26abfbc63a6b408e0d", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x8116d293dcc4f00dbe8b05151d14e314a786dc33a722f7dc27333d3a8e60bc0803827b324ca92db7bfd132865d4101a204740b72002bc159756c4abd37798bb220991dce4464384e45cb6c3b8fcad1caa7473dc0d1d6d0d6b758bb58698ee771", + "slot": "12859415" + }, + { + "pubkey": "0xac4a06350eb9c7a7561e3cb415e7515e3757b2070ac30f807ae3fdf03d670cf0ea1c3b76419252c81ecb0f4791db5401", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xac9d1b284f1226b627aae6bc1a77ace2fafe616f1ecda6474f94c5ef4167d63702de7df62e582c9cb56251ddd5afc1f613a8ef5bf1e0923ab61346cd1ed1a8450bde898ace3352b49a5985bf7540d91ba75ecfbe09c8da1c531100ccbb15e920", + "slot": "12859415" + }, + { + "pubkey": "0xb7f66c8d461cb3a3c5fe63a8aae8ab59129851d5426aadc3e7915e91fb0d627dc386f1e4b3fff67ab71c15168e73a442", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x980c922329dd6f0d84428aa967c27beef5f27b57976c1a021aae20e162b8af714f542c13e77f48a2afb2cdb7107b3a080aa606832479c12ef4dd14df5b89797c49bf592fa933463a8f1a26451b98bedf3112fe3d416aa560f594baab67cf90ef", + "slot": "12859423" + }, + { + "pubkey": "0xa516b6ade3aff764c0a53197ce33c94af8bc6d36d220cec67af343db04485477a1e2f798c9e2165cb0d1bb76296c90f2", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb22086e0549b41119db8ea4535e0aa0633066d3f81823a9efd343688d443a6af8407536b41d6bf19840f7d6e29c14fb1133351cdf2c04c6187a157b7c39967ef88f43be0004ae6445192e012b36ac50051af8c4e94aa23159aea097d1c6849de", + "slot": "12859426" + }, + { + "pubkey": "0xb6e7ade2d43ccaf85080d5f0eba7c717067489fd880de533346db96e1894c6f3a12f5cc94f809bcb1af30e2cd16e8608", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x90e40ce7b60840067217a948657af7a9346ef527859a42fbf925251546fd726d2c479dee2f2d02a85a0b528ea52e488c04e3c0c6c256a4d90677210246444e4f505325aba79678a35a8b0f1c1d29facd9bc1bc7264a485ee523aab98ebc69cdf", + "slot": "12859431" + }, + { + "pubkey": "0x90728c5cfde54dbdfdf40c12442b142c5d4bcef91dfd323eea791908d0380137164a8b41ed5fdceb92eceb03e55f5737", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0x9600303097675cc0b0919a00d6c4c457b8d03657a23afc569e360cc97d6d109af8096e2c68e8b0a6e21dbd225b7d5f911343f1110d220c7be5254a2be758bb448a53e8bdc95a5be137c5362c8f40b2816230064c01361eafe09b5318eb87857e", + "slot": "12859432" + }, + { + "pubkey": "0x83e4f9b56221ff426616a38de24ccf0b63b4832881d2b45022d491e39e503332cdf151ca8bf7d45126b384568fe454c0", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0x97b32033b067c524c24f6f52dbee905658e12277a8c760964461d5c697aa7e4431ccc0a2cd7281ad7f968eb46383cd7913d6c496735c2744fc6e880619e8f0feb6b6dcae576bcf853d9df132b6170c3afb9e688cd669d99f28498566e3167c45", + "slot": "12859432" + }, + { + "pubkey": "0x80a531cddc88a206224065c87cdd9fe0b44297e9beaeaf9f448ec8e814d9c9bc7f0eac7c66fa102232a68a7ea6605d41", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0xb223d73d4c3c4a828742f8ba8eee7b7d5d8982b6e88ea3f09571d01e81338763e30c104e730fc3f1b70a29b7296b9de906db37af3113ba0bcf27e60497b5171022370c8a23626035112e4b50176611513da49e9b89d98e93704dc4e0d4c24fd2", + "slot": "12859432" + }, + { + "pubkey": "0x91caf45343d743d24beed50629d462dabe7ad7030b58bcc2944e3563ee037f4cd1011c3142d0dddd9264344046d0efe6", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0xb0c0a04dd40ffb00b353fe8af194fee91ea5fd3df13aa1cf06871ff98e9969a2ceb18b7a392b18492da027ec8fd0c97305a0c28f29c680c4b778c7a2a28234b960bf5a2b40ff10c6e6c023ae0758d86c565fe5d2a72c01b3b54c76e05beb192b", + "slot": "12859432" + }, + { + "pubkey": "0xb3ab537c6518cad80b594b8b5c702807b1301b6b0836422db3efbce8372cb70544b251b2040fc8ef18e1c7eb69663b83", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0x91cf634534c160460ca14a8e014e7f1ecb7f53f5eea33efa0cbd84f7cf4145b439b7259cbf1602b2280edf9cd89238fb090e605a339b30b57d1de0962b8d2c45dc075a8696ffc8b0679058a9294516aa64ffdaf9ee50ec9833c699caea3fc46c", + "slot": "12859432" + }, + { + "pubkey": "0xa861bf8363ccbc8faab40b5dc0d498804a85ca3701682ce9ad426ad1dde20bbe979eca9751270934276fd65cc28f0bf9", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0xaf921745f819704f4b2851e0336f5f807ab5eca80d538ab7748c9fdd1548ffdb9b4355ba4e0756d927c41db5200874c0121ff35e66d9e253409864ed7ffa11db471e2527e66a270c06b7bfb6505ec49427b024f2f99374c9eca148936639195d", + "slot": "12859432" + }, + { + "pubkey": "0x858f6dfae346bc4ad2f93192a4bfad8969129190ae6202c2fc83af0cc7ed3a44d8680f6697b1796ca042d6534ed762b2", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0x86c9208a310efdae0ec073028274e2f9d68a95a6022731d8c74b2bb095c206d4178fd51696d466ef7f80e774d8ca1bb6103afee019dc60c26e650ca89a7f46fa0e4c8360d8f64ebd56bfbd1592b18aee349b30e34e1238d0882e1ec7147328ed", + "slot": "12859432" + }, + { + "pubkey": "0x910b8302cc580eb669cdff26a7cedf239f8285cbae647830df364c26ffddabc20d9cb8df291fed86f5217732ad6b8e5e", + "withdrawal_credentials": "0x010000000000000000000000116ba62f273a0859b9b29bbe4adc310c749a5e9f", + "amount": "32000000000", + "signature": "0x8d6a14c1f7dc826c8a0382fdbc51c5d4710e1e075324e9ded1ba1ed3316dfe1795d4ee70ed91d8f307506265ac6bf5e0003427a3d36a279e15c619f8288e5abebec1d1193525ba1263b2e921d9782c335b4f5677b8d8fa37e6eafe91c93ed279", + "slot": "12859432" + }, + { + "pubkey": "0xb8217615f438bdcf7af2462eab5c38d76b5f7dca669ce4685eea96b903276df858b255340e8869e358d8e146720aab52", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x8eba1bf91ff1eae1de30cdaf2527f11edd55f266e37728bb963f2321ad8a44a5a2190a4db8411f562cb07d0bcf448b620350626202c18ac14c7bb01371526ceb59ca09e32ac01096f2230cc89612f8d877ba96b23647c83ab37b16e242c5de08", + "slot": "12859438" + }, + { + "pubkey": "0x9837a8c64934c7b42ca283f32a42eaef5b01e37d918d098dd949fb6eac237a8780d4d48bf65d222dc574edf48c928abe", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x9437d5c4a1f501ca33b53f1801c2b4b163a90fcc4aa1b6bd029a59337e65dd68a935faaf4751309295facf9754e24aa7197e8e809072c5f9ec88342cce52f34392fd29d63f2a58598dc77bff3fdd0f2602d15baaa8bb7f92cddf711b4ab6d795", + "slot": "12859439" + }, + { + "pubkey": "0xa050a289b0faaf5f0a786093f8ecd26e77f631673124bd6725596635b34e06c873020ccc2cc011d538346fca77169430", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xa0786c07c19021a71dfd2135a612dedccff0b7b09a6f36eb30c23eae31bdd65c666d5e452ee316048f3de2adc8680f1511e9056209eab34cb77331d3fcb79b4e34f25ea684f971779b7d01943fc2b131557165f3ed30969b46ceb84581187f2b", + "slot": "12859446" + }, + { + "pubkey": "0xa616877c608d76ca346249065e12a89446380044ad4081b1496b7c1a6a7cc5a6e4c96d47e6deaa56f0cff5eb096515a3", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb5ea4835067f1848ef319044eab17d29fcf5546a87544b8528a3cf01e7c69b6b343a501be7ee66d12c2c4a0073fb41ba0d04e4131dd97f4c3144bb583d5a3fee81c2e3feb18c086da05e4ce08a97af0ed7101a1dc258705035a2150f26158c9b", + "slot": "12859450" + }, + { + "pubkey": "0x98fd00d99dc57ad3685627254f80fdac412f3bc36f6b30502643ab7b9e628631d867f3c9ee8e5d214c5506f07786b6ce", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x9866cdb3afa4d30047107bf68e593e09e540dcb8b4352712da34dbd9407bcbe9bcb69cd32b3b5fa47e7ba2b0330bdd7f085d14382db45a6b09cd24412682ddc95388ea9f3b9ef295f70ee831c0073df3f49751646b2cfef1182b17540fe1aef9", + "slot": "12859454" + }, + { + "pubkey": "0xa1644186479be5638da7e3c11a9da31adbd6a52070f4b02847ce44207d9a9869066e7806691b9920491f4b9c3ca10f85", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb2d100d5bd2a2d41599f00db0e3c78066722af77661d8a50292e8c9a3c1cb1a742263cb97ab9f5da2eed42cd413edb370593d71e131ca8163853ec8f77f53d46e4f233a17ec65ff7a1099919893ddaf62de571fcb3c0856c52232891a9df3d92", + "slot": "12859461" + }, + { + "pubkey": "0x87b1cc1997e2c3891fbbf1d30ccc1688ca112850c05345b76acf2fffba7be49ee9164fb42f8cc084fcacce073422fdc9", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0x9823f0c10b703b50b12395dabf53488bf4e1d4f379f4b32863ffc0d20ab8f139e9be691b2a7344f390f1281f44a618d3155740fba3d555f479f8a86dd9954ed388cad907f0ef730ea4b5fe89291dfc4962441eef03494c3557b5a6a3d165b8c8", + "slot": "12859462" + }, + { + "pubkey": "0x89310f3e288c40a949d42e3180b8efe47536c8408029f83557a53228089159a0e377ed9a38ee9d3c41fa3770be9dc332", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa1895cb4a0a4c51144b7d212fb1278d7cf27f3678fee6297843e951d5d0b283d07c11ba390ce07b64024b16af8cfe964181f41f48d058aa6dd9553b2ecd02a9b2bb8c6fe823dd0c7a6cc627ee6f21b3e7be718dd072c7c8c6588dc1b1f1bc2af", + "slot": "12859466" + }, + { + "pubkey": "0x90c804362d8ec6642c1030d2b7255e25867267cb595adc683913013be1e9d285bf3b80a5450c2a2a6a0143dd66aad37c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xac30a43d4fece022d79d6c4b8bf71ae506d2396993c3ca08a23721bdc24fc5fb1b9195e738e3c386b9d86ac071f69dd1046ce4df3e043587aa02d683ce48e40d9b4a23c8a940c71398de6d28344dfc602d12885bf1d2c98b09ac2aedd1168c27", + "slot": "12859466" + }, + { + "pubkey": "0xa0f93bb32f9ea67c1a0b917612548c7600df26625599eb0cd36a7f5eb7440bb0197093227628180e92bc55b9a2e645df", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa49c8382721dc11392295a6a5cb06671ffcd2f29ea47ec796601aa52f41f810a09f63e44755e2e937c902dcd23eabfcd0d7aecb2461b0fca769351d830fd37915b675fab2ed3e3d008ebe4ca2a44616ec3d464bc77d9440612d2cdda0c2b3c5d", + "slot": "12859466" + }, + { + "pubkey": "0xb291078b98132bc7e9be52157545e03c05aca19fc3a75ba640063c53f24f9b61026d1deef3e3e225998d6e77da67918a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xad9942c2bdff6bc0a68eeac3343150a7d4031a51ff8e1e899790d418c79d2d41a98b87649038501fc004b761afe53f6c0b29be13f5c7d1bd1fbf46eaf95180b21a334249c4c5d306775bfc341eafc82fffbcbe1bf78474bbf40892009ddbc425", + "slot": "12859466" + }, + { + "pubkey": "0xa57cdac4445c6f64cf1bb677c30e32189b3c7de2fe86a91abcc99741a9742e51ee2232f2ee6b3c5fb4029a92743b8367", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaba0e13b0ad2ea50d3aac07c2228349e7f6b833d9b3af095c7989a99ad97cc6e2be7920d2ef42e7960714431433bc135054e0b996227076544fced70f13944ffb3ca8b67a77128b7ad4c24f9b2292d7f4b642536d8b451c64d355264fc2411c3", + "slot": "12859466" + }, + { + "pubkey": "0x8aeb966e235566ce541f8ea5dd756e9752d2586e08f8c3cd42e988aaadf854ede2b8d56810ecd9fba210368dea5c3de4", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xab1af44d6fabc9089d97323e6cb03d3a79f0623e6e91efecbf029909e2d7c2a112897069edb97b1fa83459c4cb5bba7506d8c67899b591a59dd6197037a8d6373e00a324114ddcafc69020286c275fec4ac773900114728d377c4397b49ba7a0", + "slot": "12859466" + }, + { + "pubkey": "0xa9bae29d0a0212bb6fb8fff6091a70dd588d8390574b607a4d4963e74123904d9cfa58c8f92826bb53aed4acddc4168a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb5bc73a8adc05449f25ae3b1abbd7a322d02487a68a03e30789d687915ed43e79a0083bca8580af5ac508d2c561f9820081f8b7b05c14c14e506b529d18c3ed3aa196e27119fcd683800d4e16e2cd30963c6c7d852a039a1fddf7b40325f69c4", + "slot": "12859466" + }, + { + "pubkey": "0x96f88159e68a329760c1fc6499df024f28f7cd38816704f185be46f5e67be0cd3fc86c1913c3437b0d7c7e81262bad2c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x939592fc8c63de55b1a12f0fd5b30fac261d747a6bcead6b8e5c38345915e6657cde7d53d64e41be18b7837ad743c46701218d56a93f4e2d2e11a353bd70346d09e07a8a1fb4da8e67c26b34f3c2d3e95aa236c10857bc5e801d779ad13b0df7", + "slot": "12859466" + }, + { + "pubkey": "0xadcc640d89b68abf573f94a656e8bc1319bee03c7b3e2b26a5ad02a4db0e561ab5c5142e95d3505e3a393deefce86d2a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x94d20a793fafea3cab009102361f2837ff25a7b67db3e58ced3dbbe2ad982bfb9ee8cd322c1343d07fe4c6476ebf5da30a8840f8b5e433c3a46443a190242f557be02f7117f34f80d9e18cbe53a647141648fa0a885006c656692bf0dccafc79", + "slot": "12859466" + }, + { + "pubkey": "0x977bee7fab42e8a969513768e0ea1298b7c48a38f9a56996547deb5df4ec7eca102fd076b406c7b82aeb8e87d83e64a9", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x80d9438fa8cae66f3c4847201efa49668118d17ee855befa02c25fe7be174ae786db4a0541fe84e85c1ff27ff80224760636a87e47341634cabc547b9288059ba84844da167ee88e73cb964edc6cdf22f95b17a667a079b1142105ee17bb39de", + "slot": "12859466" + }, + { + "pubkey": "0x87f095f0797d2f47587ddaf340f124c99a6642dca9e15d27b5a7befb21b3feaf19f4a29adddde8af5d3a89db0d236b34", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8081320d99655ed79712af93cd59510fd8c737e1acc65dcde006018d79ad2d54b353cb4440f924ccb050f37371b6df2e19dcdff95ec4643bf1c9b4ee40b8b40f7480eb1a1fdd116e1c825dad02a9a31a6c1347766e7c19fdbdfe4fe12092ddc9", + "slot": "12859466" + }, + { + "pubkey": "0xb23be80803d66f4a26a6da2215fde1be659e27e2a5a755b4b2f60a93d8e969635faaacdc02fd1751c8a1fee73c7e8b90", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb01539e9225cc1ef0dd612bc44e770cb4be792be7f827c943497632a5840915d2f8bead4aa9dc5340a2fa94163bd2d7e194b3a8dd8d8dd95aacfbc5df1eb44191d54e9abd7fdc2bba71caafb0ce5c9a8afcfefaebdfc4edaf781e8ddf4173ba9", + "slot": "12859466" + }, + { + "pubkey": "0x96f48b024aaa2f0b1d28d6c8c32e0e65ba357777d18993cf58a6cb36923b82b72b8a9556bc3d13076c867344d9573eb7", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa74e381994e6c007e8218e31d312fe9abf3552f04217e1ef7f3ffd68d72b48f4a4bf1385f6aa826ba5592420d750732a0e247f31c975322ef1d1410765067ee5b4dbe770bdca387153d7ec8ed41a2f876e2a3773327adaa65710f87c27419ea9", + "slot": "12859466" + }, + { + "pubkey": "0xa546d455037f2348cf2f7bf28d30afc3cc15bdf22c5e20956feda60124387c2b7a3e0ce4f89dc2d0fc588d5f89549f34", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb8c47a55211dd2f7dc86f7c0fad1d3cdd4c450014663c1baa28195abdd0828e4a8a3f04cb960a1f8cb511d872066a2ff0b7fa537ba9b45ad4ff0f12d9bc1f835a6b5b12ec289487fc67e4ae1e3a52c38c9cffa70934caac9b5a4575caf901cf8", + "slot": "12859466" + }, + { + "pubkey": "0x88e8dc29f30180ea4daeaafc48d1685dafbc6f933bf594363309ed790f4ea5a0166dc66dc701ccbd936e6b4eb886ff62", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x95f009bf76c79c655f9172235613c53c4c427ebf115c790cedfd900f7eb28389c1c433000a73d1146e9c677c0d6ff10d0f66ba1f0b63cdda773542f6ca9d27de2bf6a3aaefe866f5fd18077f435b6a8a815560747003278c638da04b7cf73d40", + "slot": "12859466" + }, + { + "pubkey": "0x88a24dd41b56cd01b813d006b38330d44cf512b458bc1b05a306cbefa4ecd13506297302cd7a0fcfd7569cc42ea2ffcf", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8a9792595066e89dd11908d215cf273141aa3234b33f2413493e9379e7187ddb73a1c21f43ca1b1efbc3d870a47a0f020f26fda82e0800c33d774afaeec991ce4e1100da8c951ad6d1efa08df12a9c5bcc3710dc64f2c8faa9c87f4c846b1b52", + "slot": "12859466" + }, + { + "pubkey": "0x89bc9e4f77d94236f565b90c5a7d8cca2fe060ebe8a36f05549623cd5e73b84ed21bdec17f6252b54c28c0e83a015d6d", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xad5f831bd6c1af16fc4955051bfda0afb186746742a9c4235cf8446e7d4e0efd8c951f367d6a102ced3953fe9799ca7701a5635a91f4389a2fb27e4b2c70972d9b865d437ec0f7c23e8d21a1697ff2f851125a9383983f3ccd76500ff7716899", + "slot": "12859466" + }, + { + "pubkey": "0x87a626cc3092200f8d1db14a0b66b0a1a8f1bfe8f8d10660908accfc769a8f7b522e06472e2a4e4787e004a4bf905470", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaef85c95bbbc17a906c45ce32a97d4e6d762123bde686f29344f63ee065745afcb5d02ca6ed53d1b9fc0a036e01263e61251294c6e53b2fc797ddd9694269b65f9e29d5cdb6790fd57a7d9e8cf27910f1910f015ca1d9dbf35cb79ee75de9e29", + "slot": "12859466" + }, + { + "pubkey": "0xa58c6ac2bd9ed0fe351ba6d489f451ddf8098b201dbdbe3164314a2f5b37c3e5e812efa990f57864bbccff2881d47446", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb6a5789043fdd5b358a66843be5e7e58070c089e0a2840eb7c3ca8dc3c59096267f50fe2ba2c72c7dee56d293ba69a44016e9ad5f145e37be5b8bed1794949cfe95a0718b9d3af13054f391482eb78a10794ba18a670a8c97d6a87ad45fd0bab", + "slot": "12859466" + }, + { + "pubkey": "0xaf5766b8279073887268148a764badb75aaee3f4a8be97dade1ceba463c4d27fc9972734873b693f7eb35ce324cca1de", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x801c4465488b071471e1dc7997fe4712d63d4bbc5d6033abd290fe55846ce2c79cd5c8a2c142c3f3579fcba14e922ea90e1a3c66c97d2fe8f1ae3a98c27ea9bfd943062e64452ae2463af1e6f86ed2b91708918ee2f3f32683b662a1ebcc2953", + "slot": "12859466" + }, + { + "pubkey": "0x88e19e7976ad91382e757263284ed4ada65506d7db9528b2dd22fef82c1b8dd18497b56a2bf6be7ab07cd858e9b435ac", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8bbcf9807928b2e34687ccc041112e93d2e28d7b962fd5c321a396d74aeaea2ac1273cb8d455e242ebd981e275dbb114170371f33d77629330b47b142d656c33f8d5e139ce403355ed188ebcdc06229bbe577685e09edc0590f99c433f3013a1", + "slot": "12859466" + }, + { + "pubkey": "0xb4cf3462b02f30e5f1773d74b8cef5471597fce31237d4320a4f062348546a5659fe6be22b356255b476638d723a3825", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa595aa39b8890db89ae38fd66703c8f7793403309e2b573e044d1837a00c9f2616f9a28114425cc26a6db34e16c043e608e3661a2d116f7e810641d2c1d2ed605f599cc8488d314fd1eb0a5cc1363f7a3d0013d4dc9c0f38809a0937ff9576f7", + "slot": "12859466" + }, + { + "pubkey": "0xb82bcaeaa5c35714e47a5188ed0a17ba5d09a53b01568def0322a14a5a2634881d1470ff9ee4ae626a45f961b65e764e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb9d8537482dabb08472538e1030e7ad5d7db3b52a2b002f90974d62c1158d6b4d196e01d7ca52dadc7bcefb0a587d16d1892fabb5defe39985c41ab6688a8b581a2bf5bf626de3952f3e6fa19f77680dfc85da0d37e153f07b1035e96342b508", + "slot": "12859466" + }, + { + "pubkey": "0x82e234816553e7b306023047929fe261ba7df1b48a262c96ec07f1d4d28eaecc559b4de0b60be6069503b994022a7802", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x86622a7eb516fcc5e50e4c922622f02c891f69da0badd01129a589e8a964f4317d9783d2ba6515f954074aeed99ca78213051a3b0a388ed0787fc8a492f053a80e53c457a2c7385fb0419b63e4b2c56d0bff92e7d9761ffabbc358fd27f42cb0", + "slot": "12859466" + }, + { + "pubkey": "0xb49f75ac6215ac5d5df747301e8e5b3b3263670bdee18bc2143d05c46bac7a87f856147d23b04cd348b949b36c7e0a65", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa66cd0fbbd028dde44f96d5359d0fe80da0d64f05fb36be329f1c80c4ff4c20d3ce2b056aef426353376504914e5d78606dc304b0245cc5fc40f9a260e4708b5592f63f6e4769188aa4a819f79f29015bf4f3b860f12a908825636a9a7e325ac", + "slot": "12859466" + }, + { + "pubkey": "0x810b848de96454ddc71d4e7fab91f64b8446fa089e6e563fb321961088d42cd53c516e68be42d62dc93b498fabea80f0", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa919311d4edc3121c18723ec02e8be2cd7e39498b5fd968c30950cded2af916c71ec36cc98c4ed9b0d23662418d5622d0a674a79ba92dccc21a029bbc4b9f6e2cd19d63b919abc4f122c28efaee1eb63c91a086d186e0accbdbc5ac653787aec", + "slot": "12859466" + }, + { + "pubkey": "0xa887d4111e214a32dacbe33a88c66b8504e711d8fe05c89330fc7f2badb9064761f2210a31f3ddcb6d9ba0cdf9f56a6f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x9714ccfbba269d7f948665ea1096b7b68b7e626d7c2085ad58f5db46c0a83f57eee86e4571359c1392c4f8fb6f2e5b100fd3eb1396841f1f8531a14ede161fb79d8e4e26875e5dee36806aa2c812c26f2d1188470f2e122dda97fac761822770", + "slot": "12859466" + }, + { + "pubkey": "0xad22f46a9879370be49e2346183ef7333b28d37ce48bb5b6e03bf3354cdbef6eaefd63073c1616c6b8b515ea02ea52f4", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8c0fca0e3fa3ec2c9dd43a12fb14478cba13934547fc1afcb705818e7073fad1a8b6409173f82e6aa6e2886da65bfa3e07e6a832cd74e14b0385e57be8c088c410af04afeae61455d4e3a5552f4759fb576592193eecb47d4846d426201de475", + "slot": "12859466" + }, + { + "pubkey": "0xad82af4c9b772022ac3e1897741f45afd4b0e479be5701e0262fed714308af9f78437b0ba4e45612e3564275269962bd", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x897591214dac2649e434cd7c686580f2c385269694247455c585f02c7934b13fd004c30d0df86766eed56685fe70ea6c150ae0811730a441936177a1b31d29f5ddce999e9910ea89ca5bbd9ccb2332d1cf313f2b95e28e2c583a7a460abb2b03", + "slot": "12859466" + }, + { + "pubkey": "0x85bd72e371314464145af02adad33f294c3268e67e0a80e0d3cd58220708c08d17fbd699fcc2eaeb628853c64af529aa", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa5b346fe3e8685981baf1ca04def353741dbd7983f5be27a0eeb6b0a4a652c3f3350e32e649e7b8e2503348f5084c1230114c52c00256a9ebfe95632b9ad3e6cd6907f9a172520bb8453e97a2219747d0120503d3c3d805b80d3b36efc16f838", + "slot": "12859466" + }, + { + "pubkey": "0x8b6aa1163ebde0c98970212e3b06dd640e077fc62e3b4689d27b5e70a8a2be4660d4891a5e28e66664a63389e53e6560", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa2986653b01380248c48f1f50a3726916aae880950121fa7a852b09235de80f5324bead2c72533de6880686e0099adcf03abf8d5587fd4dbc1267c06e4d5547f525da2f5e2bb714cfebcb35b994bad90be8b4c05670ef2090a00b7a35fbfd36c", + "slot": "12859466" + }, + { + "pubkey": "0xac1500a9322d747d42b85c9b50e3db1389c9404bac7e912e732542cfb0621d5a8da0388c682909adc58a44e47546608e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8a6b505289f44ac58a05bf8a6c13342db5822c82e4e20c0890336f100a5e0cb0988cf596a94334a28f068a6dd5ecd83a139983da023a236eeeebdd0131588441f687bcbd01881765a6f1e7ccc0cd22089003504eb91e2cbbc9a34e3465966305", + "slot": "12859466" + }, + { + "pubkey": "0x929acbba9c524876808d1ad7275d87a85a49bceea599de8b799dc7fb854b481258d78ac804f514fda41f8aa3c2c992f8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa18c848732498d1153a1a9d82f20f88138c8760a0b4c326abb74afea326e8d69b27f672ef5719b308cef8e0c78b5d31d0796ae378145ab38f253dfda161226a72209fd7e1f9a7e7cf384131cdecb81c1e8cff241de7d46bdd6514433066f2d3e", + "slot": "12859466" + }, + { + "pubkey": "0xa19c99344c3855dcb448e43328ae06a40849cc2f914f9c29a7800dbab4880a419421da90ac0495f5f6d89a2bf8b62900", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x847790abaf6703f9790610c15027879a8b397c848f58993e2dc44cf8a7ddfc08ab210bda4a6dbc6f524fe4e7b351eb24179e2a7d079e2a56316447c15abacc62327b76e211c50911732164e8eb17c2d83f50c2b3cbba3a746d713efd33e6fcc3", + "slot": "12859466" + }, + { + "pubkey": "0xb9eed8949f83b79017011e3624ff975dff6d427644483560860c5d0d78ad267e0c4ff09d96245e7fb753e9e4280b9cb4", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa1c6c0444fca10d0f694b728e8ade542788bac47e389bbd5392cff67d467eff4d1cae760bee940c8a481d904c51ababb066138de7e75b79de3bb79c57d64b668bfd12ca8b91c80d37c8b2b0641e35ef5e686177cfeb1bb951aadd5a441bb3d84", + "slot": "12859466" + }, + { + "pubkey": "0xb6248c5ef9bc2bf727241452166bfa76583877f629c4e10be1096bb3da2b2b7e71f41454d7f723a54445ddaf001f3e38", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x93358544b4806331df90cfb9162d56dad54a061d47af6ac0e3ce50f3cb538d7d5ffa938fc6eb15dffb5160afd6c4011400ddee6a9e1407086a71aadd6c9a7fb02aaffdde063f401f68cb898623a3c4975c270a07d69a3b10b73805a4eebf5058", + "slot": "12859466" + }, + { + "pubkey": "0xa1c2c5deaf36a29eb1725b7435754200b2c7316e7b9bb12435bd47513a74fba8b9e9357e9ccbecb5a0a9623886fe3bd4", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xafb09f5df403e01a111fe36c95477c51602dfac17543bf737a17dd70ba6390d2e17a3f75f91801dff3e5268153799e631077599755af25eccd5292072adef0220ca42a4a710250c83a60dca4fce6ba978dcc45275bbb641d062f45b0aef46005", + "slot": "12859466" + }, + { + "pubkey": "0xad1eeb428f7a2a04af5f55964e0c7ccfe0d9845c5fd7b33ce0e8a24485098b0db65254e8f73c08a86129c046332f5cc3", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb13f7971438ee341f8fbd27338d42076660edcf7237bd082418d8de7f3845fb0119a0f882b90847fd7ee0ab8df17be300c01edb29e97f9f1da35b857fda45b31ff81644e1da6d17ec2ff7dd2b0d5ca7d912175cf9465ce2eebd496398524f546", + "slot": "12859466" + }, + { + "pubkey": "0xa4b438025f6def42ffbd2edba04020bf4b79053a40870b397fc9c40707fe8079a771808a57ed9c1ad102a8df67743425", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb76d44cefe820ccccfa5a10ae419a1ac6d36ab68e26829e32c52f032763653f830296fb43ade78d794ca1a29b5eee37d0d702ff1aa7a133390dee3bc03b0e822e2e7f447e9dd254fe9634e569b27f2620b276fa6db945ba9b84e871710ad071d", + "slot": "12859466" + }, + { + "pubkey": "0x8b226ac3ec6e839bbe01d9f6596f585a203b64215b1c7082d87e38018325cd9601eaa513466ef8faf0721c3f50219dfb", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa835ea7d78e6fecf6367627aa7bf9155c5e76ffe76e3d3c507b4d930e16e465036940c301f4f27486349168b78d494d911a63939e9c8ed6b4914ae8a2d76b4843376abeae05ea7b4cc390bc6e7894b22049bc2c3b537366a34e3e28d104b7dae", + "slot": "12859466" + }, + { + "pubkey": "0xa03470d0076dfd309e6a80b8dc8991bc55a38dfffe147e62047a5a8686cb04411184024ae335fb31ff4c81a3c05dbd84", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa2f578b177e323b9595a636e5d825c3ffa6ee1e67a600b02d67fb8fec384759a9af1af0aef5ad7ff2f6bf42fa05708ba171b3fcb033ff316cba1442c922ea2314b5297aa5f2529308e45629cb16f54397b519716b7e38d4188ed1aea5386264e", + "slot": "12859466" + }, + { + "pubkey": "0xafaa2a2fb6c391d4815146325d138f87823a25d3915a94ee1cf35513afb39be5b712636218d9c18b87056b0d188d8c13", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xac40a450ac1727d647387498df2498bb6708ed94b34d3e478775494937267540d3c777169e6fa4b7380a7db134b38b8911b34b07a3c7f1032463e115cb096496767a8cd8fa4017ee8fece2885c44dcee0b351715a57ac0fb677546ab7b53e4fb", + "slot": "12859466" + }, + { + "pubkey": "0xb7fe762d426e309143798b57ebf542841759cb41742134f2629503ce65dd4996776a3874a4fe0f243ad698c600a35014", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xafda58299282c6199263cf9a6575c32ec2a12ec19d05b0fadc32a2ebbe0045db738075867cd296530278c3df7a5165a51830db5123770604fef4266c1e6ed649ae1e3d66115b6ea51eba08d6397db6fd69f2285bfb0f60402396981653086c93", + "slot": "12859466" + }, + { + "pubkey": "0x95421d73cd65f0523ce411af4c9d9b689e33ebc10b5cdac93ef9113badb5b11be8dcdb1d1dbf24de979c62d407b6b1d9", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8f09ec0a37fff2a28607be665af70043f23904ccebf93c66590bdb45dd00175dd77e04c23ababe0cf68af243ce01bbbd0cfd6a97d5d53dc8a6ac53188db0faceb402c7de051cec34944153be00303c773c8de3820e2e495dc8b6157c1bfef3a9", + "slot": "12859466" + }, + { + "pubkey": "0xa3432a80b08390ae07bb10e66f27d776258a02db96ba90ac624470bae0b4ea8b7e1d17cb622364c2dcbf534a7d562305", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xae6f4c3d340ab78e3f90abfe963d7dd31d5f152bf75229a1a629e44cd00d7c31b739195530fb2afd047e1636457c1d2b100a9ab374bf4dcbf7d0ef9dc67dd964f24751c23bd506d83db93995d5de5d9204604ba950ff47bffebb52f0b5d3163b", + "slot": "12859466" + }, + { + "pubkey": "0x92bddb29315a6aa269e028e6e79a6655e2c716689f4503bc852d2ad2c5e1929587b0307cf0da7756e778c61ccb2540c5", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xae8a3671df47ee8742d06aaf2482b9051068ccd01c86e7e616509b20317480db0bf52d4eaff9bf52039bb7a1cba90573140d4c274947e587df8ff35c93eedc2b62b34facac6b931af7bb2edde3b7c21e0923471e52f3d828e7ca07e1a5dc8fc5", + "slot": "12859466" + }, + { + "pubkey": "0xac48acd57d2ba4307deef1259cb0138c6a382dea4f6fd7f6947f7a952c7c280a3b53e4b35d08e8adf8894f9be1d611a2", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaecc4025906cc77689ca1c8a04f1bfd50e5dcc41f03830be7a196af4c2a0f979646fb638b6832433d9664cde8a19a10b0a31d130b4be364d0aa2b350ecb83a9a307d8e0cbfa812b9fecfd4805fd242e5146e6f2bc08d27cf1596ff8787c2506d", + "slot": "12859466" + }, + { + "pubkey": "0x8fbfad913e56c63f01aa55a7448ecdf48e1955b48bade927f46482d1f9414878705142ed63fa25f4a7275f0e1d6cbb67", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb0076d2d5a91ae7b8a6da883b6fa66e0b90f102748eb6d4b2ee08a8d9f8ea2c68e48f81498c8b3c72a53ab949706a695142977b0e7b6e8eb14f1887411a76e540b1f853a35c88534f7ee3343983e7921bda9d93240dbe63779b03cc9d3b631d9", + "slot": "12859466" + }, + { + "pubkey": "0x833fe3be25132a3a1a94f035ea19ce2dc72697d892301c633d672b225dbc18b8f78106b4cacd531c8527a55068bfbfb3", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x90fb31404f2794996a7845f4d3b14ea54e67e952500f98d0a510d9e15e9e1282bcb821f8b4dd8c4982567c875c4529790e8c455d2bd48d1a248e937269a9708d99c61669884c9a9c4acde9801808c80e770f41f955294895303444a1b9f5083d", + "slot": "12859466" + }, + { + "pubkey": "0x84d2a53fc0ba55b6865d5022aa785735f98ac408cd639d881dd6a80c961a10740bc910c7a8fd80d832eb61e4ea885200", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x84561cd96e782e401995bb2b6632a63a14fb233f11fe3a511f29ef121846eaf674af92b14c3dcbbe6ccf72100fde64bd0cd9affaf7e6370685aae659aaf3197f78d95aee255e42ee9ced433a74e4e2a31128d6c519df3c9ccd660bc206954778", + "slot": "12859466" + }, + { + "pubkey": "0xa5e3279efb648000daf005c0e33e1f6932b462e715214c1c919823c08142549855ce886304292b5627a9ae9ee3374694", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xa0e52398f6ac123d1e3e6fe4334a78e91f90daa6c25c31abada40bf1ceb6645e32dc7e4e2b1a3d6daf1e4c778e7c079604bbd103bd2826ea06db22ca47a12e36d73d91a1f5b51da2200dce946532a4c5aeb3d241cb8f3f6bfa077e7438ee3029", + "slot": "12859471" + }, + { + "pubkey": "0xb42de09040ed584263409cb84442dc88bfe9cca00500520dd79f055a58049fcf9a3a9c052b6f0c0b73163d2ec43000e9", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x80cd220ac1ef490d17a42df9e9d277fa13af7f5ae8a9a61f844660ea3f6143d9b767abeeac94bcff1bb764f66ddb5b7c0984da13bd3e1071fadbeca217efb4f874a14fb6cb6cba20e1eb5b6e38c27cab4e3ec6aae66d8f484f0119d1ca780490", + "slot": "12859472" + }, + { + "pubkey": "0xb596162560b978f0282a1f9bfd10f61c0cb07dab0f0faead8c7fde2397aede2e2d96447d8c2744dc9cee2de4b2e877ce", + "withdrawal_credentials": "0x010000000000000000000000c494588aa736c18bb1e48204bf635d3519782f57", + "amount": "32000000000", + "signature": "0xad458cc60e89dac260c0bd40290a0369477a4356beeeb66d56023f35fa2f2eab9f18247e1317f802f60053de469f276d0b9ec5eab700ae638c8c0aabc9da5a072c1f86c21c5f68697fd4b13828f7711b4b75bbba0305a4898369b807807eb480", + "slot": "12859478" + }, + { + "pubkey": "0x8bc439b5c1dbbdedf2a58090d8a3a9cc51e9c72cff593579287e964bf0b63a3fc9a699eb8ad99c656802737a83013f9b", + "withdrawal_credentials": "0x0100000000000000000000001bb8a806cba4ac0a4e4ed89008180e055dd0098d", + "amount": "32000000000", + "signature": "0xab2b4b94492056f82a080722779fe4cf7c9a6d56d3367be8066b52107e61293d462eca90ab34648a3d5d9a830aeeca8104929cbefef856498c9552e8b7a69c24485483ddd7453b52c36325ca2cdc83819b62a1e6ea6d9bc5e67fb128ac08496e", + "slot": "12859482" + }, + { + "pubkey": "0xa1ea259535032deca4281f79deb7e0d3da62adc4ce8be7e76d890028357472179e58d956f7a732585c99b9df65759b89", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0x8c220029c4211e100ac54db579a0c6f135d9e162f80fe6e1753e4593f6a69e69cf8e170e6f835d4a203aeb0403e530cb130dcd7e688440266a743ad927edf32c777bf40870fa3aadda2f951a990c9b696020363fbeacfed1564d0d1fb62acde1", + "slot": "12859484" + }, + { + "pubkey": "0xb05dd03feab18f682df5777a3bf1641300b28e02c347b38e61bbb770404903bc6fdf02ee1fde9f9c09f5de4d772eb3a5", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb95da73f4046cd81f36c8845d7214174faefc3e63fa2d4bc28be66b9b7b740faacb7c431e7bfdcdc63fc495f591f2d4700d6032a366808d4c8061bbc70b093ad611572571c763a481abc780c3cc28090e591871a02b401584c3a37ebb75d3602", + "slot": "12859496" + }, + { + "pubkey": "0x9751d1eb52d2f05c5921fa9bcaff9c192ffeeac2cd98f788900717166141eb21841ad049e7ab36d13a8314c1c63cbd5a", + "withdrawal_credentials": "0x0100000000000000000000001135fa96848f34bff9d003f4c1699ae97418de29", + "amount": "32000000000", + "signature": "0xb52bbc0de1453fd429e67bf1d9ea0793fc0f8730c71f027af4282bde1da5e8f7ec8c9ee782fd84eb233ea3dc9e9311840e813243645221b6da3a4033b4d28f04b79c073b1fd077417b69559b2b928fd461b51d421fb036b2423bb5cab2a6758b", + "slot": "12859508" + }, + { + "pubkey": "0xaf8b53b3403158497c3ec39c298f0d049fb309bae9b2a3f58bfab8b36ee568867f24b9f03e87d1e24157d06b2b29918c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa674b35599fcc1fe50ab94b07dfde8c3ad70ba7debe8ca0d6eba31a3e60372406dc7416d020bb60a380c78517a65ef9313077af8a5dc46f14b1d5e0a7f3593c1512d5ac475b167050a656ebcc78b37bfe67342719847b1ecb80191c82b6806ce", + "slot": "12859561" + }, + { + "pubkey": "0x97d6664ef7a9ded3e1ae3ef1c101d807ffc65e005f6ab72f541d112ddb0b9bb43e7232c60eb6d39b8f8a2420f0d6d084", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa91e5894959874de26056eb69af2d61db598dbb09c730468a7fb1b86194af2bd34d628b2d66f79910820084d3276c371106cc7cd3bf96a442c33b66f959240715a5acac5d53f50f144217165dacc83a3fc642f834bbf4d3df1e1c58d972a539b", + "slot": "12859561" + }, + { + "pubkey": "0x864b417c9066e7781624b865a3f45b3541fb968f9742250b679d0fae397133e083eb0b419859a72775a3bdddb2fe258e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x943628fd59c98d7dc3e272bfbda5218ddd8007461f448923220a444a5fa282ebb6a030184d9178d4623c3603748225c212b7b9880fa21d635634aab1236f60fb8d16ecd4122604c5a8078a09e9d1fa00930205aa2028be6ffdf40fb0a2450cad", + "slot": "12859561" + }, + { + "pubkey": "0xa38608bbc93009d76183c733fb858d6e90f35c0a21f9b193e667980221a36307d022dcf87b2923888c276612092705d8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x949f48687a97c8cd0b7de28207a21dcf06141b05936db3c676f98dc87a3055ad0a242ceec56aa77ed5ae3bda791d3cb9128f2fe51286cde1da04172a6642891cd8e4eb28cb38621b25f72f8b72e73f039385fb55dfe464f536fa9f7fb62e972f", + "slot": "12859561" + }, + { + "pubkey": "0x8e527190f15db6d7ee527b9974bf29f753e6f5e4399594a83d85095aea1582b3f045ab6e0a046f4e9a709e6c6d37ef2e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x926c992749b5517727ec2e747d487ded220f43340c2f702e07f1f0ee91f5403524bcb85724c57b746f3b36d63fe1f19f0c0335543f4108740847b2008194917e2e630c7f67fd2a127f81043489a088ec78fbb59f78e143de698d60fc93c4a38c", + "slot": "12859561" + }, + { + "pubkey": "0x816f74928ed04e9f78d83256e427f7969309ee34efd02495e4440387641986ae77131f2f85acc62e0f206c4a0d4d94d2", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa971119219fabd1fd4762231bc7bdbcda9605f2f4714b3318fb437b14fc4239997110f3274a512702985d81636cc1d3a0b4f7de5719e795e04fd85b8bda6f801caef139a391d767715db78ab068e232acca9dba5cfd02ecdaa84490029963446", + "slot": "12859561" + }, + { + "pubkey": "0xa727234646a1f2a2f8f366f3d7186a80b14683dd26594b150b761ad85c815684f7a6227e6aa17e5d0fb26ec359d9e159", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb16705515ac4747d08523cd83f9a96efef8501fb737a644c6477c8d7dbf96eba068a7e3b404f894bd1607860e59010761791353a4aed5e2d3dd382437fe6f24bf0d8649d339e198a8bb7b758c4246fe6145de20537f89d7071ab2070cacdf2a0", + "slot": "12859561" + }, + { + "pubkey": "0xaa279d7341ab3124266d7ce8639828178523ce7bc3bd32dfbd7399354e16ab90b05d94768fbc03d3125588af9522d2bd", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8e68a08f418c1c625dbe30d9fbb637d0d140e6e29b3f6acf5ffb610d577d74ba3aab9bf3ba16be1cfdac276a922091730c42d621f2b43ad54098f25519f465b1dd4166951e83f33c72b018b557d0f9c53efa05628ca44e8d6cbabd573472ad01", + "slot": "12859561" + }, + { + "pubkey": "0x9697cb04f5527653a3fed3d64aaae6cc73effbf4cb9cd8009909923fcf9213ef961059078e53d7f1b889b809e5dc3abc", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xac48a1b79476f509617d9329049957f27c4f8d64c53992d2cf630660d9cc744e203d378d2b81df4e131f6e64936dec7b084d831811d13ad7be37a1ff214989a86726e6330e898e6b389f9cabdfddafa91d8d3be0759db408ea6a7a39720a7fcb", + "slot": "12859561" + }, + { + "pubkey": "0x949e51ed47ef811dd68880d12b1a4b03784c7dd2628b9ceffab44a48e9640bedc46a16141d2eb534a46d5e340160933b", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa893eba9968e7c923833b927075563b6617df43e04d1d2597bcfee2b8a9b919cdb3bad473b0cc1421319c81fafc34dcf14438af1c8de8b8b22ab08476fbfcf74ea9a5f87a60155ec04fea0d4f87a39f54568db2aa508b050a9f1c0cb5c9ddbe4", + "slot": "12859561" + }, + { + "pubkey": "0x8a76f3bda58e44d75cb1a5d41fa6696c71930fbb14ef127422e591288f21313bc44cf3017509e26ec14ede5b99006030", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa9184e8c8d9c49f4e8050133dd9a44208d0fde8842e500f0133ba567e1349340f30cac093d7184601206b254eb74e89c0021f754d3405d488323920692a3f56835491c77012e9b7e993833295277b01ced6db524c87e45d21c9a6cf04557d121", + "slot": "12859561" + }, + { + "pubkey": "0x94f62accde956fd36bbae8cde87a7013982808a226403baf6720604351691e1c197277daecb26ba1d4cc4fb96a1f6180", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaacaa3a8afa86cafecd8d9538871ed8d564e870185e0473e1128b471970f33821466f67ecb242f2b6b726c1315791a1012f38eacc8caae988ecb24189710925ffad26645b52268a0a509b367b23e4833d569f3dfab4d796f2db46ca084f513e4", + "slot": "12859561" + }, + { + "pubkey": "0x983f06b4d9b8e9e1981e7c030d87c2248f81cafe06212ff5936c55d69a07a7557dfa3e65dc7d6fdd82de2db100406335", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xab91173fbf7fb766c8925453e512bb718b798b6147d7edd064ce94d7917d57ed0bcf634ed5cc7b7347bdeda7619d9cd005074c60097e748c29648d373ae95fa2cac82ca20b978747c2752e1564af5212ec6cd2a589d78008af1f91f8ad7b3fb4", + "slot": "12859561" + }, + { + "pubkey": "0xaf4c70aba87d97b44fecd94f0448e8f6aca9ebb59788a4d704d6350b4e3f2d39a13f46b71d7353af1c05314048b39fa2", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa7a663925f1bd2795f6c7f96293179e8acd49ebdffe5378b9b4b0f85462490f6a38a42ad90b20666f3bd20ff2f5a01f311a8996ac1ed38ac2f123e677d52229651e20331ab1f2c6f5f167dc0bd8a87a0c850307871d99c0352770de4c1dc39dc", + "slot": "12859561" + }, + { + "pubkey": "0x8d1a1c5fab45550ba28ca984c11c6502d70a001a534f92a2ccf0b1266650dc8ff2617a6bb2bfbd0228b80c118274432a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa345a433696e26c2c1653216fca3eaca843e51e4114a96e0e8a7992352dc65abe9c11226d68b2f2e0919eb896538e4320c3b5c936072c010d1fa21ece777ea37eeab510c0a35654563b6ea3f8bb0dbe101054007e1cd9dcf56423a15d6f48c9b", + "slot": "12859561" + }, + { + "pubkey": "0x99ac3d70b13f1ffbd5dd78194b9f5f32416bc6177fe19bdfa3ccb5baca78a4c90d56c826122c2b177e2de370cc08cf48", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb97c0427c8bcd90d319d2c8e3fd2bbcd273cb060e421eb2c5d8ba121a3fd2239b186f173ffd0e66ce18a0f4c4582c3260c4268707225957f70ef93f6116db3a61af5c487e7f7056a10aa02b5ff15c729e8dc33eafddf734c5f79a01ce0dd188f", + "slot": "12859561" + }, + { + "pubkey": "0xabb35784fd7538d470c4c28d9b9c668aae2dd74431f2629b009ddba6375a56ab4969b7de346ff39fd02b6fd4944e490a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xae74526704361916b342b6bd7b06adac42207b1999801dcf6e13f6889936d1bf577067b454cd64b90449d61e5f520ee808a19b4a14ee3051cb997e97e305f7727767d1838f3fdd98983c34e3959e8d6816e72f43c443efe1de3e062138998287", + "slot": "12859561" + }, + { + "pubkey": "0xb0dca787ee8911661039e98b7d70c6d0468104ca6a3c07ec03e71f542a4912d382167c5af1bb9c3e982012c319a1c7e8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa178781e1e939addaf681074d3e3abcca642f1558e7451d0cbd2e1ad20e7cbca3f5ac74d72bde1f3f8c02bff1d1cf1f218aae5bd91e893927d70a1c074331bf67183d99b10b32adadc2ba8219f666c2cb72bb91a3795d591ffcf5d85461606f1", + "slot": "12859561" + }, + { + "pubkey": "0xa5a6bec6f6e78abf587ee567052c9a04c5e97eb7468d7de92af7deda95338731edaf0ce02e72360825e95c4c73ee2f4a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x97c445f8d799a74f31980739f7133844951253c96bc222c79adc70d2a501ab65aa03b32f65fe2bf1691d2554f8ad8a9b018694a600178cd71e33ed1744da72adee1ed618ac31825e7c314079b056ac7c3fab6d33ef5719566aad62c9cee2ed3d", + "slot": "12859561" + }, + { + "pubkey": "0x90d918e1db59ef6cec132ddc042861a937e58db72a9c3de97ac4589b467da65faa07cbfbea6754fe60994c4355f45164", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x90d05e9b2331ca75e29ca63c14a840b12d74e345234cde0e0046c2f7193156699be72b8c496264e60a8bb64ea6926d3718656a6d1c8a23bf86fd37d11073d916c82cc3cf9facadc6d9ada199b0c0dde1cd6b8ecb51898cb83a1a40159e935903", + "slot": "12859561" + }, + { + "pubkey": "0xb6e305a7cea2852518c3671e962177e13768dc4a40f4c8d1ed36a024b305837816e48cc1f179c7ea09d464e90ee84022", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8b9f22f323770fa4844e86f5360b17620c0f08ddbbe81ec389da8dd0333e556002f0dfe06abd4a5aade1d51e1539483603d07b029a2333c7e49f249f437c0629b479140216dc2c7f755ddb3f138cee463192ed3b9cd4561795ecff0a97f87a61", + "slot": "12859561" + }, + { + "pubkey": "0x8112e32ecd4ae5c549f506bc10ca87ea8559adcb3abc8e93dcf936631b14c28300dc55f2e6cc07b5ea0d1118f78a965e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb01897e359e56c6a762fefb018dcd2663162bc9652e3a97f449f9e9dd1a4dcffd56b4b826f28e35a8f1d25bc959f11a319ed47edfc29c2e6458558b49f8bd214651f00f4551acd855f9b790163408d6ef4153d66bf943845746b917dfb7637f5", + "slot": "12859561" + }, + { + "pubkey": "0xb803c98201f770b128e7dfa8004bde7aebf12e7185eb88c52640dcac81c19ac747d7772d4f109eca6167c224faa2aa76", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa5491c9f8aa84fa586bd5b5af4814a4971122521a4cec470b91fb4a75348e20d2b1888daa6f0d8dafd0ea50049d84c5e051d9564ca95782c9d780c651b7487938554f625206ccd54db1879f0af236e4bdbbe328b2407a8b7e3f76f1f4efc82c6", + "slot": "12859561" + }, + { + "pubkey": "0xa344f7c6e5770bf8ec78981b7f81151016b1ba7a3e3c8f00e0db3461da81a5c7de7a50633ea69c3c39724f5a84758ef6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x812f12f79eec420a98d93f10bc34a663467652db52222a1d4e6d702254bd9a5ad2bcd70e3d0ef4586de5b70d8242c16611f500e3c5380b30cbae9d3198e9eda2be2b9f0a33233fa8317480771c68949d2d6da7a95287a9ba1666936218e53fbc", + "slot": "12859561" + }, + { + "pubkey": "0xaed1e7aac3d0c512be25e6e3788f367a4cc9b5584bd82944e8e2e695ccc5847fc5e019a7b4cc26f6df623281c7d955ba", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x946db337f38584c611b50bfdc7f87a9bcc1210be1347a5863b364d41bc14ae6c8d184534ede396d29416dc6fd646ef2e0d22135e8cc7e0f6f22bf9705242fbc20cf355711296aa427b1686c044200e678487fcb84371f8f57d71821ba502f3e7", + "slot": "12859561" + }, + { + "pubkey": "0xa348eea2b80f335e89f6674605c21fab27057d96f26ac5eb824df8cee6bbb8450d3967aca3d3da28068846b4a7c15b7f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb41f816f90b63c113281bb394b82890498f3c513541d2f2661d74ecc3852d75de1be1dfce34d911a1a0625f4035caf4501b19b92d3ba68460ca593ae7eb9e04eefa3dec7b3e4cf5ad8e7c1aa6c597cba24b4b7ce6c089d10c937e61859adfb70", + "slot": "12859561" + }, + { + "pubkey": "0x8c61d73c855bcae7b5bd905ed8a0b176f8b772f2c619a5fbd2554b124de02d00a31904ddb389ae3ae679031f2a5ab729", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb6c9c313d2b2d08b6e70926e75aac6c0bc075f539ceac4557a72da7ff5dbedac9ce368168e42cf74dbefe642b67b0e3208f80006585c750cfc7413c7b0b56c2f253af938a113c570c06d159c7923b90a98ab67c9fb80ff6c9b6f6ec90212560f", + "slot": "12859561" + }, + { + "pubkey": "0x818ad41014c623fbe7c7c9a01cddcee6e2124ed8504e5a3429ff4819ea3d4fd8f44000039932939bfe31a15e4307ff60", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa966e461ce019ddcf2f0f10f77764b2fb2a7d4b3574d4ad526b6d1a0a5b166c89141534db0989930ba01a9b812ac84d909c782c97c73a905a33d06d059e41c1defb99c5686b42ea4368c90c07f68f53913f919f6a151b2debb68e90eb62db155", + "slot": "12859561" + }, + { + "pubkey": "0xad7756fc701bb1ff57a564c3602a1f26075632264bd2017ea965ab834e78201b5800df246036a8ac7ef100153116ca1f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x884173781e0bbdf66a13963e6509f9528120700cedbc76bdd85c1b22602de3daa802c88d61c812e9c2131554740e648f161b93b455070c61c9d7b7c7ff0f17c192c4fbe6296940fdf6be271390343e135873177cadcd0eeed406792592b41799", + "slot": "12859561" + }, + { + "pubkey": "0xb504b132ad3bb8e20b234ffc7e118bf51d93e080b6317d0d7b25c46165ffab2c5098de4df4016529384f88bf8ed001fa", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x90e583412828323fc85bbcb814c733bda9cb9f74ef3481466136552678138d923a700f7a4a04ef83251bc26eb16a94d91593e693453de304bb627e4a844fac1d24886caea4bdb5020d04abc547be58f4cb160400d48773a84e488583088f8ef5", + "slot": "12859561" + }, + { + "pubkey": "0xa2e415eacdcd2f3bdb4b60f8cba074934e5906a67a462f74aa49da786ff6059f3ff80a5bf0f6f2d9c5638822e71e122e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaf9519999250c1a3df4de854eb3dc86495e78d46ca7b54cac74adc5bf2fd977b2d50a1f55a822cea811903b69e431bf90d9d5a8656ec46385c028b25aac29964fa8da7a91eb41277f5d79da36804ae94b306c269c8c8b096678829fdc81fb98d", + "slot": "12859561" + }, + { + "pubkey": "0x92ad307934c296c317db967920a22e106c065e04ffa303e0965091cf072d6d6467320fd914ac51d19ae23583bc7ce0a9", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaa5dd0f193a29b04a5056bd2c8b68a56ce12c90c5d244ecbf4cda62fd00e21eb661f240ed13f96e89e4932256c0095ef0e0a3875ebdf11d2509e1c5326d1e53dd13fba64c15087450c86c2483c585f9b195e18cda7a6654e5269b0396887ea4d", + "slot": "12859561" + }, + { + "pubkey": "0x86103079f9da77a2c40b6e3d0a33f8ef49c69841d2161051be0aea5152d392cc9ff1fc78f6e2cd40c6cfb45e73c979b8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaae777cbd53142f2ef90cff9c858363c253352686ac6bc69993e3ab6d975cc55f2f713254de8600bc22a8a967905868717da19f8c881e1f201a275511f59e171d6e3666a67e18210caeaeb73bd4d6a884bd3fbe21d2cb7e7e6b2e5686c9b57a3", + "slot": "12859561" + }, + { + "pubkey": "0x8a3a25221274fbcd304fbbe36a342a048e7177f99f43e02d288627626ccd4fb620e91e1d1cabcee3a66ff1146c13a2fb", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x82e236cfbd9a71140b8be74e0e3dd0c3c03fa0e37eb10549520c4e74c60161b4dcaff9fce154f07731829a710241aa2b19f02587f2649c08c3b8625af0b21aa4d8ae726e42cfa54d785edfd16c72057ebbaafe64d76bc85b0afde222fb3f01f3", + "slot": "12859561" + }, + { + "pubkey": "0xb723bf133ff634730c46d0cc3577ae092c25a3d127f236c1877e2f84e55f9a3c73b0dc1e1119e44da1c4d9ab58d981a3", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa3fba7b90933e9e54c5b067ed6121a777ff722ace53666de5558f963b6d9a9488cda5596350388cb451c4120690938ff060ed064c70d19787004ea7c87f945418ff91fe001b79f96172a689b7b9e4b7761d7ec5259f6e170d99e3ca847019835", + "slot": "12859561" + }, + { + "pubkey": "0x84ae5c2454f12d3358ae67d2242ecbeff927b60dedf32ce4f41028c8ee3866f509defe33a09d87f43ac3c32c9ebc45dc", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xad61c87ef9aafc3c0b585ceb162f097403059f00a58fe46427791dc978e446dda7cde2fe447538788eace1b177cbe8de01efcf54c0698be3157ff19288379daba4a8f35c4aaa4963cf9d3612b10c5257d5414d24130412179a5e71b4d8254eed", + "slot": "12859561" + }, + { + "pubkey": "0xb990ea2d8c5ca2e039bd012fd77c122681dfe096847f494e87140143bb61be8ca7152a3d5bf2ef4b765b6d3474f9de13", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8199b50de885ea470a53701ec4efac1b1a30e7e3444230cac7a4c2f5b6550ff797e30731d78d4f4cb1b73c418d4f9f39115d327b7e5aeeaef616c56489a1d99c325f44e3eaac7c0ca725c152d443655d613948c47839b07010ab67fff4de4dcd", + "slot": "12859561" + }, + { + "pubkey": "0xaede8dcda6a95d16a35c0a77803e4e90de1c4ee0d4c2f763e8ea53f922c9fd4c39f4788303f83315dc002e0ff05c7642", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8e689359370da5fa330e947c5a12a9b97aa56b1bc35bfb98b89c99e0cd24cdec58cd0b6908f87119f8e57bf92f3be5d3055f796aa923ed937fc4cb44f82ea4436260d134b3ccba257ca2db1b20ccd36811127496895bca49af4ef43f3b474680", + "slot": "12859561" + }, + { + "pubkey": "0xa030c793765014ae005977bb31f97cbe78728343f177764240648a0c79ed26d5fcc947dc6595411596d578e2060c3856", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8afe86fcda4f8b7e45656fc7dadde6770c5e14abf338f409d3ef804898d62d9e055ad33530e8cd711fdc8afe4b97585d03969a1312c7860d47a0909f4dadd39607dc605299f9397b372d385a973203d21bee729919769b48513d1e9c305960fc", + "slot": "12859561" + }, + { + "pubkey": "0x86efdc6dcd95dc7c08f805efbb2d90bb0d7671589720c79659e2e90d95296223ce33dd62b3bf8c02904e2d6ae58b712f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x87fbdf90d952b8af6832e49e414cf0d01e20626825a143e4ef6e66851d8c18cce25aa28a2690bee474a681a02877ad560be5595f3b8555baee4851dc498d50b9d8fe7fa2e82cfc01ca657750b58ba6e185c1f0ee49641bacd764d00da0aa81ac", + "slot": "12859561" + }, + { + "pubkey": "0xb05c5450cb6a84dac0c15fcb1b7ff78dfd87ed37e58b798621d3de3be28de33821a35133d170fffeab6f9e3d30540c98", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x895de9c30bc6751b27e806e2c081da3187e0b0a6c36d90b27e561e45add7ddd9a03ef782fb83be7e86e893fd24e41fb91543046f85cad672dc1ae2e24058e2483cf1a7f29df0f558c723b575169b98c3ee1ec2eaa8ada7edd802cd19d5df8205", + "slot": "12859561" + }, + { + "pubkey": "0x91b4392f136b7e13cd7a2275a6f12946c096f00ee69a0aba2dcaa97db299eebd71399bf5068f00a2ac99cdfdcb079fa6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb1a1f2b7ef1096650cb237be547ca445482e626527fb89e723e82242095c93ded124842b0ea5900ebcc3132066bc396512435a100dc44e191de769b709010efb8e19863d7b39dad8562edd0c48e8cc94ec5d559244b3402dcf7cf1b2dcc7faa4", + "slot": "12859561" + }, + { + "pubkey": "0xb963208a11243f5a2dcdeebcb6d238e9a266c6662eb2e9cb04b636bc2ab8ba467a8f400cf6c6ddbf8e7da4d249fd243c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x852336c2ceadf4d6a9de8a89f99be75f969874042aace60255ba1fa6eeca6c8ab26509bb1eda2d75d9dc91b3bc10184609bc603cfd53313845ad89474156ddaf72f3dbbf73b289321c5d2c0a86af8eed7be142133d59b87117562027be4e194a", + "slot": "12859561" + }, + { + "pubkey": "0x97b39809a385db99124ae758019efb8b7025c5e5ed20738b0461815c920a7b9327812c95dc2a8807568c76c2cf63479a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x9343a13dc78fefc8a1523b50eb47388bbb4098b10c3f57bb0edf41ed327cbf0047cbf097aab4ec96bf407af19f5f0b97022c078df0f65f9c2a3b66247e3dbab9a620510d19076b609393d5e2196f533cd57162c75ed4799525022920de8557fc", + "slot": "12859561" + }, + { + "pubkey": "0xacdeb673dd2b67b18f1df2e260824ebfeb1ac0d900e3e43e142d896fdd7cd35189a1925d997e3c905fc68be3672a8d2c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x95dc8b9554df082fce09eb122928d242a308ec1f0438d52b5fe389c6ac0750dc78eb1db883e90a137f13160ef02c5bc807c77480f7b2c46b67763d51c8c4a3eebb52555334bccf524f7147e6c3d91de71cee442b487b3eb374a32a836ff6d0cd", + "slot": "12859561" + }, + { + "pubkey": "0xac3452bde1eda05812b3a2540fa154dcb2926cf456defa6bbe80c347aebbeb341fb9401756c2cd72ffa865ffbbaec0b2", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaed3792a81a28c845a3dd687a29803c997ba90630e0b30338c941b0b38e46d57639ef2a054de5f05bf4f2e8f65130f3c0876e1c54e7ebebd9554c02ba502245a9062fc954c1ebfb0f0f2256b8eb781b5dae0f257d4851c9475b1e7b32061d21c", + "slot": "12859561" + }, + { + "pubkey": "0x989046a5cc5a25b3eba21bb6689464c2afbdfb86acfd72fa4f95ac6a316c3a0c2a4e2d9c660707f8fa9a1c26b8015262", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x91ff8388e178f76e4a019212dd61c3192a79f0607df98cd6aee4a8c98c3c4aaa1e7a3e2a878ed4ae73f99c6d668724e618e58d19a736a84c30e54dee25a2415505de6e73b0ff11eed0f61dc6afa3d29d4293f6937c3ea4b37ea6a34b8ca54e47", + "slot": "12859561" + }, + { + "pubkey": "0x952d63f5bdb6f0d219109de0939ea7ce09d9f520e08a22c0c6debf2fe8ea592e6ef35a213578edd8c27daa92fe339805", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x907da31ccbc809c97e52ecc5b20e87de13a00b1201454ddc0321792aa2cb165a9a4c7e68b54ffe7690b078754d1bb86d17ab4fa6298a44fc08e4a71df5691f8e72a6868c9ea09abaaed6e526942664df0c99f7bd02678b93bb7ead48bee23de8", + "slot": "12859561" + }, + { + "pubkey": "0x87177d42d432c182bacd849f8639aba7884d48dda2bb214b72294df8890cc612e5f40457787a8cbb5283e4292eae89e0", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x882ce841273e0ec0133de9a3da5e20a5da7b6bf1330551135893418e3e4d7d4319827092d4772d1747d90827c3dbd8720bb7ef8a59716d2725a0549013102dd9b43b9860b9f75a33e168f00bb4ad6308f3f60e245d291ef64d353402632c82b7", + "slot": "12859561" + }, + { + "pubkey": "0xb77d92f7bd659ef6f8f328d05aeacd1095cca54c1f745425d66a929344ce4058c544267598d14c0ea98e58d7751efa0c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x81f4ae2c75e60da68e99081f74c4650e07fd69fe15d547d8113e97eacd742306bd77bc9cf6347fe639171e484e6610220feaa29f0bbbbcbc192aca703d8368fd2595cb0140001e67227eb375654053b2fd0b240c49f1d055960dcf9d62fd283b", + "slot": "12859561" + }, + { + "pubkey": "0x99e921175f473a5ea7f7a0aee8b8c139bfcebf94e66a672b173e62b48ef1eb972e61b0d92a76cf876ae59ac0222d8c6f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa8652e0afd9dcb5053e6ab6ce909ef89d52cc621f78d101cef699dec4fb762a45e6350bf475c9641063d55e59056eb9b112a57fc4b9eb1761982d8cd0fc2533fd2df129321eb2e2dc74869428bfaeea4c3cbcca69244acb4d9c3c61943767100", + "slot": "12859657" + }, + { + "pubkey": "0xa8d7386cb2ac0a325314a9b4e173e3f38dcf5a2f919f804c7e3a156b9c3c417b44ba643d036de5eb151bd8e937a86ecb", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8040ec43b824992c1246bec70efd5e6c6cc80700382cfb6a387c154d87f3cb4eb86c6c3dfc0a7953fdacd63c9505dd7501817a9a216f59def3d7eed794064bf47ccd306719ec7f0f7ef98c6109ad3f52e05dd6ccccbb490bcb2613c8c4536f74", + "slot": "12859657" + }, + { + "pubkey": "0xacdd4d82fefac30ef090eb98e3b158cd5779d0a84fb13e9d92a96bbb2cf22576c1ccf1f9a50bb32c87470fd67ba7e625", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xae124c605ebcfb91da1b3a5bf13d6fad84262dea74113b355703f2a3327140ba16cdcc72480ea09145e285fd92fd277d09c6f0acd076decf65deb4566cc3a99e52fc8a5c41beccefd74bc5ea488edabe7a03107da3329cffc631b22c2547fab1", + "slot": "12859657" + }, + { + "pubkey": "0xb9eb351fdf5bbef2c5734c3c1bd06e010838a5560af6e910a9dd010041f797e38b0167a351419d17b3d1d3bf4efccd81", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb2e4a9db3dab707d75b95594827c974f601e0a1ff96509da2d7195598ca7b8c0d0174492acf0fa44f5fe32a29a5b6a200baf2ace226b598dbbfaf9648ec4e41be059e490560aff90ab22b6cb2bbf4fcb0e438e00f80c9a7f1fcb62a3574c1561", + "slot": "12859657" + }, + { + "pubkey": "0x92602996bb12c0f979c4dea3da7bde1a3983323353786176d37cdd69843925c42024327575290a2d4fe0245e4086a9f8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaad0ff177cb7d3724de04afaf8aa7847950fe4ee47265669dee1efd70af58f2eee8eede4c7368934288c91c2860cf66f17e8afa157d5aff3742b210dfa79329258b38e448ddac07f29b15fd1dbbfebcdb87508cbb0808fdfa1b5d2640a75b1c9", + "slot": "12859657" + }, + { + "pubkey": "0xb3dcf149ccaea58fecde3fa9c3bba924ebbf8b0b03b648263525fd1aadc234d6bd3ee20f3f97e4f7e6cd2d630fb34187", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb27093c4e9c959dec04217e2757005eed0f5d6b03c0b3e0d38253bf203bd31103e43b92399e6f6c6237506a1265cae8409453a6f617b715067945d7b49d854cd4a702dd35c3229404fabf028f88f75e07b0faa684bdf2073ceeb3a761fdd4661", + "slot": "12859657" + }, + { + "pubkey": "0x97adf83918acd7f9f470e6438780b082833a8ae2eeec0563e7143117c665591fc7f6a10e585cfcbc9a7a1eee0f496dc6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x859116148acae2a1c3c1c7cffe4635a1151c42a86e76cdccd75e1e37639709bf7d8e0cf982fb7b931fa3a65702172f6f12251a6a4ce0245c82806e0aa0e8707f58d1e6eed5c2d770d4408b718616c4664e4f86507196f7cc4511d28737cdb45c", + "slot": "12859657" + }, + { + "pubkey": "0xb7ff5ac7385d905d075b67153eabf0916da16262396959706055b7e674869486ef2b65946c6f49080b90c7706ea03f50", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x99784387c9e34b04ff8765ec37765273c6937cf8456cc09fd0af6e8f18371c49d9385109302234855e3bda4149276af10ea83df44d085f386fd02530a4fb9abd39df0fa7c1e3dd7b4264b754514796acb662012a04d7357b31d63a0257ce1cb7", + "slot": "12859657" + }, + { + "pubkey": "0x975ac651862fb53301fccee49a4afb842f4110fcdbcaeaf8823bb2eb533fa354599c74e5271a542529ece84763c41be6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x96612db03674e1ed00d395fecef06a47a060b98c47ff5e0db0a362d164dea07f5e1d9d74019a7d1ee805984375f1058f167c19f8ea295ea35fa673590692d26b15bcb6dcf3edec504492c8651d2a1a044915e6b6ecd6cf8ffd98a3966c9cf771", + "slot": "12859657" + }, + { + "pubkey": "0x951761730f3aebc449a49e3f23c9046165691ef702a088a8eb741bc3455443229af40feb823358c2df6a12875f327bd8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x88ae02f2ca74ecd2feb35c23ed7ddc92af45ed9b9caa19727574188751603b979e309dc4d31867ce4f7fd89065cb0e3d1038bc80b87a42f6717963a3e81559a5bfcfa1873f4a8402133e212223e9cb9957cb3525da823e7f4934878b907b3076", + "slot": "12859657" + }, + { + "pubkey": "0xb2d3323c2206a6477dbdf180a40f6af0afa1bdd3fcf72ec00cdc788fe3f6a512417389e167567f474a995c8743826dbd", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x927947fa8d7709b7b60104ab51b25cffaee2f7c71e5b1361040c8f0ae769d668f0a3c90d8f9ef881f4bcc5386b9996130799f91ed9d7763459bbd4d8fd373eb775562aa471e2d92d1ea546bd01cceb8d96ceea9577aea77e0780a11ba0b4f51f", + "slot": "12859657" + }, + { + "pubkey": "0x91157011f994551c183594517a77e84c5e99cffea46594368795613a1477128981b6e1f47626d2008f27ed38443467cb", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8a417c495c41575aef236875da9f4d683259b9d363d2388cb373e28cc37012fb3769dfddbe6f10d5a41b3393e7a1e8b307d5e6aa9ead9a52504c8d6a79a66e31cf4e8212166cb70f2df38f6ec76abe1e89a85b2d2234a80b87c529fd0ddf25f0", + "slot": "12859657" + }, + { + "pubkey": "0xab71e78724a67972212f46fab109cadeb92a83c046649d07a75c63e1fdd534bc01ca3028a2ca358874026fb2e9536165", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x91cc8cb94b0a6106f2b43b0dfa37a141f29535c966ec623255c71f7f8ba57cc38d602f5610fd8412fad3f3d5472530c00dae81ae2f2014c31cc5bc9dcd245283746b5edfdb7d2e182f38c2c5bf414b6deebc2ac7347cffb94bc4d372a2c35ea5", + "slot": "12859657" + }, + { + "pubkey": "0xaf4ccb9ac795b779c371b25cc5c90f469073d2f589b416683b46dc16edca7ea44033f6e88b5cc721dce7cf7cdaf84e18", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb178d4a72188446899e5e7dc800befcf5a859d0189fa5e0af17b98c578e3dd2636126b757e6a87379ea2727a3507cb1b0e75539fe710d2b3c8db491a41d63fbf1879f01de3bd70a334a1b9f135e782374fa77c8230868b2870ea9bad2608fc10", + "slot": "12859657" + }, + { + "pubkey": "0xb8b2848903417881ae839f9d1043875b22d5b5440bbc70de161511f1fb951f0df7e82cb0eae6bf41679fb4f796cc7a1f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa933ee648b7c6f64d0aa83b93d2c42c008c84a6f47f7ddbea83fba001f91d03d313ff229639d2dcd034e67a80319f3c50f51742aecd66447ef933d533dd327aa863fe3ae00ede086c170c9afd3b4e91c5e8dfea734593352e875ce102283680e", + "slot": "12859657" + }, + { + "pubkey": "0xb89303842097bdf5ddaefb7d55d32baa9bea884abfce0bae9027620b23725ccaa3641714a0e7d462d2210b1325c0fb05", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8ef12a4750695314e7f7c76b82897870f4c75da6a7249e92364478bbe649f8c29ff9e184b4a03a3840e1e85b2cc1ac060c7cafe0e187ea8327a010796eac1511f9a4bd755ab11359fd7c9a03577229d0e2b99ee5739b2a053b2dbbc679e3c798", + "slot": "12859657" + }, + { + "pubkey": "0xb513e5fb064cad4d6d168642803bacadf9e3fc4288901880f7e3914393f5671a011def5db5823358c99d0bfd4fa47101", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb411c92c90141a1a91e1dd28f8c5bc3bb11f5c8ac8a121532d245cad14d37b1def95c58429300e45df33fd3def55ce9e0c0a1df8bcf43f374ed6344bbddf8b1d6687d6427f8b8f6f72110800f9391848e43870971baeb064ffcea59fce74c143", + "slot": "12859657" + }, + { + "pubkey": "0x81cb834a8745976289df3719ab554d98b39f569e38a8d32d9ca65039e56c15d2d976658d9c91a0ceb6db85a1cc992772", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb3d6975d518c17d81ad446b7eee92e915490e8b93e468b04ef28e05ea82527e3085862103c3fd0484883c934e265000e17aeae5851b4b3e20ce0d9eac93ca9483f3fba0def82fe987b6103c80691412e019605af95bfc65b03c32f1e1d0988e5", + "slot": "12859657" + }, + { + "pubkey": "0x9208e922d289d4ab100351cb817175d747d2d8bd11276d1f4760aba00341fe5c6c39ce825a398a656cb92368776a8949", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xab720b825a1d7f1bcee815e10c0480f9c3870f18843d1d181e28560c602191712df85e77a7361d14ca1de9b5027cc7c80b657a1e46cee52969466155f5b4f6d42d0f6aad3e408ead7c0fd206db30eacb925453d1f666c29b7cfa03076fa275ac", + "slot": "12859657" + }, + { + "pubkey": "0xaba973e37692a136c73358486827b40eddd8a95e2003fefdaafd4beffa7b3c336241feab80f9fd38a9b55888060d4fac", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8f150d057b254d8315d2c729e89499547139f033e0ec6fc8693c21ea1111aa25eb61ffc7cb1ef8fa48196fceb4a951df10ca3f8210e01bbf41084d97f16a1d78f7d8605d5901afebc9cd33eaa9b117047c819429dc4dbde3a90f1c34105dbaa2", + "slot": "12859657" + }, + { + "pubkey": "0x944c932b9c51419f791ee05d571b6ba9ab5dbe41943689943807ea34d5f056f5ad8efb23a1c1f0f89dfd8012ff34f726", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa526e9bc7932731b5117dfafba421afb9b981b5e1517d15ee58456761a88ce3ebaa177635a2398ab8bf07e1dcbc2e66b0990b0160699b5b11c2b8796d5628a109860f304819be8f7fa12572f1405e4649f35ca236a79a56703fe5e721f2a887e", + "slot": "12859657" + }, + { + "pubkey": "0x80cf2771b6df1d568b74b29fcea7fbf7dc0925c4b1db8c83be7db10c642504b21fcbd7ddecb28bc73cf9a99e122a144f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8386df0755873bffadd2a63dc6bad6394471c98def71c1619243038ae15a9b14849302f583bf5d466684d99346aba3090a3e09b8941e5bd4392f0ded4b48b398cd1beb0793bef2016943ac4b2e667c0d2452eb829e72a385c28bbddc10e6c918", + "slot": "12859657" + }, + { + "pubkey": "0x9140771069bff9aa9975c65cd8b3ff6d8f27e880b82b4efad7011a491d099fc4a6efc4694ed15d6e926f80701b4394a6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb60df37c777a7548142dd8cff5c0ca84ee7379104c807bf26ada715b477caf55e04e20bc149595172b610db9b2cbcd711102cef5caa3fb6f253297153b80ea06f91ec6504920239e4925964a883a13b5f81aa2dde3ff0ae592b96798866dd6ab", + "slot": "12859657" + }, + { + "pubkey": "0xb4c689e0859d8c493fc7d4974b77fa3a7f0b55a67bf7b69980fbc89460d674c716ee09887f0bcba91aaca0cc87f11f8a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xafe0556a46cb6e29b487115eae7c76c279ef76b9268c1dd851cfd84ec56248b5d77c21f5f0465cc6959e9a8f9af57c420ac49660ebeab3cd82a751eb0f1f9be08395804e827889a2d8f34d02d10ddd85e4063b3d2fc2bfe98ce2fdb60b0dea4d", + "slot": "12859657" + }, + { + "pubkey": "0xaecf3d491f3e94ce53d530915085f3d97d64487926691282febe8daa576d312c5251a23442b78b92ccfead4c08a22e6b", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x9844010ba127bc0f4915de29fd116d3baea1ced492f073a054242e467ab3eaa94dca5c3af9707c126b4f9041ed18e2ba16942ef9e50625254dac4074cbbecc0d0702b8f2e7381b44586aeee8657719764a02f266eda53c911097ab68131920c2", + "slot": "12859657" + }, + { + "pubkey": "0x82de4c3df1d6afe6bced6d675d353b0e789e9114da167b13775822b3aa66948cf4f7bdc9937607d51a761ee472635a2f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x98f5dbfaa2565440a17b09745caf6cb09fd7ceac89c5cf09c6324761ee2264fadeffc84696da50ff2b8f852995e650210824b415a883da1121222a85a5b881e888ae63fe2354f14b9a5604303c735166e08cc31d461c0179ecff63d50f3bb9db", + "slot": "12859657" + }, + { + "pubkey": "0x8e6587852726fbdf42ab9c8242081e52106c25bc8b1b55549d7097710293bd0e0ca0939fc342bcd340c505d721af5b40", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x883f279b3979ffb765d0d0ae95d61e59cae38e3f1dba0127a36bbd3b9477ef4571b401ad26e909d31bdd3aeb82f6543b17d7a4422a96be8d7d6bb9ca61fe71e7f482da27f5e5b1b4b9bd9be8f573f8713fe7a5c38e04c7d301771817d22ebb7d", + "slot": "12859657" + }, + { + "pubkey": "0xaab1d6a9db56e58863f71cffcad6870a34dc1235c2da5e437693f5ab12ccbd92a3787e361fc038c4875adcc15bc67ace", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb5d0715d65e6aa14694452410352a250d749d6323ca3a683eff97b26b48abd7c43bfe1b7f803d85bb578b9f6ffcbf2521064bd00bd2ce5bcd9f84f6a478d1e386d50f7937e00ea8e4a57eafe086493c3aadb946216acc95293a4a277c373c399", + "slot": "12859657" + }, + { + "pubkey": "0x8a67a142f62de6058522d92c926ef202d5f5ff19172390721829dbd734ed6b1614c36d97ee20c397492b21c560b21ad5", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa8516b30b03c8d27942c14c91a8ba897a0536259508e200f23393735be5d95c122853dc377a0157e5f9be58e15ca2a2610ee6715ee7353d5368792a5ea8d4b8ecc61d4bf483e1216770696a693f1cc5ee12e587e61529dc2a04d5794acca56f2", + "slot": "12859657" + }, + { + "pubkey": "0xb6394e8ea10e0f79483cf025bd6ed6740ac7ef8f2b08ffd4c4c6cd64d88cfec91283a864873df4a805f28b307c05de01", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x87ee1e5dda5fb8d37f01d082a2996eff5447f2e1325ad318b41377cfc2369649099e49db9e3f7bcb660e3c486a567546000bc010d56d75c5296b868ba8b04548bcf0c22b53686eedf55200a16a26c0c986959d0259c8652c23fdef84b22a47bf", + "slot": "12859657" + }, + { + "pubkey": "0x8841906b8da3ad611835a5a8d8c6ccda6f0988955030ffde6506d06c886538e0c9852470bd61eff7d6e515e88ee5acd5", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8f08915ad8a454e531de5f6e06a8c855c623184670efb9f4c756a156bfa01873d51e5dfb67b5265b611d4134997f325100014e2949acfbd40bab35fcbf16c7fc8fc87167c9d10aac96b7f9ae3a16c37b8602cf53f82fbb61453a45ac0b3cbd96", + "slot": "12859657" + }, + { + "pubkey": "0x87eeb4ba49a4990084939f1e01fc6fdd5d269ae730a7ae696669f98c7b2b3b580a2b113ae0a38dae2dc763b4882977bc", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x96b4be715dd5bc9b73e76b9d79bec274fafc4c05b74d84c4aa7d413d49198af22c8038e6298b6be6ddfb859b2bb8768a16a97e352e866b124764301aaf00d432c96f536944813ae6ca62f3d9a1dc4e9cea6586d1418bfb7e28df7a766297c3ec", + "slot": "12859657" + }, + { + "pubkey": "0x93cbe6138a52b310a46304cb5fdef50c5599b25c97037b29d51484b6a1b21240d66bc7fee6b4ed63f13e4d05297bc818", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa2cb1404fac48ef4be7d72fe880fb95305b3ac37112377e41de879ec5ddbab1532b2a5a609ae295d2fc61b7c6a531ce007e3250cdc872143b890a47a0785daaf2cb95e9084ac4243a583ed03f9842956d5d1c8cc4492edb26312a1a3d257a67a", + "slot": "12859657" + }, + { + "pubkey": "0xad445fb584b0d39684352be98d2051ec9172aa9f21252c4f9c9356ebd10dba12a732a892cf3ad8237f25084a2eaa47ab", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa795b1876f63784d38f88bb5a9bdc20987251b49ca1cf92ba349d11328dd8b4a93a3b8627e63ff34d4d3542eaa839c9f07fb407b04f7f7adfc5386cc6ddd896a94be9f3b07ecbc7174d18f3ea11696c03b978321cad49a2e6881052f033e8e90", + "slot": "12859657" + }, + { + "pubkey": "0x812ab5fa799e3d3baa5353294653f6c1de75ec760d52b07a57a8bbca3a760a1f845fe8fcba35c231a037dcd2cedad0ef", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb2d353368b46b61d64aa16c48e798b32e1c3eb0792387acc0465f3ff66fd6834d8efc32223a5f24b3402910959e46f0f12e698da1d899d59af8c6c76c12eec81af13d1e27bc53b874156bb13acedf486a4dc1b1ac5b8ace83c63b6bad1c52926", + "slot": "12859657" + }, + { + "pubkey": "0x89236c166ba6bcc8f5abf15a771a0c91e0cb4a307954a4aed469a052e06842bd1ae3fbf58043c17f2b06893c0b671a3c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xabea3f8f597cac81f9dbd86a4a17f509aa1b063539335369b4e83798b54b278a7a2ed684973960bd4cfe4ae57114663819cdcfef41495114dddb42ac136c6c18713ba5abd6146f7c482bc17390e7432b013cdff8800620f37d64fca85a019eff", + "slot": "12859657" + }, + { + "pubkey": "0xb4544001d80d65935eb0eb3a0e5af48abd6e1aafd1262d00f69bdcb89199649a1ad1b4ad27a2332288e0f1c0f2d75718", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb060ea9b33e4b35f39fda05488c47bf54b0a6bc9b758bafe0c8425879ad005218c640023a0b6421bf2f934098ca5ca3a155b0e4b907dbfcaa4931dfc29eca675c3e4a1bab355c78a7a5b5a21a04b9b209e11159e548a357acb0efa1efe48e12a", + "slot": "12859657" + }, + { + "pubkey": "0x8b6e4e7dcae8976166613663c2c5b6fac672929fd6263e3293643b8d0617ca9b9382da05ce6046534e34dec00d5f702f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa26700e77d177c0e3f97fe73a035a5b8a70276eb6d073e74ad4cb3c2798b301c8cf8b9921614f369ee67a846f2ea713c10e87b86c2c658a795823d1c45b60b4034ce46a2bf61602ace424012e5c2c24ca297f479fdabe511eb0e69b1304eca22", + "slot": "12859657" + }, + { + "pubkey": "0x99b0a5c94cba995370a889c72023d74376c44fbb5974d269d360e50df8d2598d523d441d9eb9894fb879801f4af640fb", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa1ebe45d1df5de5440b045c10b739ef63a85a7cc9c98123cb764c32ab4dc9b406ccbfc539a01e19452ff5301ca57761d0bef40819bb80f9ca5f54cb016814081eb92fd847b6f9edb5b35e19f8490a969899120cd7a8c0062d03c1ed7b542133d", + "slot": "12859657" + }, + { + "pubkey": "0x8d97e329f890635aec19ec91aec031a3206b9db2188843a1c120aadbf6fd33d49da582f78f8c8397614f544e6af23592", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x9257b756b110a37805f29d7fa64eb38deaa5c57e964ea3680aa02c61da600fa936fb5634226a21630e90e712303d393716e02185ff4c77678ff759efad5dcd22c3a1667169629a6821a21740e54015eb0a21d5c55a9ac97f523134e6d7087422", + "slot": "12859657" + }, + { + "pubkey": "0xaf8916229cb5940492e456e4d070279328ed0fad1b29625007e25a1574227a8944531227afee7bf547380160f18196af", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa612576aa095d87494ecb5e62e4c096bb23e7b59bdd42973a6e3a843cdee9f5b1f8bca7436b7c4d16fb68903f4a8326708ca2d59b0bf3e12d6fd5ecef47bb1a5e88cb196c4f4125eb1c9924da72289c0a2110bbfe6f49428fe7d8a8dc48c484d", + "slot": "12859657" + }, + { + "pubkey": "0x820a9ce35e56a45dd32ddfe2a773ae6e1f7e56a5d8d876efe828e813a5d9c595878567e47d927739c540c728b05336ed", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa6cdafd6efd053acbebf7b8634ed2a6717a8d6632ed0f73171b81523f7da3670f47f5b2fbd276721a18e74944d2d32180e3d9fc4c52bd381618b5beeb069ae26a1f51351f8ac32c3b42e94fbb822b032b5d1d476f7f88e8a8381f9ea271e5593", + "slot": "12859657" + }, + { + "pubkey": "0x8bd9de95aa3fc49960a9a7ad7eb995e879365caaeeeaba002b512841aff2df5f314ca4682e31d32b80a88d19ba94d31b", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x84f3b264e8f1ac5dd7d69eafe07d80ed352b0ea533f9d73d92dca3aa5881f59fb1b3a6bb1b3fc5f389242a2475d304470ec6f73ca2ea7663ecfbdabc77a6c56c489834f9dedfb45e0f49435b5790efc7d3f4f29eceecce1b75f9c0851cfed545", + "slot": "12859657" + }, + { + "pubkey": "0xa8cf159fb329dca78e6b9f740e9219c15c6fb0b47627a1d11e127594df112bc3d336e1a6ea97222b4812adb7ba39fd93", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x856cc741329b2e1fbb0c4c91494f9d75097a2dc62a367237c9354c20d77d9fcc64a347fb4d0859b4c2606865c017cf28130d67effac102eeef7cab9e13e8ab8fe48144cab63b7a3a722e8056e211d78dda51b5bb34176d444c544944b16a3238", + "slot": "12859657" + }, + { + "pubkey": "0x9253cfb8e56f35af9e7e0e11caed8a5f96b2013869ff00125cc9bff23feba56c1dafdffc5f4062de0104a52f67460d95", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa59a7e5547f2a442641dcbaa210849e9434bca3a932b4944e83bb66a5eace53f8fd6d0d15e9795141cb7974f8fc6b943123bfb190e2fa467d34b88e4aea390217ebb471407582d38c6e69134bd51146881baa8a49a8fccab87fd8a83f41cd3bc", + "slot": "12859657" + }, + { + "pubkey": "0xa1a72ef39d17b27634972f7fec5e852b27510a70d5044f85a27eba4a676aa88639f423e353c8ae72f8b92abcde571806", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb480228fb882e3ff1a056dc0d962cf5886e97164cbe95ee3ec5ac966042ffa865d722fb719ab96ed4e48cd679e5a2dab066b9925486b1000fae775ba091c7ecea21724a64c44e8be757bfca5e6213ba9f8bda75c09b8bd6a28f48f9f5678ddeb", + "slot": "12859657" + }, + { + "pubkey": "0xa7584d13891d59a999127fa2c75e1ecf18a57bb453996c579e2dfa4eeb2720cab9aa6099018290ccfa667672d671260b", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb162bf185988eadaea5c1accb3790083f529553800f0eb7ca33d15c0cb86cab594d1a1fdd4f86bb973653257dc9b4c6316f8b64720dbd50ecd8a6fa0c61a9f58295e8d31705af51bcbfb704d7e0bcfbd032acf6c08d417749daf7dfa2ca012e9", + "slot": "12859657" + }, + { + "pubkey": "0x949b800ee62cbb379b2d2ede331945a6f5c95b8215e16cb9d72e54df91dcd1caf3853ed8789e079b1adc264c1cd6d218", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb35ae933f4919a910175af1216d54e5e5add7b4fd3f04934a339724e06fc68653e79cbffa3bb9ab480603587e05cfbf617a54eaf44a931592e5adff07fa8316594e58d55832a4c10a12210461b58803890356c2223cb5500b32848ef051122ab", + "slot": "12859657" + }, + { + "pubkey": "0x9701ac006e05cb34116f6378557224a7c14fa25aff450adf2ec2273a9606135638bdf1b14008bf3eec14296b9ef3b8ba", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8272b34a49c8ef404da2cd30a7cb2a27dab53195e1922a130286035886e841420e665eaedd9ae80ea631e875bc5f8f7d106eb75136f68ca04de1e740ac8e42fbf2f7ebb67932665ffbdf560840a9abdfa2e453195d32b851ab4488960412c795", + "slot": "12859657" + }, + { + "pubkey": "0x889d4bbd27fc91d9a674f7b44760911cb4f073bce8ba4cf762e697acd41640bff0b245a806079a2f8228f9198b9af647", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8bfeb3aad678c339ac7ed829ee10f17581bb7255791ee69f0a90671e82a6bf5b5b76b34c7d6ad52a29b5528b62ebc50f052c54f706b841cc6cb26517ffb02fad0e989022243493bc41ce8f352d444b07e201e44852a28912213341068a66b966", + "slot": "12859657" + }, + { + "pubkey": "0x9145530c69b25f7db827577e19081b3cacc2a549197763e06525a7e398457dd8e694ec3b0af277f7262af507645a9e62", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x88722c011149c0ff86a3f0301f44abab46b4bc13615b3a36399ee0e3c1235a3182bcd6b43f5ffe193ac6d38a6c87391803f61d97b4584dd8bb296e97eec4645b35336a96b63e53a0f1145b0677ba11a14b4ca34f0a2aa473ad78d33bdb3bc3f5", + "slot": "12859661" + }, + { + "pubkey": "0x845d2108f9a6d4b22065922703ca7021d0f4479b912760e9f6e83f77c4d4941543218cbb034167b2f6feed5c54e43e92", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb49f84673e695ddeab064896277949e7402ce279b49fd1376a8d99391646b1a3d9e564dd900f1e6cf8ad5d67681edd2319c61a4d885584a022d386193bf86d2b08fe252d8b612b182e161d92dc235af41593a7dff30d352d4f09b9111f482c2f", + "slot": "12859661" + }, + { + "pubkey": "0xa1c4c4e61e3ec22dc7ecab47473cc48e2c0bfdc7510b12b0613ad0954b5d1c633586387edc3b5cc7b4aee7a715930740", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8106dc82c662043baa7237e8d0f34a5a33951845a7a7afc4faf7dc135ec2f7dd8e49b2b6d48d17f3f80458e7dd37f7050bda3ad3940396dd417150b76a26469bd9673d5c9cc3bc56fe586cf35581090c32a5f3825ccaf965b9908788ab830f00", + "slot": "12859661" + }, + { + "pubkey": "0xa80b8b448633b01b3d798ffd00412a32e97346a22b0243389b70df5debc6bfcc9fa21e013d8f86aed91f6c575f13da5a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x835fe4172e8fa21124d2543228af252eef4d3b0bea6ad23104106cdb7c050212de39df797fb3c6948a9ebc3456d576c4041199b17fec0608f9fce60bad0e1689ef1f9857e7c42406f734093778977ac855a1a7ed1479f020b08bc1b4dfb883b5", + "slot": "12859661" + }, + { + "pubkey": "0x842a438557ae949a4347bbef6698be49ee77b761866bcb6398173b15f290d7de673cee080d0d9f93b20e8a89d237fb6c", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xad6fde81726fbb2fb1ddd82f12ca7d81022d06d85296568c8c4e4f8a8208519d24c8beda87c9459d18ea19f0758c2d58048186a246408ed2ecade3c139ea09409385986ff87cdf9122f765f8f071810027fcaf33559a287a81ac827108cc18f2", + "slot": "12859661" + }, + { + "pubkey": "0x8b83501d956bae1df8792f88f6457c385fa5d986a38b3b148fcdbed9968a4837ec0a2432693664c0607092d7205131ac", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xada968ca64ca06828f506903103c4df18c399fc8839a656713c3b3d6719590b6e010da41b18501028c8494675d5fc44616a369914f428d7257de2d80d162f402cc1173ffe3636d054bc025901a06e3d5b6b2ed302f06b5b43ee2cc84225c2e57", + "slot": "12859661" + }, + { + "pubkey": "0xb61adafa172f819b40c6ca2ceb3ce9613ec39fc25714e7de0d81b8ac252362767169a8b522e8f512bbdf7b2706f82f94", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x932de427bdce5a5e2bf17084e578cb93d37e41a24a156d105fa03425d66349ecb349b08d10d3d266792ceb9d13135c6b0a6d79c9639ce341356366fc41d23a73e48f1b3e03e488a49c3a287fd998df508ab357c7a1ed915704de83dbd7f8ff46", + "slot": "12859661" + }, + { + "pubkey": "0xa6ce95831199455b9d9209d765187a8815c1d2824ac0b0243677f3b7322e4cfa12abdd29f52cf726a82a1da107d572d8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaa63537eff02297792aa7461347cf4d5b95d77f6d008791da4cd55f7aa7b41c070f680872a3b6c92cde93893d7e862880eb722c6974c00c8591412de97f926e6b5313ff819c99bca241674f97c800e79d47a62896ab9b9f6245da5534f07f4b5", + "slot": "12859661" + }, + { + "pubkey": "0x876621600c40dc0837c4ca2d33ae7f4ac78da3158e497f0dd369c13ee09c2a57edf180b26a46ebe5e7d62a51cf82dd23", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa8a2f532b5ca29b77dfe30fbefaa97e330b78731cff456f563b1569e771cad1959e1e167263c6943dd3dac5b87119e8c140645afd359d4a29de95a31f88844e4a37f79e6c11012b67316293e7f45bef6143ea447880387e96187ea18f01f177b", + "slot": "12859661" + }, + { + "pubkey": "0x87eab448d9e463c8b543c1861dbcea009af9c0833bc21ccbc630a48922422ae83fe5eed0c126213f08cbb2739ce22ef0", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x9388c4cd622e3614297f66eb838dcb04f2ed6f8803a94a4dcd8fe50c0e9ea613fdd966f49a9cc9eac0195dc3786992c71699e2d20056898227afc0b64896991269b172462c66242dd74143f5f953e79599fdd61c3cd32632f85c9a5d4d7d6fd8", + "slot": "12859661" + }, + { + "pubkey": "0xb759e7b491c66a8ac50e9c0cf786334c7f9e4275cbac493f5eea42dd56868156ba621a39b7c96ec38c9c43ac1e58da6e", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb5c763bb9416215991140d145c7d13a5effc882c52bce0c515c56940685e0df8bbefb7d06a9e8031f58e633c7df171190cc9d00eec25e5b448b431786f3329d267f66a973a6a6bc596d6bf64c3e831583d8a23839108e38ec232765dbf14a402", + "slot": "12859661" + }, + { + "pubkey": "0x8708bb3a39dfce3291e5e08497cd0dacbf484a6cb85bfba030eae10382a7e3f246f4cfbde2366721636084e656aeabe3", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x94cb873c590e15076e758113f0abc0c6408cb8486e29ab7d61bc0466b1c17f1e1805cdda99fc5d8ff5c0112378cdf09c141e94c995d771b1b897f90c72a75ffba9086af4b351f628677af43121a339a502e15c012083603bc8026e44a710d8a0", + "slot": "12859661" + }, + { + "pubkey": "0xa1720f70671b29efda7309abe82f7686be0c199be473fe837d11b1bfcb0a28f371e655a7f0c22ee3b70968977e4c1772", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa75b39599803519ddb8044a15066022002478406a89712d6b6456e9d494f123ee22480d8678c1611e62fcf0313cf8d2413b51cdd17fded2f3479c3896555f681b007dbb8a1c9d81fc0dd127e6627e102e3ee0505bba02adbed091509ddf9cb66", + "slot": "12859661" + }, + { + "pubkey": "0xb7f1dce35b973fc7d09ec4e509a935d4420c863b234f17a97950cf5b56c7f863ec235855de536d3cf362f627f9a724cd", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa529387b26118da4a5d442defb7f4251c9eb81f16494e8f979855bba744380be3cca83615ca511a91dfb268f973a273b07ba66e9523e3b83b0a200699ca56d8a749732f2d1b363eeef3377d0ce27e629c11aba47a41f369534ecc055e1a0a1ba", + "slot": "12859661" + }, + { + "pubkey": "0xabe713622d108f096b31be1d1cca97b44b8d5470bf0c232bdb49fc46fc252bee1451a8f18ff4925b2836cd1fa857ea75", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa4fadc72927fc822bd22f4d1471483e68345437e0aeb68d6cfc599cac31a4b107a88a188d0dd7af761222909370283021942bc5925c808c7794a9c04fe74e164bf7b726621ab9b4522cd683fc6496698ad5dbbef1ccf03e128f2f79e67105733", + "slot": "12859661" + }, + { + "pubkey": "0x8be6e5387c112eb3ad900054d14c8351a89de9857dc34080dde4256394e0e0890887bbf7ed856bbe98ea16dca666558a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb35b568da25fd99079cb576847965604989c30e92423c32a9c47d3f9e4ea01f8fb3bc1827a777c7a1ff42938506f8a3010bc241d4d9e5e7d5608b2e228a273368a8f0aef8d075491ed8d278fdfc6d7c299b54de0fd5db5fa671641d5057deb02", + "slot": "12859661" + }, + { + "pubkey": "0x96d64e0c9925fa7a4132d826cf72c493378577f5fd6bcea6e25a15c13e29caa7d5b11ef8e26f71d86abd3a9f2922bf57", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x93f2ae2d7686226c14ee4b409bc13f6f3b983ede1be857aaffb27a8c766e9d3dfb82370620d4f645417727fbaf269386145ad42ee06af39703f5a942b3a11ff76dfee4b9888ce602c84a0f44ebe8a73d203dcb26a8e3da8cce54f8fa474b6822", + "slot": "12859661" + }, + { + "pubkey": "0xb67647e653e4272d86031c7f5eb16dd7f8e33a36d4cb2f1ed7515c1f41d62718da908ef633aa658e69ac495ce5505193", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x997d59f6689dfb6697ac8dad4213fdfc4d4d34945f8d84cc6f3e9a27ffcf81732e8ec7c39b74ca2c95e33c3cb0e4191e140d451373b88e235b5b4f7f2c8a6ea8fce7d74578d72f49dc934b75c0a344f888c60032108e583a964a089145124992", + "slot": "12859661" + }, + { + "pubkey": "0x9005c57c36afa4e530b33fa477f6480490018b5b4489ddbe99d776651394ee18509966c1b8a05c2d775387afccc633f6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x837e39e6e3de2dd68fc3c84a92399796f282ad940891c22a52ea7b042e2ad51c611d4ac51f9bf6433997556f599269c10233c3fcc7a5ae6d4cc0437fca63c8232847983e243fbb87b0a8f11640b3d3f0eedff23e2059d90570499119c9da7e48", + "slot": "12859661" + }, + { + "pubkey": "0xb4ce3ae4dc7af45c838eeb16d85aaf2650ec0cd8c17f2a89ad49a565238010ba36c9d13666c8ca479095e58a61327dc8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x900e5003672a030032a75539f75521f02d300fdbbb30139f5f1bb11dc541948650aa3fa698d65364cd840ebdec7c5912094d5aaf9bcba10972bcf645163fdf63053e38238e1bb1fec06c4d9eb7af8cc7ddcfae117028770f779c5bac09a2ef64", + "slot": "12859661" + }, + { + "pubkey": "0x9295a78d43a3441660d8017641393b52c8ad4ea1586543b6f32a577392d196c2fcb4810d0e46c4a2cb2f430b797023a8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa43c0f18a33811b7993ea262b9fac257c4e4d939f8e462250caf5636adf776ee9e86b0dfe24a89f126f2edfd4dc7d5be05dad0dffc9a6a7f9ec7b00f4e8d1c258c3247c6b553243a5c04ecba78c863aeebc10736e57b153ec14367fc3a143404", + "slot": "12859661" + }, + { + "pubkey": "0xa1187e8c34cd0ea2742b80d95b128a909e7654e5786c11bdb68dcadb8b8783ab54d6ff62b42f33bff710db5ad023865d", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8c0006284b6c4186a9e922a23ff97acebb0c3c494e2527e2318b99f5df6ad6c076871a058de05d7ff60399942efc7b3e13d474074897187e0aab7f5ef95b9ac7b45d3b4e0f20aa443086f06d2a1c9f6b46bec55a88505c52cf6f1a1739074e15", + "slot": "12859661" + }, + { + "pubkey": "0x86eb0bc0e8955e4e49b70fe479241b4c03a1f9328ea673930ee1a8735500d86e923f14b83bc8bd3377ed08918b1f195f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x865c4f93a5dbef902f305b3f79100833eeb3ac21ad967f046b8b21b40e7e9efd1f31f5e6df93db02423ffdf8c29ffbf9151d56f92af3bf1f9bd0e4dc9539acb842d8a87a7a2fad463b9e3f03ee278144661b0e5c14e832ec23bb4e773751f51a", + "slot": "12859661" + }, + { + "pubkey": "0xa1cd1ffd01dbdcae653bbdc4d36ad2c0991f622d19ac9396cfac38e88737008d0b1311499d21778d843a6dc7da315de8", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaaa8d999074007d316b5f9a2670ca220b4303cf1bdfc8270155376574b60116213010c64443b9bb717afe6a3d267f8dd0020116350a775d4f3cab0fb6205501d7b5242e646824e1f6b4d457c780c029efaa943e3c1d1425e60ed88a3ff442179", + "slot": "12859661" + }, + { + "pubkey": "0x8cd56f3c3695d7a46ffd61a09462f7d2c882f7d073a1c7e20fb618dd4e2692ae2ebf910089b2e7985469ecda88e21789", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb967066815118a4c7430d9ca50325451304d5f544dcda27f1fb38b729053b8af4a3abb501fc936017cce5a2b4b7c0a9f04fd330c3c95b0af5539084f2030ce1fffacb4fb8ea33ce037d33141ce94833b3d908153dcb537de404a3571a0e055d5", + "slot": "12859661" + }, + { + "pubkey": "0xa13355e12843cb6b042d03fc197796e6cabed383c497017f74a96e87a4dc3499be8bc3648b2f9cf84b2190f093324691", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa2f8d829f2d05010418d807ef6266324bd94b98793a7087bd2e07c0248d63cb72753dd7d280d12bad6fbcd612910d1a50ff72f15a347ab133b054f7747bfe1f16d4e18c6f85bd46b293732bafd607c74d7de2a082f4157eb68081776f5d6b44b", + "slot": "12859661" + }, + { + "pubkey": "0x899c4ed0bb681b2bdde9f6806dc45782fcfb0df78507fd877a60c6ba899f8d4a3938cd1809064e6207c2731453a781e9", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb48c00993159e271609230449cc2085831d5701dfeeb98b9d827b5ed7515658f40e3904cea2d068d177339fdb0147c0202292e6f3830e666fb7349b0e9506e106b63619ccfc86065f448650702b90d73a4672fb76ef35fbed8b404655f6cbd5f", + "slot": "12859661" + }, + { + "pubkey": "0xb3b01a187ccfd2bed0ecc714fbc7e7672903dd23c6bb7a1670759ac2c0730057c7c022aac02a755092c234098673a179", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x818688acfe6a99d6dcd451c48f80e3c78e389e65894190d5da3851f41d828220f136a743e7ced07c6a3a45111975800f07db3a2a2599838cc5ef40f961eeaf719c6661ccd0cfa059076f97e6414a90e278e155b4f06cf874f698182d39201e24", + "slot": "12859661" + }, + { + "pubkey": "0xb7b19e41be09e2b080c841d5cff87dcaf3db1ae03ace8ef22218ae29e76ca21973c09eb7ee81999b0448c4dd44bb884b", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa8c6392797cfc5f3d07a3388ad0b83a9918d85110edd53b5886a01842e7ffa92fe7e609821f7a48ae0d6285b6c5807c90ccbf091229baf54f4310e27496ecb0a777bcbce8600ee087abd2c515d750135948c4169cfb3ec2c82ef9ace192c52be", + "slot": "12859661" + }, + { + "pubkey": "0x98007f4964cde744711974348c069a9f23157ee4aff93a5935e90b91d0ef2b73c6ac464a059b848660d2011973d58ef7", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa9c53c02e608aaf0350f11cebbfbc242b7d8c01c22819ac97b5a6a006569469246adc4627a3161b019e71ab49f590e5718aafb9cba95ec3af9b1dac49b44acb2594f0d8a86ae036807ebc5cc6f786f6df1eb130045639404179fce1b396b8e7d", + "slot": "12859661" + }, + { + "pubkey": "0x875aeb541f1fbd1174acbcbcfb02cde559e290aa0727a3907c49f74c6199c898b99875da23a0bbfd4fccc3de5a6b3406", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb6653e697789be4b3587550c82e368f3da8e8c059d76795fcbf7162e9475ff4df1e5b96ae2dcb5a02941a6e79a3e46e1072fcf178e1f6d386a736ca109beb1c012d826af1838e201e1c0a6481b21a1045f9b0dfdf37babb8095849b75798c7f1", + "slot": "12859661" + }, + { + "pubkey": "0xb0eff9bd47b90001a4ead97db1ef6252069c055f6f1b49fcfc191e037cbd0ff65ccaf2aec756ab4ff9b38dd2d377d9ed", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8ec51b3ed582698dd0aafd0b7f8307e42ec06631c1c14261d08549121b5b3ce29c58fd7c91f52ddba04632d647699ac80c7f0e48d004fded22b724e511a5e622425aa4e27984f0802969472b99c0589c80b39030c26c517e38f4950ed7ee7321", + "slot": "12859661" + }, + { + "pubkey": "0x8c48dfb24a6ce21990a34829b3dc087f44895b121ff43714d4cb729c9b4d994fa4b827f46ea5758861dbc01adb9d9e90", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8c00c0e16a9803decc0f325e0cd1436ef37c3cbcd28738d2f040d628a7de21238a36e3676a8ea5ab9c40cc7b506458b90cb62c192b009af7aad4917a6fdfe55947b65ff2f71c1c3690c4496bb1834416c7b26ba6a0af235654c8eb765c8edaa3", + "slot": "12859661" + }, + { + "pubkey": "0xb72fd7c8504f2ebe17df9d94021d3c7a3a0428de1a5ac7f115b9830ac4857ad0183ba5128387729d25d1ea4bab1cf9fc", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x8e92a3eb6a96c644282c580e9e313164957b6261a6293bc744011868b11b06b57298eb4c603ea39f85369573ff48198b017acd231f73e96a20328db41f85227478509978d81bf8ec3328eb7ad5206edc047e673de6f7ceeecd8f3376e68adab3", + "slot": "12859661" + }, + { + "pubkey": "0x871a3cd6e0b801c9b403a7a81b0b0b6ca35593f89df7db229621abc9c9cfe11a4dd8aaa135224d7b2007c46779f57976", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x921a3ae3786692596f025c59602b8cb73454f3a3d3cdbb19f2305a531b51303eccd0028d69153df04a13c2bc3ff8a4480105178d8674284d91e0fd79d2691ef79d6e5b99f7f3c0835e80afe5602393348290f02b4a2133056415904cf74f2afd", + "slot": "12859661" + }, + { + "pubkey": "0xa34e3139b2911a5cfb7e366a873b2a94ca6c4b15c503179b7ee3296a396a31e9b5a129ad90e6a4878912f03e6118d74d", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xab43316a8638be306509a7e80b0a50e9c862159167cd620b51dddc0ee36043db5ff669d3417e41f1e3aad929f3b48de30b01a2a1017fb1d4058f5b3be93bdf8e360fdad83c5cee33b4c0839ca6b9ea49add4f5c74a1c91c85dac171795d96f6a", + "slot": "12859661" + }, + { + "pubkey": "0xa13755e961db3b837ab1e0cf6cd816480d9a2714ef128f5b92fad1680d59b8a7e72e3d83095a36d899732124672c35ed", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xaf7bf91cb1bb454cee30f8b0f20a8510d3ec3eb0bf9a795f62dc3db383aa2089820f92c7f1d63b9be10efa07471445c60bbf730b9fb75b34495bfda99e15424351bf92db26b177605bc531bb6009dfce39f0dd0b02fc3048b33e88b5d41e4061", + "slot": "12859661" + }, + { + "pubkey": "0x92d34decb4c980d2696cd7b9a5f46e5752f94edf3a01f49da9c34ffa37608dd0e8d403e0a811a7bc774c95c98240e0aa", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb5702f3fb7876c3374403fa43b1310987d2f90cc9af3a6f065421a231523b5b32eece32e8ddb0ba86ed9fb2f62dce0260568bca480789e69673c8274f2fca0230ddeb1d5807ec287ce4939a8ac975b8b8174bb0a6e71856345756fa25e9290bd", + "slot": "12859661" + }, + { + "pubkey": "0xab2d1405e82ba48a427206ccc4c23035e2cb9ecc2591d9523db0efe5c073a42e4b1315978389bb7d649d4b00fd6050c6", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb4a05fe67f5533b82fb6e20b1b2abe488a85e2cb02842af3b126dc26f6d1fd1c7c45842963dc5c619793739d8125a1c803d65262f73d3e7547ffb0963be2e85d377119c5099a5039a11c3763ebeb1a2323d518fc1c97cc107c258779a5cb6737", + "slot": "12859661" + }, + { + "pubkey": "0xb350664607edadff042ff721301d6ed50af0d520df397fb3feed695b01dc5f05de5590b4261ea5e9ebc893c914de35dc", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x987cddf184df53ed04ef152bfb5f91bacde8940f5f7128bb36487846b2f1a067e8b3c4db345df0d60f04528747eec6351566a44d46983d98dd0784d975c84566f926d34e5e45094772e40725e4e746550cae93f3a7e6eaeb9af935dfb3b72128", + "slot": "12859661" + }, + { + "pubkey": "0xb65384b08aaac0a4134e3af830c3bfd66bf4246294b6494a0909234aa971802a8f8488bf6f403ed8da66c672e554bc1f", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb4df58e8c86f9ad23d8f7936c3d27c8da65d8ac57e6fbfd711b182419ec55f48585f860742999f25fd6d808762a7e9420267b39c42bb5077721d07a8feddea2da18613dece3c3f84fe1cb5d1bdd92210bf72ed15e5bb235fc69b10fb3e1de36b", + "slot": "12859661" + }, + { + "pubkey": "0xabfc9bbec39cd2ffa5336ece059617b8217eab47d7cc60f88c3ee9f91a1e04fac1aee3ae7ad976aeff308632e44ab158", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb3dea603c62e75c2144fe1207a4fbfe9787dcb0b2777c775c34f345084f2ba1ff85203370786a14458c3fd2d7fa11be0157c2f30b8abdc660a17a0afe0b347fa390b87d7d3ea8d7f2341ae7a274aeec33efe31789268719807a31c6d909f4855", + "slot": "12859661" + }, + { + "pubkey": "0xaa4bdca4929a3374eb3e41968bf575b6580aa60d1a9ef37937f3efed573482677626f563dbd441ba263df491a187b476", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xb8b42e7094dc7b23170a99390e3cb835d5df70606c5c499863b1c8640491710496e4c42ed979b77a4138e10ea1f588220ac9cd0469069b6eb947b2da930d7f38e1f9664b46fca6fe0b67ebff4e89c3bf2364b1b27fb012f358e90dbf5ffb20b9", + "slot": "12859661" + }, + { + "pubkey": "0x81408e7f5b38b56d8862ac67eb3f40ba6c6bb08d57f69c1879662f26dd761f13b9867b5a6afb5d95add320625dfe8edf", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xa9bc225d5a746d5054ea9b1c0153fa6196ecb2c7cbf6b63645ed178a871dee393237da6dce26fbbeda298e575a0440b70186bec227a4a8d330a936848ab73eb8b697608a170051aa8f0b02b71754ad78b724fb67134a84716d298f0dfbeda60d", + "slot": "12859661" + }, + { + "pubkey": "0x977998de0b8aa6cbc4479daae420f57b4ad64480b06f3f62db0f52aa695b3618ab35c8014416b33b5c48c44ffa3a0b3a", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0xab0e615367d9705ddde12172ef67346b852374d8578ceefe8fd46151345b452ee729ffee06e829d0e6ddc65c4030710e0b1ff813b0141d11e4c802be74ff2adfce1fe224487433266a4f0634c23e7f4828ea0af47a09840c77098baee2e6d56b", + "slot": "12859661" + }, + { + "pubkey": "0xa937bd2ea071e5872cea57b2bb4483c4968e8c935d5dd9caa5cb0e4a0a505e2b71fce08d309b832db692d04937b05099", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x98481b3d69ec2a956ff35bb4f7497563e1396fae875b06e14703a0a7d4415266d52220d78a16b697de00e6688c19c76e0af79e62ff0f4dfde9127863a06c73162efccd80d15fdc6e62a45197b8212086b74d72f0796c66b5d7169c968311b013", + "slot": "12859661" + }, + { + "pubkey": "0x85b6232079b2f377beae7f2d022bade18acf70e57db5618b12794319b6c4964677fe0c6dcf9e139c8bd0b4dad8a02043", + "withdrawal_credentials": "0x0100000000000000000000003a11dfc18b6e54d88fd846eff967c0b9da3a7132", + "amount": "32000000000", + "signature": "0x95183a9a482c3c24e355a58d75a98d997f90b567364206ef4d34bc35a812f8e885448dcbb163d0ba51d6c41edb913be111b85bfa24f8dc997ea1c06102815beeeaa66c9f1611eeaba046a92f8354e1df0e72f3cd2130cb42e25ae1356da51c15", + "slot": "12859661" + }, + { + "pubkey": "0x8bd2b7564ca60ff71289a04038281e4e8882102fabbe9d511b750ec5e15c1872d3d20ef5239a3850afcf2ecb6c6f277b", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x8b63c221425ac72b427213828aadcb61c04c31130bf52eb1b12becc03d35e31ac2bcb5b89e0c9a8e9f13fa210823a6b8088e78eebba4c53c9b4232a3582e581a0a5fbc08d093f177753d7ce26b4f90dc783396ac5d403e358f096456a71745be", + "slot": "12859895" + }, + { + "pubkey": "0x8f89a17518defd3d455b28b02948d19442a206748f8adeff41907f6658ec00ce610dd51535ae77fa72623da7615a744d", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x8dcef3e683e488faa9eade05ec43a65aa277c2fb67276c171f523fd119b971afd106ab6ffd4d2355ddca54e44f64489b0a0165548233a2eedfeb2fe765520f487b8030fca5c39869f6c2db8e1742506afd59413cd19fcc393be330d9a18b0bfe", + "slot": "12859895" + }, + { + "pubkey": "0x9300b68bb0c43a91870b7e1ceb4c167bd581d3b265d29c50f87a7c55aaa4c630243dd9d113df6784eb4257aa962e332b", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xaeb635ba454c0cd2b217d94872c05cf2a39c7cf962eff3db4d963b420cd87f18228852d4d8a7cd23b5faf1a20a499c210cb7b9ee1079b06695b802489af46658797e134cd003e7ca6a4ed6bb73c5498eaee8dfbad618e3c2ce2fc4c560020088", + "slot": "12859895" + }, + { + "pubkey": "0x8a1ae5162c4a9738481b42432dfa333591de895f5f5ca88971e8f0b39bf9d86bd314d34586dcb7bb7231f15067f550f7", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa415ede98adbc56daefb123c447998468ce533542793e322bd67bcf519677f7d4bc4eeb722fd30768284276a2e8bf2fd0a40f2e79188537681fb49c21febcf0109481cc56d976776ff734492d9a6bf0ff506a79d0dac62d983a0428e3befe36e", + "slot": "12859895" + }, + { + "pubkey": "0xad12655a39d8d090aa7d1235fbcd2c237091948135509b009cb7cfc0dfd8ff9a5526ca2e81698fdad678a5644566f458", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x96475658f9fdfde074639e49469a6d726a04c50da435235dc34199871bb7133cddbb69b37f20fa16dff0789512d68b69157382e254c5d41ca34c53200dcd1150195a9bf53a3775ec459e68691dfcce87d905efe74f5ae1f57c9203b78418cdcf", + "slot": "12859895" + }, + { + "pubkey": "0xa543e05183340bf8ee813df88b62070f310dd9a0bda2c3566c87478d263a18d30a2bd298cddcac353057facf0aedba85", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa682f5e17f2d6e8b5c2c8ed53eb483c46abc0cd6470e7229d7cc54f17739f21027ca7f373cd6c1c00d3bef7b2db331051683289a80f8f1752d6f75c8b65fb63e295e87424e75548480250cc84e37d5f252fe95734224081330be86c5d14e8c26", + "slot": "12859895" + }, + { + "pubkey": "0x8b56b64c59e5bd21d99740aee2e0bc11ba869a775a28a88161ab77980ef251a015cf7a17cfe6e1ae6d99aad1c1d7acda", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x941448f3eccd61565a253a73ebb8c7cc31e5b1bcd256a6502fff5b911e2f5329252aa1005092f0e27b9c6629e08db3c6093edff79693f5979ca86f86a3ccebeba38cbd59a0a73c3bda510576a6ca9a1ddba4702422f513a360ed47f01b43face", + "slot": "12859895" + }, + { + "pubkey": "0x85ef7d02e84129ed0d743ae998470e901eaa25bd6eba07f0c428f5345f720d4026aed7f19d16324747e50d193941b823", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x987fc04b4fa87044b27018612bf4e4f37b654cc8faccd381be045e6cb601dd8323b5a45c9609dd03ce8d64d57946bb640397fef53bbe08e183d6d55af9706aae88978c91b2bff5718557e32b64e19e94d5e198481529d919ef00257fc34487f6", + "slot": "12859895" + }, + { + "pubkey": "0xb239e059ec585bd92f78b2a2907aa5ca3d79fa0aed6747c1ff61688976f338a38ac79def87db205b4eaeb9e3f3ad569b", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x8a1ed49f783c63f0d161f4fa74f358a9906927c7d826c151ebb24116afa6aaa6ebe41f861a011b168c2bf0d28a4a2bf60c4cf4f07c1ddd812b9ed4527d809cdb49c6b5ee85a03d5bf0930708248b7c879168c75afde17105d187701abcc7a6e5", + "slot": "12859895" + }, + { + "pubkey": "0x86735cb198864d486dedb42fb8d217984df637ee268abbabe1c77a583d631504661adec0f2d5894f458027275cfdc674", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xb84b55d6fd98921566cea800d923749deaf36463faea1b4b8f6cb285e5a945bf7caf934fa859ffef210fa1e99957aaa41918158f65245a03ce64e04c51515b218a9949cdc87383621914b8980fcee473979a95801eca757267b3bfbcc7efde62", + "slot": "12859895" + }, + { + "pubkey": "0x8d14bd0f35fb97217e816ac4a40cc31656128603eea125c63ba802e9b8e87ac7a3e2e6c7a520d441a72d279d5533feba", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa997cdc7f823803d1ad70de4cdcd8f27e213db28d701a1602eba889c78419cf51a50474ea3173f4be592c72e9562899003c4c36cc88b8a318db3f84054c491094d2741c0a69224f9c1549e3c0f35774bd4cf29a12bcbdd41ca0071473947de60", + "slot": "12859895" + }, + { + "pubkey": "0x85741ee91e0d04104a43f823d9fd7abba8e3eb791b806f1378db0fbbc3479dce42ecee012513aea9800622b418762931", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x891c035720ddd165d8301ec18db4f8e947211e82ae8fb6347ae227683370e26f11ffef1c92a62ef1c6388212d1946c4208e0a222dbb6d6545b827d1aab9cea9562d3d86251e9c2a7766fcfb972612b4d3b89304d8fe40a7444861552bc495299", + "slot": "12859895" + }, + { + "pubkey": "0xb4895f1e97b01d280a9676aec284d7e5518fcf42a079bf4d8268606ee1935601e8e8bb1de5f371feba6884dee1d2612c", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xb3a1fafd0aa07c7ac35b14962893f398bc00c226e9f40704bc8bb5c373db09d88f357baea832ca0fa55e290397bd5792190dd88ef6f07f1e705ecf47b398a7d644ba6a78d91338786d209139e307a4399653d86d1cb9a2e434a4eeeee7a1a752", + "slot": "12859895" + }, + { + "pubkey": "0xb0619418221f42b5948ee8fb931671bd46059280f9de15a35fd250a02fdd1334942425a7532d5ce7bc2b9b14b0140b8e", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x904792b841b3e2c130fc302006be2dffcdf253f0e9a0c435901fed89f0d5a28b8c42e8ff18572b9634e09191b11ca6c407da083ae3bb2311104852d6983055d8acafe3d5b8498aff93b4fe8259732ba144b0d5d0cf3138bd409a2f9b775a06d7", + "slot": "12859895" + }, + { + "pubkey": "0xb322c5acbd16a5ffcbb78ff4ceb6ecba5652733fa294766981ec5af6781d15fdd20de06c9ac2fcff3a020fc4eff16fb0", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x956037c462fcab0dc3a5043ef251575651a91a8a10bcadb2b616a181fcae26f4bf3d76054a0712667779dd1222b3de5101d7194ec6252e0150c853008c910ea2547526234845383e69616765ce03da444a8713c185adb4e149d24c25d2da9058", + "slot": "12859895" + }, + { + "pubkey": "0x99adeaef7e78a80c5d6f865a44feb34bc07ea4882718d11210cf8b7a64b0e19b1fa0527235e0da0c9db57f455617da47", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa3b597c5701e72328c04b620948b39340ad2b298f7a43c14b6c7555476cf8c0e468a6b21a0405999f3fb7272b5b224b004cbbb096b4c1ec1f7d9166dc58ee9439f7477bed37904158b8a0fe2aaa791197a3ddeeccb58f87cf5323be7bc918844", + "slot": "12859895" + }, + { + "pubkey": "0xab198c78373cc8a510ce77261f6c2d2ebd0288de6710996aed1b4a0239513f7f7ede296c87e0943b6a4093f581f4e516", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa82cd64b14c53c91b6f9e94e0d2222a4a0460e11cccd623ac26d4c7199e736fda9341256ee66dd29efc3a8d1e807affc0723a92911b1f30a03e2d88798809d092ea4156fa1483cb386f5a10bf1eae2a7f47cda2f7dca303b1c6469de7f848b85", + "slot": "12859895" + }, + { + "pubkey": "0xafeb030fe9606dbf7f25769b46d35cab8edb2aaa51eef60996d72ee6bb324b7da16f978a5109ab5ee18f918a7a090caf", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x98f74e393fe3bab5a32316fcad03a3eeb763f2b47be095e373c5a2c8ee0841930d46a68323308858fed3d0701e43b00e1945cc14552af0a48952d421e66aa06493b3202680e3e707d205ad3fd3271fcdedb127ac5454ee1a10f2e71cec0a32fe", + "slot": "12859895" + }, + { + "pubkey": "0xa1995d7d889d95fe363b8fbdeebaae543bbb6803cfbd7528a27939663488534d39517e891c45f078168a32e20fcb7097", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa1db776dccff9d54a35e145bc21dc72a55cf096d4c79d4d1a1be7242ec8b5b5d3900f775cc68d1e3d6a9cb2482bc7f0c0561e1598ece23aba119c77fa101cecbb64fa85f29536815f909b40e2128bee034fdda9a6ca327705f53fd8be82dcbbd", + "slot": "12859895" + }, + { + "pubkey": "0x967ef7d11379977f12b58edb09b4860e154a7c06fe97bc46f58feca523dd0d0e19906704853143a8a422bd87fb5c540f", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0xa0d13ad66fc2e25777af8519c39563aa5f25df78998cdaafde464728a18d4a87ec45ab3d0e39d3b86688d7bb1d0b8ccf0bc8acfe68f354b48500f46d6d87ec0cd191466679fb9fe3c67b83ae126ade87614f03ee5c522f0ce77c1fa3920274b6", + "slot": "12859895" + }, + { + "pubkey": "0x8ce675c468fa8bf62aa01bb13ea2f5c0bd97c58a4b99d9b881d2214eda0174533821b835fcfd5a488f9d65aa5185d962", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x90b37b36fa6d39a63cf257e43f846657b55f0487ad281a05e5b2404b27360c6490b25ef0987554d9fe8d555a5381678c18d37c0afb978ba2cf32fffe9957431fe832f01958e85576eaf404cefc33e30b45f1fecaec1eaa01cd0dd5146860d892", + "slot": "12859895" + }, + { + "pubkey": "0xae06bf6b24363cb2bebaf7edf5156d68ea93c2c3db59565c56183528dfd498c7c448d6c86535279079404ac7e19029c6", + "withdrawal_credentials": "0x010000000000000000000000b23c002bc65c6bb539aad4c11d606ef4f5502c93", + "amount": "32000000000", + "signature": "0x87cc0e31eb7c8142fabc88f89094430b7d052053f37fed297360c4b98218082eaa8d63aacf5c24a3988891bb6bf570640ff13cbc4e25a1686d16ddf75bc78604626041f060cc86422fed08bdebe68d7ce6e0cd09b93c85cf15d701c0d1e60110", + "slot": "12859895" + }, + { + "pubkey": "0x82f1941b32ecaa7984f89a3575aaa90c841ded47b35e0f13344c94dd924ad52d97dd088d26c0c4d996be7cb58052a17f", + "withdrawal_credentials": "0x010000000000000000000000615343de8fea4f698a9f6a4c3a277c9e6584b50e", + "amount": "32000000000", + "signature": "0xa559b4a8dc6d23bbc5a33035954527f477b92d424233bb9ec2a8d52c702fc55d2a7ee90e19606c1d0d17f93c5f560f04150cb4ac0530945bb9e05b381cc83dcf6a97c1c7c5b40704688b19a283ce3fd510f50f4082f9c81f1de306d012025dda", + "slot": "12859928" + }, + { + "pubkey": "0x8c195b8fa30522bc812afe8c0c8840b8bc79d8d779fd4b3b8c6d2bb087cfce19b27d5682ca824611e3665570fe30dbc7", + "withdrawal_credentials": "0x01000000000000000000000056f924302fd8e3a71708b65483ef40cee383f73f", + "amount": "32000000000", + "signature": "0xb1287d8d120b945cf9dd9e142c63f7d3a167065aa2d4a8db7f494dd16b087b746f2933fa08448496f341a0f8e10f3032087c6565ec116596f41bd01c42ff4ed9f8818426ced0c34929cdd6d51cdbc15cd3d27e63e1439965808359bdff207294", + "slot": "12859971" + }, + { + "pubkey": "0x8fc64c75533af23d07dd90059a8b48f9bf0bb4eb766e774cd4e0f81a5f59bbcc11f2d0075296309998417400577ba629", + "withdrawal_credentials": "0x010000000000000000000000c55c7d5e3af0818f8a567acede50d3db822dc428", + "amount": "32000000000", + "signature": "0x87fc2e3f69dd289f0ebe1aaac00ea1f99dcecf05a4e19a609674b7f84d0a5789765da4890fbe2673e301d08a6a3f6fdc18162fd60d9721ee01a563b20ad19fb1ca8b7eaf26ee0e4760dcf44d7e7d1fe73998986d058afb752f64987b9d791ec5", + "slot": "12859971" + }, + { + "pubkey": "0x97aff615ae384ca8a86fa617241ed6ba1d335fa8e7bf13d1e895f0120ca3b3a088a7a97c481bf3f8305011456bc4fb7d", + "withdrawal_credentials": "0x0100000000000000000000002b78035514401ed1592eb691b8673a93edf97470", + "amount": "32000000000", + "signature": "0xb07f54ab3a043af673b3957430992230caac92b3148fab1de51c171326b8d3a61f1fb3829de379dc44356a6e93fcd9380c7d8664eb998c55fe485463287d3da1f2773dec1e023ca9750a75c4b319ee19fb4a9394e11c6df96b4dc1319f3614a1", + "slot": "12859993" + }, + { + "pubkey": "0x871e93175508bd7d7c6989a9d4e52e922b5dbb42bde1e742b7db9b7706906ba1b080f80bcce92d3eec9734d6acef1137", + "withdrawal_credentials": "0x0200000000000000000000003ba279e4fa904d2a81f36dfeb430cc5ee498c219", + "amount": "32000000000", + "signature": "0x97b93fcff3cf74a7934a57bcb2c6206f8bb4929d63530b5f5bccb363f9c182aee88da2c20d0ddb1069ee60e037507f4e0b53abd96ac8ea48ffcc73d20f72214220ebc06c9812eff1aa75a936c76708dcd0d9c35795075b091774eff685f10312", + "slot": "12860001" + }, + { + "pubkey": "0x941a93e16516b02aff03275b3368e97cd13267a05a19ba138d364cde99c5bd7397e38a50720d71cf6720b4e41e662ba4", + "withdrawal_credentials": "0x010000000000000000000000de9f7b8fd80d88eb51b1316ad81b3e1dca2b97a9", + "amount": "32000000000", + "signature": "0x8b1ec7c18791f1accd0c1cbcb43e96c4d13cb21b465d18c3537c5be449f2a5aa1301402190e31c972f7bc8ffa36ac1c8067f745e115fa1a6fc2af28b6db5f24fa1e7b32b6b893d50c1c33fe83a1371a9b5fe99dd2afd305d198fd2ca8bbc9e86", + "slot": "12860005" + }, + { + "pubkey": "0xa717c63666be163ee87e08f28cd5edd1c79aa67ad3cf28b0132c8d25a423c6670c08e5e42b6813f9cef55708ca347ee0", + "withdrawal_credentials": "0x010000000000000000000000de9f7b8fd80d88eb51b1316ad81b3e1dca2b97a9", + "amount": "32000000000", + "signature": "0x8980854b6cc3466c2390df51864f20c0dcb5001687ff3e9c2d6498a0fb89f12843f8f0cfdad3a27a9e477c67954ea673172c49d4d2897a89e22bb5e095e500d633f4e416062800b8ddbf8d1f1084a20fa3e8da668aac65e1dd759ab0eece1ecb", + "slot": "12860005" + }, + { + "pubkey": "0x85b33356552125db1a860520eed7a46b63d047c17bae54becdfd80e137f80d28a58dd3cbc8b82dc94181f0d43757fb24", + "withdrawal_credentials": "0x0100000000000000000000009a72b5337b6ff3c1fed2a0f52efe2d2b1a0536e2", + "amount": "32000000000", + "signature": "0x8ffe15dea564b56ad959417d557d89d95f7aa84814e663d3efe1ca5afc33ea2ce680b484d118a112266358d796d463bb0abf439ed50da2b0c967826bdb4b9c95ed98159f0b0e9fffc73ad8b3a5ac393597a3de7a599282860c41412fa8133c9e", + "slot": "12860058" + }, + { + "pubkey": "0x921c1751f139dd6785dd887e6c75eef427c3accdc3c8b6ef0af945582bd190290c73025f6ad6ae5ffc91cc5a461e4e10", + "withdrawal_credentials": "0x0100000000000000000000009bd494dcf339c599d814e473441d468a3341e998", + "amount": "32000000000", + "signature": "0x8484f3cff617eaa79713bd8547e6850d53c529a8b679ac79bf6239eca1b861834dc570584336b37c2a4848a7ab44a58a11c00d2a4c377cca3dd98bfd9e8860c516b5a9f7fece5012c3892f545c9f8237d3659996ead25b0a14fa1b745f61c938", + "slot": "12860074" + }, + { + "pubkey": "0x91e84ed0b8c6c602f785050dd54272d9f57d304c1dfa4bad633a36e1166a64a28ca2fbc80e14c7935f97db4c3755815e", + "withdrawal_credentials": "0x0100000000000000000000009bd494dcf339c599d814e473441d468a3341e998", + "amount": "32000000000", + "signature": "0x9757ac00ddcfb81a82ddb58d27125b5bc377b221a1597db878eed1c73a669146f0b2a940961901ff1cb106f0a69a63460ed15eed85cf2bef6631e82b9d61c59bb93ef2c0a6ec361cba7901d5cf5288fdb6db43cb38d7cb4b45bb6702f3b470bc", + "slot": "12860074" + }, + { + "pubkey": "0x8a08858f074668821d11fe1fe68ff318ada967f84a58eff8bd4d904c7bfcb67cfca0159dfcd93d9983f57503fc24dcf6", + "withdrawal_credentials": "0x0100000000000000000000009bd494dcf339c599d814e473441d468a3341e998", + "amount": "32000000000", + "signature": "0xac9189090b004eb8613a7b7b6726376949757d18e7d9996c90dfc981cf474c7032709a1a31d9781f3161fc9935b429de0c185b063109ab5217d80035eb1a59ff5cc0e11d4483b78132b545392dbde24fa8e05fb608af07eeb1dad4ffc321dcf9", + "slot": "12860074" + }, + { + "pubkey": "0x8826fad9dfb8daca767e034192c8b9788dc3bba915c3c56c38c6c76244f86bc7da31b4c8e741c0e8d49bcfe0c092aca8", + "withdrawal_credentials": "0x010000000000000000000000adc010fe31156643f4ce5da2beae4681d6c6dacd", + "amount": "32000000000", + "signature": "0x8d0014e27cfd8eb4d0e4ef8461c6e98a51b8578a299e03728a8d4e7a9877d19144cca3b892cbc4a8ea9e20f06761085b088c4c92b7d83cd909efac2ac4dce2de7914cf1234f660105b51cec6ca072170ef1d955ca95821d4d6899bd728e328db", + "slot": "12860186" + }, + { + "pubkey": "0x94b1f65966d6c74f871291413dd3ff16facb821e6d9af5a69f7edbfc5ff74afc9a144710214a30fa4c1ab9a9dba317fa", + "withdrawal_credentials": "0x010000000000000000000000adc010fe31156643f4ce5da2beae4681d6c6dacd", + "amount": "32000000000", + "signature": "0xb3ba1ab8417b4568dc540a3e09ed3a0f57ce1c743fc4b063945a5e658b38e413f6c58660caf1a370e3ac59eadd6bd78e1388b2c70d06be28c27be63ef34240510e3dfa9bed13d55a8ac3cc11958749cba4b2851906be0bfd85917b1f96b4e5e6", + "slot": "12860186" + }, + { + "pubkey": "0xa8bf25d9b3b746fcf267c68adb9b0774ee0224b8910d20b499d9832f7062e14222aa5a1a085b328b0c7ae5e775df28e2", + "withdrawal_credentials": "0x020000000000000000000000857ef454724d3057a7c9ffe79dadc615eba9baa7", + "amount": "13659052", + "signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "slot": "0" + }, + { + "pubkey": "0xb5014b070e9ef71f841a1cdb05cde2cc6d1c814eaa2ac212de7aea0c6465af40b76626b57b574f9af9dbcddc39422bc7", + "withdrawal_credentials": "0x010000000000000000000000973846d98a952745f2e2113a76c5d62ae04e1eaf", + "amount": "32000000000", + "signature": "0x96ae75ca1c3faef1acefa6c15f0ba38130287a21b43271031885471fe260859e2890a5a43fcd46db6578a72c2333713d0d1fc1072d6e3f9292b78790b700f8116c3e54f35563145d406fcd4eac0b520e9d4520c683316a96923fdb26fcf6c90f", + "slot": "12860301" + }, + { + "pubkey": "0x98859db9e906d6b585c5a0b54d5fd85b2d47563abfcab676125da99bcfea76ec5b8cb65ce60a181b545fa1a3316280b2", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb08a656d53e432dac8b7df3ea935a4b4066261061f283b7541be998462d80c1b1e892d362ec0d606510ae4fe30d0fcee073e79d76febf273ba117d419f3a88901035bc863da3f50630b7dcd7443a48c35ea35749ea592e074443e53b4348c96f", + "slot": "12860461" + }, + { + "pubkey": "0xa07520401da51c216db2b96a1321388bc99d55ff07b69409728f56c9aec2933be922e22f4b0452060bc57529ad761ed6", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb3588eb2b19b9c152ccfd3efe277b7f83233647a5d2d12fce225d16214ddc2d7188cc417ae64f440053a9ea46c5c8e1e113767a0333f7b26190552af828952998f3ab057b5bfe2f3a1e40357d4073bd84bc08799c5422a020306767567cddfa9", + "slot": "12860461" + }, + { + "pubkey": "0xb0184c3919de0d2dc325f93e74e8f9567eaa94e89c826babe3fdfdcd2a1f996874f7d0836817399c2b785932c464fe87", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0x947dc6eacb436053a923694025d6e162ba57366574105ccc38f3631ec64548aff1190eb82217354435fbe47dea53e4ec00c6282b484191ceedbb8e4f1158e47ef7821011703dd87a24b9aebb6e4ee9e7f938d184c9d77b5933720c84a0c49985", + "slot": "12860461" + }, + { + "pubkey": "0xad6d96dbba35b8529a0a9037b18e10beb6c87faceb3a8fbd7d07b2161f4dc19b639c184fd549e188d868043944c3e77f", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb1a8b869788ae9f88167b325778def519f0dfec0e784dd3f29e45f232251996a614813c09ca864bd74ef5d86b8d9a7f90faed14fca9dfd58ecf3e9ed7984508be32a36522a890b1dd56eeae16eecb1ec4461bca1b66aa459bd66d2cb0eff1663", + "slot": "12860461" + }, + { + "pubkey": "0x8ab648635cc9b437da49982fc495d3b5829cf21a3a000d5aa73c4ccf4b5b22dda85881a9403a13264d44944c96be9d7a", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb169c5b851692ef4e43b3075ce9c4bd2478a29120cffa6b528bc81af024cfe352f69186306ae95b80045dcfaba02d86b13726070f14d7c0a447137a63b6a3b54f760b1fedaefab3d9c7ecf16bf407bfc227a3d1429711f3577d79e5cbb3a14ef", + "slot": "12860461" + }, + { + "pubkey": "0xa38be649fe5995ccd277b2dfec7755fe35de640bed137b7715116e3f1a9cb2381733bf3e322486823e63e4920452aef3", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xa71f5f4ce10ce402db1de4f85d0b4ff390648dfc1cb76e1e7fba789c71311407eda0681f7d51ae2c277f4f3ab6a553ce133bd9ba272e2089e67cdcab4c8d59f48419bc357516ae34d4956d2a9c47f0322f117144029cc3d7e240bc8ed114725d", + "slot": "12860461" + }, + { + "pubkey": "0x8c5ad2a18413f882a049c21a50a25d492146eccfd3935c4ecd009f06cd8c9b3eb94b00b50260f302193b9d87064fa08d", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0x916232e92120dfe519294d9340992b01cdf87c746176110d0154513aff03c4eb296d6491a369faedb5f38942df00931e1749c186da2ab3557b9e73dc3d5fcefb72eeaa0a3cd93f1b300eee38952b24bc8ffc07567383042b6cd6758456f87d35", + "slot": "12860461" + }, + { + "pubkey": "0x928994d235b5f74f74cc6c229f02f54750902ae52ebfe7830ed10f65b0df5bd77918ce9454f17891304f9e67fef7a05f", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xa2552a42a646cc8fbef5e11afc37942d777bc1a057c9217007cfdc3265d91f5c08c3c58cab1f30737565fee61e2268c21782ffaf707f905458b63130aa9f846f28fb2786f4bf475692241c127f4e0b2c3a7b3bf7910f2b05761aad02c6a46ed3", + "slot": "12860497" + }, + { + "pubkey": "0xa1cb0cb5db2d371627ab9dcfafc2db2cf7e4d09b39281a927a7853a4f7046ba6dbc5428f595a6517e205929642555543", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xa8c401b58c129a41bd6d85868d8f6fe22862612eb7d72d86bd6e9403c59a07d20fbfbba1b80fac11b1c05c146a4b8793117fe8e08dbbbae20cd7fe3a2a75dabbcfe5f3c0c39e0a0f217b6cd537c6375cbf59f58d8fb5c6a027cdaf0d15f06d27", + "slot": "12860497" + }, + { + "pubkey": "0xb269646f3501c0281111e3e2b2f0b18b8f1686d1a9993e0e86763dcecc4494006cdd2814e0b45feb11fb3dba9ba7a7a2", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb382029d10f4859dafdc2fd63b43f9a7203e48c70fbf1bfb00861be6e7e8200a33bfad5d5dee20fe05efe18b867b81000266387c1054fe49fc2367e100459f0c00d39cd08238a511b035b6f243cfb57dbef80b8cc6e4111583ae3da2801b72bb", + "slot": "12860497" + }, + { + "pubkey": "0xac8e2561d6586be3082a6328fe5b93a61673934565ee2f825f133250bcd25507ab87c2fa075c1e68ecb1eea66fa95304", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0x8b04978ff1b8763f62ded56c2875618eff641decdc4a77e2f7ad948f6a02469ad844b41f39c4a940f20cef241ff25bfe03cae58a709e25a5bcd62b2aa7d8042c699a609592cb444dae63ca11b9c6daaf5e802aaafbab5319e8c9392a5f7bf44b", + "slot": "12860497" + }, + { + "pubkey": "0x9528aa227553a8a48c37e396ebb75fe676e5d2c1806c9fb36bdf6088e709fece2cbf2091141df9596f89f4bb60c1ee88", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0x81a60dc4c6a99fc8ef5095432c670d04ee5fd633b864bab37822faf0fd38f8136216c4fffac71093a9c2f3c43170f6581315ff08a22a470c1458a48ee81b575b78f3a7f7978c0423e764d63139d4289a53bcb8a3adfc9cbc3a854c9455a027ac", + "slot": "12860497" + }, + { + "pubkey": "0x80c585b60f901c759d5a69c9c38c820d502fe2f880623a2dc499d1722d7aa084bd01e95a1d7953b2edecae2c3714231c", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0x8b102fae81c6809b2da53aac0043ac098163f96ae61960a58d2546be7ea01cf526dfd53f8548aa7e15105d9f97bc70e1004d68a5d610bd8614e1d741b988f2564028c2b670a95c80f77fc55dd6c8736d4f1c46c6091a362c06aa3e4709fbd69a", + "slot": "12860497" + }, + { + "pubkey": "0x8272bd6485f3d67d877c8acf9c6ad4e70a872e1e2f62eb611d4c3779101953da3fbfa1fd58be4d517c19cd93640ad0b0", + "withdrawal_credentials": "0x0100000000000000000000008fa1d8e84df25175221e4003c420ab033e06e443", + "amount": "32000000000", + "signature": "0xb5244bf04626095835a3a8594a40a3cbef5b244298e3af815ee25cb5f14cea9fd369c906aa4cd725bec320525baf8c21105120d8a9e5b1dcc62a5cad33fc320dd2338ce376c8228966fcddc710cfa06f08f207b65ba7b9fc2bd52571ea0709d7", + "slot": "12860497" + }, + { + "pubkey": "0xaa2037e605b9cbf7a8dc3d0a165e3bf07707ed9b8faca7a9537f89b9fa15551c495f23365166619e5c4c19cb1e963bd6", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb49514518c518d6d5b4f961940ff37dea1c698e0f4aefaba18f89419dccd6afb091784915df8913b0b934181bbeffa8b0d2923f6e73ef520aae0a39d21e298a7976fca66b736d88ad747bfd985334275b19a2d50f36c6ab5f08803b5eddcb633", + "slot": "12860532" + }, + { + "pubkey": "0xb7dbc6ff621c020550e489f3b75befb7dd06acbf465df4a6ab5302e84ba31da7cefe06ae31e0df25e7395869f62dd354", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa1d4edabd7d8638edfecadc0c643816ce1f21f5d039b70c6dcfffb9d66638b5e1b4622fd4a87c345c703feaf743dc8ee0074bd2c757583554ab6b55b3d19ced28b854c10989b5d196050e9e44117dde35c3c67c7dd8154690f3fe242c7404115", + "slot": "12860532" + }, + { + "pubkey": "0xa1a9b90f8964ce9d36fd9c61ce7b9f773c18ff2b2e214e56132b114eaf6acb3d4ad932d483095ab23c182fc917d07b30", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa4de6090dbbbcdb8f74bfca41990b2edefc270c87004972d4875724bb59af0e0ccb9f8c66982bb77464ea84e9a8f165802c0b5d7b6a7f7fc7af558ab2e550ca18eb200d15d5379367b77666fefbe334f453fffdb63b8ff687067a99c57b5dd2e", + "slot": "12860532" + }, + { + "pubkey": "0x90253e74b2329b80ba814cbdfe0054f1dfd055d701438e9edf2e8ca0dd0cfd43707fbfbce4cf69587a26dfb5de36caaa", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8d698b31e3ca4f9e5fd70ee40e4f8a07091e96ef1d58a87564a6fc9256c03c68ecf033a85deb1f41b5517e024fb377f516a41eb19cf3912158d0627500a7b0dd1af5d919aa98dc67f41c1b1b36f64a0068d709514e791286cdb71944f079a33e", + "slot": "12860532" + }, + { + "pubkey": "0xb4047b8281653f74d101aafe1fad056a567aff2376e45aedd737e0c779b04af0bc759097230c43f537819df3f0cbf011", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xaa26d63c8e8cd30be82fbaf982e54b2189a06a3ce282e764102e89e10e4ea77c3fd5fb54808ecac5c4729087d06e6edc18a56f37483e169d6c0b4bff4147dc201ea4049c1e28fab96bf61c3d22cf132cf547a3072fc658e58a13a2e5be04cafe", + "slot": "12860532" + }, + { + "pubkey": "0xad6eabab5f15473b8835ba008168cd4cf67b383c8fd500eac4c365a3f4ff7b869b68e8b1d55fac5c7c624b28361c9be3", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb0f598d3b60d76b528f76064fb78bddc734f2f0b957afb2b33829ebed2895c352ca7145b44f1c354e9266f17c39572750e4bf59add9d9566b3a2ee91efddc69615e9c4c65e9c07dd8cffdf361899ce1862b6be7b8741ec8c1ae5b413072b614d", + "slot": "12860532" + }, + { + "pubkey": "0xb8bba047c5adefc0c67f7ca70833a9328501cbababb9a9810174c9159620728c476ac0369bfa205691ff22053419467b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8492e695c326871ac062ec9626a3d1fb3032fd0635f25f61bb614ff05de3f7c287fc9f80cb571a664030d99cc1a41bf8192af4a59198c6ab01ab0d202d92d6bd13b3581fb7e859e7cfdeaaec8b19593fc67f9b81424fd176197ccc4484b773c9", + "slot": "12860532" + }, + { + "pubkey": "0xafabb10d329d62e60b7b1cc772e418278d1e09254ac9ae420883112fe315b30baff48796696ece10796bacc99de961f4", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x81661dfc9996752a4bbb8c7e004ac98b6944fc8212a4ab57caa6fe78e5469fb678d24ea4533ea550840949f98f7b9dac093f30b5190072e5da6a989d3e373cee762abde861790dd4733554cfd57f0c49e045fe1f2ce59cf6e047d001e7c78acc", + "slot": "12860532" + }, + { + "pubkey": "0xb25ac7a2fe72181e82f2b15fc321542b0b428a50c91974a215a2f477d09b1d6a9c24265808b7d887579509ba8e97aeb9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xac7586075b3b9bd666a24daa4545dcc720e435b5a305933209ee30057c7ba8604a25baa00bcbf5eb7afa27ed9eaed79d0a531fc828383c6171cf741424f32004be2ae7a20f41c9d18147df243bbf001fb4991a5a62aaffb59ad029e9da4362a8", + "slot": "12860532" + }, + { + "pubkey": "0x8c4942eda0b928f9287836b8629edb54322afc1c62e65f14518816b9f86feda639ac6569b8c0585b79b76e068356a72a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb1b8dcac4d59a6cb8325ba03c56852de271690e6825dac6c8eb6c005aa55f5dadcaa57dded0e8f535963654417365ea115d6f188795ae1cc7f9222831d2e4148afbf914634faaf175c1be41f3d822d7e78400c32e5af2250b34984468a8397cb", + "slot": "12860532" + }, + { + "pubkey": "0xb896795f12d3d5804041f439f9339a99f809a5a093a66253ac7e82392dd7e718400977cccae1a4ef874aed8955f06ba0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x96ebd226f5ccaf4d85b766c3a20ecd2d8d802c54a4f7461b758686c2161d24a8f6017d3f670eddf0c8f701401fed87491693894c9f6f8d828ea308a98e9fc7f67e923a2b99bf3fd42c04a2adf5cf7d87c669294376a26ea4695e02c8d308a0c9", + "slot": "12860532" + }, + { + "pubkey": "0xa597e17407209fbfd4e9258c193773353f137f9ee92db3d4c8d609d01a17a7206ea27b3f6e657b4ec73b4429043619bd", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x948d5961a95ec57b44bbdbeb0b68397e37b2ad99b92a42b0c4f0d0ba24511b20e3ca9ea374782706635a087c659824010ed5da78100d703f1197065f6e96c22f20f1e760388fdccaaa5bfdd9847db70f178335bbcca6c18e131b1e0e729c5019", + "slot": "12860532" + }, + { + "pubkey": "0xb4af4a8a34abedd0cbe75487b6df52dd29cc31a78c9dd09c7d45b3be312866667ec3e9a5625a58c1eb053569945249fc", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x883cdeab52e85d6c018782ca0a3c633d9a0d87ee5806b049fd675448840e07b051abac719be428439076f717383a49160eac1212747d2580b56bddf5d8527c95a7e0e38637e9746f3cafa5b5e3558f9a8a2a2523e45bd86204403691d28a3940", + "slot": "12860532" + }, + { + "pubkey": "0x81c8dc2a2f88a4db3c026a1af4f43169f659e56c7ae38497af3ca7068c2012cbd0768f4ffb9610e425e8e19636b132fe", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8f493d61db280acd8ad8a1f2faba13ce2d447228b38500f90c180e8d92328e879a19d2a01f621ca5cca3cdb9ac13a0ad10c9ccd2c933a5ad87e6472888e540107c32f8485fe44b0dbf11b8f3ab6030bf2d70aee3f253d8f9bff44e4079359c01", + "slot": "12860532" + }, + { + "pubkey": "0x84896fcd4e7621430dc47d6ba5cfb3ea4d319a0cf6f6499a8d431f5fc6ef313a95d6671b7c3a431ffda8c714e5451df1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xad2f834faeb2bbd5df1d14736cd4cf3b90961db00f80ca48d99c90711817235dd01b1622174bb05c667b8d93a2335b4f05479ed9a85992b70d4627a600a9dcf19ac7f0d8f3bba38d659cc3f9009034e6229b2a67df375bbbf2bec5f4ec0a2ebc", + "slot": "12860532" + }, + { + "pubkey": "0x96b976a1a8bed929f2e7241262f57637c534d176e15ce5493f70a0759bb3861e8b24033953761dffc643bf75588c9a84", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x82bfe2625aac52e1e19dec81b2dcf1916615de21f1108ea68b363c0a6a42980b021cd371721751105c10bda4e46051690fe31e6c3d1d7c0baa31e6839275e5f64aa76b4a1c64418a3e2c1609f488b7d5f2dc374f4575f24cfc4951f3a5cf92fb", + "slot": "12860532" + }, + { + "pubkey": "0x91ffd4cfb42c40e93ff2e4fd3d74acfa4e6fbe1a01e155f00d144ffd867b23fce9812f8cc089c8881fb7c87b97e88a71", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x91ce53775b3a9507f9c2a66545da9a0e3d298ef66701dcc170acb944d223ce0d1c0c4c5efbf2a4f437f4a9632c35fd4d17eda4ef864153b06f4d7b1d112ad179c3436e35125046b308266df27ea923a4a1eb6a25866865239432fa878c582054", + "slot": "12860532" + }, + { + "pubkey": "0xadd6ed09912080e3b5e152122f7582fe90523867c5542b1c682486144d654bd4a6245bee2a3ed1fccf3793c543afbfcc", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x84b287ff9eccf2ce352ce5efebe8156d8cd766b694a3c3b423d11fc09a149116b0684d168a729fbb1b830c70faa3c08b0e6605fac7220ff04343779df2bd4adb9d8ee8b5b8ba581f2d6b6d4cb3ac3d4a48928a4b4c3d8af72bfd31e804534550", + "slot": "12860532" + }, + { + "pubkey": "0x9797fb4d558df515f553e30fb4af400d76473f294d12c382be82926e519441600b19b33f248b50e84e6b0068f709ad81", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9002d4a992aa9052aaa8805b5679bcd891115ea8bb21546f9528bc9b3eb39058c67724a29c27665238c1a0f37296fa3d07a7e3512687881f4172957bf8409200cfa9e14aaa4933d9a76968cd2c46ab83073f735b9fdd106b10c3767fb2bd4057", + "slot": "12860532" + }, + { + "pubkey": "0xa024af501ff79979b5b36e7832bc488c66d3770b88a4782f74b66635c91b051cf310185a83a51c7fce75497cef793a6a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb8d10a2e314fad64b723a534f4aa7eb7d111296f352bb1efbdd7d6a9c9dc8236b3b67892fcf45fa90b572c1a34617ff019fb36b33fee628eeedebfa684b0e6b7042d47cc6a81bd63ab66e37b37106c187e83cd6ce7e2e0f2a2f143d58970232b", + "slot": "12860532" + }, + { + "pubkey": "0x839bcb2926fa042139797d3c0b4a30f2aadbe992ead44f86e0b2a01d0f207ef859c03d588e0aa095c2cc7284d6e328b0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa3afdb0a60f55fc91e34787515009fc87394d532a0c3160e3b3dd62f31f686885d7cc5de9dfbe2b51088665a61da7d380959e4b558f841346593576a21dcd26f21693a6ff6bb4a53c9526714d8436d4f29f9ec7e7c7d007ce0e89bbccdc605e4", + "slot": "12860532" + }, + { + "pubkey": "0x953599a704e891b474ad494be44e4e96dfbf5571b1c664cd9fcdab62db7e5d4e45d947723db93526c501d9dd57d08bf8", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8e43400e1167fa6a4c2007bd49b8fd9c8e0508c4d07b8adc87427563d28d15ea8348f6bce1c273ed46bbd0029a73276200a867580ca4d84a9a4b64977e1def11f2337d1062c7d9bcb38459f9f4964a6ca52a27c6adc60f79fad0d30e91b717cb", + "slot": "12860532" + }, + { + "pubkey": "0x983a1d409e66f171288984d7f2c2e97c4aa0c268de3e526273e46fa8f5f67337034c4a6c2aad980a825aa6399ba26717", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb9f533f1618ef47c6eb91a69b5536544802528ee3e2d9c8055783d6d04e914b8f148d860f5968b0f9deb178ba257ffac09e5342314d05540cd3151128d8f83cf980caa7baefc063e564e0cd835eefe5ef326e7552890388509481dfdd2ba398b", + "slot": "12860532" + }, + { + "pubkey": "0xb846e798bf2b0759c9df6e6e4cf08a949ac02c63daf871e6ce46271948c05ece970c4c52083c45d6dd34cc7c3e2f3700", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x84ea43205418ced81863895948908ed107386fbced927d7d497bba11aa719ab8c373d6ccc340d1f450cd938a21d4ac5e12fec68703251cca39a0a3704b3b20ceaf736836d8d2f67907993ee94b247786a5e8c9b1223d1c3a6a661f0886f5d304", + "slot": "12860532" + }, + { + "pubkey": "0x814e7e6f33ee68714285d1790d986f604364c6c9063856b62435c2a3ef6a1a74b5c116dc748364ef24d4931a28f643d0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb8dd4f51b3c5edd10e6522b80aa02b40fdcad6686a6529bcdfd31c790f50d805435e811d7480fa77bfc8bbf58e5a4b2f1010e09be25cf39a27bb96cb01b245ac9db8c2d2fa4f857b92a14a14427ea078ec87c297d22237bc969ac9e0928744ba", + "slot": "12860532" + }, + { + "pubkey": "0xa29c51d6ca7a69392dcf1918a3a5fbfb6184ae7c7481b5f067c758465ec895aa9729d2c153b703ac2333544f20d142ae", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb246532ab6f16ee13c958373940edd697e14c6e529c36a07db4e1e05313311f1d08dbceb5c155a1dc40174b4eb821e7f18c9082e78cc9a9f0606d8d82d1623194178c5fd0941815ee84b40f20c6a05f77937585de1c1f9099b2e36f8692f3823", + "slot": "12860532" + }, + { + "pubkey": "0xada69a24820bf33a7ce959c262877aebc50a93be249885901518dfc03e52b3d161bac47f3852ac9db5a03422083f26a1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa43836fb5ed402c9c66cde9d45734da500a5fd6b66554787442c3ebc189356205036c687f6312dd9ebec858d5d51da78004c444038a3ab09c683b6387d98548bfbb07574e0694ee55a2e7af8fd18bce87a2e2953e2304213cd626af0b9abf2e1", + "slot": "12860532" + }, + { + "pubkey": "0xabf740ecb132929b7e4469b6d53c06c8004170ac6b71bc36d6c57f7ac5e32db3792e057c36fb7367aa014f7bacb0fa00", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x80f516b04d675b4e4c44c521ed36080d75088432d3312ef9aa004147cc2281a352dd91a035db50a8de7649e31cc7aba309fd07ba7bf81c0686ef4052942a94bb2cc6165f24fc0cf89dc3c15fdbe17480e251daa9eebc2e3d8d38548b29650cf1", + "slot": "12860532" + }, + { + "pubkey": "0xade9ffa3b2acb2a705e786e6e269e0884660128332ff2c5fb62b955c6f0fa6f2766cb199715d833bd28b8dedb1162a26", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa7d041e5192238e15544b6bc49fd141fef08e2020832853eba930dd44dafa8fff54d57875e9832571204736cafd3843d1703f4ddf70701cbe45222e23848fbc3e79a1cdb02bd566da0490bac187c0162d141d2ccf0d4cae38835afec95100312", + "slot": "12860532" + }, + { + "pubkey": "0xa40fa47aa8e8292acd28879d071e2e304c88e9454087d9a45f50289e925ed0a4f2d992cd3e93f83c1507094918f9a18e", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9214c50b02cf0b8f25d1afb049b1c80275cd3afbc011c29ddd8c10b92a071abb4fd84e494876bb79e0cd876da2451af3107473185032d0a01e2e74cf005232c3f41b56c89367c7cbdff8227d8e5c7fec1cb656122788fd7c5127cfe38def81f6", + "slot": "12860532" + }, + { + "pubkey": "0xa38ea261a33d49dbb0ad8cddd1dc9bfd0428a99a2f68ee1f0b3156d534aa9c0a61770cfdb32e18ac603f9385fa54a831", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb62f96b77a513be632725a50213845bc4393f062668a6ef3fc9492428247bf1361ad964daeba8581b886b32ed5c731b70223edf22f270c4d91c7e7460f093fd222326c467f1ecfe9f86df8f37b1900bdbdc0cb89caeed74790bb4e03921455b6", + "slot": "12860532" + }, + { + "pubkey": "0xa883bd39d5e380ea9b27d1b8f08fd8fc6ce0faf313cc0cd91507f76cf88ee27ad02fac1da6aeb757e2ee365641b3ed5d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb3b95cb01c3aef50e672f02c8be17ad54a0bc2bd147a410c87e21211146f8f22ed1d5b82de4f35b70e59a94538b365ae0d54a87ebd8d20592700795eddcedad590c870830cc3569d03674921c12a94a1ffb3952db8a554a34fdbb3f205c4675c", + "slot": "12860532" + }, + { + "pubkey": "0x83ae1cb066455bfa6246af8a60b5bf7a5de9cc7cb287b12924add7c20b88773afdd9564aa5344136f87e4f642768f4de", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x87bf7f4229dcc0fda937002430a1b38f9cde62e484c7232719a98962d1e11c85fc502035893522f2931282939bdbb5ac026822a83f7f5d493bc65445ace14801c8230f1f1504a2b70afb6b6e4a10a5654c6a2549c076b3851193186cfa8f3821", + "slot": "12860532" + }, + { + "pubkey": "0xaa4be8feb2b80bbda6130e76387a7611d8d3a46a453a6ff1521912f230a306458b9e96c15bcd4b0e53ed975bf9728267", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb79188381bfd692f9f17a87c35f72546ec3177b07cd34079fa40595355ebc02dada24d1e0d1e2d8c3fb40342f754fc300b067e86994610f0b95100ea984e7d058221a6c1594f94268a204e7426f435cd56201cf5ce7e8a07254b870654c67f1b", + "slot": "12860532" + }, + { + "pubkey": "0x82a6ebc1cc1ce15801b6056bafda8f5658edbc9a13d7483364ba181484e633dc75f424fe8cfadb931f420982afab3e2c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa3edc365729e72c989c6d1f73b8c9ca5303e09efe61cae0e566e2d32f0f97af6a54cc58c801a594777ba5e4b0a0e67d416ae731be7ebf4e3753caa845659aea0c51618b6e0f891bca8cb42a0bba0767f23f924ee4d03f37880624205d504fdde", + "slot": "12860532" + }, + { + "pubkey": "0xa581ac0f2091746e115c44f1d5abb507f923d169c9465bb78c2d2429c6cae0955694c583556ef1b1f1f5efc68395c995", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa12d402c34958dd643d84b662a8d9bc1542cf54c5db7129826f68b6147a972b9a37a9ebe5d2114ed35f49aa54f8b0f9305a7397ed10aaa509bee1da6897c7a160fe852b5cbc0820698d6f7d1309a33b8c73cf18d597d1e058164ead445554b8b", + "slot": "12860532" + }, + { + "pubkey": "0xb3f7827c715ce6b3372e58ce33d7b22df09eb5f1eb456a0726672984bce5cdc8939d7495bd49c44f99ab8796def0dad7", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x99f2574d3518fba3bcbcfeaab96274b14a1c58be80bd47ec491f8235cd4e129f7918a41c4d74fe74ab50c940995453310f2a1e6a6868c09620daf6589e1453d0a3bb0105996947a96646996b43f32eeee2af809490e944b218c7362597a8944f", + "slot": "12860532" + }, + { + "pubkey": "0x80fa54f70f1c6858abe44ae017ebeb25aa5864dbdcc56e5450a4322041e7d54e40fec4095f0617ac490b71896cd25f08", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8352150173fb3bc5bd598150f685d42000cefdab67dc6a666680c2a27e697d0826e4c0eb7848cbf8a4db733d5be6f5dc02c074449ec4cf59e5d243f6757ab483d248f4d1a40eceedaef2e810d2c719a682e034b07251ac0c91a888413ab69c31", + "slot": "12860532" + }, + { + "pubkey": "0xb600b053bd24f7475bdd18b1f2762eee61b34eff4c4f2ad1aac18fe17653a90b5b52444256ff1763ea717d016ce1744c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8aa42aff872c457fc6d12a909edf574d6d1b677f981a3bc78c581fd511c5a28c1e0153a4fab7d851ba3205fd70fd235d08e21f7d9509ae3736153cc39473d0172bafe27f978fab3e7d8cc8dd0d7fd651accc5a55703a794282013db15e2c25bd", + "slot": "12860532" + }, + { + "pubkey": "0xb703fd5410b25ce963b282b1263a46a68c11f72b2faa2dd913664ba96b2e635e8692b420571d02a6055195e079028e98", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x85abe9b2bb207240e5e0c0126532833242698f156e8baec976ead6ddc962813d1abebc1fd686698ce2dc83cdfc66ef7e03076f95c669d0a5a3942d3ea88e8a0dd97334d4bfe69d9d2776a27abbca1e1770872a757d86545bf340f1fda9f3e498", + "slot": "12860532" + }, + { + "pubkey": "0x99b31ad98692243d8be48210cf3b73ce4e3a1b65b7d35335789bc873858b31aabf9652551c71af76a5c742ed8e03d209", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa271b670990f0c898357391a5a7f561d6cd84797a6a176735affa2c4b3eb188ccc5b75e0b113912a834c9aa27ee47a43033cf6903523ad2e870ffb8d8072f10fb05f3e1e17df610b5a834b61a768fe79f932b431229bc3a75cc224f40c10a59c", + "slot": "12860532" + }, + { + "pubkey": "0xb68baa8052b5a4dc814f21fa6cae0fe82efab99920135cf6c560a0530a6f625d4b4f601a7e844e0842cadec38915bb93", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb33fb9cda87da99292de4368ac9e22faf170d9ea13ecd0d5e40fbf7d9b8da761fada0bcd28950cc1c3a25349a69086000e56cd7284b0961e5bb973ba80c0f346167717d985a12cc0b85a552162e669b52db854846c8b58aae39ebab6d8c51495", + "slot": "12860532" + }, + { + "pubkey": "0xa336a691a13172f89e027a18ec2d3c5b62a33bbd6d0dae942319cc33a18eb2e308a99b578c01afaae91c3acc6aa4c28d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xaabad807e4481129cd842521a3c06a419748c0af2b5a08f131e788fc06448feb56cdd68a1ca3e18f874c35744ebcc11e00469d906dd7412c42319a5c2e141f83e41dff31e73987d4cecdc588d9e81f12dcee77aecc7d86e6260b6f264331f284", + "slot": "12860532" + }, + { + "pubkey": "0x873f019b953575804360ef2626a7c92e4df0081a516d0fa0f00f381915973d40ba3f2f9954934efe0ded1b44bad11661", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x877501cc13aec6c6f776ecc2f767a83f7e0d7019e382357a72720675583cc7713694a6dec7e054fae11f73c3454d12aa0ba9e90f2429292395216c06954b6fe79cccaa0e8177d54191b7ae867633f78e08b3c2f5e0890c23af49d5eb7b6bc616", + "slot": "12860532" + }, + { + "pubkey": "0x8b32c9035ec85aadfeb3770ef2041b3e9705012d3c1bcf032c7ba972b91e6df8ae57a62ca20e96c57433c23c5e95eff3", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb8ff241dc9be37ff3420fc41fda60b7924f52ec1d9ddb53f7829e20dbc38b54bfd7b1b310da9000ad1e865acc56bb2070e78151ae930bda21c7f68279eb3f428c7ac06dbc67051caa3a9363e1ac27e0e0e1f35b800a7f0f2d97bafdf9050e6f5", + "slot": "12860532" + }, + { + "pubkey": "0x8bfca3d3a6a7808490e666777526a4a3484ba357b49af0994cc5fa188dd0af6a9fbdf950d6e5317b45f35a9e47101f13", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8516e27da52bbf03048b3fa3eb6faefbb06ab38595efcaeb06c3550af1e382b3f5570ae16425d0e1ce1b3e8af677d6db10243c84606d4fd640c37e4b8e658be9faa4613a2be4d9223357c997815a9ac1cc39da45405c4e6925bf0cbc7986b2b6", + "slot": "12860532" + }, + { + "pubkey": "0x8e484862877be06673ff9aadf21e97ed0574999bdd1adbffabff9efd41d2b70779f1a6e29e281641a3902e1c28454232", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9686c9f2a207a45be4f7c88c627a2cf2812040a318437ee363d5a551695fba49e2ceae83d6a6dc57a2fa08dd00bce9480f81f38a84e610803daffdb21e40eb6946f8dc90ab9b0262d727412199fd9804f9001d8997d64b92f072d2f7a06d8479", + "slot": "12860532" + }, + { + "pubkey": "0xb882079cd5742a4714de0ff82ce0746470fd09dc4fce0b26117bdbd91e4ed0e04f6f365bfee6654f1c41b3697246456c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x950cdcfef75352cdcd9134deac6e81ceea532736f619b43b15df525a66aaba09e303f4e916ef0b0f25dbbf2f9edc524c089250cb0c0c9d5ab8c8b03ec5fbc342cfd5bb15bf135605d3a86ada8b4d466edad783988650a1486a57a2cd1fe7e690", + "slot": "12860532" + }, + { + "pubkey": "0xabe517c7d0ff0518b107871a9152b8319d1829858c749ba47c2323f9b329ff143f469c1824fc2bc4ec1d6a0f114d4755", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8c74344cfd128319b91acfa496c0f9604b76643bfd109a2007c9d488a0d0b85c34d37b90e5d2fd4f32fba33ef7bd87bb110c0b4d65b10b3f8016455db6a0e48064360fa4e5e0b348e6a748d9097845e51ca60b0edc7875e61ab3cba5e3a57b97", + "slot": "12860532" + }, + { + "pubkey": "0x884b8140c734813968b3991f8729882b796c97d4f455c21ee0a8ec04d81b0db10493d5d66d2a487ec64ddd20df1f0fc1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa3ce9608d3f0ae8c6da5b2ea47eeaa304425bc55beea34c87c7c300937fda89e3b5c96558d02ff3bd7780a5e25d642a8180aa7aa2e5668b3bea038a4ee18ebe74b7f927715c4a4aef0114c65d9a90fba595769ecf4893cb0ec8d1eb98094abe9", + "slot": "12860532" + }, + { + "pubkey": "0xa715459612a8f688d9bab59a4a627ca1081c9ab4c96edc21d3f4ea05a957f2d29b8d045f1b073f0d0894b99f67bab02a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x85451d7ae7f75b0a95676ff2e61b944290d527fbe4c361e053b0eeeaa0015464d25a498afecbb1939c1e4318f8b5771f0dc83f54e3bdbc15470be8c3c13f646480057c600841c6a0c8aab6d8d55a665403d2a68f657d94929d11eedd687a1e1c", + "slot": "12860532" + }, + { + "pubkey": "0xb3cf5a79d6af77dfcadcc2d0f8509dc3ebf8b470caa7267293caea242041232226d52ef9b2c97dfa09ac7e4652eb536f", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xadab5eda9ddc2441139645a4c35e6d7fd2faff423933d723d1f7e84c65c9a8f845a68ef49c37870107a2a7e6f39a6f8d19f8c98ae332c53e11f8493c3f1e2eecbc0b247d67f7d4ac46cfddbdeb16db7db2d26aea6361a621bc006ee4176646c3", + "slot": "12860532" + }, + { + "pubkey": "0xa0319def87d068ccbc940225ab705ef8f289eb7b0861df6808083ee0daa39b1f41f65c16234190cedc418f793d98f1e6", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x92036f3adb2a54e9055a47e0c4d33898f90848169d078c50a3f8a872a8e97598322cb8e7d583eb33fe8fc6647ceb30f20abb5270ab80d0e03fd57f30f50c5dde890fbffa4dd08957cb2395922dd5638429b8cb35e16af4d1b0c447021cefb482", + "slot": "12860532" + }, + { + "pubkey": "0x86ce270f88a14546e369139c0eb7ffba33dc6ca81cda302b10d7bf987c09d0a59da07c8da9cd888ab19ab1136ca893cd", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x976f7c9b4b49c062e2c74a6b0e3a239cccbd2d983a82ef6ee27f10d24398af74713ef55e07f9f68ad8d9c85c6af7d7c30fe4cc8fa2455fcddf56afa9da3f2ea1fcbff344ac526bef5f3ecc178ab97415670afc586b8afd51f811143f95a45b7d", + "slot": "12860532" + }, + { + "pubkey": "0xa183c955c1c4a21784fb124414314652748e93a355d8ca180091cc97d67914eaa294fc328ab5f8271e42fbff951d375f", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x98f9c34d757c92fbb73120bfa17ec729a115f133c75b07543ce7dcdea9da6a5ed6d851d3714070cf2603e04dc2a13b4010f0c4267c0feba96179ccd71eff8d942604997d04998a0824f4e2c8850955c93bd203122ee95529a0de993fafea1473", + "slot": "12860532" + }, + { + "pubkey": "0x83ff266bbefd3a05161df26dd08d182c7644535b396789c5142a4821cbee5a11b2f28967998ed3d46eb0e379205f56a1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb6a3694859f0e5d4aa6bd597480db4fd78af12fe6eac6940bca3b40f9373b5d8d59384d7c25669200a9821d4ddc2d8e1195d4113cbba26ee36d7aecc52393970e507c3e85285fe4e91609a3705d92b3cc1e048b69f377b37dccc8379ed5f42dd", + "slot": "12860532" + }, + { + "pubkey": "0xafc89b3f10649321eae774b3cf3a2dbb4af0a43ba61bdb2f5503c7443110b480872e3678c589f2d2638396d610aceb13", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xafc9dbb6bed074de0498ad261a553a6371b69e06d604ef99df53bd8c26febe0abe4b900d790581fd17e4e7e102ec573802d9d70342ecbc41de5283f4c50dba6e17b0c374716096df584fe3aca881dda67b2046b5f1ca92c171fb51864d39f76b", + "slot": "12860532" + }, + { + "pubkey": "0x8aafa67fe18e0729e3a09f4b0c86451de45e38cafffa8138d11a662472a8f4d29d1213eb578afcaae7836ee6a3e93fbb", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9085c9765a557c796d9cd0429d8a9430f278eb513e87f8a9759607511012badc5f824a949de7ed6f88899dd77165de6e133d2924a87ca5f483678d94fe29bbb2f5694e73c127f481057e27a0b658460ab4a3179d3a4b55e5e44d2c20d9ab1450", + "slot": "12860532" + }, + { + "pubkey": "0x8a79a92fa86158d44d7a9480b642d5c27985ed99d3c22746937c03c36149381c850fce1585f3eeacfbd1d2a0c839df84", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb59bfdff9befc1f80bd0e4301e1d5b637fc36ac02dcf760b4496bc3cd0738edcf046146fb38ae9421036740302b682ca10aa8d214e4da275fa2e74d277afe9be68dfde84f8bdad19bc7aab0d1f16ad2d7a56a88c10cf72b64cdf3a65ee727ca7", + "slot": "12860532" + }, + { + "pubkey": "0xa60953983dc2287bb353e2b837c34f8eb92c6ab186f837f2ac001dc45bf842b7c961064cbc2b9205cbd8d4d3124eb694", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb03b417ee89978433bd84323b60a6c8410aa35fe1a8861dbd2a8c3ac234abdcf9f75a3329f57203440b8b1c6b4f048970bb472358c8bfee40123d0203b081af0e57c8776bb259432b1084d141761d87dda916b9056ec56d8667f6787181e5901", + "slot": "12860532" + }, + { + "pubkey": "0xa49afc8f8cd710313df52d8a32ed4a972e6f93450f95ab2038be54ac97b500c500d72033a479079ed4e326a43ebd13b9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8dc2bf63966db1abe67b2ebd817cec9cc1313c0ae7525dcd70cdfb0b4ac177b9754a966ba16bcc78cd9934eff96796d41016e193cd0fabc3eb9828bca954f4199d0352023bb4c6fda8584bc99e0e7c1f35af0d4245dc4d8816d4350f08dc9430", + "slot": "12860532" + }, + { + "pubkey": "0x8d4d8a4b1fa9d69b6d9ef100a15f9f82818ae1628ede1e16cddcb5646f62f6ac5e33f8c9b979713f25eb6f0ec6c80adc", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8ef9a86c8505baa96a5765c08a8808d5c30a57c83a2a31eb0ed98bb40b64e6ea1f3c5bbfdc7f58c6738200ae500c28131480044d9d77fe8a024ee88526f1fce39c4e2629cb37249edef570baf0798f23e229a4dea999062b69d9e3e3f10e93ba", + "slot": "12860532" + }, + { + "pubkey": "0x8d562b32d70a3fcd8e435523a85411a653ea9343505c0999d72351c6c15fd4d70a7de9f0e2e9eda9a849ff18e4088f43", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x86b73bdefeac8e13f56a5e5b49e7423e29e2ca2c73300ddd0a9e070f1a15f49d94b82d1c8636e249aabcc4368c182c92036899b545d5da4ae40eed9c9d72ed1b8a8539956466bef1b9aa41d6ec1bf1e54809d42748d606952cb6c079a37877be", + "slot": "12860532" + }, + { + "pubkey": "0x8a3aa4bc7514d3ec7a7cff3da1b0db13b9e9f023dc6a4e7b01e359041301240af8293a21fb7c979e5fe8a34948f1c6b8", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x90b1369cc25440b17ee62a024693369c35fb5f504ead327103be7a6a537baad8789d8043658d05081c39382da34b499a14ac2fba4c2954c7219c39b621d3dfd71f2253d8576b395af108a87a7c4ce4638d3331c1c23eb277985c906170473bc9", + "slot": "12860532" + }, + { + "pubkey": "0xb6cd1cec896a5c000dd3782b9629616ae57a115d66177b2f758cb19aab0f7b40fa860cf3b2a3a4ccba8f226e9876bfbd", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa69dc076d91a7e5ee8cec30d0a40c3260575282057d80e12c3fd1d101b98cd1cd8e3d2cef7ca2df974d2082979540c4e0fef13d6dc9feaced49e307359d30505de68cda0d294101247f5a4d6c5cec419f8b41d3725152a5b99f1fd0c579505be", + "slot": "12860532" + }, + { + "pubkey": "0x8e40d2b0ccf47ba91032048c312584ea1086e6a07e3d461a97044b51d1e487205af9dc5c3b21fc7a1fcb6fccede986b7", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x818be897036d90b26f9889170cce956c1df1e859817035e8db75706d762acd1fed4f75510284f713996dc451dfab6fcc151b222e1cc3ad6460c909cb1e499064c8b1f526b992525df35c35597027c46fe441a9ccfb307a56c59dbfc170647196", + "slot": "12860532" + }, + { + "pubkey": "0xae4ffeb53c7d16dc424f73da35aebf719e8c695dcd4a928dc3bcd8c54c50379618aedce491c2903f27fd538164fe3ddb", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x80bfa981f679ee44965c5ef776eb8c375d807291ad700f889b7879fe09bbed5eb104e749a7921bb2f6f68659ba24dde9037a37eb933a48cb71c6f02bd891b3f640ff35870e8ea1d2575f4e9d94473c549c23332e3f3986ecafabdb78089c67ee", + "slot": "12860532" + }, + { + "pubkey": "0x97a60288d9fb872dbbee948a1584525c0a7e877420a6a2d0fc0b874c66898f06d7e944f1cb98521ea80b9f5f58216b36", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8fadcaa2935f5c9e434f331feb154c30f7766992759e3400cc91cbbe7f8ab1889a6c5dcf45dba15bde1bbfa4d8a906210db6cef4301d87091b6b34eb428a5f142628fd0f1dc6d7d4f23e6bdd13bdbb663fd045e7bae52e10ce4c24b20231339e", + "slot": "12860532" + }, + { + "pubkey": "0xb7adbcc2d394c843718f2785921d9e6f5369cfc6f9640d327661356b1207716377ccd87dd7864560980cc061de243b92", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa0dbbcc74a49656c5fb0bbddbfb5a4f51a9599c14c0e1baa9a630cef6a3e9fd51e3c231af175563944229d78d5d85dba0a12685588d292dc007556f3114cfbc52a8a46dd537d862ec41d9c0107163a03bd42666e802240b977b47d1ddab14fd7", + "slot": "12860532" + }, + { + "pubkey": "0xa5cbc74406253d415c86c0d00e140c2aca4967f61197c285823e77df3d5108074bcf67e1d2eb57899639172cc8580b8e", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8d0186a50a98e689cde8f120491b4e62e6ab63b109625951b244c798a762b8133685bf3a6055b3c56a3064fd656524ed15b27be073671bc9b17e839bcfd78dd36682febb41052e8460f86c4b2d1c496e356026d7eb04edfb7144d81156316b58", + "slot": "12860532" + }, + { + "pubkey": "0xabb0b47525db313cdd0572f841de9321920770e4975bb3b63916a5b587127af66acad6f0fa117050ad2403e2cc23619d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb60cc223886ccec91b75c0e8b5e44ef3a3a57d32ca3d222110e699691877a289c5c1a9e3c55d91db586d8313aef991c4183993cb6b63d751e652a197ce5781c3b5bca489529de89818902cfe151639510a8e996baf1cbbe71d93d657c486a245", + "slot": "12860532" + }, + { + "pubkey": "0xa0cb3e4de71207504875c2f944959e80f616721a09e64d05907252c4eb3d69eb19d47668a44f7b5888b1962e52eba182", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa9fc6470ab437281ee42816352cb136ee44506304e0f306a0c4b3e4de0c2ae1abf7a1eff335edeab0df5058829ee70cf0b5c250a4c061d01dfa3c518aeadaa0417647a5f9cba795c8cd47ab8bdd3a9c5c9765dd90a89506d59ac8dc045ec42bc", + "slot": "12860532" + }, + { + "pubkey": "0x8686a995261352bb1b67b4e179830768ece0416298c5c2c28dae45518e80ee5d0ece28abbfe32c45a4a721ff971b7a1a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8294d6ce911e205c72c838e85c439805e70e13f3ddb4e2bfd1ee38c1e3ec5c736b873e659c1fe47a265c591e755138af02f4f76c1697cd9ccc9dd43428303e3da0230b6bcdc518c4e3f44ac8886ccf40a072af558bc7f5ee03a262105afeaf95", + "slot": "12860532" + }, + { + "pubkey": "0xa4166d9d55307ca5b40ae5c48f5d3abe20ee1f3f6a58bc7713683b1e97608e35c8f0ad1616c98853cb3151cb99a488a0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xab3d1f39188fd2adcbb36e1922764e53eb53a5c511bb86bc61d6fdfc9fa927202e04718262e215f2675daee0bf38dfe1191f71b3d34c81dc2c5855090eb099441c67e1d03f84754c676cc22139cc2d5425927f80e480862409f69bef7bff4b72", + "slot": "12860532" + }, + { + "pubkey": "0xb183185e895c57c3f1d99e9eb11934fbfc48e5fe0130dae8c3ee1726312fc7c5a6f075f48a355b3f4c774d0a7ff1c52a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x807cd3fafe405a68b070b7d9f8e7740a1482e39eb2f26c5328dfaeee58d7ce35b40e4cb287a01b2ea4440c3f10918e2e05151fc3f0f97edd80c1ce7ffdded19f475bae4b3c19c6ff6583e6057b6967d5ba789e033884649a3cd0b5cb631534f7", + "slot": "12860532" + }, + { + "pubkey": "0xb5ffc9a56f523a7826882179cadd7349e03470c4d50b695ee7258727ca1f4ab5bbe9ffc4c2183bf585779d5f173c9683", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x857fbecb136a2aa0ad8915c26435363a009b1c2978c774b8e51714f6edcbdc136b9303b61e93a2d37b9e56977c6ddf780d7c6b139f19c52a8f267592cf8a922aed23957d2bf1baf360a4805ad519f14f38013b760f7727e4c8322533951dcf1f", + "slot": "12860532" + }, + { + "pubkey": "0x8db603bd34e05631e0951a53625940f79f3869458e647bacf8467dc0379c688363b6829889b246884e49ec11309b11b0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x92c79cf0467b8178144c941e0864e11b5b477ada919a5e0d669f8cd10c13060ea2f5f4d3c4e2d4784662151164edeec3082e910ab62a1e6492e63359135c55afa0bd09e263c363c2ecc0c63bff5a8d6b661f30d5fe03937225237198c568338e", + "slot": "12860532" + }, + { + "pubkey": "0xaf1f3f8a1d8bb5d8eba3a7bdd4e37530ef76f8a896807147cd832dcd12a0fcdc84e9f21a74a8ca1a41d442b8170ab5d1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb29de807e761f04e5479ff287ff2eef6cf2d57f2538b79a067402cf2faa943a14c14f579a6049ef2294d55e5c7449c210f52cab07f02ee2aecf8a4dcd865ce5571fd789f91cd6dd4fb381960894ed87826c98cb8cf48437bf9a972e1b951d843", + "slot": "12860532" + }, + { + "pubkey": "0x80f246c54fdcc7d712aac3f160b8a03ebdeffb98fde877b82cee05746498e18b9efba634d0346fad8d55274d687de0fb", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x87a553c209def7621693fac01864ada045ab60eeb2f35ec5975d2d52c40fe93a896e38f37079344653c11f0bbaf32a4405969193fcd76fc8e7bca3bdbfaf0ae81ea4002d6dfad14450ac448947a526821ba1b33b1f865a95d5360e3167bd84c6", + "slot": "12860532" + }, + { + "pubkey": "0xa675bc498f154b8c113fe331273399bc499879fa71e811a4fc3ed1b187a8ddb4e1bb005c87c7942755b84ccf6d2d015c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xae8f7b7f6d931c4d7a6a63dd8ade619c67bf85e61a1c110e5d383f1ed70e122d8397c7cca55cfd4a30f59af564e1a77417147ad6ebf5ee15297655ab9d76917822fb4c0f3765fdf11a855af380b6c4ff242aecc1a0c2a1330edbaf9aa60f754a", + "slot": "12860532" + }, + { + "pubkey": "0x84ad497fe5c85443bc1bc30cd4c6a142573ce1d0643758b649ceff6251ead1b53a79fa9e74fa3f3de6879febb31a06a6", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x86efb8304a26a39b6ffda45f1d05c3d85b0b2067070498664977da61898f3803294b89f7ffa2bf643a8332c3b81cca580c7a68c5c342e3ccf90c9a86f74918779ad8d594b59162675b1d1add87ffb367d70281b495bc1ac85a263322b1b4a505", + "slot": "12860532" + }, + { + "pubkey": "0xb843f340b492bff18d7cdf38e57cb250a02b97631d9ffa4f49487d98c0049e5124dae73374cdc6eec3f8d8765e47dd6a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9494da2a8d17bb854c60b0ef714cd3b9e62ba83e66c51415d085cd308c35a4717a6ff3a678a135d03917b20d7cc9bcab12085101ea098afa289179116c6cce5bfaf46e513f35d330b64956c6f9137b79b358e3614b2570d9e724d6c8b9b2cb41", + "slot": "12860532" + }, + { + "pubkey": "0x82f4558046858de4f10fe8583a4ed543785812bf666d1813cc3b30da8354ba878c2af4fe23539984331310c920267143", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8f993c34a0877b8a2850494ca494f47b614608030d91335c4cee549dadf378a69982bc570bd25d2885e9258e493228c211a225b1e9a1a9257293d6ec05b39e9e13de30537148f573f9b8e8fb6cb8d121f8b13e59d1a7df95d63dafc66f7931dc", + "slot": "12860532" + }, + { + "pubkey": "0xb95fc7db66ff68dcbae3f1a991ca350b5013fd8d206785346e4768eaafa7f268724ab6d73ebb63d03263c51938028ee3", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb4ebda9b556a65f7b13c4e8187eb04ae75c0348651e76b416ed3e814f3f83891e17f510d19796c5384db6d4edbb64ce51502cc49d56e3b18c06820c50463b0e56871382feef3b52a7ee41a7e83303059037dbf80af95865fc90fb6d120882f02", + "slot": "12860532" + }, + { + "pubkey": "0x847c6e7066dbe18ccf48abd31e821dffc4e2e2e93c72a3a9c2019d31552718240d7ad2e1112ba5956f5eb131192cbc23", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x808625ee44fd510d4f9e8f8338a7995b48426ddb3b2002f64667832d7fa1eafa65e4d2a306b9015810a1d16cdd29b0890cd8287aee62e5044725b57a3d063efad4821923599928e677196ce0dc24071c1fbf3f54ba08482083c34c13ca6e9486", + "slot": "12860532" + }, + { + "pubkey": "0xa315fda5874b8f8b366d5dca9add3caacbb245355e94720eb3f94fc064262b5babb003864690d3f5f31d9253fcb50973", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb058dc611ac8bcc5d14f9ebe8a056436003d3c52bf2ca3d6f69ee7f17ee6cd7b375ddfa16881fb5ef748dfaf90baac100b0ee3b30b6ad3777cbcc5d34dd0cedac9ae4d92a9cfc9e69857d6e79dcaa46f91853f77375d817b0df0da0fe241e665", + "slot": "12860532" + }, + { + "pubkey": "0xaeb6aacd2cdfd73f96450f2a627ba66a81c4431356d7a986305cd45d263ad4881e23b459af00dde7d1a1552b9664b711", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb3b7b9fab29cf2eb4cfd4c36fc1c17946f6d76dc19cdf9491151c1789fd0c0a8bc1a869a3251bcc2ab536909a17612ae13a5ef9c57c5b46634a2355cce54617f725d5fb11e52c605574b501c47e8a31bd6296451fa77bf6f18e1c4c4152a3ae4", + "slot": "12860532" + }, + { + "pubkey": "0x82d7a66a5de7d4c2d70531e14a65fa0b443a4677bedbd58c9b576baacc80ac008fd8e69b6c77bc1b7ea6818cd693236d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8d266f4798a00d346cb46833a276a0b9a4139f24949133aadc61c68b9d5fb6270eab5bd286a43a56305489331ed4440d15127dd6d93f761caeddaee0d5a49f3ffad978eac977bfb230d048c09d90ef18956deb4f72a4e2329c9e001c92a64dda", + "slot": "12860532" + }, + { + "pubkey": "0x94a6b543ad21518c7befc1f292009c69e858594097d831a921e5bebbd24fb65f824efb2071a9fffee1011556181ae77c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb01ee2cacfb0c46b807669ee1cb1355ae3583c3f59c65ce7e3b9b44e1def0dfa466210b1e3486d16be36b198f560ecbd088cc02e4c8c90cc8c7e43960a3a0205fcfef5362efedf93de5250903ef595b93cbd1f85ceabdcfd4d9113a34b81a06f", + "slot": "12860532" + }, + { + "pubkey": "0xa8d8d22613dd0c3ce95b85f25a434fc005a4306e8aac701c6004f32a4f62d9ac7ae96f2f11e14bfe10728548a909e618", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa3869d16305306598ea0e2d7185eb8e4ff44a7487b4c18667b4c04670a72f9d4addbae63ac6dab023a6cc28ca7921a7e1757ca8f1260fadeb1b0f558c38852aac8ec366d44934e0249918d9206057b88f580f0e7da1effa4c20e98a0069c263e", + "slot": "12860532" + }, + { + "pubkey": "0x9832e78e8e6b8f17d825f05461b8b3a3c56c64762f87879ca718993f906d884aafc41217f60f35f6ad296a804bdc3759", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8df902f26abdf8df8eaeb7d03937ad17aef3340f38256c7799cf4defa14cfbcece50b991ae4df0561877c11d6fae422108a900a3a13f902b9921ce7e4edebd98b78ead61eb87634917a769ce737b5abc13787c7b26a8857c0502a0b5e794dbdd", + "slot": "12860532" + }, + { + "pubkey": "0x8b9606bca83e57571304d24fc69d0ab79c65a981a30be70e8086d7c196b116d9dddf3f2ecf206bb92e0f372aaafd245d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8e44d82c6a1481374934f1c59dbed18ee84cbf0b1d084be988e6e6da036af97ab1007225f58b30bc6d0ea42ea1312f5a1230aaae72b5199eb6593bba34638106c06f77f22d2e9318fbfc65d0d774730470e46a651792c1307b6873f4b70899ae", + "slot": "12860532" + }, + { + "pubkey": "0x91acd5ba87e8a959d553cd9cdfe0bbd999841f06bbe5c85c0755e531c16363aca63a16351cfd2ad007b121a197bda871", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa2fbfda5e93a8277ea245367b56961950080a46fa9d51d1325e93e56bda44fb623b8d08133fe2a4aab29abee260081be0efb22919a5e60e11532e8d59702b7a89529c76ccf59afe9ca8e30b343a7c94f54f3c30b458ac70948277f4527f4e100", + "slot": "12860532" + }, + { + "pubkey": "0x965189260325a00fab2c20314e644d8d8fc24c64f81334b71e0f1bae9657541ad0232d16fcbd7d070787de221068f096", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb384824458caa398b7a650c53fcfb1787ee5d5fc95fb7fbcaf37094761e9bc368624726470e7a070bed3539945ebefdf161ea9178fede4d721ae5511cb6d7021014b46fd957e4bd25e9d86a9ce4462430b269f839c0092811e9c34bf107448dd", + "slot": "12860532" + }, + { + "pubkey": "0xad72cb1c497196022b7240499d135a37cf7ffdbbd9e504527d9455d2efcd5555610bcbf560f06f1944538056f2bd11c6", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb7415329a7fcb6e4457755a6a94949266d5365e4996b66f1785bb53e954d92b55a4051a1d32f2d9fc14e2ed81ca4e714091d86c634225e155a757fa763da09efdee6a09ad2adac26543efa3a789586439086b7a2419e27bdacc73b36100966ae", + "slot": "12860532" + }, + { + "pubkey": "0x84f43bc06fe224afe72d25da7fd1d384acf3e500216adfa4adffce4312f77ff6ea73853c852a0a2e7e821620a6788ac9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9331f12d7b440268cf807422d26a26fd50a69f6a9a06e6b7bdd1e9adf3009b1bc02913f0afef24ec93db45170dc85c4d07e99102c44c0b671d5657ddc57bd7c2c825644e53554c34b6c03065f65a586638c1437743afba0e591674e0b45328ca", + "slot": "12860532" + }, + { + "pubkey": "0x981cbe0e5fecdeb768bf8aa2e8da79fd890d0239d548e01d1a4daa633728a7c30de5ff246d0d82c852bcb4acf429735d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8acd8bdc8a750fb1de966b8b243c6aa443f9da06367aa2b1345e284d98fd89c1fe66ae8a5ce3a7358706969cba51b4380e3d329bf026aaa550a2d72701bdf5f9956d9c86df9926f601a796fdf98e8fd11f646ddf9488b37d42023404efa3318f", + "slot": "12860532" + }, + { + "pubkey": "0x89d1210519979446dd9f6df9dfe6ff096666f531b4fa423df66a4ef465e835f09522672e74d533a24033bf74308d1919", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa2c3ed1ca99c9d47047337ac0041ff03117b2a56fb62bf5c122246a9e4289f287dc27f42c38957319a47f26c3d05112207885d97bf2752192e22dd22bcd40485dca742ee8c4f319112773470b2ca4bd9d0ef1921e127e488ceb58fad2fd08a4e", + "slot": "12860532" + }, + { + "pubkey": "0xa91f8e6ef3f282d3c54597caf7504f3568d33545d001f411a836f94eff2383502fc09bab835bc743fc0e1d7ac7fa2a16", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x80046787557b63c30693940265be7a7adde5b5fdb92f2b2c396ad9464aadc29440e9d50f16d0c87f7776c871f9d1354b0438fd6d0ef283492f0fba6852db2024b432e737e3eeb277b7cff4f487ee8c7e975b2c6c39685467f2f2ba070736c5ad", + "slot": "12860532" + }, + { + "pubkey": "0xa5c48dbeaeee7b7465bd9b69d9aa1e327eaf1baf68778838519a60c61305593b243188005e158ac3e572b806af20f2cf", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x979c75b70594b8d0938b1db8391aea25a6346ce6fa1b067f3291a0a67e959292034a7b1b65ac46c86433cc2722c74816151bccd3e5a9cd43b141e4996958e61277a0bbfda0d3e8d6328e452bccab16e3bfbb05c3726e1708715ce050136f076f", + "slot": "12860532" + }, + { + "pubkey": "0xb9c74d09dd189a80a03df20f10b25d37fcb1f729b1cfc01b5cc09bfecb4e4ed0662097f4a7e4eb2eeaa024f6692ad6f8", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xaef60540164ef4ee5e596a8f2158f361ee4cd6fa10b4b32da5f9ab9d9cd818f1752fca7713feec17bbcb731e6839e64607fe3e563433f3194047f78628e729e16c0cdb3fdee54925521ca6e93725ab814088aaf72716ebd754ce947ebf362540", + "slot": "12860532" + }, + { + "pubkey": "0xb86ff63077971d026381c789108b4ad27642b565be70aaae9db3e9ce0e487797170a815ba81ec7a3e7d365192b9c96a2", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x843247e6802c45adfd40028bfbebaf3880042336735917df529234eefbad565455ea6d0d7f704512159994f6ed41bcf00e191394935a5c4bda3e2f18397084f71d051aaf3e6265293b3dee68166e20d737b691ba6a510c78d667e123f87c46e9", + "slot": "12860532" + }, + { + "pubkey": "0xa5ee766dddea368f17e9ffa9d2aa2b8ef5f11210ff9bab9dcdf4342ab580c15ed3323e25aad2fe1dc6f9141f45a93cd2", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9638c0201d33f2d546a51ce04e2c85f52f60d5ad003738b20c0a346543bb6624541605151b812667ebf7782406b4ba1b19c9942c786c86be1cd94a803cd07384c8a451bd9abcd6281e422817cae7cf427e15a539c6cfb60ebbd76b852c03d6d9", + "slot": "12860532" + }, + { + "pubkey": "0x8e2c18d7e02537e02532d253521bdd3a8d306f36820ef6d8c52d74b72fe03c5f2f4f2914ef8efcc4a8d69f6accc8ca5d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8090bfa32debe403299ccbfb3cfdac5256e33a924cfb2dbf637daf53c01de6f722b505c33a8f489c0a2b8031f772083b09fa086f3f94a7348c4af562e6016b4729f44e82b9d9994416ebf861b59ec6d72ac67a47ec67399e8b4a6d3972459ee6", + "slot": "12860532" + }, + { + "pubkey": "0xa07039155565885b5eec11602c9b86de704c6b30cd8833c01e9124ac52195baa05daad84d5c6a91b89f35800c5abc0ac", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x89979aa849bd9e90b4002883066c5ea7337ff0eaee08ed4f1f1cd40fc2b9dbcfadb2f41e1af876047043baf8349b20ea0a5bfc408c666597b63070456192985a360cbe0feef311852e3ed6baad5b112630f48ce304376854578e1986d8dbc0fa", + "slot": "12860532" + }, + { + "pubkey": "0xacf978b4de8ba827716206f3a58f5781aaf9476eff20b9682f2d902c8e348d54497ae8e6ebe5706eb656f3a58b288b1c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa68a1dd1787d0e473803bd52dc9177de61a8c47d60787f82b99060df5e2e578dd3a788b667197c70fc2a25d4e35d268f12374d8b98a342d9edcec30a67a4f8f8238e38f342e55f60508a5b0bd5d938bfb45a0d83ff91af2a3f1a959b68d34b93", + "slot": "12860532" + }, + { + "pubkey": "0xb3254f6f313cee9dc5177d35f728dfc3ba535469171afd66d58bc8c30a9155ba72c6b5815d3d119c899cef94a29cb414", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x91ff366b20aa6f9cab508dbf130fdd6092d6044dcfce3f77d23150fa4452caa3edfbc215f41372b4029cff66d3bf03fb020dddcc472c7182b770b86922904bf893ff0ecc4d380eb7e08041de7ccc3fd778f242bcb7c5bebd78bde3ef5e3f18a7", + "slot": "12860532" + }, + { + "pubkey": "0x8e76d1c464ac5cfd0d4da00083d3d26da2bf94c863c4a718bf61fcf3ac22c0786f37bc819a21589a22ee60c8ee3e8996", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa5af40d12fc827e775483b1d22e14478680e0affdeacbb39e75aa8ce31f444e7f4a6373c532488d0cbb05d88543777270fb1caa25fb1f99a390342f10e7405f0c135c3abca05d31686d0d49fdc7ac758e889b02dd8fb9b30d6b63778b049d77b", + "slot": "12860532" + }, + { + "pubkey": "0x987f44330a6117c68b30d5e3930488a93df776b1b0d83fa5f0271c0770223053d616abd699b258b8a2d9e93fc57ba5d1", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa6e65600271083e690a551a67c99ac92bc7139a628ff939e176a2e5d102ed592ebc7cc522eadc70390396102043f93ee146071642c7a426a7a9a34f0a050d5bbbaacfa6a6e9c3663e176b0df24b17c3823328050c1581d28cd3535c996813958", + "slot": "12860532" + }, + { + "pubkey": "0xb02a2be4bf4361116492b7df9ee2276f8709d1d7e6f601b4546c7459f12795db7d93aca829838c82081a74683e1ff72d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xaff56e56e6a59e69eb26fd24d3aebfd1e2e3db8fdb2e32f564498c73e2711fb49f5e043c117e70e7af35988caad3a4160c2c0b8c2e70f63d81699104ab9008b8e8f0f60653e34601d5f1961b18775491de92fc24b59b16752eb2aba6e2d065ee", + "slot": "12860532" + }, + { + "pubkey": "0x97d37a3b4cb8084d6bb6faea13ed4b4939e41143863104f7c1196919cacac962dc51072da36293da9580cac00b2c0286", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb6804f2729d34dc5520f0502b853f47c5bca80c967b2896f9edf4cc5f370db7f5bbb2ec735736b7452852c3349dc65ab1011d0281085e5a0ff8b94660b014a248a6367f7fde573f88d295a194e53d0750801e0f34ebd3bb838f15ee98dc08914", + "slot": "12860532" + }, + { + "pubkey": "0xb426f9036d0a8d3f78021304c40fa4b89755a77c4d2d477254ed6abff50493ed2ab00c9330a4a70735d852827c43584d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xab94c75a86b04df7c5e5a1a8e5043be039795731b96a88d494ad8bf4db470ece3e32b66b75a0384fd11a0e4000219c3c04e79c97789b6ffee9a10dd006c9904d93c0a657514594cf455205c77f8a30f16c48e4ddfa04a00d902d978ad5aff791", + "slot": "12860532" + }, + { + "pubkey": "0x834e99f1d4f7896f713360373f8219cb692096299deae5ca68d481b1490b07177387e43b7d728fd62b9581819fed7e17", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x87a5145290c57ef9c08a77fff97b345f7ad893d18b586f7052ae19a74d4e26d9cb8d4c23a10c8a7eed170f781219ff1911e5a405d0d43ee471e1f417a1abe9329d0e354b7fab0659662c726e2d900ced711ed4be6e2e0bb49e3cd60847fc1edc", + "slot": "12860532" + }, + { + "pubkey": "0x9899bf1eb133446292287979af36f5ef1963b11d686da3c974011d29f504d0ca6d812a82b1a30402455636aef782db46", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb2ebb654dd7f47a8ba8b35bcc59afdbb29c3c8d2c19c99999639523d63becd7e7ef6b3a3f86b4e9394f1c7e5c7e5091f04f1bccf10c3724606ce0e6dfb2ae6cfc3f5434c2eba3e1e230bab6dfb73dae94bec0d695ec0da598a96c74dd027562a", + "slot": "12860532" + }, + { + "pubkey": "0xb7e4b438c261faff5055df896a115e7ffdac59a9b73a06e6c4f59f99871cac5ec8d9e8d0d2dca0a6b00e0f80bbbe5c4a", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x95f9fd3e95fc78800b224aa8485aab41d11fcfcb29e95a9c41565a76f9e32c0f6ef54bc4ae227e7584098fa8894b78c20f40dc88511dc0e38c0953b134c0576b85e776a5da00e9983dcd16fe78c0e5e6a1f8f56f074a9a4bb8092529c1fdeeb7", + "slot": "12860532" + }, + { + "pubkey": "0x94037eafdb50228a3576c506b75e50d22f2a8fd6afe5b55d7613517a08240e23a3af871ce9e64c6d2f21e5b502a94f5e", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x92b63ec7cbbd82595daa49f671d36f9480ef8db2031be137c896a5b3acb7fbca4af324f23b2c69af7adea44ceefd164a0962f8e7fd8d60c746af62bfcc89cac1d94a2af0ccd90e6f76c31e075d679d4498d47711e69c19c22c1a971ff9968dd3", + "slot": "12860532" + }, + { + "pubkey": "0xa4b13a7634fe89a819899ff5f9180aa24e283350d4e3bd16108654287c99f0e8975bab6470a48aff04fc2cafd874aef7", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb53d36d7e48731e0e2c18faec44d235ebe5f3280d7defa26aa7fa75b9f596c03a5d51dd303bc3aa6db1009b11e0515c403c75103a178bd95e1aa77f1e125ce355e09650433c4190bbe5dcbca656ce4495d9138c9599626f6aa7b6f4580afd4ea", + "slot": "12860532" + }, + { + "pubkey": "0xab17ed5f427623352e3ca2911fbf5234c1487da79a5d6d65ac8215bca05ee57a97ceb6b7d3e37d9234ac647c6ce8812b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xadaca699485f6a994e1e5c759f50381d66bd6a52f2a7ffcfb34bbc80712812a68f758c4641959354728b4f73fa9db1e8161b9aac75d1322586c88165ccb0d6a04bea4b74cd8f5ac1275baae55dc8744bdb36c43be23ba5321c41cc403ecec301", + "slot": "12860532" + }, + { + "pubkey": "0x83268f09fa272d68cf2286db186bea31ee214b2c87f75471d1ac6049b28c8babde4ce348fa803654cabf5e4af1deb4ed", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa134be877413ae2c075254260e70db5094669a0654c334d76527d6d67d7779ba0428741f050158f580159a1946bb5e8a11b577eb8c930c6afbc95704696f4ad867f8317f7dfabcdbc197681d60f68c4bfdc5d6f67c254a54299a4a67f858456e", + "slot": "12860532" + }, + { + "pubkey": "0x888240c65cf7a41f58e0f76359f14f068677eab9efea26d4b0d832c27967b53f7b9fd704b7a1c303f3e2888345eeb2a2", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb3704ee2b9a5a32aa940532466039b6ca55c997fcf7507c71b6c9a2d22751a8c6d331dc288eef9b6382d937af552089216c00c5343b5771f0c05bf5111d25fa3dd387fd2b0610d9d70e0613f04578d835dcdd47dcff099792fcb81254eb80b31", + "slot": "12860532" + }, + { + "pubkey": "0x8f04c6902daa9e661657abc9c790c340c9a68326ffe9ccc5f1cabfc705090756768b188f7586a26bd33ae4f4de603e91", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8755f2d798014434237e8d78c931891699e03f288c5398528eac93baf43b6df1c3644f5e16eac04fa48e5f33caec1fac088423defeccaa712b713070cf7384c3772e1ffe739d132b41f2d0fc7d2162cfbca2e121ad9f40d1f21abb8b8f747ff0", + "slot": "12860532" + }, + { + "pubkey": "0x86e93fc28f8ab194a83d6698344729b35deab92b4ad8d32dc4e529b8b405beebc55dc0e5c9ba150dc7091d35e8ffc0ca", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb3f9688a578d845ac15ed9ef89e6ea37d946bc0eafb8e0a84737a281afe7d7ed18ea3cad43fbb51079b057e4ef134f3a030d59e263f844ef1f67c9c2878cdce8dda8c287c53063e3a1679e41f2a6b03dea0d8954609aa5c320c273a260f51275", + "slot": "12860532" + }, + { + "pubkey": "0xb8c867e767f8aaa000f1cc3ed7805d24f92507647bab768802e4a25fae535ab224b2fc103627a8c18a764a2f2238e113", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x96937c67baeae02d0fce991e7a64a876243edc36c21adbf2023270d7fe6a6895195048ed6fc830d0e47161f9eb712ada1857528406bba616b898d7c6e0800f1dbe5fcd4d781551c5f79be0ddeba6fc54da1f3abbdb17803c0c135e2b60982e87", + "slot": "12860532" + }, + { + "pubkey": "0x8cfb890adaf44d3cad9f2e791e8bc5ecadaecb2d70195d2fedc4d8729350cf17b537d628c9e8e1beeda55c5feb185988", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x96577b490df65c5e3e2eb51042e8ba5b4b1e69579340d11961d8ff31f6844a1213f918092f0e29278497a8ffb5c43eb10f0e1850174592f3b1057cc8db68ea2c851bd5ea2d2198aabac785bde92f72f3e2b64a6b32e2ba317fe86ea5f4f3a396", + "slot": "12860532" + }, + { + "pubkey": "0x88609c6e91daaac1f2918f61e39c9cd258911876e15e61d1bbd974cf02066a769f1affb054704c46ae146fa6906f8d25", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa29dd0b3cf2b0554def166260fa7e932927b8f88b635ad598a55e46d6131ce6689064c2bb2531856f8aacd7d786a08d513376ddc5cd726e37612326379ab5241c022890b0ec6ace4f3ed10a1b3da9fd4ed750ff1ed8e740dd2cdb7d11968e24b", + "slot": "12860532" + }, + { + "pubkey": "0xb68c9e4de3204bf729ea28eb8c6be6628714f70c9b344accdfe3005f51eb4e09343f7d84406513cca4b4e3d179525bd0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x96bb90c41206d74193112b9d4496e81f12dae99e28132e2b3dac5e897a4b6992a67694acfb2a0bf7013dc12f19df868004debdc9fb4f5927cc20b00ab67b557891a9c4161668090ea1a1a46fe0aea114423c9d6e5acd9ee57ea32f085bed4476", + "slot": "12860532" + }, + { + "pubkey": "0xb19e5525dcc89aa4d1a32ed2b4a286b9f0855a947d90f2821f5686f3081d9b2c5bbb3ff9bd4b78600bccd9685694d18b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb497815df1c5dfcebcb2ada98c47270b74bfd7d90098c99345e5b4a5f808619e749591f96939a60033cecfdc57af63ef00fa2beb5eb9a067902bca95e630e597f3857a4f698d463c27dd6a5f821cb2c9cb454fcbab688f8f5ab730a1f77fa7cc", + "slot": "12860532" + }, + { + "pubkey": "0xad9b0ddb66f05dfada23252e892d9ef53ee91a4c4738f323c0fae348f75276cd57519ccd9a635b5c1435cf7ee5906f3f", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa2eee7965026db2bbb931f3ca1560ba71cce3595f6bc0f1b91dff2c0d5344cf9507a707853af2a1eda0ff17efd632b1a104608271edf70e1c423a5d833a646df997fab9b0016bcb08f55acdc3a804551f1020a939000c42f70e9d51d9de15810", + "slot": "12860532" + }, + { + "pubkey": "0xa3e0f7f215059ff9e9bbfdb8b5e55a6cfd14aa8ade83b1115412ec6e7acd0c2401ae6919143593d61f37e958e3c6e8e7", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x86257d68ae2890cf984e45c57a4bcacbf38da62c6d5531809a62eec80b0129b7613b09490b3daa336c3c29e4d47f75a502131b18547675196d5804abe9e9dad9478bb5f2e5716e2035e1d7fdeb2362ada6f4c1cdf0bc57e92bd493189c0fc5f7", + "slot": "12860532" + }, + { + "pubkey": "0xafdc8a4eee249bb9b91b626e1297e8cd708f6ba9c6b3d122f2ab5f305de254b8a6e11d68bbc295e1731e514f67b2449c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb353ccc70dd7c3b41667e4f6b5c62dd50e9cbe9cab6f8c5eb7f360482c814c2c2596a3b0a9847e01f8c0f2e858ca7d1819dbbc1ea3005aad1b0b63c363d1a67ca99d5212b6cae7a74e0c6c4d6ba81c4f910012f4a554b5d5f9d71d391814d138", + "slot": "12860532" + }, + { + "pubkey": "0xb535f9043003fa532f942644d0db398ffc90ba9aba888cf4e79ffd7236504df8aa05387de52cb7dee1302b38de2b6c4f", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x86211838b4ed8c547b18a3cdd2bb2bf2a26ff7683f0e3e315774b9eca6be97a716f7b3e4e5c75f1538b105e7640b5a2218defd1ab9b31bec599df4018f61d013d50a462f5909910afed0ada212e2ed8d643754629064c854f4fc168317f22a40", + "slot": "12860532" + }, + { + "pubkey": "0xb4cac30157c93c44c86bff84274a5a33d5e64055788f6e29c93ceb079e56146d1adb5080f957a8cee205019b5dbe2f9d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb092081315b9895730d00a4ab1061577c82e11bf99f5409afa1e8d0079b8a9e5fcff704fe67ec6825a5dad1d9f8796c91372ac5daf4900d3eab12a701c3bfe4fc12e8a957b9b2e7a4eb9cde7e934915a44bb37b28a17ba6fda844bd8da91cfa4", + "slot": "12860532" + }, + { + "pubkey": "0xa4d242d4f093cc57c006e291ffa437e6dda18b43c7d8503096040256e2c9850f54708082542ea27632ea0f7f7d2c0b8c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8be81061a27211a21a5990a8b9f515ab3f5410333012fa46de208de2ad20c5e2d1e7d0b8b9dbe5afc15d9f98b17e7963108100eb348a77ff991d37d55e4083f777f896a4812942d78eb89fbf62fe9ecbd1de340b3295868c7adf09ee0ac7967a", + "slot": "12860532" + }, + { + "pubkey": "0xae604a3a8fa6ee472a9a3bae81f060b30afbc5ff2bf6f7cc323bc77ba28a0197ea979d704af3bc13bb6aca94645c7c29", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x969e970d223a94b54e9e5d56723cc8a0c23f1d0cbdbc635d009a157cec8e71df64379aeb67f951497a9acbc105d827b708620ee00e6dc1f29ad637febdd6f55d574d03058875c3951461a2e1d6a0422e23f17b6a8197b838dd0a51bd3faaaf13", + "slot": "12860532" + }, + { + "pubkey": "0xb94c5ab6d018ad394a6b005e56a20912ad524d620c866907923ecf5f350f73c0508ed86dea5210a4cf413298d66b2ab4", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa4c5778ec7bba20115de827afb44b23bbe0f0e7d430691d013f81287bc2a406b347cba50e546835fbb20398f1403a2920721b5cab1b21884fcf5db9658c012577de5f0eaa6bb650b065c75b8e544ff582d9eb50ca4372136e307fd8d96779ce0", + "slot": "12860532" + }, + { + "pubkey": "0x8059e68709805fdc5ac30c2ac56bc6602f9621110b81ee470f6527912a6cb4ac1bcd486d1267a6dd25db0dfcc6298de0", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xab77f873fdf1b1944616bcfd313216fa68bfefc8d2214ae2c609ba12db376bdfa1b0927ea54f1e734ecd44fe95a7811900035ddd69250e97399964db357377ec770ddfbcb930aa47f32c14ec1e4d24b90c4155a21446369011a759518b426e1f", + "slot": "12860532" + }, + { + "pubkey": "0x89a7e6f7fd7a6276d53b54aa219a0760ed65c6675ec1ee4c58a8f939648b6a0d9261284cc3b97285ebc7b812c79124d6", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x887b94f980514abe511cc160194ddfa489446aaed4a18b84875ff5a30af8c0a92d3a4604ed708e510112ca8eab38d885146674f75ebd8baccac53840da0e011b25b4bb03a3b1aebbfece51d25845bfcff52a17e5caba69ddbd48a287b0a17e0e", + "slot": "12860532" + }, + { + "pubkey": "0xa1c135b46707d923d2de528b18cdb37fda367d0ef11d9e268339526178791165f6974e701cde97c5d6f3801b08897d9d", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb514bbdc8c81391e1d90492e4a7db0087acf584e4697c6d5e04f372a5c85582171a945423f1b377c4c8ef099ee921c39035c11cf23752dcb7cec43cd8ad6e6dcb29107df10adefeed5519cc26143d24683fcbc2dcc9728d63dda5a8a1f66e18a", + "slot": "12860532" + }, + { + "pubkey": "0x85f85021d4433894578294bf8fcca064c2eabc8ab68526ac02e18cb6a4f9f86079db5207234838790c1c9f71df407914", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x9306389b6cea350e07aaba3bf4e32ba9e154a1373ae7cf7b78b6371b61e89c13ff99be7d146846bfbdff22a5827b055a104aec98a3453a4bd588e9cb306ecedf3c7a25c45bdad99ea337900c4a9cbe6aa9a7cf86577a2fae6aca044fa7588c99", + "slot": "12860532" + }, + { + "pubkey": "0xb503fa7df463f777167e7f2573436c54eef7ca6641bad28db0e00cd7254946d63fb41238ba33bb698d845144943fbe7b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x918f63f68bdd6439ffbae3f1661e9eda26fa7ec606bca80ff332fc10eea9176001ab10ffc2929fab59badc9cbd7217f3160a5c8fc80fc0e3881e82905ffb81cb77fc62ca71e0113873dabd5094b14341e5df35d2961d4f7de5fc252d57ef5f56", + "slot": "12860532" + }, + { + "pubkey": "0x8ecd19f8df034937ecee5b0aee254608b9d67a4a7a203e021ccaad1b0c5db65ee8134adcfa93906c2f64fd71f01bdb6b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x85fc5dda98f10134021d9a34328c430c8f2fc7bf3cca93548e14a1613a456e9fa33c6b0edf55fccf63cc06242591bbee15ce28bde943393b6090527d4d5767f03241ae1a6e82fd099618658afe967b113ca937bf7f6d5177ebfd66fdc2a69ea8", + "slot": "12860532" + }, + { + "pubkey": "0x93788e420b5a90bf265ed7a7222becbc1a098819898a180c4528830ebe6a50576968d670d3d847d48473d9a784e0a01c", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa28401606da8fd6397c444dafdabc9a81d192a25e78d49f6c660182ad89ab31eb3d8464de9ec30ed935e04adda11262713563d39c1b1078c10ac499b5cd4fc7b2e8b3e1295a22fa7c7b6ce199b5ae675d8310643e816b8ef3183a01de65834c0", + "slot": "12860532" + }, + { + "pubkey": "0xad2cfd4487fda17266716254ba3199b238dcaf70c72b33f209a0362b523afeb0bd9cdabb0352ebfbb3d15f5d0712f0a9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x8340daeb08724581034a79d1bdea64b96a242ba0bc618b96feddfae5fea47d37ed716e4c8bb0edd81793093fd8698e160912d07be4b6b914eefa463b91ffe10d5ef62c75484f348c01fe0d378ba98e403ca02f02b93bd12aba3cffdda3e562ca", + "slot": "12860532" + }, + { + "pubkey": "0xab29ad46fdac9b2a1cdc3a699e0e9e5d3b64f1154cb82e4dccfc24fee2c97897d693fe71fd19c4aff00813a9358daf85", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x986d47d35279cef0f00bf6efd4810cd74033c21e1ccd172f178097d61a92768a5347eaf1351ec3531975b051910990620ed33f91e6d89ef8e67e11b8e67bbee5a3e495ab86c95e57245191b8020503e08b23c28af65d8d821def355ada39f92f", + "slot": "12860532" + }, + { + "pubkey": "0xad9f7970f4f83bd70792137296a6f3c24947324d8c2bc4a9cc1208ca8ed5906c9a52a459288e10ffe4477ca95c9c2b62", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa51e326564ccb19366486b4345804a7ac7a8fed6ab5b93b8698cd1049291185592dd36fd0fb25ba7337c55af2a9920b914fcc2deee600549381c1d16a122efe5b8c1498d8cd2a4a261cba548fceacd67d7465e516b014028d7ffc4594f1822ea", + "slot": "12860532" + }, + { + "pubkey": "0xb99be2808a2ad3fb0567eec23356bfe6a1532ff5f19eb90b5764ac2bfbf25bcefc565d2a42079eca37ef8fd9e614576b", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x953e921ea0f8ff98d0c30f3131cb6f04debc5473b98c22d2f3c1cd48908841e7e0d1d13742e4a526877323ac01b10f04169faa1e59a429aa56628d558711860e2a6004f2c290ee141d2afc5cd2efb8070deb90dc2380b5c66b049dbd9c149a2f", + "slot": "12860532" + }, + { + "pubkey": "0xb05d2517a8d21cd12467c09be16cdce6ca701fc59890a2ea28764f79d3b781369adf3686709c6199eab827c89b6b31d9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0x87f7942704e01b2cc86ecbe9dc54e21267ad886266aafaffa6fbc39a87c05a24d1480a3fb52f355ba1c79e7012556576066169a516c505224e1b3b6bc0dbee141131a8ce26f04949ee2b9200d9c80576801b0eeea780d6737c9ee37fafacc9f9", + "slot": "12860532" + }, + { + "pubkey": "0xb9c14d3b50ab3e03b28cf4c48da149deb270ef9a985e006eccc3abdaafea9c0d0847bcaaf0c59c48a6660b00e9b271a9", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa99f00599946a38deffd276998210e2c513756e0a4cd61462223f9b6521bcd169b5afa6180e35431088a813ff0485aea162df8eb0c0e27b806ef903ebb584709b3e4c47da6b2884d05b0e6ea0a9624d24663bd6267e11c680d56507648014b6b", + "slot": "12860532" + }, + { + "pubkey": "0xadf5ab06683ac989932b8dfa881f643eb40b9149ba068e5edc300cd91f3ca99482b3f23ebe9725b49f4573b618a4ad97", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xb1625a5288f76688ef3aaf4c0e0d46025a4cf20e3731c71d6d5e84e5ffdf92434c210300e2cf4e3f24d0a427175d69221168d1157eeffb167911d23940994f779601938600dfed99e29cb406acb0f095c53177bb62f0276f3f5eaa051d9d217e", + "slot": "12860532" + }, + { + "pubkey": "0xad99511cc485ebe4acd4d9fc8d2b98945cb4d5b0108c70ba9510627b33c6cfdf83a05fef8666cfcb3357ac25f357f86f", + "withdrawal_credentials": "0x010000000000000000000000882e747c5c2e0366d4fd6f9f95780467c6549732", + "amount": "32000000000", + "signature": "0xa6a728cc64c2ba8e53b85c56ee3e656613edc229e1d5f9f5bae7d2fa376ad95a87bd7881a3d67bc40a91011661c9592f147c25a8bd5cd22b9917776e02d8aea2eb137d32fc95759870b3d757dbc29b700657ce53a310c60d13065843949f6499", + "slot": "12860532" + }, + { + "pubkey": "0xa98d2c5178466c6d2fa2047015740db090ae0d59a4c5f6d970a96bccd0e7831aab3f39e2a5a1b635540ac408276d0966", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xad056dca61e104f5133ea27af8071587270e2f13ea2cf296be827d05af897131e9261e87528b66f8012f5cf0b8d2196900ee6dd2026bc3f60546fafe96c040387cab1badc6b2a89953c791218a1ecd2feac673352ed8b49d3495c9473cce013c", + "slot": "12860587" + }, + { + "pubkey": "0x8d0973e3d54cd62e0d2903af87a228f4af9459fbca5f402270ccc989b5f5028de841d8e4936f88ccb1b78bd485032fc0", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xa9794b7e97366f247f67515fe0d895940cf87a3de11a78ac09047fb216274ca8ff1489f3430416368fef63841ab1ec960c5008c5d7b36e6ff2f1beed6c104171b5f0ff3c4da9f64cd37d8fe615502ccbdfd3dc56966403304bc3b03a0c084197", + "slot": "12860590" + }, + { + "pubkey": "0xa9e87b606f557e3e549780dce298026b1a2abb560f9afddf9aec5833c9929cb227f6bf6b52dc5f022881817799d5084e", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0x91255a6cb8f939cc0e0dd0b37a6718abd04789544aab0f09a9d7efb11a923d833e4300585c8d7a887b29fedffba05007137aa5f2224b25b88ade621ef88c52c1bee3a060c4c5bd99ddeac999696fc4dfaa5b2b7d105e17c9c89139d5081abfad", + "slot": "12860592" + }, + { + "pubkey": "0xa56a056753e75187b051cbdc1c55edec862fdabffec8ce33507de6b5cdac0a0ca60e41c005a39fae25e16d881cd08a76", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xb9cc521964d26f95bde5bbf6931abe75c864570f884acb0649e69bf188af4789981a8a73b2cfb4f07d0bf3ff6188eb9b1866f0c3f08bd3dfc3940b40439b64391da9fe1259e3910d77e831d5c34b3cc7be2888342522485a49faf9dbf2e92477", + "slot": "12860593" + }, + { + "pubkey": "0x8778605e2372ed1c59098e706203bbceb1dd2f486406c14aa6074e4f25e183d3d8f327311012ed17a5b69863b0ca52ff", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xafab3b1114e3cadca125d40a6d45a531110b604881d7226864a33701fe6196ada403c1f76fa03d166cb800249242e25b0581be32111a741b386e7f098f87c14b44ea4d5f82d4b8659c3c17964a0ac5aa20d4440c8eb7dae6c3fc015397fb458b", + "slot": "12860595" + }, + { + "pubkey": "0x9457b15012c45703e285861721da497544459df2c2eb456da7c0937af39e28ad10648bed06af8616692094fb3630f59e", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xa4573b613c59a275d04f64c44e6bad734d2a5295ed245b5c4242bfbdecf191b2475e3833495f48e125d4b0549a0eda691209bc21e614bd532687f70ba0d2fd95a3367055fb91afbba7c61ffb1ef4d3fc97301a7db398a8de47dd379964f9764a", + "slot": "12860596" + }, + { + "pubkey": "0xb56607748d667a1b138bba4cae415dbd7e56b2869b5539bd2c1b525657191c2a193adecc1ccbf9bf15456fe909a54b5b", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0x90b7858502278d9743a02503e601c05e515aefe4ae67511393ad66f55706f32822c4b80caa2fbef90534c5ed2bb33df9120c161673960905e2d93f63cdb595770a619d6a614d9deb0b947f7727e87191f3dbd79742bae15b9c8fee6b8dae8f7c", + "slot": "12860598" + }, + { + "pubkey": "0xaaf695101c045a4039a016065c63cf45909747aa1e7830a29c66e17dfc39410195cfa1c0c7a6f7b433c034ad170a9e89", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0x8129c4fb83584641281091095005299aaf321536b262726f61933c6a287bb930db33effdd27eabeb449d1996707c01220ba8a71218ef5e1a8d51410f0a218ca7970df0660262dbffba4cb98cd181cbc688a7eba244599692f681331e64815700", + "slot": "12860599" + }, + { + "pubkey": "0xa2e91c2376868b9a2f7632103abbd913633f14ccd9ff666304d0afe4acbc85b0f1aab566e9429809f5b65f6604121f6a", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0xa7cf854f1ec4c9d56102a914af16dfa963fedd8a78227f399078dfcab4a5218f816cffb7041ad7930342f8e278ad45fd186964ec783054ae348c179555bf0ce3134d42c372f5405e21fd6b38b65fd5852eead435a471460335e8f5768f82f705", + "slot": "12860601" + }, + { + "pubkey": "0x81523805f2560170cbd0e6d99e566864fbf6d6f77f30b9899bcc770c8988c398466acce87ad9298673e0b64b3003e20a", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0x9570aa6992a2babf5d8dfecaae3973d6a93f4dc8a92678d28f82f866696495636b511b3a6dd230381c743e02aa51f17a181d5922b034aed18f3dd6c30a5f017b4dd861789d95c4a5b3de7232c77520025bd94b08f9951bfc82abbfb5ac969dfa", + "slot": "12860602" + }, + { + "pubkey": "0xb15a7398320b88a49c8f5b8582c4f7c5b3a58bd1e246133731efafa232f1fee6886c8280d78089587af469141c0acc2a", + "withdrawal_credentials": "0x02000000000000000000000016beb8782c2607fd90ca93f3119cf12c00255c6e", + "amount": "1800000000000", + "signature": "0x820f6e720dd32e7a5b40d76e02f80546232b76d40598894210ecebd3852ed79965a28ab40cdfbf725aa30cba29820bb90000fa8a96b87d5292cbd7363f9897ea11452a7af99b2a9f73ffcc7293d24fd5d17ea2f5ed1ba91afd19de2a842e871e", + "slot": "12860604" + }, + { + "pubkey": "0xa5be2b3abcf25a416c15d493a5350a656eff481ca87f394d51c0cf7aaf52e9ba712a9f6707a0341b58ef1a01b28e9838", + "withdrawal_credentials": "0x0000000000000000000000000000000000000000000000000000000000000000", + "amount": "217000000000", + "signature": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "slot": "12860605" + }, + { + "pubkey": "0xa0f7bce8bd6148c2c6c88de3f3f924b24e2420b30972f90dc6d18b175fe52f78cccab78f9591b99d2a2a6f08f876bd03", + "withdrawal_credentials": "0x0200000000000000000000002871cc399373bc5944ecb661e6dec29ca451808d", + "amount": "160000000000", + "signature": "0x8e1042845fa8a48b506a27d23ea3399691dccea565943ebd28d9a538ace09abbd36342a8d6d6f4fda00bb241bf5eaf0e06b6ed9e78e57fb48eb0619adedb0a3ce6cfae9374148862f4dbf443e56494a153abce1c1f7ef6fe8e681e40c52ce9f0", + "slot": "12860799" + }, + { + "pubkey": "0xaeb36892fe88356c3f0739c909bd412b32249049a0032993e85d92158298e91f9cca68a5e6cd574d7bf550442c475742", + "withdrawal_credentials": "0x0100000000000000000000001d9f1fea4f3cb1224d50828c0371147b7b1f7e8f", + "amount": "32000000000", + "signature": "0xaa26e3b6e1b9d39a9a3d7237546b93a133a142b3c148bbb138cd09ec353876cfa897966c864f4d2c43838165fb8a25b9199835f53f8b48c616c55b32eb8a1761cbf3ac2c6ce8409e1807e68a252ef4235e2466085ab545d462d94878e3fc8156", + "slot": "12860852" + }, + { + "pubkey": "0xaba08f5339a0d06c504ea75be64a7093ab2acff4ab7027400f61e1ca2f58310186ff86e69b943056b38c54cff9d0d14b", + "withdrawal_credentials": "0x010000000000000000000000341c2c475178747bc11369e798d6ff8bd66529ae", + "amount": "31000000000", + "signature": "0x917ba71c59e86d7b1fd43ad180329be01e46b89b33626e82074fdc01a18e9e22685a6fae96b365486827d7c9358b772c00f1b0992d75cc609a275d7d652352b0dac8ed5580cc28a1992d68da2a14ba258248418d6bf62eb7a989fd8c6d1402b2", + "slot": "12861039" + }, + { + "pubkey": "0xae00175b18cae7466b7fc33f081dad5547a232efa0cbef1ccd6075cfefbc09638f447ddaceaae52078bf9493a7ed13b5", + "withdrawal_credentials": "0x01000000000000000000000008ec37e2eb451ab6fb29fc14d215b0aeef170040", + "amount": "32000000000", + "signature": "0xa6ef9ae0132eb1385bc3d08a9fb0a1f304331aabeec2ef51a4a5716275c80a05249d0cd01647f95c6b9fd85c64d2ba4716a7e424187f078d856b268de4fd764af67622694ccba03778a463a9ee07328515485d2633d9f67d8cd6089f711ddd36", + "slot": "12861060" + }, + { + "pubkey": "0x96315b65969c031c66b4bcedd4b9293fcc60ee4cb2066ff1bdf01900e0f642414b89879ab375a4715dfdd49a8d3d6df7", + "withdrawal_credentials": "0x020000000000000000000000a3723b19f45ce4c3d369163da160f0cd6072a3cb", + "amount": "1800000000000", + "signature": "0xa3f6ef211794c0d981d04d9dda67c1a456393c00dd8ad036a6726f1da3d291d3b403507f1e5a3b48edc4b058ec644b8e0a40371496494e6e3da940b2649e878905d7d19055f15b8810482e77c54d17a6a9a39b30dc1015fea2ba581eaef90e97", + "slot": "12861169" + }, + { + "pubkey": "0xb27556c9b5df04e85979e2e17a1b2762eefd637debfaa1e1167d2ff8800152cb635a16544b0553cebe465f822644739f", + "withdrawal_credentials": "0x020000000000000000000000a3723b19f45ce4c3d369163da160f0cd6072a3cb", + "amount": "200000000000", + "signature": "0xaee69742d6bef4ef1136c25084359d6991ab896820b438c4e49dc247f6d58a224ba56139be1c571cd451fdaf39127efc0316d766ca693202663cdf6924b5bfe3d2cd0efd942fd30af77d6297c2c2325c5db115595bde1b4c1f5958e9f7481844", + "slot": "12861169" + } + ] +} \ No newline at end of file From 8ebbc6cb94be2ca33566d7b9dd3e87a26797dd66 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 19 Nov 2025 18:51:18 +0400 Subject: [PATCH 0478/1006] chore: Handle `NativeCoin*ed` events on Arc chain to make dual token balances synced (#13452) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .github/workflows/config.yml | 1 + .../lib/explorer/chain/token_transfer.ex | 15 ++ .../transform/address_coin_balances.ex | 113 ++++++++-- .../lib/indexer/transform/token_transfers.ex | 200 +++++++++++++++--- config/config_helper.exs | 1 + config/runtime.exs | 7 + docker-compose/envs/common-blockscout.env | 3 + 7 files changed, 292 insertions(+), 48 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index f21729cfd1cc..1e2a746636d3 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -52,6 +52,7 @@ jobs: const chainTypes = [ "default", "arbitrum", + "arc", "blackfort", "ethereum", "filecoin", diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 4cea3d7c1164..6cdac016fd72 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -163,6 +163,15 @@ defmodule Explorer.Chain.TokenTransfer do @erc404_erc20_transfer_event "0xe59fdd36d0d223c0c7d996db7ad796880f45e1936cb0bb7ac102e7082e031487" @erc404_erc721_transfer_event "0xe5f815dc84b8cecdfd4beedfc3f91ab5be7af100eca4e8fb11552b867995394f" + # event NativeCoinTransferred(address indexed from, address indexed to, uint256 amount) + @arc_native_coin_transferred_event "0x62f084c00a442dcf51cdbb51beed2839bf42a268da8474b0e98f38edb7db5a22" + + # event NativeCoinMinted(address indexed recipient, uint256 amount) + @arc_native_coin_minted_event "0xb049859d09b3a7d0189a07db4d4becee1a2aa269023205478b1360ab6fc12114" + + # event NativeCoinBurned(address indexed from, uint256 amount) + @arc_native_coin_burned_event "0xaaf1ef013644e67c5cea90217acdf0accd334f8437fc9a89a53cfc9b25fb5c25" + @transfer_function_signature "0xa9059cbb" @typedoc """ @@ -230,6 +239,12 @@ defmodule Explorer.Chain.TokenTransfer do def erc404_erc721_transfer_event, do: @erc404_erc721_transfer_event + def arc_native_coin_transferred_event, do: @arc_native_coin_transferred_event + + def arc_native_coin_minted_event, do: @arc_native_coin_minted_event + + def arc_native_coin_burned_event, do: @arc_native_coin_burned_event + @doc """ ERC 20's transfer(address,uint256) function signature """ diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex index 1efb3e76a981..77201eeb5481 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex @@ -2,8 +2,15 @@ defmodule Indexer.Transform.AddressCoinBalances do @moduledoc """ Extracts `Explorer.Chain.Address.CoinBalance` params from other schema's params. """ + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + arc_native_token_system_address: [:indexer, [:arc, :arc_native_token_system_address]] + + import Explorer.Helper, only: [truncate_address_hash: 1] + alias Explorer.Chain.TokenTransfer def params_set(%{} = import_options) do @@ -28,22 +35,32 @@ defmodule Indexer.Transform.AddressCoinBalances do defp reducer({:logs_params, logs_params}, acc) when is_list(logs_params) do # a log MUST have address_hash and block_number - logs_params - |> Enum.reject( - &(&1.first_topic == TokenTransfer.constant() or - &1.first_topic == TokenTransfer.erc1155_single_transfer_signature() or - &1.first_topic == TokenTransfer.erc1155_batch_transfer_signature()) - ) - |> Enum.into(acc, fn - %{address_hash: address_hash, block_number: block_number} - when is_binary(address_hash) and is_integer(block_number) -> + filtered_logs = + logs_params + |> Enum.reject( + &(&1.first_topic == TokenTransfer.constant() or + &1.first_topic == TokenTransfer.erc1155_single_transfer_signature() or + &1.first_topic == TokenTransfer.erc1155_batch_transfer_signature()) + ) + |> Enum.into(acc, fn %{address_hash: address_hash, block_number: block_number} - - %{type: "pending"} -> - nil - end) - |> Enum.reject(fn val -> is_nil(val) end) - |> MapSet.new() + when is_binary(address_hash) and is_integer(block_number) -> + %{address_hash: address_hash, block_number: block_number} + + %{type: "pending"} -> + nil + end) + |> Enum.reject(fn val -> is_nil(val) end) + + # for :arc chain type we also need to parse the `NativeCoinTransferred`, `NativeCoinMinted`, `NativeCoinBurned` events + filtered_arc_logs = + if chain_type() == :arc do + handle_arc_transfer_logs(logs_params) + else + [] + end + + MapSet.new(filtered_logs ++ filtered_arc_logs) end defp reducer({:transactions_params, transactions_params}, initial) when is_list(transactions_params) do @@ -61,6 +78,72 @@ defmodule Indexer.Transform.AddressCoinBalances do end) end + # Handles Arc chain type logs. + # + # ## Parameters + # - `logs_params`: The given list of logs. + # + # ## Returns + # - The list of `address_hash, block_number` pairs. + @spec handle_arc_transfer_logs([ + %{ + :first_topic => String.t(), + :second_topic => String.t(), + :third_topic => String.t() | nil, + :address_hash => String.t(), + :block_number => non_neg_integer(), + optional(:type) => String.t() | nil + } + ]) :: [%{:address_hash => String.t(), :block_number => non_neg_integer()}] + defp handle_arc_transfer_logs(logs_params) do + arc_native_coin_transferred_event = TokenTransfer.arc_native_coin_transferred_event() + arc_native_coin_minted_event = TokenTransfer.arc_native_coin_minted_event() + arc_native_coin_burned_event = TokenTransfer.arc_native_coin_burned_event() + arc_native_token_system_address = arc_native_token_system_address() + + logs_params + |> Enum.flat_map(fn + %{ + type: "pending" + } -> + [] + + %{ + first_topic: ^arc_native_coin_transferred_event, + second_topic: second_topic, + third_topic: third_topic, + address_hash: ^arc_native_token_system_address, + block_number: block_number + } + when is_integer(block_number) -> + [ + %{address_hash: truncate_address_hash(second_topic), block_number: block_number}, + %{address_hash: truncate_address_hash(third_topic), block_number: block_number} + ] + + %{ + first_topic: ^arc_native_coin_minted_event, + second_topic: second_topic, + address_hash: ^arc_native_token_system_address, + block_number: block_number + } + when is_integer(block_number) -> + [%{address_hash: truncate_address_hash(second_topic), block_number: block_number}] + + %{ + first_topic: ^arc_native_coin_burned_event, + second_topic: second_topic, + address_hash: ^arc_native_token_system_address, + block_number: block_number + } + when is_integer(block_number) -> + [%{address_hash: truncate_address_hash(second_topic), block_number: block_number}] + + _ -> + [] + end) + end + defp blocks_params_reducer(%{miner_hash: address_hash, number: block_number}, acc) when is_binary(address_hash) and is_integer(block_number) do MapSet.put(acc, %{address_hash: address_hash, block_number: block_number}) diff --git a/apps/indexer/lib/indexer/transform/token_transfers.ex b/apps/indexer/lib/indexer/transform/token_transfers.ex index 74892e9eb130..b12ff5cc8079 100644 --- a/apps/indexer/lib/indexer/transform/token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/token_transfers.ex @@ -5,6 +5,12 @@ defmodule Indexer.Transform.TokenTransfers do require Logger + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + arc_native_token_address: [:indexer, [:arc, :arc_native_token_address]], + arc_native_token_system_address: [:indexer, [:arc, :arc_native_token_system_address]], + arc_native_token_decimals: [:indexer, [:arc, :arc_native_token_decimals]] + import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] import Explorer.Helper, only: [decode_data: 2, truncate_address_hash: 1] @@ -18,9 +24,23 @@ defmodule Indexer.Transform.TokenTransfers do def parse(logs, skip_additional_fetchers? \\ false) do initial_acc = %{tokens: [], token_transfers: []} + erc20_and_erc721_token_transfers_filtered = + if chain_type() == :arc do + allowed_erc20_erc721_token_transfer_events = + [ + TokenTransfer.constant(), + TokenTransfer.arc_native_coin_transferred_event(), + TokenTransfer.arc_native_coin_minted_event(), + TokenTransfer.arc_native_coin_burned_event() + ] + + Enum.filter(logs, &(&1.first_topic in allowed_erc20_erc721_token_transfer_events)) + else + Enum.filter(logs, &(&1.first_topic == unquote(TokenTransfer.constant()))) + end + erc20_and_erc721_token_transfers = - logs - |> Enum.filter(&(&1.first_topic == unquote(TokenTransfer.constant()))) + erc20_and_erc721_token_transfers_filtered |> Enum.reduce(initial_acc, &do_parse/2) weth_transfers = @@ -211,59 +231,89 @@ defmodule Indexer.Transform.TokenTransfers do # ERC-20 token transfer defp parse_params(%{second_topic: second_topic, third_topic: third_topic, fourth_topic: nil} = log) when not is_nil(second_topic) and not is_nil(third_topic) do - [amount] = decode_data(log.data, [{:uint, 256}]) - - from_address_hash = truncate_address_hash(log.second_topic) - to_address_hash = truncate_address_hash(log.third_topic) + if arc_native_token_transfer_event?(log) do + # for :arc chain type we need to ignore ERC-20 Transfer events from the native token as there are + # NativeCoinTransferred, NativeCoinMinted, NativeCoinBurned events instead + nil + else + # handle the transfer for other cases + [decoded_amount] = decode_data(log.data, [{:uint, 256}]) + decimal_amount = Decimal.new(decoded_amount || 0) + + {token_contract_address_hash, amount} = + if arc_native_coin_transferred_event?(log) do + # if this is NativeCoinTransferred event for Arc chain, there are 18 decimals for the native token, so we need to adjust the amount with the token decimals + {arc_native_token_address(), amount_18_decimals_to_n_decimals(decimal_amount, arc_native_token_decimals())} + else + {log.address_hash, decimal_amount} + end - token_transfer = %{ - amount: Decimal.new(amount || 0), - block_number: log.block_number, - block_hash: log.block_hash, - log_index: log.index, - from_address_hash: from_address_hash, - to_address_hash: to_address_hash, - token_contract_address_hash: log.address_hash, - transaction_hash: log.transaction_hash, - token_ids: nil, - token_type: "ERC-20" - } + token_transfer = %{ + amount: amount, + block_number: log.block_number, + block_hash: log.block_hash, + log_index: log.index, + from_address_hash: truncate_address_hash(log.second_topic), + to_address_hash: truncate_address_hash(log.third_topic), + token_contract_address_hash: token_contract_address_hash, + transaction_hash: log.transaction_hash, + token_ids: nil, + token_type: "ERC-20" + } - token = %{ - contract_address_hash: log.address_hash, - type: "ERC-20" - } + token = %{ + contract_address_hash: token_contract_address_hash, + type: "ERC-20" + } - {token, token_transfer} + {token, token_transfer} + end end - # ERC-20 token transfer for WETH + # ERC-20 token transfer for WETH or Arc native token mint/burn defp parse_params(%{second_topic: second_topic, third_topic: nil, fourth_topic: nil} = log) when not is_nil(second_topic) do - [amount] = decode_data(log.data, [{:uint, 256}]) + [decoded_amount] = decode_data(log.data, [{:uint, 256}]) + decimal_amount = Decimal.new(decoded_amount || 0) - {from_address_hash, to_address_hash} = - if log.first_topic == TokenTransfer.weth_deposit_signature() do - {burn_address_hash_string(), truncate_address_hash(log.second_topic)} - else - {truncate_address_hash(log.second_topic), burn_address_hash_string()} + {from_address_hash, to_address_hash, token_contract_address_hash, amount} = + cond do + log.first_topic == TokenTransfer.weth_deposit_signature() -> + {burn_address_hash_string(), truncate_address_hash(log.second_topic), log.address_hash, decimal_amount} + + arc_native_coin_minted_event?(log) -> + # there are 18 decimals for the native token, so we need to adjust the amount with the token decimals + normalized_amount = amount_18_decimals_to_n_decimals(decimal_amount, arc_native_token_decimals()) + + {burn_address_hash_string(), truncate_address_hash(log.second_topic), arc_native_token_address(), + normalized_amount} + + arc_native_coin_burned_event?(log) -> + # there are 18 decimals for the native token, so we need to adjust the amount with the token decimals + normalized_amount = amount_18_decimals_to_n_decimals(decimal_amount, arc_native_token_decimals()) + + {truncate_address_hash(log.second_topic), burn_address_hash_string(), arc_native_token_address(), + normalized_amount} + + true -> + {truncate_address_hash(log.second_topic), burn_address_hash_string(), log.address_hash, decimal_amount} end token_transfer = %{ - amount: Decimal.new(amount || 0), + amount: amount, block_number: log.block_number, block_hash: log.block_hash, log_index: log.index, from_address_hash: from_address_hash, to_address_hash: to_address_hash, - token_contract_address_hash: log.address_hash, + token_contract_address_hash: token_contract_address_hash, transaction_hash: log.transaction_hash, token_ids: nil, token_type: "ERC-20" } token = %{ - contract_address_hash: log.address_hash, + contract_address_hash: token_contract_address_hash, type: "ERC-20" } @@ -479,6 +529,90 @@ defmodule Indexer.Transform.TokenTransfers do end end + # Converts from 18-decimal amount to n-decimal amount. + # + # ## Parameters + # - `amount`: The given 18-decimal amount to convert from. + # - `new_decimals`: The new number of decimals. + # + # ## Returns + # - The converted amount. If the source amount has more than `new_decimals` decimals, the new amount will be truncated. + # Example: If we have 18-decimal amount 25148712000000000, that will be 25148 for 6-decimal representation. + @spec amount_18_decimals_to_n_decimals(Decimal.t(), non_neg_integer()) :: Decimal.t() + defp amount_18_decimals_to_n_decimals(amount, new_decimals) do + amount + |> Decimal.mult(Integer.pow(10, new_decimals)) + |> Decimal.div_int(Integer.pow(10, 18)) + end + + # Determines if the given log is the NativeCoinTransferred event emitted by the native token system address on Arc chain. + # + # ## Parameters + # - `log`: The log to check. + # + # ## Returns + # - `true` if this is the NativeCoinTransferred event from the native token system address on Arc chain, `false` otherwise. + @spec arc_native_coin_transferred_event?(%{ + :first_topic => String.t(), + :address_hash => String.t(), + optional(any()) => any() + }) :: boolean() + defp arc_native_coin_transferred_event?(log) do + chain_type() == :arc and log.first_topic == TokenTransfer.arc_native_coin_transferred_event() and + log.address_hash == arc_native_token_system_address() + end + + # Determines if the given log is the NativeCoinMinted event emitted by the native token system address on Arc chain. + # + # ## Parameters + # - `log`: The log to check. + # + # ## Returns + # - `true` if this is the NativeCoinMinted event from the native token system address on Arc chain, `false` otherwise. + @spec arc_native_coin_minted_event?(%{ + :first_topic => String.t(), + :address_hash => String.t(), + optional(any()) => any() + }) :: boolean() + defp arc_native_coin_minted_event?(log) do + chain_type() == :arc and log.first_topic == TokenTransfer.arc_native_coin_minted_event() and + log.address_hash == arc_native_token_system_address() + end + + # Determines if the given log is the NativeCoinBurned event emitted by the native token system address on Arc chain. + # + # ## Parameters + # - `log`: The log to check. + # + # ## Returns + # - `true` if this is the NativeCoinBurned event from the native token system address on Arc chain, `false` otherwise. + @spec arc_native_coin_burned_event?(%{ + :first_topic => String.t(), + :address_hash => String.t(), + optional(any()) => any() + }) :: boolean() + defp arc_native_coin_burned_event?(log) do + chain_type() == :arc and log.first_topic == TokenTransfer.arc_native_coin_burned_event() and + log.address_hash == arc_native_token_system_address() + end + + # Determines if the given log is the Transfer event emitted by the native token contract on Arc chain. + # + # ## Parameters + # - `log`: The log to check. + # + # ## Returns + # - `true` if this is the Transfer event from the native token contract on Arc chain, `false` otherwise. + @spec arc_native_token_transfer_event?(%{ + :first_topic => String.t(), + :address_hash => String.t(), + optional(any()) => any() + }) :: boolean() + defp arc_native_token_transfer_event?(log) do + chain_type() == :arc and log.first_topic == TokenTransfer.constant() and + log.address_hash == arc_native_token_address() + end + def filter_tokens_for_supply_update(token_transfers) do token_transfers |> Enum.filter(fn token_transfer -> diff --git a/config/config_helper.exs b/config/config_helper.exs index f39a86881d13..98611f611083 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -379,6 +379,7 @@ defmodule ConfigHelper do @supported_chain_identities %{ "default" => :default, "arbitrum" => :arbitrum, + "arc" => :arc, "blackfort" => :blackfort, "ethereum" => :ethereum, "filecoin" => :filecoin, diff --git a/config/runtime.exs b/config/runtime.exs index 74313d8d4e38..dd19cb692bed 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -964,6 +964,13 @@ config :indexer, :ipfs, ConfigHelper.parse_catalog_value("IPFS_GATEWAY_URL_PARAM_LOCATION", ["query", "header"], true), public_gateway_url: System.get_env("IPFS_PUBLIC_GATEWAY_URL", "https://ipfs.io/ipfs") +config :indexer, :arc, + arc_native_token_decimals: ConfigHelper.parse_integer_env_var("INDEXER_ARC_NATIVE_TOKEN_DECIMALS", 6), + arc_native_token_address: + System.get_env("INDEXER_ARC_NATIVE_TOKEN_CONTRACT", "0x3600000000000000000000000000000000000000"), + arc_native_token_system_address: + System.get_env("INDEXER_ARC_NATIVE_TOKEN_SYSTEM_CONTRACT", "0x1800000000000000000000000000000000000000") + config :indexer, Indexer.Supervisor, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_INDEXER") config :indexer, Indexer.Fetcher.TransactionAction.Supervisor, diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 2ef005b42431..33cd0e44e0f8 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -361,6 +361,9 @@ API_V1_WRITE_METHODS_DISABLED=false # SCROLL_L1_BLOB_SCALAR_INIT= # SCROLL_L1_BASE_FEE_INIT= # SCROLL_L1_BLOB_BASE_FEE_INIT= +# INDEXER_ARC_NATIVE_TOKEN_DECIMALS= +# INDEXER_ARC_NATIVE_TOKEN_CONTRACT= +# INDEXER_ARC_NATIVE_TOKEN_SYSTEM_CONTRACT= # CELO_CORE_CONTRACTS= # CELO_L2_MIGRATION_BLOCK= # CELO_EPOCH_MANAGER_CONTRACT= From 1160540f52f3b4b775ed92d781dee0d0625948de Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 24 Nov 2025 13:06:14 +0300 Subject: [PATCH 0479/1006] chore: Elixir 1.17 -> 1.19 (#13566) Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- .credo.exs | 3 +- .dialyzer_ignore.exs | 2 +- .github/workflows/config.yml | 2 +- .tool-versions | 4 +- apps/block_scout_web/config/config.exs | 10 - .../lib/block_scout_web/application.ex | 59 ++- .../lib/block_scout_web/captcha_helper.ex | 4 +- .../lib/block_scout_web/checksum_address.ex | 2 +- .../account/api/v2/user_controller.ex | 2 +- .../address_token_transfer_controller.ex | 7 +- .../controllers/api/health_controller.ex | 2 +- .../api/v1/gas_price_oracle_controller.ex | 4 +- .../controllers/api/v2/address_controller.ex | 3 +- .../api/v2/advanced_filter_controller.ex | 3 +- .../api/v2/smart_contract_controller.ex | 2 +- .../controllers/api/v2/token_controller.ex | 14 +- .../transaction_history_chart_controller.ex | 4 +- .../controllers/tokens/holder_controller.ex | 37 +- .../verified_contracts_controller.ex | 4 +- .../graphql/resolvers/token_transfer.ex | 8 +- .../graphql/resolvers/transaction.ex | 4 +- .../lib/block_scout_web/plug/check_feature.ex | 2 +- .../prometheus/phoenix_instrumenter.ex | 16 - .../routers/chain_type_scope.ex | 2 +- .../templates/account/custom_abi/row.html.eex | 2 +- .../account/watchlist_address/row.html.eex | 2 +- .../templates/address/_block_link.html.eex | 2 +- .../_verify_other_explorer_modal.html.eex | 2 +- .../templates/address/overview.html.eex | 2 +- .../new.html.eex | 2 +- .../new.html.eex | 2 - .../templates/block/_tabs.html.eex | 2 +- .../templates/chain/show.html.eex | 10 +- .../_minimal_proxy_pattern.html.eex | 2 +- .../_link_to_token_instance.html.eex | 2 +- .../_link_to_token_symbol.html.eex | 2 +- .../templates/transaction/_tabs.html.eex | 2 +- .../verified_contracts/index.html.eex | 2 +- .../visualize_sol2uml/index.html.eex | 9 - .../views/api/v2/address_view.ex | 2 +- .../views/api/v2/filecoin_view.ex | 5 +- .../block_scout_web/views/api/v2/helper.ex | 17 +- .../views/api/v2/search_view.ex | 4 +- .../views/api/v2/stability_view.ex | 2 +- .../views/api/v2/zilliqa_view.ex | 9 +- apps/block_scout_web/mix.exs | 12 +- .../api/v2/search_controller_test.exs | 4 +- .../test/support/channel_case.ex | 2 +- .../block_scout_web/test/support/conn_case.ex | 2 +- .../test/support/feature_case.ex | 2 +- .../lib/ethereum_jsonrpc/geth.ex | 10 +- .../receipts/by_block_number.ex | 5 +- .../lib/ethereum_jsonrpc/rolling_window.ex | 2 +- .../web_socket/web_socket_client.ex | 6 +- apps/ethereum_jsonrpc/mix.exs | 10 +- .../explorer/lib/explorer/account/identity.ex | 4 +- apps/explorer/lib/explorer/application.ex | 4 +- .../explorer/arbitrum/claim_rollup_message.ex | 2 +- apps/explorer/lib/explorer/chain.ex | 4 +- apps/explorer/lib/explorer/chain/address.ex | 2 +- .../explorer/chain/address/coin_balance.ex | 2 +- .../chain/address/current_token_balance.ex | 2 +- .../chain/cache/latest_l1_block_number.ex | 2 + .../lib/explorer/chain/contract_method.ex | 2 +- apps/explorer/lib/explorer/chain/import.ex | 2 +- apps/explorer/lib/explorer/chain/search.ex | 418 ++++++++++-------- .../lib/explorer/chain/stability/validator.ex | 4 +- .../lib/explorer/chain/token/instance.ex | 2 +- .../lib/explorer/market/fetcher/coin.ex | 2 +- .../explorer/migrator/filling_migration.ex | 2 +- ...eindex_blocks_with_missing_transactions.ex | 2 +- ...eindex_duplicated_internal_transactions.ex | 2 +- ...l_transactions_with_incompatible_status.ex | 2 +- .../sanitize_duplicated_log_index_logs.ex | 6 +- .../lib/explorer/tags/address_to_tag.ex | 2 +- .../third_party_integrations/auth0/legacy.ex | 23 +- .../lib/explorer/token/metadata_retriever.ex | 4 +- .../explorer/validator/metadata_retriever.ex | 4 +- apps/explorer/mix.exs | 10 +- .../test/explorer/account/identity_test.exs | 8 +- .../explorer/account/notifier/notify_test.exs | 8 +- .../chain/filecoin/native_address_test.exs | 4 +- .../test/explorer/chain/import_test.exs | 7 +- .../token/metadata_retriever_test.exs | 3 +- apps/explorer/test/support/data_case.ex | 2 +- .../catchup/bound_interval_supervisor.ex | 2 +- apps/indexer/lib/indexer/block/fetcher.ex | 2 +- .../lib/indexer/block/realtime/supervisor.ex | 3 +- apps/indexer/lib/indexer/buffered_task.ex | 12 +- .../indexer/fetcher/arbitrum/data_backfill.ex | 2 +- .../fetcher/arbitrum/workers/batches/tasks.ex | 2 +- .../workers/historical_messages_on_l2.ex | 2 +- .../arbitrum/workers/new_l1_executions.ex | 4 +- .../arbitrum/workers/new_messages_to_l2.ex | 2 +- .../lib/indexer/fetcher/beacon/deposit.ex | 2 +- .../lib/indexer/fetcher/block_reward.ex | 2 +- .../fetcher/celo/epoch_block_operations.ex | 2 +- .../indexer/fetcher/celo/legacy/account.ex | 2 +- .../indexer/fetcher/coin_balance/helper.ex | 2 +- .../lib/indexer/fetcher/contract_code.ex | 2 +- .../indexer/fetcher/empty_blocks_sanitizer.ex | 2 +- .../indexer/fetcher/filecoin/address_info.ex | 2 +- .../indexer/fetcher/internal_transaction.ex | 2 +- .../internal_transaction/delete_queue.ex | 2 +- .../balances_export_queue.ex | 2 +- .../indexer/fetcher/on_demand/coin_balance.ex | 2 +- .../fetcher/on_demand/contract_creator.ex | 2 +- .../indexer/fetcher/on_demand/first_trace.ex | 5 +- .../fetcher/on_demand/token_balance.ex | 2 +- .../token_instance_metadata_refetch.ex | 2 +- .../indexer/fetcher/optimism/operator_fee.ex | 2 +- .../fetcher/optimism/transaction_batch.ex | 2 +- .../indexer/fetcher/replaced_transaction.ex | 6 +- .../lib/indexer/fetcher/rootstock_data.ex | 2 +- .../fetcher/signed_authorization_status.ex | 2 +- apps/indexer/lib/indexer/fetcher/token.ex | 2 +- .../lib/indexer/fetcher/token_balance.ex | 2 +- .../indexer/fetcher/token_counters_updater.ex | 2 +- .../lib/indexer/fetcher/token_updater.ex | 2 +- .../lib/indexer/fetcher/transaction_action.ex | 2 +- .../lib/indexer/fetcher/uncle_block.ex | 28 +- .../indexer/lib/indexer/fetcher/withdrawal.ex | 2 +- apps/indexer/lib/indexer/memory/monitor.ex | 2 +- .../migrator/recovery_weth_token_transfers.ex | 2 +- ...in_pending_address_operations_collector.ex | 7 +- .../indexer/temporary/uncles_without_index.ex | 4 +- .../transform/address_coin_balances_daily.ex | 4 +- .../lib/indexer/transform/token_transfers.ex | 2 +- apps/indexer/mix.exs | 2 +- .../indexer/block/realtime/fetcher_test.exs | 4 +- .../lib/nft_media_handler/application.ex | 4 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 12 +- docker/Dockerfile | 6 +- docker/oldUI.Dockerfile | 4 +- mix.exs | 10 +- 136 files changed, 551 insertions(+), 514 deletions(-) delete mode 100644 apps/block_scout_web/lib/block_scout_web/prometheus/phoenix_instrumenter.ex diff --git a/.credo.exs b/.credo.exs index 444b59323dc9..37ea310347f2 100644 --- a/.credo.exs +++ b/.credo.exs @@ -89,7 +89,8 @@ # or the `schema` macro in Ecto schemas to trigger DuplicatedCode, just # set the `excluded_macros` parameter to `[:schema, :setup, :test]`. # - {Credo.Check.Design.DuplicatedCode, excluded_macros: [], mass_threshold: 80}, + # todo: reduce mass_threshold number + {Credo.Check.Design.DuplicatedCode, excluded_macros: [], mass_threshold: 800}, # You can also customize the exit_status of each check. # If you don't want TODO comments to cause `mix credo` to fail, just diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 05fec4076945..02d124e136c7 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -1,5 +1,5 @@ [ - {"lib/ethereum_jsonrpc/rolling_window.ex", :improper_list_constr, 171}, + {"lib/ethereum_jsonrpc/rolling_window.ex", :improper_list_constr}, {"lib/explorer/smart_contract/solidity/publisher_worker.ex", :pattern_match, 1}, {"lib/explorer/smart_contract/solidity/publisher_worker.ex", :exact_eq, 8}, {"lib/explorer/smart_contract/solidity/publisher_worker.ex", :pattern_match, 8}, diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 1e2a746636d3..2447c266ce97 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -32,7 +32,7 @@ on: env: MIX_ENV: test OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + ELIXIR_VERSION: ${{ github.ref_name == '13566/merge' && '1.19.3' || vars.ELIXIR_VERSION }} ACCOUNT_AUTH0_DOMAIN: "blockscoutcom.us.auth0.com" jobs: diff --git a/.tool-versions b/.tool-versions index cf38ec3ee8cf..043e0817eb10 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -elixir 1.17.3-otp-27 -erlang 27.3.4 +elixir 1.19.3-otp-27 +erlang 27.3.4.5 nodejs 20.17.0 diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 808135c5ef62..01d273f77049 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -83,16 +83,6 @@ config :prometheus, BlockScoutWeb.Prometheus.PublicExporter, registry: :public, auth: false -config :prometheus, BlockScoutWeb.Prometheus.PhoenixInstrumenter, - # override default for Phoenix 1.4 compatibility - # * `:transport_name` to `:transport` - # * remove `:vsn` - channel_join_labels: [:channel, :topic, :transport], - # override default for Phoenix 1.4 compatibility - # * `:transport_name` to `:transport` - # * remove `:vsn` - channel_receive_labels: [:channel, :topic, :transport, :event] - config :spandex_phoenix, tracer: BlockScoutWeb.Tracer config :block_scout_web, BlockScoutWeb.Routers.ApiRouter, diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 0c293d2e6c0e..78d3e3276cf2 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -9,27 +9,47 @@ defmodule BlockScoutWeb.Application do alias BlockScoutWeb.{Endpoint, HealthEndpoint, RateLimit.Hammer} alias BlockScoutWeb.Utility.RateLimitConfigHelper - def start(_type, _args) do - opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + if @disable_api? do + def start(_type, _args) do + opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] - if not @disable_api? do - RateLimitConfigHelper.store_rate_limit_config() + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) + else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. + base_children = [Supervisor.child_spec(Endpoint, [])] + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children + + Supervisor.start_link(all_children, opts) + end end + else + def start(_type, _args) do + opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + + RateLimitConfigHelper.store_rate_limit_config() + + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) + else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. + base_children = [Supervisor.child_spec(Endpoint, [])] + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children - if Application.get_env(:nft_media_handler, :standalone_media_worker?) do - Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) - else - # Endpoint must be the last child in the supervision tree - # since it must be started after all of the other processes - # (to be sure that application is ready to handle traffic) - # and stopped before them for the same reason. - # However, some processes may depend on Endpoint - # so they need to be started after. - base_children = [Supervisor.child_spec(Endpoint, [])] - {first_api_children, last_api_children} = setup_and_define_children() - all_children = first_api_children ++ base_children ++ last_api_children - - Supervisor.start_link(all_children, opts) + Supervisor.start_link(all_children, opts) + end end end @@ -49,7 +69,7 @@ defmodule BlockScoutWeb.Application do defp setup_and_define_children do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.Counters.{BlocksIndexedCounter, InternalTransactionsIndexedCounter} - alias BlockScoutWeb.Prometheus.{Exporter, PhoenixInstrumenter, PublicExporter} + alias BlockScoutWeb.Prometheus.{Exporter, PublicExporter} alias BlockScoutWeb.RealtimeEventHandlers.{ Main, @@ -61,7 +81,6 @@ defmodule BlockScoutWeb.Application do alias BlockScoutWeb.Utility.EventHandlersMetrics alias Explorer.Chain.Metrics, as: ChainMetrics - PhoenixInstrumenter.setup() Exporter.setup() PublicExporter.setup() diff --git a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex index 816aa9bcdd6c..54c9169734a6 100644 --- a/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/captcha_helper.ex @@ -122,7 +122,7 @@ defmodule BlockScoutWeb.CaptchaHelper do # v3 case defp success?(%{"success" => true, "score" => score, "hostname" => hostname}) do - unless Helper.get_app_host() == hostname do + if Helper.get_app_host() != hostname do Logger.warning("reCAPTCHA v3 Hostname mismatch: #{inspect(hostname)} != #{inspect(Helper.get_app_host())}") end @@ -136,7 +136,7 @@ defmodule BlockScoutWeb.CaptchaHelper do # v2 case defp success?(%{"success" => true, "hostname" => hostname}) do - unless Helper.get_app_host() == hostname do + if Helper.get_app_host() != hostname do Logger.warning("reCAPTCHA v2 Hostname mismatch: #{inspect(hostname)} != #{inspect(Helper.get_app_host())}") end diff --git a/apps/block_scout_web/lib/block_scout_web/checksum_address.ex b/apps/block_scout_web/lib/block_scout_web/checksum_address.ex index 7aeeac2671eb..ca6b53236ee1 100644 --- a/apps/block_scout_web/lib/block_scout_web/checksum_address.ex +++ b/apps/block_scout_web/lib/block_scout_web/checksum_address.ex @@ -49,7 +49,7 @@ defmodule BlockScoutWeb.ChecksumAddress do conn |> Controller.redirect(to: new_path |> BlockScoutWebController.full_path()) - |> halt + |> halt() else conn end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex index d3b82cf0263a..7e493ae4322a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex @@ -54,7 +54,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do next_page |> next_page_params(watchlist_addresses, params) watchlist_addresses_prepared = - Enum.map(watchlist_addresses, fn wa -> + Enum.map(watchlist_addresses, fn %WatchlistAddress{} = wa -> balances = Chain.fetch_paginated_last_token_balances(wa.address_hash, paging_options: %PagingOptions{page_size: @token_balances_amount + 1} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex index a6ce243ddffe..ae54aedc92f3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex @@ -193,10 +193,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do end end - def index( - conn, - %{"address_id" => address_hash_string} = params - ) do + def index(conn, %{"address_id" => address_hash_string} = params) do ip = AccessHelper.conn_to_ip_string(conn) with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), @@ -209,8 +206,8 @@ defmodule BlockScoutWeb.AddressTokenTransferController do coin_balance_status: CoinBalanceOnDemand.trigger_fetch(ip, address), exchange_rate: Market.get_coin_exchange_rate(), filter: params["filter"], - current_path: Controller.current_full_path(conn), counters_path: address_path(conn, :address_counters, %{"id" => Address.checksum(address_hash)}), + current_path: Controller.current_full_path(conn), tags: get_address_tags(address_hash, current_user(conn)) ) else diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex index 50b665b7528a..448a1aeb424a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/health_controller.ex @@ -132,7 +132,7 @@ defmodule BlockScoutWeb.API.HealthController do """ @spec readiness(Plug.Conn.t(), map()) :: Plug.Conn.t() def readiness(conn, _) do - unless Application.get_env(:nft_media_handler, :standalone_media_worker?) do + if !Application.get_env(:nft_media_handler, :standalone_media_worker?) do HealthHelper.last_db_block_status() end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex index 1c99dcb17136..0d6cb8e3eca3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex @@ -36,7 +36,9 @@ defmodule BlockScoutWeb.API.V1.GasPriceOracleController do end def error({:error, error}) do - Logger.error(fn -> ["Something went wrong while estimates gas prices in the gas price oracle: ", inspect(error)] end) + Logger.error(fn -> + ["Something went wrong while estimates gas prices in the gas price oracle: ", inspect(error)] + end) %{ "error_code" => 6001, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 2a8b26c78cc2..9f644f85fd48 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -192,7 +192,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params) do case Chain.hash_to_address(address_hash, @address_options) do {:ok, address} -> - fully_preloaded_address = + %Address{} = + fully_preloaded_address = Address.maybe_preload_smart_contract_associations(address, contract_address_preloads(), @api_true) implementations = SmartContractHelper.pre_fetch_implementations(fully_preloaded_address) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index ba37553eff23..3ff0aaa6a30d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -92,7 +92,8 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do params |> extract_filters() |> Keyword.merge(paging_options(params)) - |> Keyword.update(:paging_options, %PagingOptions{page_size: CsvHelper.limit()}, fn paging_options -> + |> Keyword.update(:paging_options, %PagingOptions{page_size: CsvHelper.limit()}, fn %PagingOptions{} = + paging_options -> %PagingOptions{paging_options | page_size: CsvHelper.limit()} end) |> Keyword.put(:timeout, :timer.minutes(5)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index d450e8a3d66d..0a68e666adb1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -77,7 +77,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), _ <- PublishHelper.sourcify_check(address_hash_string), - {:not_found, {:ok, address}} <- + {:not_found, {:ok, %Address{} = address}} <- {:not_found, Chain.find_contract_address(address_hash, smart_contract_address_options())} do implementations = SmartContractHelper.pre_fetch_implementations(address) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 1046b2927b94..26e61388a74e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -271,10 +271,11 @@ defmodule BlockScoutWeb.API.V2.TokenController do {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, {:format, {:ok, holder_address_hash}} <- {:format, Chain.string_to_address_hash(holder_address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(holder_address_hash_string, params) do - holder_address = Address.get(holder_address_hash, @api_true) - holder_address_with_proxy_implementations = - holder_address && %Address{holder_address | proxy_implementations: nil} + case Address.get(holder_address_hash, @api_true) do + %Address{} = holder_address -> %Address{holder_address | proxy_implementations: nil} + nil -> nil + end results_plus_one = Instance.token_instances_by_holder_address_hash( @@ -361,7 +362,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do Instance.nft_instance_by_token_id_and_token_address(token_id, address_hash, @api_true) do fill_metadata_url_task = maybe_run_fill_metadata_url_task(token_instance, token) - token_instance = + %Instance{} = + token_instance = token_instance |> Chain.select_repo(@api_true).preload(owner: [:names, :smart_contract, proxy_implementations_association()]) |> Instance.put_owner_to_token_instance(token, @api_true) @@ -752,13 +754,13 @@ defmodule BlockScoutWeb.API.V2.TokenController do defp put_owner(token_instances, holder_address, holder_address_hash), do: - Enum.map(token_instances, fn token_instance -> + Enum.map(token_instances, fn %Instance{} = token_instance -> %Instance{token_instance | owner: holder_address, owner_address_hash: holder_address_hash} end) @spec put_token_to_instance(Instance.t(), Token.t()) :: Instance.t() defp put_token_to_instance( - token_instance, + %Instance{} = token_instance, token ) do %Instance{token_instance | token: token} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex index dec6f4f90ee3..4f564ad12a76 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/chain/transaction_history_chart_controller.ex @@ -15,8 +15,8 @@ defmodule BlockScoutWeb.Chain.TransactionHistoryChartController do transaction_history_data = date_range - |> extract_history - |> encode_transaction_history_data + |> extract_history() + |> encode_transaction_history_data() json(conn, %{ history_data: transaction_history_data diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex index 6e65b36521a1..27ef2589abaa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex @@ -1,14 +1,9 @@ defmodule BlockScoutWeb.Tokens.HolderController do use BlockScoutWeb, :controller - import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] - - alias BlockScoutWeb.{AccessHelper, Controller} - alias BlockScoutWeb.Tokens.HolderView + alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.Tokens.{HolderView, TransferController} alias Explorer.Chain - alias Explorer.Chain.Address - alias Indexer.Fetcher.OnDemand.TokenTotalSupply, as: TokenTotalSupplyOnDemand alias Phoenix.View import BlockScoutWeb.Chain, @@ -57,31 +52,7 @@ defmodule BlockScoutWeb.Tokens.HolderController do end end - def index(conn, %{"token_id" => address_hash_string} = params) do - options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] - ip = AccessHelper.conn_to_ip_string(conn) - - with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash, options), - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do - render( - conn, - "index.html", - current_path: Controller.current_full_path(conn), - token: token, - counters_path: token_path(conn, :token_counters, %{"id" => Address.checksum(address_hash)}), - token_total_supply_status: TokenTotalSupplyOnDemand.trigger_fetch(ip, address_hash), - tags: get_address_tags(address_hash, current_user(conn)) - ) - else - {:restricted_access, _} -> - not_found(conn) - - :error -> - not_found(conn) - - {:error, :not_found} -> - not_found(conn) - end + def index(conn, %{"token_id" => _address_hash_string} = params) do + TransferController.index(conn, params) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex index 7245892e9d8a..dbec2a6abab6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/verified_contracts_controller.ex @@ -29,7 +29,9 @@ defmodule BlockScoutWeb.VerifiedContractsController do verified_contracts_plus_one = full_options |> SmartContract.verified_contract_addresses() - |> Enum.map(&%SmartContract{&1.smart_contract | address: &1}) + |> Enum.map(fn %{smart_contract: %SmartContract{} = smart_contract} = address -> + %SmartContract{smart_contract | address: address} + end) {verified_contracts, next_page} = split_list_by_page(verified_contracts_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex index e61622260e6d..bf155c1748fa 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/token_transfer.ex @@ -12,17 +12,21 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.TokenTransfer do def get_by(_, %{token_contract_address_hash: token_contract_address_hash} = args, _) do connection_args = Map.take(args, [:after, :before, :first, :last]) + replica = Repo.replica() + token_contract_address_hash |> GraphQL.list_token_transfers_query() - |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) + |> Connection.from_query(&replica.all/1, connection_args, options(args)) end def get_by(%Address{hash: address_hash}, args, _) do connection_args = Map.take(args, [:after, :before, :first, :last]) + replica = Repo.replica() + address_hash |> TokenTransfer.token_transfers_by_address_hash(nil, nil, [], nil, []) - |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) + |> Connection.from_query(&replica.all/1, connection_args, options(args)) end defp options(%{before: _}), do: [] diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex index 9230fa0834dc..e297a13a3d0f 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/transaction.ex @@ -11,9 +11,11 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.Transaction do def get_by(%Address{hash: address_hash}, args, _) do connection_args = Map.take(args, [:after, :before, :first, :last]) + replica = Repo.replica() + address_hash |> GraphQL.address_to_transactions_query(args.order) - |> Connection.from_query(&Repo.replica().all/1, connection_args, options(args)) + |> Connection.from_query(&replica.all/1, connection_args, options(args)) end def get_by(%TokenTransfer{transaction_hash: hash}, _, _), diff --git a/apps/block_scout_web/lib/block_scout_web/plug/check_feature.ex b/apps/block_scout_web/lib/block_scout_web/plug/check_feature.ex index f858e4f5f50e..5fdda1a5c8fd 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/check_feature.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/check_feature.ex @@ -42,7 +42,7 @@ defmodule BlockScoutWeb.Plug.CheckFeature do @doc false @spec init(Keyword.t()) :: Keyword.t() def init(opts) do - unless Keyword.has_key?(opts, :feature_check) do + if !Keyword.has_key?(opts, :feature_check) do raise ArgumentError, "CheckFeature plug requires :feature_check option" end diff --git a/apps/block_scout_web/lib/block_scout_web/prometheus/phoenix_instrumenter.ex b/apps/block_scout_web/lib/block_scout_web/prometheus/phoenix_instrumenter.ex deleted file mode 100644 index 5b25969f3f1b..000000000000 --- a/apps/block_scout_web/lib/block_scout_web/prometheus/phoenix_instrumenter.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule BlockScoutWeb.Prometheus.PhoenixInstrumenter do - @moduledoc """ - Phoenix request metrics for `Prometheus`. - """ - - @dialyzer {:no_match, - [ - phoenix_channel_join: 3, - phoenix_channel_receive: 3, - phoenix_controller_call: 3, - phoenix_controller_render: 3, - setup: 0 - ]} - - use Prometheus.PhoenixInstrumenter -end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/chain_type_scope.ex b/apps/block_scout_web/lib/block_scout_web/routers/chain_type_scope.ex index 9d9e5bee4e2e..ba80577bf6da 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/chain_type_scope.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/chain_type_scope.ex @@ -24,7 +24,7 @@ defmodule BlockScoutWeb.Routers.ChainTypeScope do quote do # Define pipeline if not already defined - unless Module.has_attribute?(__MODULE__, unquote(pipeline_name)) do + if !Module.has_attribute?(__MODULE__, unquote(pipeline_name)) do pipeline unquote(pipeline_name) do plug(BlockScoutWeb.Plug.CheckChainType, unquote(chain_type)) end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex index 36c2740aa809..85521bc965c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/custom_abi/row.html.eex @@ -9,7 +9,7 @@
-
+ <%= gettext("Remove") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex index 0755957ac208..daf4a0dce5d2 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/account/watchlist_address/row.html.eex @@ -18,7 +18,7 @@ data-placement="top" data-toggle="tooltip" data-html="true" - title='<%= "@ " <> to_string(@exchange_rate.fiat_value) <> "/" <> Explorer.coin_name() %>' + title='<%= "@ " <> to_string(@exchange_rate.fiat_value) <> "/" <> Explorer.coin_name() %>' > ) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_block_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_block_link.html.eex index 935d6e76e07b..50f9e2998a34 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_block_link.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_block_link.html.eex @@ -1 +1 @@ -<%= link(@block_number, to: block_path(@conn, :show, @block_number), class: "tile-title-lg") %> \ No newline at end of file +<%= link(@block_number, to: block_path(@conn, :show, @block_number), class: "tile-title-lg") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex index 462ec8ed6e81..0dfca9df5022 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_verify_other_explorer_modal.html.eex @@ -19,4 +19,4 @@ <% end %> - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index 3deb067abad2..f3db1f9251ba 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -43,7 +43,7 @@

<%= @address %>

<% address_name = primary_name(@address) %> <%= cond do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex index c8969a8eaa77..0a46ca12fa70 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification/new.html.eex @@ -62,7 +62,7 @@ b) otherwise you will be asked to upload source files and JSON metadata file(s).
4. Verification of Vyper contract. - + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex index bd60af3a5b34..75145090b88d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex @@ -98,8 +98,6 @@ > <%= render BlockScoutWeb.CommonComponentsView, "_loading_spinner.html", loading_text: gettext("Loading...") %> - <%#= submit gettext("Verify & publish"), class: "btn-full-primary mr-2", "data-button-loading": "animation", "data-submit-button": "" %> - <%# verify-via-multi-part-files-submit %> <%= reset gettext("Reset"), class: "btn-line mr-2 js-smart-contract-form-reset" %> <%= diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex index 885aa9b98af1..0666dac3aac3 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex @@ -6,7 +6,7 @@ to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"]) ) %> - + <%= if Chain.check_if_withdrawals_in_block(@block.hash) do %> <%= link( diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index c0156a2cdbf5..6f065e603380 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -30,10 +30,12 @@ <% price_chart_legend_enabled? = Application.get_env(:block_scout_web, :chart)[:price_chart_legend_enabled?] %> <%= if Map.has_key?(@chart_config, :market) || price_chart_legend_enabled? do %> - <%# THE FOLLOWING LINE PREVENTS COPY/PASTE ERRORS %> - <%# Explicitly put @chart_config.market in a variable %> - <%# This is done so that when people add a new chart source, x, %> - <%# They wont just access @chart_config.x w/o first checking if x exists %> + <% market_chart_config = Map.has_key?(@chart_config, :market) && @chart_config.market%> <%= if price_chart_legend_enabled? || Enum.member?(market_chart_config, :price) do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex index c3f069b3ca29..8d641c59869b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex @@ -7,4 +7,4 @@ to: address_contract_path(@conn, :index, @address_hash)) %>.
<%= link( to: "https://eips.ethereum.org/EIPS/eip-1167", target: "_blank" ) %><%= gettext(" - minimal bytecode implementation that delegates all calls to a known address") %> - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex index 88273f4bc093..0302e9b20b9d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_instance.html.eex @@ -1 +1 @@ -<%= "[" %><%= link(short_token_id(@token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@token_id)), "data-test": "token_link") %><%= "]" %> \ No newline at end of file +<%= "[" %><%= link(short_token_id(@token_id, 30), to: token_instance_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash, to_string(@token_id)), "data-test": "token_link") %><%= "]" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex index 73cb4a2c2cd5..d1c8ccc3e602 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_link_to_token_symbol.html.eex @@ -1 +1 @@ -<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> \ No newline at end of file +<%= link(token_symbol(@transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @transfer.token.contract_address_hash), "data-test": "token_link") %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex index 75f28203e232..7e513a0fbcf7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tabs.html.eex @@ -29,6 +29,6 @@ gettext("State changes"), class: "card-tab #{tab_status("state", @conn.request_path)}", to: AccessHelper.get_path(@conn, :transaction_state_path, :index, @transaction) - ) + ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex index 1149eeab1f3c..a4bcd583757c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/verified_contracts/index.html.eex @@ -42,7 +42,7 @@ ) %> - +
" placeholder="<%= gettext "Contract name or address" %>" id="search-text-input">
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex index b78d3ef514ec..199ae079854d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex @@ -5,15 +5,6 @@
<%= gettext("UML diagram") %>

<%= gettext("For contract") %> - <%= link to: address_contract_path(@conn, :index, @address), "data-test": "address_hash_link" do %> - <%= render( - BlockScoutWeb.AddressView, - "_responsive_hash.html", - address: @address, - contract: true, - use_custom_tooltip: false - ) %> - <% end %>

diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 1051e52793e2..da77ea408c58 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -228,7 +228,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do @api_true ) do # `%{hash: address_hash}` will match with `address_with_info(_, address_hash)` clause in `BlockScoutWeb.API.V2.Helper` - {:ok, token_instance} -> + {:ok, %Instance{} = token_instance} -> %Instance{ token_instance | owner: %{hash: address_hash}, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex index e6c09d839aca..0ebe78bdc3a7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/filecoin_view.ex @@ -5,9 +5,8 @@ defmodule BlockScoutWeb.API.V2.FilecoinView do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] if @chain_type == :filecoin do - # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release - alias Explorer.Chain, warn: false - alias Explorer.Chain.Address, warn: false + alias Explorer.Chain + alias Explorer.Chain.Address @api_true [api?: true] diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 2c8e71c1eecf..6dce261e43f3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -185,9 +185,22 @@ defmodule BlockScoutWeb.API.V2.Helper do def smart_contract_verified?(%Address{smart_contract: %NotLoaded{}}), do: nil def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true - def market_cap(:standard, %{available_supply: available_supply, fiat_value: fiat_value, market_cap: market_cap}) + def market_cap(:standard, %{ + available_supply: available_supply, + fiat_value: fiat_value, + market_cap: %Decimal{} = market_cap + }) when is_nil(available_supply) or is_nil(fiat_value) do - max(Decimal.new(0), market_cap) + Decimal.max(Decimal.new(0), market_cap) + end + + def market_cap(:standard, %{ + available_supply: available_supply, + fiat_value: fiat_value, + market_cap: nil + }) + when is_nil(available_supply) or is_nil(fiat_value) do + Decimal.new(0) end def market_cap(:standard, %{available_supply: available_supply, fiat_value: fiat_value}) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index c24b3ff18711..f25482920faf 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -208,11 +208,11 @@ defmodule BlockScoutWeb.API.V2.SearchView do |> Query.encode() |> URI.decode_query() |> Enum.map(fn {k, v} -> - {k, unless(v == "", do: v)} + {k, if(v != "", do: v)} end) |> Enum.into(%{}) - unless result == %{} do + if result != %{} do result end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex index 14176e656a6b..c5f2589f8279 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stability_view.ex @@ -89,7 +89,7 @@ defmodule BlockScoutWeb.API.V2.StabilityView do defp do_extend_with_stability_fees_info(transactions) when is_list(transactions) do {transactions, _tokens_acc} = - Enum.map_reduce(transactions, %{}, fn transaction, tokens_acc -> + Enum.map_reduce(transactions, %{}, fn transaction = %Transaction{}, tokens_acc -> case Log.fetch_log_by_transaction_hash_and_first_topic( transaction.hash, @transaction_fee_event_signature, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex index ecbc419cc542..ee02d462a88a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex @@ -5,11 +5,10 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] if @chain_type == :zilliqa do - # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release - import Explorer.Chain.Zilliqa.Helper, only: [scilla_transaction?: 1], warn: false - alias Ecto.Association.NotLoaded, warn: false - alias Explorer.Chain.{Address, Block, Transaction}, warn: false - alias Explorer.Chain.Zilliqa.{AggregateQuorumCertificate, QuorumCertificate}, warn: false + import Explorer.Chain.Zilliqa.Helper, only: [scilla_transaction?: 1] + alias Ecto.Association.NotLoaded + alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.Zilliqa.{AggregateQuorumCertificate, QuorumCertificate} @doc """ Extends the JSON output with a sub-map containing information related to Zilliqa, diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 596715881071..2f22397dec21 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -14,14 +14,10 @@ defmodule BlockScoutWeb.Mixfile do plt_add_deps: :app_tree, ignore_warnings: "../../.dialyzer_ignore.exs" ], - elixir: "~> 1.17", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env(), Application.get_env(:block_scout_web, :disable_api?)), lockfile: "../../mix.lock", package: package(), - preferred_cli_env: [ - credo: :test, - dialyzer: :test - ], start_permanent: Mix.env() == :prod, version: "9.2.2", xref: [ @@ -48,6 +44,10 @@ defmodule BlockScoutWeb.Mixfile do ] end + def cli do + [preferred_envs: [credo: :test, dialyzer: :test]] + end + # Specifies which paths to compile per environment. defp elixirc_paths(:test, _), do: ["test/support", "test/block_scout_web/features/pages", "benchmarks/support"] ++ elixirc_paths() @@ -136,8 +136,6 @@ defmodule BlockScoutWeb.Mixfile do {:poison, "~> 5.0.0"}, {:postgrex, ">= 0.0.0"}, {:prometheus, "~> 6.0", override: true}, - # Gather methods for Phoenix requests - {:prometheus_phoenix, "~> 1.2"}, # Expose metrics from URL Prometheus server can scrape {:prometheus_plugs, "~> 1.1"}, # OS process metrics for Prometheus, custom ref to include https://github.com/deadtrickster/prometheus_process_collector/pull/30 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index e9fdaf5817a6..c33e9560f3ce 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -508,7 +508,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end test "search for a big positive integer", %{conn: conn} do - big_integer = :math.pow(2, 64) |> round |> :erlang.integer_to_binary() + big_integer = :math.pow(2, 64) |> round() |> :erlang.integer_to_binary() request = get(conn, "/api/v2/search?q=#{big_integer}") assert response = json_response(request, 200) @@ -517,7 +517,7 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do end test "search for a big negative integer", %{conn: conn} do - big_integer = (:math.pow(2, 64) - 1) |> round |> :erlang.integer_to_binary() + big_integer = (:math.pow(2, 64) - 1) |> round() |> :erlang.integer_to_binary() request = get(conn, "/api/v2/search?q=#{big_integer}") assert response = json_response(request, 200) diff --git a/apps/block_scout_web/test/support/channel_case.ex b/apps/block_scout_web/test/support/channel_case.ex index c0a7f4939a5b..386ccdc6596d 100644 --- a/apps/block_scout_web/test/support/channel_case.ex +++ b/apps/block_scout_web/test/support/channel_case.ex @@ -31,7 +31,7 @@ defmodule BlockScoutWeb.ChannelCase do setup tags do _ = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) - unless tags[:async] do + if !tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) end diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex index fc86ff109701..3692914d9e2f 100644 --- a/apps/block_scout_web/test/support/conn_case.ex +++ b/apps/block_scout_web/test/support/conn_case.ex @@ -41,7 +41,7 @@ defmodule BlockScoutWeb.ConnCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) - unless tags[:async] do + if !tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) end diff --git a/apps/block_scout_web/test/support/feature_case.ex b/apps/block_scout_web/test/support/feature_case.ex index f6b1aedf2412..a3b811f1d79b 100644 --- a/apps/block_scout_web/test/support/feature_case.ex +++ b/apps/block_scout_web/test/support/feature_case.ex @@ -23,7 +23,7 @@ defmodule BlockScoutWeb.FeatureCase do setup tags do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo) - unless tags[:async] do + if !tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index d191b38bfaa2..25b4c289c1e4 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -313,7 +313,11 @@ defmodule EthereumJSONRPC.Geth do FetchedCode.request(%{id: id, block_quantity: integer_to_quantity(block_number), address: address}) {id, %{type: "selfdestruct", from_address_hash: hash_data, block_number: block_number}} -> - FetchedBalance.request(%{id: id, block_quantity: integer_to_quantity(block_number), hash_data: hash_data}) + FetchedBalance.request(%{ + id: id, + block_quantity: integer_to_quantity(block_number), + hash_data: hash_data + }) _ -> nil @@ -441,7 +445,7 @@ defmodule EthereumJSONRPC.Geth do ) "" -> - unless allow_empty_traces?(), do: log_unknown_type(call) + if !allow_empty_traces?(), do: log_unknown_type(call) acc _unknown_type -> @@ -451,7 +455,7 @@ defmodule EthereumJSONRPC.Geth do end defp parse_call_tracer_calls({%{} = call, _}, acc, _trace_address, _inner?) do - unless allow_empty_traces?(), do: log_unknown_type(call) + if !allow_empty_traces?(), do: log_unknown_type(call) acc end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts/by_block_number.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts/by_block_number.ex index 0207976aaa68..c44588ecf45c 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts/by_block_number.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts/by_block_number.ex @@ -19,7 +19,10 @@ defmodule EthereumJSONRPC.Receipts.ByBlockNumber do ## Returns - A JSON-RPC request map for `eth_getBlockReceipts` """ - @spec request(%{id: EthereumJSONRPC.request_id(), number: EthereumJSONRPC.block_number() | EthereumJSONRPC.quantity()}) :: + @spec request(%{ + id: EthereumJSONRPC.request_id(), + number: EthereumJSONRPC.block_number() | EthereumJSONRPC.quantity() + }) :: Transport.request() def request(%{id: id, number: number}) when is_integer(number) do block_number = integer_to_quantity(number) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rolling_window.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rolling_window.ex index 65657e7b92c5..3ec70dc966a5 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rolling_window.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/rolling_window.ex @@ -45,7 +45,7 @@ defmodule EthereumJSONRPC.RollingWindow do duration = Keyword.fetch!(opts, :duration) window_count = Keyword.fetch!(opts, :window_count) - unless rem(duration, window_count) == 0 do + if rem(duration, window_count) != 0 do raise ArgumentError, "duration must be evenly divisible by window_count" end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client.ex index 4098e5e42a1c..56684ea575e1 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/web_socket/web_socket_client.ex @@ -92,7 +92,7 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClient do def handle_connect(_conn, state) do Logger.metadata(fetcher: :websocket_client) - unless state.fallback? do + if !state.fallback? do RetryWorker.deactivate() WebSocketSupervisor.stop_other_client(self()) end @@ -447,7 +447,7 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClient do request: %{params: [event | params]} }, %{"result" => subscription_id}, - %{ + %__MODULE__{ subscription_id_to_subscription_reference: subscription_id_to_subscription_reference, subscription_reference_to_subscription: subscription_reference_to_subscription, subscription_reference_to_subscription_id: subscription_reference_to_subscription_id, @@ -522,7 +522,7 @@ defmodule EthereumJSONRPC.WebSocket.WebSocketClient do request: %{method: "eth_unsubscribe", params: [subscription_id]} }, response, - %{ + %__MODULE__{ subscription_id_to_subscription_reference: subscription_id_to_subscription_reference, subscription_reference_to_subscription: subscription_reference_to_subscription, subscription_reference_to_subscription_id: subscription_reference_to_subscription_id diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 670013bec089..4c5a080c17b4 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -15,13 +15,9 @@ defmodule EthereumJSONRPC.MixProject do plt_add_apps: [:mix], ignore_warnings: "../../.dialyzer_ignore.exs" ], - elixir: "~> 1.17", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", - preferred_cli_env: [ - credo: :test, - dialyzer: :test - ], start_permanent: Mix.env() == :prod, version: "9.2.2" ] @@ -35,6 +31,10 @@ defmodule EthereumJSONRPC.MixProject do ] end + def cli do + [preferred_envs: [credo: :test, dialyzer: :test]] + end + defp aliases(env) do [ # to match behavior of `mix test` from project root, which needs to not start applications for `indexer` to diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex index 79231f4dc9f6..07196fc774d5 100644 --- a/apps/explorer/lib/explorer/account/identity.ex +++ b/apps/explorer/lib/explorer/account/identity.ex @@ -75,7 +75,7 @@ defmodule Explorer.Account.Identity do - An updated Identity struct with populated virtual fields. """ @spec put_session_info(t(), session()) :: t() - def put_session_info(identity, %{name: name, nickname: nickname, address_hash: address_hash}) do + def put_session_info(%__MODULE__{} = identity, %{name: name, nickname: nickname, address_hash: address_hash}) do %__MODULE__{ identity | name: name, @@ -84,7 +84,7 @@ defmodule Explorer.Account.Identity do } end - def put_session_info(identity, %{name: name, nickname: nickname}) do + def put_session_info(%__MODULE__{} = identity, %{name: name, nickname: nickname}) do %__MODULE__{ identity | name: name, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 213d12360c3d..8b20286ef082 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -56,7 +56,9 @@ defmodule Explorer.Application do Explorer.Repo.Replica1, Explorer.Vault, Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer), - Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor}, id: Explorer.HistoryTaskSupervisor), + Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor}, + id: Explorer.HistoryTaskSupervisor + ), Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor), Supervisor.child_spec({Task.Supervisor, name: Explorer.GenesisDataTaskSupervisor}, id: GenesisDataTaskSupervisor), Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor), diff --git a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex index 8826cd3a7d5f..d6185dc76879 100644 --- a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex +++ b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex @@ -236,7 +236,7 @@ defmodule Explorer.Arbitrum.ClaimRollupMessage do } else Logger.error( - "message_to_withdrawal: log doesn't correspond message (#{fields.position} != #{message.message_id})" + "message_to_withdrawal: log doesn't correspond message (#{fields.message_id} != #{message.message_id})" ) nil diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index aabe1dc5b280..c43975016ea4 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3985,7 +3985,7 @@ defmodule Explorer.Chain do |> Enum.reverse() end - defp flat_1155_batch_token_transfer(tt, amounts, token_ids, token_id_to_filter) do + defp flat_1155_batch_token_transfer(%TokenTransfer{} = tt, amounts, token_ids, token_id_to_filter) do amounts |> Enum.zip(token_ids) |> Enum.with_index() @@ -4009,7 +4009,7 @@ defmodule Explorer.Chain do transfer end - defp group_batch_reducer(transfer, acc) do + defp group_batch_reducer(transfer, %TokenTransfer{} = acc) do %TokenTransfer{acc | amount: Decimal.add(acc.amount, transfer.amount)} end diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 85ca17fef299..bb3b0775a5b1 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -475,7 +475,7 @@ defmodule Explorer.Chain.Address do For more information: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md#specification To bypass the checksum formatting, use `to_string/1` on the hash itself. - #{unless @chain_type == :rsk do + #{if @chain_type != :rsk do """ iex> address = %Explorer.Chain.Address{ ...> hash: %Explorer.Chain.Hash{ diff --git a/apps/explorer/lib/explorer/chain/address/coin_balance.ex b/apps/explorer/lib/explorer/chain/address/coin_balance.ex index ce88b1a00b87..79075d16e2db 100644 --- a/apps/explorer/lib/explorer/chain/address/coin_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/coin_balance.ex @@ -324,7 +324,7 @@ defmodule Explorer.Chain.Address.CoinBalance do |> limit(1) end - defp put_transaction_hash(hash, coin_balance), + defp put_transaction_hash(hash, %CoinBalance{} = coin_balance), do: if(hash, do: %CoinBalance{coin_balance | transaction_hash: hash}, else: coin_balance) defp add_block_timestamp_to_balances( diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index a692f5d7af62..566dbaa0957d 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -103,7 +103,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do offset = (max(paging_options.page_number, 1) - 1) * paging_options.page_size token_contract_address_hash - |> token_holders_query + |> token_holders_query() |> order_by([tb], desc: :value, desc: :address_hash) |> Chain.page_token_balances(paging_options) |> limit(^paging_options.page_size) diff --git a/apps/explorer/lib/explorer/chain/cache/latest_l1_block_number.ex b/apps/explorer/lib/explorer/chain/cache/latest_l1_block_number.ex index d7d675b0eef3..4c717f744891 100644 --- a/apps/explorer/lib/explorer/chain/cache/latest_l1_block_number.ex +++ b/apps/explorer/lib/explorer/chain/cache/latest_l1_block_number.ex @@ -11,5 +11,7 @@ defmodule Explorer.Chain.Cache.LatestL1BlockNumber do @dialyzer :no_match + defp handle_fallback(:update), do: {:update, nil} + defp handle_fallback(_key), do: {:return, nil} end diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 0ef47726521b..1bf5d6f865cc 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -35,7 +35,7 @@ defmodule Explorer.Chain.ContractMethod do end end) - unless Enum.empty?(errors) do + if !Enum.empty?(errors) do Logger.error(fn -> ["Error parsing some abi elements at ", Hash.to_iodata(address_hash), ": ", Enum.intersperse(errors, "\n")] end) diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 9fce0a8fb796..66c69aa00e55 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -343,7 +343,7 @@ defmodule Explorer.Chain.Import do end) end) - unless Enum.empty?(final_runner_to_changes_list) do + if !Enum.empty?(final_runner_to_changes_list) do raise ArgumentError, "No stages consumed the following runners: #{final_runner_to_changes_list |> Map.keys() |> inspect()}" end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 0fa068a8505c..cc84d79fb231 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -52,114 +52,121 @@ defmodule Explorer.Chain.Search do @label_sorting [{:asc, :display_name, :address_tag}, {:desc, :inserted_at, :address_to_tag}] + defp search_result(nil, _paging_options, _query_string, _options), do: {[], nil} + + defp search_result({:address_hash, address_hash}, paging_options, query_string, options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) + + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) + + trim_list_and_prepare_next_page_params( + addresses_and_tokens ++ tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) + end + + defp search_result({:ton_address, _ton_address}, paging_options, query_string, _options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) + + trim_list_and_prepare_next_page_params( + tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) + end + + if @chain_type == :filecoin do + defp search_result({:filecoin, filecoin_address}, _paging_options, _query_string, options) do + {filecoin_address + |> address_by_filecoin_id_or_robust() + |> select_repo(options).all(), nil} + end + end + + defp search_result({:full_hash, full_hash}, paging_options, query_string, options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + results = full_hash_search_if_first_page(paging_options, full_hash, options) + + %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = + await_task_with_paging(tac_operation_task) + + trim_list_and_prepare_next_page_params( + results ++ tac_operation_results, + paging_options, + query_string, + %{}, + !is_nil(tac_operation_next_page_params) + ) + end + + defp search_result({:number, block_number}, _paging_options, _query_string, options) do + {block_number + |> search_block_by_number_query() + |> select_repo(options).all(), nil} + end + + defp search_result([{:number, block_number}, {:text, prepared_term}], paging_options, query_string, options) do + prepared_term + |> search_by_string(paging_options, [], options) + |> union_all(^search_block_by_number_query(block_number)) + |> order_and_page_text_search_result(paging_options) + |> select_repo(options).all() + |> trim_list_and_prepare_next_page_params(paging_options, query_string, %{}, false) + end + + defp search_result({:text, prepared_term}, paging_options, query_string, options) do + ens_task = run_ens_task_if_first_page(paging_options, query_string, options) + + %{items: metadata_tags, next_page_params: metadata_next_page_params} = + maybe_fetch_metadata_tags( + query_string, + parse_possible_nil(paging_options.key["metadata_tag"]["metadata_next_page_params"]), + ExplorerHelper.parse_boolean(paging_options.key["metadata_tag"]["end_of_tags"]) + ) + + paginated_metadata_tags = page_metadata_tags(metadata_tags, paging_options) + + items = + prepared_term + |> search_by_string(paging_options, paginated_metadata_tags, options) + |> order_and_page_text_search_result(paging_options) + |> select_repo(options).all() + + ens_result = (ens_task && await_task(ens_task)) || [] + + (ens_result ++ items) + |> trim_list_and_prepare_next_page_params( + paging_options, + query_string, + %{ + metadata_next_page_params: metadata_next_page_params + }, + !is_nil(metadata_next_page_params) + ) + end + @doc """ Search function used in web interface and API v2. Returns paginated search results """ @spec joint_search(PagingOptions.t(), binary(), [Chain.api?() | Chain.show_scam_tokens?()]) :: {list(), map() | nil} - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity def joint_search(paging_options, query_string, options \\ []) do query_string = String.trim(query_string) {search_results, next_page_params} = query_string |> prepare_search_query(prepare_search_term(query_string)) - |> case do - nil -> - {[], nil} - - {:address_hash, address_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - - addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) - - %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = - await_task_with_paging(tac_operation_task) - - trim_list_and_prepare_next_page_params( - addresses_and_tokens ++ tac_operation_results, - paging_options, - query_string, - %{}, - !is_nil(tac_operation_next_page_params) - ) - - {:ton_address, _ton_address} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - - %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = - await_task_with_paging(tac_operation_task) - - trim_list_and_prepare_next_page_params( - tac_operation_results, - paging_options, - query_string, - %{}, - !is_nil(tac_operation_next_page_params) - ) - - {:filecoin, filecoin_address} -> - {filecoin_address - |> address_by_filecoin_id_or_robust() - |> select_repo(options).all(), nil} - - {:full_hash, full_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - results = full_hash_search_if_first_page(paging_options, full_hash, options) - - %{items: tac_operation_results, next_page_params: tac_operation_next_page_params} = - await_task_with_paging(tac_operation_task) - - trim_list_and_prepare_next_page_params( - results ++ tac_operation_results, - paging_options, - query_string, - %{}, - !is_nil(tac_operation_next_page_params) - ) - - {:number, block_number} -> - {block_number - |> search_block_by_number_query() - |> select_repo(options).all(), nil} - - [{:number, block_number}, {:text, prepared_term}] -> - prepared_term - |> search_by_string(paging_options, [], options) - |> union_all(^search_block_by_number_query(block_number)) - |> order_and_page_text_search_result(paging_options) - |> select_repo(options).all() - |> trim_list_and_prepare_next_page_params(paging_options, query_string, %{}, false) - - {:text, prepared_term} -> - ens_task = run_ens_task_if_first_page(paging_options, query_string, options) - - %{items: metadata_tags, next_page_params: metadata_next_page_params} = - maybe_fetch_metadata_tags( - query_string, - parse_possible_nil(paging_options.key["metadata_tag"]["metadata_next_page_params"]), - ExplorerHelper.parse_boolean(paging_options.key["metadata_tag"]["end_of_tags"]) - ) - - paginated_metadata_tags = page_metadata_tags(metadata_tags, paging_options) - - items = - prepared_term - |> search_by_string(paging_options, paginated_metadata_tags, options) - |> order_and_page_text_search_result(paging_options) - |> select_repo(options).all() - - ens_result = (ens_task && await_task(ens_task)) || [] - - (ens_result ++ items) - |> trim_list_and_prepare_next_page_params( - paging_options, - query_string, - %{ - metadata_next_page_params: metadata_next_page_params - }, - !is_nil(metadata_next_page_params) - ) - end + |> search_result(paging_options, query_string, options) prepared_results = search_results @@ -252,54 +259,65 @@ defmodule Explorer.Chain.Search do end end - @spec prepare_search_query(binary(), {:some, binary()} | :none) :: + defp prepare_results(query) do + %{ + address_hash_result: Chain.string_to_address_hash(query), + ton_address_result: Ton.parse_address(query), + filecoin_address_result: maybe_parse_filecoin_address(query), + full_hash_result: Chain.string_to_full_hash(query), + non_negative_integer_result: ExplorerHelper.safe_parse_non_negative_integer(query), + query_length: String.length(query) + } + end + + @type base_search_results :: {:address_hash, Hash.Address.t()} | {:ton_address, Ton.Address.t()} - | {:filecoin, any()} | {:full_hash, Hash.t()} | {:number, non_neg_integer()} | [{:number, non_neg_integer()}, {:text, binary()}] | {:text, binary()} | nil - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity - defp prepare_search_query(query, {:some, prepared_term}) do - address_hash_result = Chain.string_to_address_hash(query) - ton_address_result = Ton.parse_address(query) - filecoin_address_result = maybe_parse_filecoin_address(query) - full_hash_result = Chain.string_to_full_hash(query) - non_negative_integer_result = ExplorerHelper.safe_parse_non_negative_integer(query) - query_length = String.length(query) - - cond do - match?({:ok, _hash}, address_hash_result) -> - {:ok, hash} = address_hash_result - {:address_hash, hash} - - match?({:ok, _address}, ton_address_result) -> - {:ok, ton_address} = ton_address_result - {:ton_address, ton_address} - - match?({:ok, _address}, filecoin_address_result) -> - {:ok, filecoin_address} = filecoin_address_result - {:filecoin, filecoin_address} - - match?({:ok, _hash}, full_hash_result) -> - {:ok, hash} = full_hash_result - {:full_hash, hash} - - match?({:ok, _block_number}, non_negative_integer_result) and query_length < @min_query_length -> - {:ok, block_number} = non_negative_integer_result - {:number, block_number} - - match?({:ok, _block_number}, non_negative_integer_result) and query_length >= @min_query_length -> - {:ok, block_number} = non_negative_integer_result - [{:number, block_number}, {:text, prepared_term}] - - query_length >= @min_query_length -> - {:text, prepared_term} - - true -> - nil + + @spec match_search_result(map(), binary()) :: base_search_results() + defp match_search_result(%{address_hash_result: {:ok, address_hash}}, _prepared_term), + do: {:address_hash, address_hash} + + defp match_search_result(%{ton_address_result: {:ok, ton_address}}, _prepared_term), do: {:ton_address, ton_address} + + if @chain_type == :filecoin do + defp match_search_result(%{filecoin_address_result: {:ok, filecoin_address}}, _prepared_term), + do: {:filecoin, filecoin_address} + end + + defp match_search_result(%{full_hash_result: {:ok, hash}}, _prepared_term), do: {:full_hash, hash} + + defp match_search_result(%{non_negative_integer_result: {:ok, block_number}, query_length: ql}, _prepared_term) + when ql < @min_query_length, + do: {:number, block_number} + + defp match_search_result(%{non_negative_integer_result: {:ok, block_number}, query_length: ql}, prepared_term) + when ql >= @min_query_length, + do: [{:number, block_number}, {:text, prepared_term}] + + defp match_search_result(%{query_length: ql}, prepared_term) when ql >= @min_query_length, + do: {:text, prepared_term} + + defp match_search_result(_, _), do: nil + + if @chain_type == :filecoin do + @spec prepare_search_query(binary(), {:some, binary()} | :none) :: base_search_results() | {:filecoin, any()} + defp prepare_search_query(query, {:some, prepared_term}) do + results = prepare_results(query) + + match_search_result(results, prepared_term) + end + else + @spec prepare_search_query(binary(), {:some, binary()} | :none) :: base_search_results() + defp prepare_search_query(query, {:some, prepared_term}) do + results = prepare_results(query) + + match_search_result(results, prepared_term) end end @@ -337,6 +355,75 @@ defmodule Explorer.Chain.Search do defp run_ens_task_if_first_page(_, _query_string, _options), do: nil + @spec balanced_unpaginated_search_result( + base_search_results(), + PagingOptions.t(), + binary(), + [Chain.api?() | Chain.show_scam_tokens?()] + ) :: list() + defp balanced_unpaginated_search_result(nil, _paging_options, _query_string, _options), do: [] + + defp balanced_unpaginated_search_result({:address_hash, address_hash}, paging_options, query_string, options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) + + %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) + + [addresses_and_tokens, tac_operation_results] + end + + defp balanced_unpaginated_search_result({:ton_address, _ton_address}, paging_options, query_string, _options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + + %{items: tac_operation_results} = + await_task_with_paging(tac_operation_task) + + [tac_operation_results] + end + + if @chain_type == :filecoin do + defp balanced_unpaginated_search_result({:filecoin, filecoin_address}, _paging_options, _query_string, options) do + [ + filecoin_address + |> address_by_filecoin_id_or_robust() + |> select_repo(options).all() + ] + end + end + + defp balanced_unpaginated_search_result({:full_hash, full_hash}, paging_options, query_string, options) do + tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) + results = full_hash_search_if_first_page(paging_options, full_hash, options) + %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) + + [results, tac_operation_results] + end + + defp balanced_unpaginated_search_result({:number, block_number}, _paging_options, _query_string, options) do + [ + block_number + |> search_block_by_number_query() + |> select_repo(options).all() + ] + end + + defp balanced_unpaginated_search_result( + [{:number, block_number}, {:text, prepared_term}], + paging_options, + _query_string, + options + ) do + [ + block_number |> search_block_by_number_query() |> select_repo(options).all() + | search_by_string_balanced(prepared_term, paging_options, options, nil) + ] + end + + defp balanced_unpaginated_search_result({:text, prepared_term}, paging_options, query_string, options) do + search_by_string_balanced(prepared_term, paging_options, options, query_string) + end + @doc """ Search function. Differences from joint_search/4: 1. Returns all the found categories (amount of results up to `paging_options.page_size`). @@ -347,7 +434,6 @@ defmodule Explorer.Chain.Search do `balanced_unpaginated_search` function is used at api/v2/search/quick endpoint. """ @spec balanced_unpaginated_search(PagingOptions.t(), binary(), [Chain.api?() | Chain.show_scam_tokens?()]) :: list - # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity def balanced_unpaginated_search(paging_options, query_string, options \\ []) do query_string = String.trim(query_string) ens_task = Task.async(fn -> search_ens_name(query_string, options) end) @@ -355,57 +441,7 @@ defmodule Explorer.Chain.Search do results = query_string |> prepare_search_query(prepare_search_term(query_string)) - |> case do - nil -> - [] - - {:address_hash, address_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - - addresses_and_tokens = address_hash_search_if_first_page(paging_options, address_hash, options) - - %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) - - [addresses_and_tokens, tac_operation_results] - - {:ton_address, _ton_address} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - - %{items: tac_operation_results} = - await_task_with_paging(tac_operation_task) - - [tac_operation_results] - - {:filecoin, filecoin_address} -> - [ - filecoin_address - |> address_by_filecoin_id_or_robust() - |> select_repo(options).all() - ] - - {:full_hash, full_hash} -> - tac_operation_task = Task.async(fn -> search_tac_operations(query_string, paging_options) end) - results = full_hash_search_if_first_page(paging_options, full_hash, options) - %{items: tac_operation_results} = await_task_with_paging(tac_operation_task) - - [results, tac_operation_results] - - {:number, block_number} -> - [ - block_number - |> search_block_by_number_query() - |> select_repo(options).all() - ] - - [{:number, block_number}, {:text, prepared_term}] -> - [ - block_number |> search_block_by_number_query() |> select_repo(options).all() - | search_by_string_balanced(prepared_term, paging_options, options, nil) - ] - - {:text, prepared_term} -> - search_by_string_balanced(prepared_term, paging_options, options, query_string) - end + |> balanced_unpaginated_search_result(paging_options, query_string, options) ens_result = await_task(ens_task) diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex index c2eee52fdc9b..1c4282eb8b67 100644 --- a/apps/explorer/lib/explorer/chain/stability/validator.ex +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -157,7 +157,9 @@ defmodule Explorer.Chain.Stability.Validator do result = case format_missing_blocks_result(response) do {:error, message} -> - Logger.warning(fn -> ["Error on getValidatorMissingBlocks for #{validators_address_hashes}: #{message}"] end) + Logger.warning(fn -> + ["Error on getValidatorMissingBlocks for #{address_hash}: #{message}"] + end) nil diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 278bb2ec6ee0..4bf0b224f404 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -680,7 +680,7 @@ defmodule Explorer.Chain.Token.Instance do is_unique is true for ERC-721 always and for ERC-1155 only if token_id is unique """ @spec put_is_unique(__MODULE__.t(), Token.t(), Keyword.t()) :: __MODULE__.t() - def put_is_unique(instance, token, options) do + def put_is_unique(%__MODULE__{} = instance, token, options) do %__MODULE__{instance | is_unique: unique?(instance, token, options)} end diff --git a/apps/explorer/lib/explorer/market/fetcher/coin.ex b/apps/explorer/lib/explorer/market/fetcher/coin.ex index 2d66dd93c47d..618626b6d057 100644 --- a/apps/explorer/lib/explorer/market/fetcher/coin.ex +++ b/apps/explorer/lib/explorer/market/fetcher/coin.ex @@ -75,7 +75,7 @@ defmodule Explorer.Market.Fetcher.Coin do :ets.insert(table_name(), {secondary_coin?, coin}) end - unless secondary_coin? do + if !secondary_coin? do broadcast_event(:exchange_rate) end diff --git a/apps/explorer/lib/explorer/migrator/filling_migration.ex b/apps/explorer/lib/explorer/migrator/filling_migration.ex index c2cf7ecc0584..a3b5a5fadc14 100644 --- a/apps/explorer/lib/explorer/migrator/filling_migration.ex +++ b/apps/explorer/lib/explorer/migrator/filling_migration.ex @@ -259,7 +259,7 @@ defmodule Explorer.Migrator.FillingMigration do |> Task.await_many(:infinity) unquote do - unless opts[:skip_meta_update?] do + if !opts[:skip_meta_update?] do quote do MigrationStatus.update_meta(migration_name(), new_state) end diff --git a/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex index 7a0cb767439f..573080488000 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_blocks_with_missing_transactions.ex @@ -68,7 +68,7 @@ defmodule Explorer.Migrator.ReindexBlocksWithMissingTransactions do case EthereumJSONRPC.fetch_transactions_count(consensus_block_numbers, json_rpc_named_arguments) do {:ok, %{transactions_count_map: node_transactions_count_map, errors: errors}} -> - unless Enum.empty?(errors) do + if !Enum.empty?(errors) do Logger.warning("Migration #{@migration_name} encountered errors fetching blocks: #{inspect(errors)}") end diff --git a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex index 732dd462eea9..b489fc469247 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_duplicated_internal_transactions.ex @@ -129,7 +129,7 @@ defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactions do case result do {:ok, inserted_pbo} -> - unless is_nil(Process.whereis(InternalTransactionFetcher)) do + if not is_nil(Process.whereis(InternalTransactionFetcher)) do inserted_pbo |> Enum.map(& &1.block_number) |> InternalTransactionFetcher.async_fetch([], false) diff --git a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex index 96e18ac95f78..5fabaa814e8d 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex @@ -104,7 +104,7 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus do Repo.insert_all(PendingTransactionOperation, params, on_conflict: :nothing, returning: [:transaction_hash]) end - unless is_nil(Process.whereis(InternalTransactionFetcher)) do + if not is_nil(Process.whereis(InternalTransactionFetcher)) do {block_numbers, transactions} = case pending_operations_type do "blocks" -> diff --git a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex index 5f68da48e067..3b76d207454b 100644 --- a/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex +++ b/apps/explorer/lib/explorer/migrator/sanitize_duplicated_log_index_logs.ex @@ -71,7 +71,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do {ids, logs, ids_to_new_index} = logs_to_update - |> Enum.reduce({[], [], %{}}, fn {log, new_index}, {ids, logs, ids_to_new_index} -> + |> Enum.reduce({[], [], %{}}, fn {%Log{} = log, new_index}, {ids, logs, ids_to_new_index} -> id = {log.transaction_hash, log.block_hash, log.index} {[id | ids], @@ -106,7 +106,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do Repo.insert_all(Log, logs, timeout: :infinity) token_transfers - |> Enum.map(fn token_transfer -> + |> Enum.map(fn %TokenTransfer{} = token_transfer -> id = token_transfer_to_index(token_transfer) %TokenTransfer{token_transfer | log_index: ids_to_new_index[id]} @@ -148,7 +148,7 @@ defmodule Explorer.Migrator.SanitizeDuplicatedLogIndexLogs do ^QueryHelper.tuple_in([:owner_updated_at_block, :owner_updated_at_log_index], nft_instances_params) ) |> Repo.all(timeout: :infinity) - |> Enum.map(fn nft -> + |> Enum.map(fn %Instance{} = nft -> %Instance{ nft | owner_updated_at_log_index: nft_updates_map[{nft.owner_updated_at_block, nft.owner_updated_at_log_index}] diff --git a/apps/explorer/lib/explorer/tags/address_to_tag.ex b/apps/explorer/lib/explorer/tags/address_to_tag.ex index 23402a970958..b78facceb5bc 100644 --- a/apps/explorer/lib/explorer/tags/address_to_tag.ex +++ b/apps/explorer/lib/explorer/tags/address_to_tag.ex @@ -106,7 +106,7 @@ defmodule Explorer.Tags.AddressToTag do end) |> Enum.filter(&(!is_nil(&1))) - unless Enum.empty?(addresses_to_delete) do + if !Enum.empty?(addresses_to_delete) do delete_query_base = from( att in __MODULE__, diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex index ee8face6d66f..c16e50fe50ee 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/legacy.ex @@ -177,11 +177,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do defp maybe_verify_email(_), do: :ok defp merge_email_users([primary_user | _] = users, identity_id_to_link, provider_for_linking) do - identity_map = - users - |> Enum.map(& &1["user_id"]) - |> Identity.find_identities() - |> Map.new(&{&1.uid, &1}) + identity_map = get_identity_map(users) users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() @@ -203,11 +199,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do end defp merge_web3_users([primary_user | _] = users) do - identity_map = - users - |> Enum.map(& &1["user_id"]) - |> Identity.find_identities() - |> Map.new(&{&1.uid, &1}) + identity_map = get_identity_map(users) users_map = users |> Enum.map(&{&1["user_id"], &1}) |> Map.new() @@ -217,7 +209,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do |> Account.merge() |> case do {{:ok, 0}, nil} -> - unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user) do + if !match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user) do update_user_with_web3_address( primary_user["user_id"], primary_user |> Internal.create_auth() |> Identity.address_hash_from_auth() @@ -229,7 +221,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do {{:ok, _}, primary_identity} -> primary_user_from_db = users_map[primary_identity.uid] - unless match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user_from_db) do + if !match?(%{"user_metadata" => %{"web3_address_hash" => _}}, primary_user_from_db) do update_user_with_web3_address( primary_user_from_db["user_id"], primary_user_from_db |> Internal.create_auth() |> Identity.address_hash_from_auth() @@ -243,4 +235,11 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Legacy do :error end end + + defp get_identity_map(users) do + users + |> Enum.map(& &1["user_id"]) + |> Identity.find_identities() + |> Map.new(&{&1.uid, &1}) + end end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 4c9836efd7b2..70a3761fab13 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -389,8 +389,8 @@ defmodule Explorer.Token.MetadataRetriever do contract_functions |> handle_invalid_strings(contract_address_hash) - |> handle_large_strings - |> limit_decimals + |> handle_large_strings() + |> limit_decimals() end defp atomized_key(@name_signature), do: :name diff --git a/apps/explorer/lib/explorer/validator/metadata_retriever.ex b/apps/explorer/lib/explorer/validator/metadata_retriever.ex index 10794167b13c..fb3f8acedde4 100644 --- a/apps/explorer/lib/explorer/validator/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/validator/metadata_retriever.ex @@ -9,8 +9,8 @@ defmodule Explorer.Validator.MetadataRetriever do fetch_validators_list() |> Enum.map(fn validator -> validator - |> fetch_validator_metadata - |> translate_metadata + |> fetch_validator_metadata() + |> translate_metadata() |> Map.merge(%{address_hash: validator, primary: true}) end) end diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index de2a060509b8..15e11769dba2 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -15,14 +15,10 @@ defmodule Explorer.Mixfile do plt_add_apps: ~w(ex_unit mix)a, ignore_warnings: "../../.dialyzer_ignore.exs" ], - elixir: "~> 1.17", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", package: package(), - preferred_cli_env: [ - credo: :test, - dialyzer: :test - ], start_permanent: Mix.env() == :prod, version: "9.2.2", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] @@ -39,6 +35,10 @@ defmodule Explorer.Mixfile do ] end + def cli do + [preferred_envs: [credo: :test, dialyzer: :test]] + end + # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["test/support" | elixirc_paths()] defp elixirc_paths(_), do: elixirc_paths() diff --git a/apps/explorer/test/explorer/account/identity_test.exs b/apps/explorer/test/explorer/account/identity_test.exs index 1acde2e96607..2bcb06708a12 100644 --- a/apps/explorer/test/explorer/account/identity_test.exs +++ b/apps/explorer/test/explorer/account/identity_test.exs @@ -34,13 +34,13 @@ defmodule Explorer.Account.IdentityTest do id: identity_id, email: "john@blockscout.com", uid: "github|666666" - } = Identity |> first |> Repo.account_repo().one() + } = Identity |> first() |> Repo.account_repo().one() %{ id: watchlist_id, identity_id: ^identity_id, name: "default" - } = Watchlist |> first |> Repo.account_repo().one() + } = Watchlist |> first() |> Repo.account_repo().one() assert {:ok, %{ @@ -80,13 +80,13 @@ defmodule Explorer.Account.IdentityTest do id: identity_id, email: "john@blockscout.com", uid: "google-oauth2|666666" - } = Identity |> first |> Repo.account_repo().one() + } = Identity |> first() |> Repo.account_repo().one() %{ id: watchlist_id, identity_id: ^identity_id, name: "default" - } = Watchlist |> first |> Repo.account_repo().one() + } = Watchlist |> first() |> Repo.account_repo().one() assert {:ok, %{ diff --git a/apps/explorer/test/explorer/account/notifier/notify_test.exs b/apps/explorer/test/explorer/account/notifier/notify_test.exs index 5968c16dace5..72884d5dde9f 100644 --- a/apps/explorer/test/explorer/account/notifier/notify_test.exs +++ b/apps/explorer/test/explorer/account/notifier/notify_test.exs @@ -44,7 +44,7 @@ defmodule Explorer.Account.Notifier.NotifyTest do wn = WatchlistNotification - |> first + |> first() |> Repo.account_repo().one() assert notify == [[:ok]] @@ -74,7 +74,7 @@ defmodule Explorer.Account.Notifier.NotifyTest do wn = WatchlistNotification - |> first + |> first() |> Repo.account_repo().one() assert notify == [[:ok]] @@ -113,7 +113,7 @@ defmodule Explorer.Account.Notifier.NotifyTest do wn = WatchlistNotification - |> first + |> first() |> Repo.account_repo().one() assert notify == [[:ok]] @@ -137,7 +137,7 @@ defmodule Explorer.Account.Notifier.NotifyTest do Notify.call([transaction]) WatchlistNotification - |> first + |> first() |> Repo.account_repo().one!() Application.put_env(:explorer, Explorer.Account, old_envs) diff --git a/apps/explorer/test/explorer/chain/filecoin/native_address_test.exs b/apps/explorer/test/explorer/chain/filecoin/native_address_test.exs index 22325d39a5ba..c8d2dcaa1966 100644 --- a/apps/explorer/test/explorer/chain/filecoin/native_address_test.exs +++ b/apps/explorer/test/explorer/chain/filecoin/native_address_test.exs @@ -3,8 +3,8 @@ defmodule Explorer.Chain.Filecoin.NativeAddressTest do if @chain_type == :filecoin do use ExUnit.Case, async: true - # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release - alias Explorer.Chain.Hash, warn: false + + alias Explorer.Chain.Hash alias Explorer.Chain.Filecoin.{NativeAddress, IDAddress} doctest NativeAddress diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index dad0d7adfd22..23eb193f9f8b 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -1637,7 +1637,12 @@ defmodule Explorer.Chain.ImportTest do }, blocks: %{ params: [ - params_for(:block, hash: block_hash, consensus: true, miner_hash: miner_hash, number: block_number), + params_for(:block, + hash: block_hash, + consensus: true, + miner_hash: miner_hash, + number: block_number + ), params_for(:block, hash: uncle_hash, consensus: false, diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index bedb9acc8942..f18a07b94097 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -1144,8 +1144,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do assert {:ok_store_uri, %{ metadata: Jason.decode!(json) - }, - url} == + }, url} == MetadataRetriever.fetch_json({:ok, [url]}) end end diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index bd5895bba191..bf94e66cf49a 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -37,7 +37,7 @@ defmodule Explorer.DataCase do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account) :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.EventNotifications) - unless tags[:async] do + if !tags[:async] do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()}) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, {:shared, self()}) diff --git a/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex b/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex index 45a5b7e81b59..bdcdb20efcc1 100644 --- a/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex +++ b/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex @@ -63,7 +63,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisor do {:ok, state} end - defp new(%{block_fetcher: common_block_fetcher} = named_arguments) do + defp new(%{block_fetcher: %Block.Fetcher{} = common_block_fetcher} = named_arguments) do block_fetcher = %Block.Fetcher{common_block_fetcher | broadcast: :catchup, callback_module: Catchup.Fetcher} block_interval = Map.get(named_arguments, :block_interval, @block_interval) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 7d7b289c433d..eb9719f9cf70 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -638,7 +638,7 @@ defmodule Indexer.Block.Fetcher do blocks |> Enum.map(fn block -> fetch_beneficiaries_manual(block, block_transactions_map[block.number] || []) end) - |> Enum.reduce(%FetchedBeneficiaries{}, fn params_set, %{params_set: acc_params_set} = acc -> + |> Enum.reduce(%FetchedBeneficiaries{}, fn params_set, %FetchedBeneficiaries{params_set: acc_params_set} = acc -> %FetchedBeneficiaries{acc | params_set: MapSet.union(acc_params_set, params_set)} end) end diff --git a/apps/indexer/lib/indexer/block/realtime/supervisor.ex b/apps/indexer/lib/indexer/block/realtime/supervisor.ex index 9a3d3b184a56..ea27e3d2c5ee 100644 --- a/apps/indexer/lib/indexer/block/realtime/supervisor.ex +++ b/apps/indexer/lib/indexer/block/realtime/supervisor.ex @@ -14,7 +14,8 @@ defmodule Indexer.Block.Realtime.Supervisor do children = case Keyword.fetch!(subscribe_named_arguments, :transport) do EthereumJSONRPC.WebSocket -> - transport_options = + %EthereumJSONRPC.WebSocket{} = + transport_options = struct!(EthereumJSONRPC.WebSocket, Keyword.fetch!(subscribe_named_arguments, :transport_options)) web_socket = Indexer.Block.Realtime.WebSocket diff --git a/apps/indexer/lib/indexer/buffered_task.ex b/apps/indexer/lib/indexer/buffered_task.ex index e459ade2117e..dbed47071397 100644 --- a/apps/indexer/lib/indexer/buffered_task.ex +++ b/apps/indexer/lib/indexer/buffered_task.ex @@ -718,7 +718,7 @@ defmodule Indexer.BufferedTask do # Handles the successful completion of a task processing queue data, updated the # callback module state, removes the reference to the task, and triggers processing # of the next batch if queue contains data. - def handle_info({ref, {:ok, new_callback_module_state}}, state) do + def handle_info({ref, {:ok, new_callback_module_state}}, %__MODULE__{} = state) do {:noreply, drop_task(%__MODULE__{state | callback_module_state: new_callback_module_state}, ref)} end @@ -745,7 +745,7 @@ defmodule Indexer.BufferedTask do # the next batch is triggered. # If all entries are needed to be retried, the `retryable_entries` should # be `nil`. - def handle_info({ref, {:retry, retryable_entries, new_callback_module_state}}, state) do + def handle_info({ref, {:retry, retryable_entries, new_callback_module_state}}, %__MODULE__{} = state) do Logger.debug("Retrying batch with ref #{inspect(ref)} and specific entries #{inspect(retryable_entries)}") {:noreply, @@ -885,7 +885,7 @@ defmodule Indexer.BufferedTask do # Updated state after removing the task and potentially spawning a new data # portion for processing. @spec drop_task(t(), reference()) :: t() - defp drop_task(state, ref) do + defp drop_task(%__MODULE__{} = state, ref) do spawn_next_batch(%__MODULE__{state | task_ref_to_batch: Map.delete(state.task_ref_to_batch, ref)}) end @@ -942,12 +942,12 @@ defmodule Indexer.BufferedTask do defp buffer_entries(state, [], _front?), do: state @spec buffer_entries(t(), nonempty_list(), true) :: t() - defp buffer_entries(state, entries, true) do + defp buffer_entries(%__MODULE__{} = state, entries, true) do %__MODULE__{state | current_front_buffer: [entries | state.current_front_buffer]} end @spec buffer_entries(t(), nonempty_list(), false) :: t() - defp buffer_entries(state, entries, false) do + defp buffer_entries(%__MODULE__{} = state, entries, false) do %__MODULE__{state | current_buffer: [entries | state.current_buffer]} end @@ -1261,7 +1261,7 @@ defmodule Indexer.BufferedTask do # - The updated state with the new flush_timer if a flush was scheduled, # or the unchanged state if flush_interval is :infinity. @spec schedule_next_buffer_flush(%__MODULE__{flush_interval: timeout() | :infinity}) :: t() - defp schedule_next_buffer_flush(state) do + defp schedule_next_buffer_flush(%__MODULE__{} = state) do if state.flush_interval == :infinity do state else diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/data_backfill.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/data_backfill.ex index 86ef3308d2b5..11cd87de837f 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/data_backfill.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/data_backfill.ex @@ -58,7 +58,7 @@ defmodule Indexer.Fetcher.Arbitrum.DataBackfill do def child_spec([init_options, gen_server_options]) do {json_rpc_named_arguments, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless json_rpc_named_arguments do + if !json_rpc_named_arguments do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec` " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex index 0fddd4f30b2f..c7552ccd9afe 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex @@ -348,7 +348,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Tasks do l1_block_ranges_for_missing_batches = DbSettlement.get_l1_block_ranges_for_missing_batches(start_batch, end_batch, lowest_l1_block - 1) - unless l1_block_ranges_for_missing_batches == [] do + if l1_block_ranges_for_missing_batches != [] do discover_missing( sequencer_inbox_address, l1_block_ranges_for_missing_batches, diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/historical_messages_on_l2.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/historical_messages_on_l2.ex index 2803b7477b29..7273c37b3f88 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/historical_messages_on_l2.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/historical_messages_on_l2.ex @@ -123,7 +123,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.HistoricalMessagesOnL2 do logs = DbMessages.logs_for_missed_messages_from_l2(start_block, end_block) - unless logs == [] do + if logs != [] do messages = logs |> Messaging.handle_filtered_l2_to_l1_messages(__MODULE__) diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex index 0bdc6e8bbe20..f6f3f7be8684 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex @@ -245,7 +245,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewL1Executions do {lifecycle_transactions, executions} = get_executions_from_logs(logs, l1_rpc_config) - unless executions == [] do + if executions != [] do log_info("Executions for #{length(executions)} L2 messages will be imported") {:ok, _} = @@ -263,7 +263,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewL1Executions do # the execution transactions that have already been indexed. messages = get_relayed_messages() - unless messages == [] do + if messages != [] do log_info("Marking #{length(messages)} l2-to-l1 messages as completed") {:ok, _} = diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex index a2a1c04524ed..f0cbc2fc3b9f 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex @@ -211,7 +211,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do messages = get_messages_from_logs(logs, json_rpc_named_argument, chunk_size) - unless messages == [] do + if messages != [] do log_info("Origins of #{length(messages)} L1-to-L2 messages will be imported") end diff --git a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex index 9854e18feda0..d698db9d7808 100644 --- a/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex +++ b/apps/indexer/lib/indexer/fetcher/beacon/deposit.ex @@ -41,7 +41,7 @@ defmodule Indexer.Fetcher.Beacon.Deposit do json_rpc_named_arguments = opts[:json_rpc_named_arguments] - unless json_rpc_named_arguments do + if !json_rpc_named_arguments do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." end diff --git a/apps/indexer/lib/indexer/fetcher/block_reward.ex b/apps/indexer/lib/indexer/fetcher/block_reward.ex index af5bee84fa2d..d2f4cf5cdcfd 100644 --- a/apps/indexer/lib/indexer/fetcher/block_reward.ex +++ b/apps/indexer/lib/indexer/fetcher/block_reward.ex @@ -45,7 +45,7 @@ defmodule Indexer.Fetcher.BlockReward do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex index 89a87e66562e..1b5d75affcd6 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/epoch_block_operations.ex @@ -42,7 +42,7 @@ defmodule Indexer.Fetcher.Celo.EpochBlockOperations do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec` " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex index b03e17aa4622..2d84e6fb3925 100644 --- a/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex +++ b/apps/indexer/lib/indexer/fetcher/celo/legacy/account.ex @@ -34,7 +34,7 @@ defmodule Indexer.Fetcher.Celo.Legacy.Account do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec` " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex b/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex index 80e1689369c5..dc6bfc77fd5d 100644 --- a/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex +++ b/apps/indexer/lib/indexer/fetcher/coin_balance/helper.ex @@ -18,7 +18,7 @@ defmodule Indexer.Fetcher.CoinBalance.Helper do def child_spec([init_options, gen_server_options], defaults, module) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{module}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/contract_code.ex b/apps/indexer/lib/indexer/fetcher/contract_code.ex index 6abb7f16f912..a673fd54b023 100644 --- a/apps/indexer/lib/indexer/fetcher/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/contract_code.ex @@ -79,7 +79,7 @@ defmodule Indexer.Fetcher.ContractCode do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex index 665ed7fb9aec..32d82fc48766 100644 --- a/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex +++ b/apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex @@ -86,7 +86,7 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do unprocessed_empty_blocks_list = unprocessed_empty_blocks_list_query(limit()) - unless Enum.empty?(unprocessed_empty_blocks_list) do + if !Enum.empty?(unprocessed_empty_blocks_list) do blocks_response = unprocessed_empty_blocks_list |> Enum.map(fn %{number: block_number} -> %{number: integer_to_quantity(block_number)} end) diff --git a/apps/indexer/lib/indexer/fetcher/filecoin/address_info.ex b/apps/indexer/lib/indexer/fetcher/filecoin/address_info.ex index 3317b17c1510..07d22a3321f8 100644 --- a/apps/indexer/lib/indexer/fetcher/filecoin/address_info.ex +++ b/apps/indexer/lib/indexer/fetcher/filecoin/address_info.ex @@ -65,7 +65,7 @@ defmodule Indexer.Fetcher.Filecoin.AddressInfo do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec` " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index 4e9604febd89..166be6c0d0d7 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -75,7 +75,7 @@ defmodule Indexer.Fetcher.InternalTransaction do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex index 935a530ad889..f283614d70d2 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex @@ -72,7 +72,7 @@ defmodule Indexer.Fetcher.InternalTransaction.DeleteQueue do case result do {:ok, {block_numbers, transactions}} -> - unless is_nil(Process.whereis(InternalTransactionFetcher)) do + if not is_nil(Process.whereis(InternalTransactionFetcher)) do InternalTransactionFetcher.async_fetch(block_numbers, transactions, true) end diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex index edf3b4696174..a8ae9e2fcb69 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/balances_export_queue.ex @@ -66,7 +66,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.BalancesExportQueue do coin_balances ++ token_balances end) - unless Enum.empty?(all_balances) do + if !Enum.empty?(all_balances) do all_balances |> Enum.sort_by(&{&1.address_hash, &1.token_contract_address_hash_or_native, &1.token_id}) |> Enum.chunk_every(@delete_queries_chunk_size) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex index 3445a23a95e8..d67f106e56a4 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/coin_balance.ex @@ -79,7 +79,7 @@ defmodule Indexer.Fetcher.OnDemand.CoinBalance do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex index 82ce3004d755..01bd5820239f 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex @@ -199,7 +199,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do |> Map.get(:blocks, []) |> Enum.map(&Map.get(&1, :number)) - unless Enum.empty?(imported_block_numbers) do + if !Enum.empty?(imported_block_numbers) do cache_key = @pending_blocks_cache_key # credo:disable-for-next-line Credo.Check.Refactor.Nesting case pending_blocks_cache() do diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex b/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex index 13ea834560c4..4925c0f5c9e4 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex @@ -13,11 +13,12 @@ defmodule Indexer.Fetcher.OnDemand.FirstTrace do require Logger def maybe_trigger_fetch(transaction, opts \\ []) do - unless Application.get_env(:explorer, :shrink_internal_transactions_enabled) do + if !Application.get_env(:explorer, :shrink_internal_transactions_enabled) do transaction.hash |> InternalTransaction.all_transaction_to_internal_transactions(opts) |> Enum.any?(&(&1.index == 0)) - |> unless do + |> Kernel.!() + |> if do trigger_fetch(transaction) end end diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex index bded260c244a..d7511b125496 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex @@ -209,7 +209,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do address_current_token_balances: %{ address_hash: to_string(address_hash), address_current_token_balances: - Enum.map(ctbs, fn ctb -> + Enum.map(ctbs, fn %CurrentTokenBalance{} = ctb -> %CurrentTokenBalance{ctb | token: tokens[ctb.token_contract_address_hash.bytes]} end) } diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex index 7a1bb17299d0..7d0128f36eb2 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_instance_metadata_refetch.ex @@ -60,7 +60,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenInstanceMetadataRefetch do end defp fetch_and_broadcast_metadata( - %{token_id: token_id, token_contract_address_hash: token_contract_address_hash} = token_instance + %TokenInstance{token_id: token_id, token_contract_address_hash: token_contract_address_hash} = token_instance ) do case Helper.batch_prepare_instances_insert_params([ %{contract_address_hash: token_contract_address_hash, token_id: token_id} diff --git a/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex b/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex index c50f36fc71c6..841404005550 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/operator_fee.ex @@ -43,7 +43,7 @@ defmodule Indexer.Fetcher.Optimism.OperatorFee do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index d05a3ec2649e..24bd66b27393 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -532,7 +532,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do json_rpc_named_arguments_l2, blobs_api_url ) - |> Tuple.append(last_block_hash) + |> (&Tuple.insert_at(&1, tuple_size(&1), last_block_hash)).() {_, message_or_errors} -> message = diff --git a/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex b/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex index 152e719340d6..6a4fadb339b4 100644 --- a/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex @@ -80,7 +80,11 @@ defmodule Indexer.Fetcher.ReplacedTransaction do {block_hash_bytes, nonce, from_address_hash_bytes} end - defp pending_entry(%{hash: %Hash{bytes: hash}, nonce: nonce, from_address_hash: %Hash{bytes: from_address_hash_bytes}}) do + defp pending_entry(%{ + hash: %Hash{bytes: hash}, + nonce: nonce, + from_address_hash: %Hash{bytes: from_address_hash_bytes} + }) do {:pending, nonce, from_address_hash_bytes, hash} end diff --git a/apps/indexer/lib/indexer/fetcher/rootstock_data.ex b/apps/indexer/lib/indexer/fetcher/rootstock_data.ex index 09b683e6563a..552804dd0039 100644 --- a/apps/indexer/lib/indexer/fetcher/rootstock_data.ex +++ b/apps/indexer/lib/indexer/fetcher/rootstock_data.ex @@ -48,7 +48,7 @@ defmodule Indexer.Fetcher.RootstockData do json_rpc_named_arguments = opts[:json_rpc_named_arguments] - unless json_rpc_named_arguments do + if !json_rpc_named_arguments do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." end diff --git a/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex index cbb1306b880a..9b7e7261021a 100644 --- a/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex +++ b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex @@ -111,7 +111,7 @@ defmodule Indexer.Fetcher.SignedAuthorizationStatus do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/token.ex b/apps/indexer/lib/indexer/fetcher/token.ex index 63c618ca37a4..b8c083d4a202 100644 --- a/apps/indexer/lib/indexer/fetcher/token.ex +++ b/apps/indexer/lib/indexer/fetcher/token.ex @@ -21,7 +21,7 @@ defmodule Indexer.Fetcher.Token do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index ad45a62a8170..18613a958b85 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -59,7 +59,7 @@ defmodule Indexer.Fetcher.TokenBalance do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex index e9cfc9996b95..c25e113fbc15 100644 --- a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex @@ -28,7 +28,7 @@ defmodule Indexer.Fetcher.TokenCountersUpdater do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/token_updater.ex b/apps/indexer/lib/indexer/fetcher/token_updater.ex index 4d59fcf59341..91502c8d897e 100644 --- a/apps/indexer/lib/indexer/fetcher/token_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_updater.ex @@ -29,7 +29,7 @@ defmodule Indexer.Fetcher.TokenUpdater do def child_spec([init_options, gen_server_options]) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." diff --git a/apps/indexer/lib/indexer/fetcher/transaction_action.ex b/apps/indexer/lib/indexer/fetcher/transaction_action.ex index 5d8c9e7652da..c67bd354c705 100644 --- a/apps/indexer/lib/indexer/fetcher/transaction_action.ex +++ b/apps/indexer/lib/indexer/fetcher/transaction_action.ex @@ -138,7 +138,7 @@ defmodule Indexer.Fetcher.TransactionAction do end end - defp run_fetch(state) do + defp run_fetch(%__MODULE__{} = state) do pid = self() Process.send_after(pid, :fetch, 3000, []) %__MODULE__{state | task: nil, pid: pid} diff --git a/apps/indexer/lib/indexer/fetcher/uncle_block.ex b/apps/indexer/lib/indexer/fetcher/uncle_block.ex index 1b65dc2966fb..f0fa3cdaa29b 100644 --- a/apps/indexer/lib/indexer/fetcher/uncle_block.ex +++ b/apps/indexer/lib/indexer/fetcher/uncle_block.ex @@ -49,20 +49,20 @@ defmodule Indexer.Fetcher.UncleBlock do @doc false def child_spec([init_options, gen_server_options]) when is_list(init_options) do - {state, mergeable_init_options} = Keyword.pop(init_options, :block_fetcher) - - unless state do - raise ArgumentError, - ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> - "to allow for json_rpc calls when running." + case Keyword.pop(init_options, :block_fetcher) do + {%Block.Fetcher{} = state, mergeable_init_options} -> + merged_init_options = + @defaults + |> Keyword.merge(mergeable_init_options) + |> Keyword.put(:state, %Block.Fetcher{state | broadcast: :uncle, callback_module: __MODULE__}) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) + + _ -> + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> + "to allow for json_rpc calls when running." end - - merged_init_options = - @defaults - |> Keyword.merge(mergeable_init_options) - |> Keyword.put(:state, %Block.Fetcher{state | broadcast: :uncle, callback_module: __MODULE__}) - - Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) end @impl BufferedTask @@ -226,7 +226,7 @@ defmodule Indexer.Fetcher.UncleBlock do loggable_errors = loggable_errors(errors) loggable_error_count = Enum.count(loggable_errors) - unless loggable_error_count == 0 do + if loggable_error_count != 0 do Logger.error( fn -> [ diff --git a/apps/indexer/lib/indexer/fetcher/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/withdrawal.ex index b8a7707bd93e..6cd99ad9c060 100644 --- a/apps/indexer/lib/indexer/fetcher/withdrawal.ex +++ b/apps/indexer/lib/indexer/fetcher/withdrawal.ex @@ -50,7 +50,7 @@ defmodule Indexer.Fetcher.Withdrawal do # withdrawals from all other blocks will be imported by realtime and catchup indexers json_rpc_named_arguments = opts[:json_rpc_named_arguments] - unless json_rpc_named_arguments do + if !json_rpc_named_arguments do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." end diff --git a/apps/indexer/lib/indexer/memory/monitor.ex b/apps/indexer/lib/indexer/memory/monitor.ex index 81f7e6f0567a..b25b2aaf0e67 100644 --- a/apps/indexer/lib/indexer/memory/monitor.ex +++ b/apps/indexer/lib/indexer/memory/monitor.ex @@ -47,7 +47,7 @@ defmodule Indexer.Memory.Monitor do @impl GenServer def init(options) when is_map(options) do - state = struct!(__MODULE__, Map.put_new(options, :limit, define_memory_limit())) + %__MODULE__{} = state = struct!(__MODULE__, Map.put_new(options, :limit, define_memory_limit())) {:ok, timer_reference} = :timer.send_interval(state.timer_interval, :check) {:ok, %__MODULE__{state | timer_reference: timer_reference}} diff --git a/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex b/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex index de42f171cd3e..7c0f1b757446 100644 --- a/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex +++ b/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex @@ -151,7 +151,7 @@ defmodule Indexer.Migrator.RecoveryWETHTokenTransfers do |> where([log, tt], is_nil(tt)) |> where(^Log.first_topic_is_deposit_or_withdrawal_signature()) |> Repo.all(timeout: :infinity) - |> Enum.map(fn log -> + |> Enum.map(fn %Log{} = log -> %Log{ log | first_topic: to_string(log.first_topic), diff --git a/apps/indexer/lib/indexer/prometheus/collector/filecoin_pending_address_operations_collector.ex b/apps/indexer/lib/indexer/prometheus/collector/filecoin_pending_address_operations_collector.ex index 231a8048d6bc..6e8341bdf5d3 100644 --- a/apps/indexer/lib/indexer/prometheus/collector/filecoin_pending_address_operations_collector.ex +++ b/apps/indexer/lib/indexer/prometheus/collector/filecoin_pending_address_operations_collector.ex @@ -7,10 +7,9 @@ defmodule Indexer.Prometheus.Collector.FilecoinPendingAddressOperations do if @chain_type == :filecoin do use Prometheus.Collector - # TODO: remove when https://github.com/elixir-lang/elixir/issues/13975 comes to elixir release - alias Explorer.Chain.Filecoin.PendingAddressOperation, warn: false - alias Explorer.Repo, warn: false - alias Prometheus.Model, warn: false + alias Explorer.Chain.Filecoin.PendingAddressOperation + alias Explorer.Repo + alias Prometheus.Model def collect_mf(_registry, callback) do query = PendingAddressOperation.fresh_operations_query() diff --git a/apps/indexer/lib/indexer/temporary/uncles_without_index.ex b/apps/indexer/lib/indexer/temporary/uncles_without_index.ex index 79abdc18147a..3b759306e66a 100644 --- a/apps/indexer/lib/indexer/temporary/uncles_without_index.ex +++ b/apps/indexer/lib/indexer/temporary/uncles_without_index.ex @@ -33,7 +33,7 @@ defmodule Indexer.Temporary.UnclesWithoutIndex do def child_spec([init_options, gen_server_options]) when is_list(init_options) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) - unless state do + if !state do raise ArgumentError, ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> "to allow for json_rpc calls when running." @@ -123,7 +123,7 @@ defmodule Indexer.Temporary.UnclesWithoutIndex do loggable_errors = loggable_errors(errors) loggable_error_count = Enum.count(loggable_errors) - unless loggable_error_count == 0 do + if loggable_error_count != 0 do Logger.error( fn -> [ diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex b/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex index 6f7858f41022..dd446f5873de 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex @@ -49,7 +49,9 @@ defmodule Indexer.Transform.AddressCoinBalancesDaily do coin_balances_daily_params_set end - def params_set(%{address_coin_balances_params_with_block_timestamp: address_coin_balances_params_with_block_timestamp}) do + def params_set(%{ + address_coin_balances_params_with_block_timestamp: address_coin_balances_params_with_block_timestamp + }) do coin_balances_params = address_coin_balances_params_with_block_timestamp |> MapSet.to_list() diff --git a/apps/indexer/lib/indexer/transform/token_transfers.ex b/apps/indexer/lib/indexer/transform/token_transfers.ex index b12ff5cc8079..4844876ff79b 100644 --- a/apps/indexer/lib/indexer/transform/token_transfers.ex +++ b/apps/indexer/lib/indexer/transform/token_transfers.ex @@ -82,7 +82,7 @@ defmodule Indexer.Transform.TokenTransfers do tokens = sanitize_token_types(rough_tokens, rough_token_transfers) token_transfers = sanitize_weth_transfers(tokens, rough_token_transfers, weth_transfers.token_transfers) - unless skip_additional_fetchers? do + if !skip_additional_fetchers? do token_transfers |> filter_tokens_for_supply_update() |> TokenTotalSupplyUpdater.add_tokens() diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index ef8827242c9d..cae5d8cf52ca 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -10,7 +10,7 @@ defmodule Indexer.MixProject do deps: deps(), deps_path: "../../deps", description: "Fetches blockchain data from on-chain node for later reading with Explorer.", - elixir: "~> 1.17", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index 089e9630c996..acd71a7e6acc 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -571,7 +571,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do errors: [] }} = Indexer.Block.Fetcher.fetch_and_import_range(block_fetcher, 3_946_079..3_946_080) - unless chain_identity() == {:optimism, :celo} do + if chain_identity() != {:optimism, :celo} do assert [ %Address{hash: ^first_address_hash}, %Address{hash: ^second_address_hash}, @@ -831,7 +831,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do errors: [] }} = Indexer.Block.Fetcher.fetch_and_import_range(block_fetcher, 3_946_079..3_946_080) - unless chain_identity() == {:optimism, :celo} do + if chain_identity() != {:optimism, :celo} do assert [ %Address{hash: ^first_address_hash}, %Address{hash: ^second_address_hash}, diff --git a/apps/nft_media_handler/lib/nft_media_handler/application.ex b/apps/nft_media_handler/lib/nft_media_handler/application.ex index 1f415898b080..a037dd2bfde3 100644 --- a/apps/nft_media_handler/lib/nft_media_handler/application.ex +++ b/apps/nft_media_handler/lib/nft_media_handler/application.ex @@ -9,7 +9,9 @@ defmodule NFTMediaHandler.Application do File.mkdir(Application.get_env(:nft_media_handler, :tmp_dir)) base_children = [ - Supervisor.child_spec({Task.Supervisor, name: NFTMediaHandler.TaskSupervisor}, id: NFTMediaHandler.TaskSupervisor), + Supervisor.child_spec({Task.Supervisor, name: NFTMediaHandler.TaskSupervisor}, + id: NFTMediaHandler.TaskSupervisor + ), NFTMediaHandler.Dispatcher ] diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 9e02ecfe9138..0fc135b4aad7 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -9,7 +9,7 @@ defmodule NFTMediaHandler.MixProject do config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.17", + elixir: "~> 1.19", start_permanent: Mix.env() == :prod, deps: deps() ] diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index ee732b654fd8..7dd3598e1a77 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -9,14 +9,10 @@ defmodule Utils.MixProject do # config_path: "../../config/config.exs", deps_path: "../../deps", lockfile: "../../mix.lock", - elixir: "~> 1.17", + elixir: "~> 1.19", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, - deps: deps(), - preferred_cli_env: [ - credo: :test, - dialyzer: :test - ] + deps: deps() ] end @@ -27,6 +23,10 @@ defmodule Utils.MixProject do ] end + def cli do + [preferred_envs: [credo: :test, dialyzer: :test]] + end + # Run "mix help deps" to learn about dependencies. defp deps do [ diff --git a/docker/Dockerfile b/docker/Dockerfile index 20232af3064f..bf3b5a77a673 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 AS builder-deps +FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 AS builder-deps WORKDIR /app @@ -17,7 +17,7 @@ COPY apps/nft_media_handler/mix.exs ./apps/nft_media_handler/ ENV MIX_ENV="prod" ENV MIX_HOME=/opt/mix RUN mix local.hex --force -RUN mix do deps.get, local.rebar --force, deps.compile --skip-umbrella-children +RUN MIX_OS_DEPS_COMPILE_PARTITION_COUNT=$(nproc) mix do deps.get + local.rebar --force + deps.compile --skip-umbrella-children COPY config ./config COPY rel ./rel @@ -49,7 +49,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 +FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 WORKDIR /app diff --git a/docker/oldUI.Dockerfile b/docker/oldUI.Dockerfile index a2552f144449..9cb00f832ad7 100644 --- a/docker/oldUI.Dockerfile +++ b/docker/oldUI.Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 AS builder-deps +FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 AS builder-deps WORKDIR /app @@ -65,7 +65,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.17.3-erlang-27.3.4-alpine-3.21.3 +FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 WORKDIR /app diff --git a/mix.exs b/mix.exs index dd6a3994bfe1..f00d258ab37c 100644 --- a/mix.exs +++ b/mix.exs @@ -11,11 +11,7 @@ defmodule BlockScout.Mixfile do apps_path: "apps", deps: deps(), dialyzer: dialyzer(), - elixir: "~> 1.17", - preferred_cli_env: [ - credo: :test, - dialyzer: :test - ], + elixir: "~> 1.19", # start_permanent: config_env() == :prod, releases: [ blockscout: [ @@ -34,6 +30,10 @@ defmodule BlockScout.Mixfile do ] end + def cli do + [preferred_envs: [credo: :test, dialyzer: :test]] + end + ## Private Functions defp copy_prod_runtime_config(%Mix.Release{path: path} = release) do From e5aa0d4148cfea148eb59535720b62d86f66797d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 24 Nov 2025 14:02:04 +0300 Subject: [PATCH 0480/1006] chore: Add migration to drop unique tokens_contract_address_hash_index index (#13596) --- ...rop_tokens_contract_address_hash_index.exs | 50 +++++++++++++++++++ cspell.json | 4 ++ 2 files changed, 54 insertions(+) create mode 100644 apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs diff --git a/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs b/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs new file mode 100644 index 000000000000..0410ca660104 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs @@ -0,0 +1,50 @@ +defmodule Explorer.Repo.Migrations.DropTokensContractAddressHashIndex do + use Ecto.Migration + + def change do + execute(""" + DO $$ + BEGIN + -- 1. Check if the index exists + IF EXISTS ( + SELECT 1 + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'tokens_contract_address_hash_index' + AND n.nspname = 'public' + ) THEN + RAISE NOTICE 'Index tokens_contract_address_hash_index exists. Proceeding...'; + + -- 2. Drop the FK constraint (only if it exists) + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'token_instances_token_contract_address_hash_fkey' + AND conrelid = 'public.token_instances'::regclass + ) THEN + RAISE NOTICE 'Dropping foreign key token_instances_token_contract_address_hash_fkey...'; + EXECUTE ' + ALTER TABLE public.token_instances + DROP CONSTRAINT token_instances_token_contract_address_hash_fkey + '; + END IF; + + -- 3. Drop the redundant index + RAISE NOTICE 'Dropping index tokens_contract_address_hash_index...'; + EXECUTE 'DROP INDEX public.tokens_contract_address_hash_index'; + + -- 4. Recreate the FK constraint + RAISE NOTICE 'Recreating foreign key token_instances_token_contract_address_hash_fkey...'; + EXECUTE ' + ALTER TABLE public.token_instances + ADD CONSTRAINT token_instances_token_contract_address_hash_fkey + FOREIGN KEY (token_contract_address_hash) + REFERENCES public.tokens(contract_address_hash) + '; + ELSE + RAISE NOTICE 'Index tokens_contract_address_hash_index does NOT exist. Nothing to do.'; + END IF; + END $$; + """) + end +end diff --git a/cspell.json b/cspell.json index 0a175a2cc970..2dbb71364ded 100644 --- a/cspell.json +++ b/cspell.json @@ -160,6 +160,8 @@ "compilerversion", "concache", "cond", + "conname", + "conrelid", "Consolas", "contractaddress", "contractaddresses", @@ -455,6 +457,7 @@ "NovesFi", "nowarn", "nowrap", + "nspname", "ntoa", "Numbe", "Nunito", @@ -547,6 +550,7 @@ "relid", "relkind", "relname", + "relnamespace", "relpages", "reltuples", "remasc", From 07b44e8f9cbb8350a962f63e01764f159f484f4b Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 24 Nov 2025 14:17:28 +0300 Subject: [PATCH 0481/1006] feat: Hot smart-contracts (#13471) Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Co-authored-by: Victor Baranov --- .../lib/block_scout_web/chain.ex | 20 ++ .../controllers/api/v2/stats_controller.ex | 96 ++++++- .../lib/block_scout_web/paging_helper.ex | 61 +++++ .../lib/block_scout_web/routers/api_router.ex | 1 + .../block_scout_web/schemas/api/v2/general.ex | 36 +++ .../api/v2/stats/hot_smart_contract.ex | 21 ++ .../views/api/v2/stats_view.ex | 25 ++ .../api/v2/stats_controller_test.exs | 257 ++++++++++++++++++ .../chain/address/metadata_preloader.ex | 18 ++ apps/explorer/lib/explorer/chain/import.ex | 3 + .../runner/stats/hot_smart_contracts.ex | 82 ++++++ .../lib/explorer/chain/import/stage/stats.ex | 27 ++ .../lib/explorer/prometheus/instrumenter.ex | 16 ++ apps/explorer/lib/explorer/sorting_helper.ex | 10 +- .../lib/explorer/stats/hot_smart_contracts.ex | 199 ++++++++++++++ ...20144203_add_hot_smart_contracts_daily.exs | 24 ++ .../fetcher/stats/hot_smart_contracts.ex | 78 ++++++ apps/indexer/lib/indexer/supervisor.ex | 4 +- config/runtime.exs | 4 + docker-compose/envs/common-blockscout.env | 1 + 20 files changed, 979 insertions(+), 4 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/hot_smart_contract.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/stats_view.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/stats/hot_smart_contracts.ex create mode 100644 apps/explorer/lib/explorer/chain/import/stage/stats.ex create mode 100644 apps/explorer/lib/explorer/stats/hot_smart_contracts.ex create mode 100644 apps/explorer/priv/repo/migrations/20251020144203_add_hot_smart_contracts_daily.exs create mode 100644 apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 1cd0aec0ab61..ec39a2269c9a 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -732,6 +732,26 @@ defmodule BlockScoutWeb.Chain do def paging_options(_params), do: [paging_options: @default_paging_options] + def hot_smart_contracts_paging_options(%{ + transactions_count: transactions_count, + total_gas_used: total_gas_used, + contract_address_hash: contract_address_hash + }) do + [ + paging_options: %{ + @default_paging_options + | key: %{ + transactions_count: transactions_count, + total_gas_used: total_gas_used, + contract_address_hash: contract_address_hash, + to_address_hash: contract_address_hash + } + } + ] + end + + def hot_smart_contracts_paging_options(_params), do: [paging_options: @default_paging_options] + def put_key_value_to_paging_options([paging_options: paging_options], key, value) do [paging_options: Map.put(paging_options, key, value)] end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 8715fc4a3790..0bdd396a1182 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -3,7 +3,20 @@ defmodule BlockScoutWeb.API.V2.StatsController do chain_type: [:explorer, :chain_type], chain_identity: [:explorer, :chain_identity] - use Phoenix.Controller, namespace: BlockScoutWeb + use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs + + import BlockScoutWeb.PagingHelper, only: [hot_smart_contracts_sorting: 1, delete_items_count_from_next_page_params: 1] + + import BlockScoutWeb.Chain, + only: [ + hot_smart_contracts_paging_options: 1, + split_list_by_page: 1, + next_page_params: 5, + fetch_scam_token_toggle: 2 + ] + + import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController @@ -12,11 +25,18 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats + alias Explorer.Stats.HotSmartContracts alias Plug.Conn alias Timex.Duration @api_true [api?: true] + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags ["stats"] + + operation :stats, false + def stats(conn, _params) do market_cap_type = case Application.get_env(:explorer, :supply) do @@ -98,6 +118,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do else: gas_used |> Decimal.div(gas_limit) |> Decimal.mult(100) |> Decimal.to_float() end + operation :transactions_chart, false + def transactions_chart(conn, _params) do [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) @@ -119,6 +141,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end + operation :market_chart, false + def market_chart(conn, _params) do exchange_rate = Market.get_coin_exchange_rate() @@ -152,6 +176,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end + operation :secondary_coin_market_chart, false + def secondary_coin_market_chart(conn, _params) do recent_market_history = Market.fetch_recent_history(true) @@ -164,6 +190,66 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end + operation :hot_smart_contracts, + summary: "Retrieve hot contracts", + description: "Retrieves hot contracts", + parameters: + base_params() ++ + [sort_param(["transactions_count", "total_gas_used"]), order_param(), hot_smart_contracts_scale_param()] ++ + define_paging_params([ + "transactions_count_positive", + "total_gas_used", + "contract_address_hash_not_nullable", + "items_count" + ]), + responses: [ + ok: + {"Hot contracts.", "application/json", + paginated_response( + items: Schemas.Stats.HotContract, + next_page_params_example: %{ + "transactions_count" => 100, + "total_gas_used" => "100", + "contract_address_hash" => "0x01a2A10583675E0e5dF52DE1b62734109201477a", + "items_count" => 50 + }, + title_prefix: "HotSmartContracts" + )}, + unprocessable_entity: JsonErrorResponse.response(), + forbidden: ForbiddenResponse.response() + ] + + @spec hot_smart_contracts(Plug.Conn.t(), map()) :: Plug.Conn.t() + def hot_smart_contracts(conn, %{scale: scale} = params) do + options = + params + |> hot_smart_contracts_paging_options() + |> Keyword.merge(hot_smart_contracts_sorting(params)) + |> Keyword.merge(@api_true) + |> fetch_scam_token_toggle(conn) + + {hot_smart_contracts, next_page} = + scale + |> HotSmartContracts.paginated(options) + |> case do + {:error, :not_found} -> [] + hot_smart_contracts -> hot_smart_contracts + end + |> split_list_by_page() + + next_page_params = + next_page + |> next_page_params(hot_smart_contracts, params, false, &hot_smart_contracts_paging_params/1) + |> delete_items_count_from_next_page_params() + + conn + |> put_status(200) + |> render(:hot_smart_contracts, %{ + hot_smart_contracts: hot_smart_contracts |> maybe_preload_metadata(), + next_page_params: next_page_params + }) + end + defp backward_compatibility(response, conn) do case Conn.get_req_header(conn, "updated-gas-oracle") do ["true"] -> @@ -212,4 +298,12 @@ defmodule BlockScoutWeb.API.V2.StatsController do _ -> defp add_chain_identity_fields(response), do: response end + + defp hot_smart_contracts_paging_params(hot_contract) do + %{ + contract_address_hash: hot_contract.contract_address_hash, + transactions_count: hot_contract.transactions_count, + total_gas_used: hot_contract.total_gas_used + } + end end diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 19f159f33aa1..b81e38abdce6 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.PagingHelper do alias Explorer.Chain.InternalTransaction.Type, as: InternalTransactionType alias Explorer.Chain.{SmartContract, Transaction} alias Explorer.{Helper, PagingOptions, SortingHelper} + alias Explorer.Stats.HotSmartContracts @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @@ -257,6 +258,7 @@ defmodule BlockScoutWeb.PagingHelper do :block_hash_or_number_param, :transaction_hash_param, :batch_number_param, + :scale, :token_id_param, :token_id, :type, @@ -283,6 +285,13 @@ defmodule BlockScoutWeb.PagingHelper do def delete_parameters_from_next_page_params(_), do: nil + def delete_items_count_from_next_page_params(params) when is_map(params) do + params + |> Map.drop(["items_count"]) + end + + def delete_items_count_from_next_page_params(other), do: other + def current_filter(%{"filter" => language_string}) do SmartContract.language_string_to_atom() |> Map.fetch(language_string) @@ -446,4 +455,56 @@ defmodule BlockScoutWeb.PagingHelper do defp do_addresses_sorting("transactions_count", "asc"), do: [asc_nulls_first: :transactions_count] defp do_addresses_sorting("transactions_count", "desc"), do: [desc_nulls_last: :transactions_count] defp do_addresses_sorting(_, _), do: [] + + @spec hot_smart_contracts_sorting(%{sort: String.t(), order: String.t()}) :: [ + {:sorting, SortingHelper.sorting_params()} + ] + def hot_smart_contracts_sorting(%{sort: sort_field, order: order}) do + [sorting: do_hot_smart_contracts_sorting(sort_field, order)] + end + + @spec hot_smart_contracts_sorting(any()) :: [] + def hot_smart_contracts_sorting(_), do: [] + + defp do_hot_smart_contracts_sorting("transactions_count", "asc"), + do: %{ + aggregated_on_hot_smart_contracts: [ + {:dynamic, :transactions_count, :asc_nulls_first, HotSmartContracts.transactions_count_dynamic()} + ], + aggregated_on_transactions: [ + {:dynamic, :transactions_count, :asc_nulls_first, + HotSmartContracts.transactions_count_on_transactions_dynamic()} + ] + } + + defp do_hot_smart_contracts_sorting("transactions_count", "desc"), + do: %{ + aggregated_on_hot_smart_contracts: [ + {:dynamic, :transactions_count, :desc_nulls_last, HotSmartContracts.transactions_count_dynamic()} + ], + aggregated_on_transactions: [ + {:dynamic, :transactions_count, :desc_nulls_last, + HotSmartContracts.transactions_count_on_transactions_dynamic()} + ] + } + + defp do_hot_smart_contracts_sorting("total_gas_used", "asc"), + do: %{ + aggregated_on_hot_smart_contracts: [ + {:dynamic, :total_gas_used, :asc_nulls_first, HotSmartContracts.total_gas_used_dynamic()} + ], + aggregated_on_transactions: [ + {:dynamic, :total_gas_used, :asc_nulls_first, HotSmartContracts.total_gas_used_on_transactions_dynamic()} + ] + } + + defp do_hot_smart_contracts_sorting("total_gas_used", "desc"), + do: %{ + aggregated_on_hot_smart_contracts: [ + {:dynamic, :total_gas_used, :desc_nulls_last, HotSmartContracts.total_gas_used_dynamic()} + ], + aggregated_on_transactions: [ + {:dynamic, :total_gas_used, :desc_nulls_last, HotSmartContracts.total_gas_used_on_transactions_dynamic()} + ] + } end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index dd816fb7c55f..aa3e9feb85cd 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -291,6 +291,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do scope "/stats" do get("/", V2.StatsController, :stats) + get("/hot-smart-contracts", V2.StatsController, :hot_smart_contracts) scope "/charts" do get("/transactions", V2.StatsController, :transactions_chart) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 571da8dd9bbe..5b809bbe3269 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -628,6 +628,21 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for scale for hot contracts. + """ + @spec hot_smart_contracts_scale_param() :: Parameter.t() + def hot_smart_contracts_scale_param do + %Parameter{ + in: :query, + schema: %Schema{type: :string, enum: ["5m", "1h", "3h", "1d", "7d", "30d"], nullable: false}, + required: true, + description: + "Time scale for hot contracts aggregation (5m=5 minutes, 1h=1 hour, 3h=3 hours, 1d=1 day, 7d=7 days, 30d=30 days)", + name: :scale + } + end + @doc """ Returns a list of base parameters (api_key and key). """ @@ -933,6 +948,27 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: "Deposit index for paging", name: :index + }, + "total_gas_used" => %Parameter{ + in: :query, + schema: %Schema{type: :integer, minimum: 0}, + required: false, + description: "Total gas used for paging", + name: :total_gas_used + }, + "contract_address_hash_not_nullable" => %Parameter{ + in: :query, + schema: AddressHash, + required: false, + description: "Contract address hash for paging", + name: :contract_address_hash + }, + "transactions_count_positive" => %Parameter{ + in: :query, + schema: %Schema{type: :integer, minimum: 1}, + required: false, + description: "Transactions count for paging", + name: :transactions_count } } diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/hot_smart_contract.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/hot_smart_contract.ex new file mode 100644 index 000000000000..499b0849efc8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/hot_smart_contract.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Stats.HotContract do + @moduledoc """ + This module defines the schema for the HotContract struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + contract_address: Address, + transactions_count: %Schema{type: :integer, minimum: 1}, + total_gas_used: %Schema{type: :integer, minimum: 1}, + balance: General.IntegerStringNullable + }, + required: [:contract_address, :transactions_count, :total_gas_used, :balance], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/stats_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stats_view.ex new file mode 100644 index 000000000000..8ae98389b2e8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/stats_view.ex @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.API.V2.StatsView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + + def render("hot_smart_contracts.json", %{hot_smart_contracts: hot_smart_contracts, next_page_params: next_page_params}) do + %{ + items: Enum.map(hot_smart_contracts, &prepare_hot_contract/1), + next_page_params: next_page_params + } + end + + defp prepare_hot_contract(hot_contract) do + balance = + hot_contract.contract_address.fetched_coin_balance && hot_contract.contract_address.fetched_coin_balance.value + + %{ + contract_address: + Helper.address_with_info(nil, hot_contract.contract_address, hot_contract.contract_address_hash, false), + transactions_count: to_string(hot_contract.transactions_count), + total_gas_used: to_string(hot_contract.total_gas_used), + balance: balance + } + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs index 683605e0d003..24d51e2b9f55 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs @@ -1,6 +1,7 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do use BlockScoutWeb.ConnCase + alias Explorer.Chain.Address alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} describe "/stats" do @@ -64,4 +65,260 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do assert response["chart_data"] == [] end end + + describe "/stats/hot-smart-contracts" do + import Explorer.Factory + alias Explorer.Repo + alias Explorer.Stats.HotSmartContracts + + setup do + init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) + on_exit(fn -> Application.put_env(:block_scout_web, :hide_scam_addresses, init_value) end) + :ok + end + + defp insert_hot_smart_contracts_daily(count, date \\ Date.utc_today(), scam \\ 0) do + addresses = Enum.map(1..count, fn _ -> insert(:address) end) + + Enum.each(addresses, fn addr -> + %HotSmartContracts{ + date: date, + contract_address_hash: addr.hash, + transactions_count: 1, + total_gas_used: Decimal.new(21_000) + } + |> HotSmartContracts.changeset(%{}) + |> Repo.insert!() + end) + + addresses + |> Enum.take(-scam) + |> Enum.each(fn addr -> insert(:scam_badge_to_address, address_hash: addr.hash) end) + + addresses + end + + defp insert_transactions_in_last_seconds(count, seconds_ago, scam \\ 0) do + now = DateTime.utc_now() + from_ts = DateTime.add(now, -seconds_ago, :second) + + # Ensure strict boundary blocks exist + some_block_in_beginning = insert(:block, timestamp: DateTime.add(from_ts, -1, :second)) + _from_block = insert(:block, timestamp: DateTime.add(from_ts, 1, :second)) + to_block = insert(:block, timestamp: DateTime.add(now, -1, :second)) + + addresses = Enum.map(1..count, fn _ -> insert(:contract_address) end) + + Enum.each(addresses, fn addr -> + insert(:transaction, to_address: addr) + |> with_block(to_block, gas_used: 21_000) + end) + + insert(:transaction, to_address: insert(:contract_address)) + |> with_block(some_block_in_beginning, gas_used: 21_000) + + addresses + |> Enum.take(-scam) + |> Enum.each(fn addr -> insert(:scam_badge_to_address, address_hash: addr.hash) end) + + addresses + end + + test "empty stats works for short scale", %{conn: conn} do + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "5m"}) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 0 + end + + test "empty stats works for long scale", %{conn: conn} do + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "7d"}) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 0 + end + + # Daily scales pagination + test "1d pagination", %{conn: conn} do + insert_hot_smart_contracts_daily(55) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "1d"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + + assert length(items) == 50 + assert is_map(next_page_params) + assert Map.has_key?(next_page_params, "contract_address_hash") + assert Map.has_key?(next_page_params, "transactions_count") + assert Map.has_key?(next_page_params, "total_gas_used") + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "1d"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 5 + end + + test "7d pagination", %{conn: conn} do + insert_hot_smart_contracts_daily(55) + + insert_hot_smart_contracts_daily(55, Date.add(Date.utc_today(), -8)) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "7d"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + + assert length(items) == 50 + assert is_map(next_page_params) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "7d"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 5 + end + + test "30d pagination", %{conn: conn} do + insert_hot_smart_contracts_daily(55) + + insert_hot_smart_contracts_daily(55, Date.add(Date.utc_today(), -31)) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "30d"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + + assert length(items) == 50 + assert is_map(next_page_params) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "30d"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 5 + end + + # Daily scales scam toggle + test "1d scam toggle respects cookie and hide config", %{conn: conn} do + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + + [normal | [scam | _]] = insert_hot_smart_contracts_daily(2, Date.utc_today(), 1) + + # Without cookie -> hide scam + request1 = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "1d"}) + resp1 = json_response(request1, 200) + hashes1 = Enum.map(resp1["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes1 + refute Address.checksum(scam.hash) in hashes1 + + # With cookie -> show scam + request2 = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/stats/hot-smart-contracts", %{scale: "1d"}) + + resp2 = json_response(request2, 200) + hashes2 = Enum.map(resp2["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes2 + assert Address.checksum(scam.hash) in hashes2 + end + + # Seconds scales pagination (seed once covers all three) + test "5m pagination", %{conn: conn} do + insert_transactions_in_last_seconds(55, 300) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "5m"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + assert length(items) == 50 + assert is_map(next_page_params) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "5m"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 5 + end + + test "1h pagination", %{conn: conn} do + insert_transactions_in_last_seconds(55, 3600) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "1h"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + assert length(items) == 50 + assert is_map(next_page_params) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "1h"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) + assert length(items) == 5 + end + + test "3h pagination", %{conn: conn} do + addresses = insert_transactions_in_last_seconds(55, 10800) + + addresses = + Enum.map(addresses, fn addr -> to_string(addr.hash) end) |> dbg(limit: :infinity, printable_limit: :infinity) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "3h"}) + assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) + assert length(items) == 50 + assert is_map(next_page_params) + + request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "3h"})) + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) |> dbg() + assert length(items) == 5 + end + + # Seconds scales scam toggle + test "5m scam toggle respects cookie and hide config", %{conn: conn} do + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + + [normal | [scam | _]] = insert_transactions_in_last_seconds(2, 300, 1) + + request1 = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "5m"}) + resp1 = json_response(request1, 200) + hashes1 = Enum.map(resp1["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes1 + refute Address.checksum(scam.hash) in hashes1 + + request2 = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/stats/hot-smart-contracts", %{scale: "5m"}) + + resp2 = json_response(request2, 200) + hashes2 = Enum.map(resp2["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes2 + assert Address.checksum(scam.hash) in hashes2 + end + + test "1h scam toggle respects cookie and hide config", %{conn: conn} do + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + + [normal | [scam | _]] = insert_transactions_in_last_seconds(2, 300, 1) + + request1 = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "1h"}) + resp1 = json_response(request1, 200) + hashes1 = Enum.map(resp1["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes1 + refute Address.checksum(scam.hash) in hashes1 + + request2 = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/stats/hot-smart-contracts", %{scale: "1h"}) + + resp2 = json_response(request2, 200) + hashes2 = Enum.map(resp2["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes2 + assert Address.checksum(scam.hash) in hashes2 + end + + test "3h scam toggle respects cookie and hide config", %{conn: conn} do + Application.put_env(:block_scout_web, :hide_scam_addresses, true) + + [normal | [scam | _]] = insert_transactions_in_last_seconds(2, 300, 1) + + request1 = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "3h"}) + resp1 = json_response(request1, 200) + hashes1 = Enum.map(resp1["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes1 + refute Address.checksum(scam.hash) in hashes1 + + request2 = + conn + |> put_req_cookie("show_scam_tokens", "true") + |> get("/api/v2/stats/hot-smart-contracts", %{scale: "3h"}) + + resp2 = json_response(request2, 200) + hashes2 = Enum.map(resp2["items"], fn %{"contract_address" => %{"hash" => h}} -> h end) + assert Address.checksum(normal.hash) in hashes2 + assert Address.checksum(scam.hash) in hashes2 + end + end end diff --git a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex index 1f4b1632e48a..36a68fa56545 100644 --- a/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex +++ b/apps/explorer/lib/explorer/chain/address/metadata_preloader.ex @@ -18,6 +18,8 @@ defmodule Explorer.Chain.Address.MetadataPreloader do Withdrawal } + alias Explorer.Stats.HotSmartContracts + @type supported_types :: Address.t() | Block.t() @@ -200,6 +202,10 @@ defmodule Explorer.Chain.Address.MetadataPreloader do end end + defp item_to_address_hash_strings(%HotSmartContracts{contract_address_hash: contract_address_hash}) do + [to_string(contract_address_hash)] + end + defp put_ens_names(names, items) do Enum.map(items, &put_meta_to_item(&1, names, :ens_domain_name)) end @@ -321,6 +327,18 @@ defmodule Explorer.Chain.Address.MetadataPreloader do } end + defp put_meta_to_item( + %HotSmartContracts{contract_address_hash: contract_address_hash, contract_address: contract_address} = + hot_contract, + names, + field_to_put_info + ) do + %HotSmartContracts{ + hot_contract + | contract_address: alter_address(contract_address, contract_address_hash, names, field_to_put_info) + } + end + defp alter_address(address, nil, _names, _field), do: address defp alter_address(%NotLoaded{}, address_hash, names, field) do diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 66c69aa00e55..7dbb8bd63b3a 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -27,6 +27,9 @@ defmodule Explorer.Chain.Import do Import.Stage.Logs, Import.Stage.InternalTransactions, Import.Stage.ChainTypeSpecific + ], + [ + Import.Stage.Stats ] ] diff --git a/apps/explorer/lib/explorer/chain/import/runner/stats/hot_smart_contracts.ex b/apps/explorer/lib/explorer/chain/import/runner/stats/hot_smart_contracts.ex new file mode 100644 index 000000000000..7a126c0ca770 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/stats/hot_smart_contracts.ex @@ -0,0 +1,82 @@ +defmodule Explorer.Chain.Import.Runner.Stats.HotSmartContracts do + @moduledoc """ + Bulk imports `t:Explorer.Stats.HotSmartContracts.t/0` rows (hot_smart_contracts_daily). + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Prometheus.Instrumenter + alias Explorer.Stats.HotSmartContracts + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [HotSmartContracts.t()] + + @impl Import.Runner + def ecto_schema_module, do: HotSmartContracts + + @impl Import.Runner + def option_key, do: :hot_smart_contracts_daily + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :hot_smart_contracts_daily, fn repo, _ -> + Instrumenter.stats_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :hot_smart_contracts_daily + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [HotSmartContracts.t()]} + | {:error, [Changeset.t()]} + defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce HotSmartContracts ShareLocks order (see docs: sharelocks.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.date, &1.contract_address_hash}) + + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:date, :contract_address_hash], + on_conflict: on_conflict, + for: HotSmartContracts, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + :replace_all + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/stats.ex b/apps/explorer/lib/explorer/chain/import/stage/stats.ex new file mode 100644 index 000000000000..3f702097b877 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/stage/stats.ex @@ -0,0 +1,27 @@ +defmodule Explorer.Chain.Import.Stage.Stats do + @moduledoc """ + Imports stats data (hot_smart_contracts_daily). + """ + + alias Explorer.Chain.Import.{Runner, Stage} + + @behaviour Stage + + @runners [ + Runner.Stats.HotSmartContracts + ] + + @impl Stage + def runners, do: @runners + + @impl Stage + def all_runners, do: runners() + + @impl Stage + def multis(runner_to_changes_list, options) do + {final_multi, final_remaining_runner_to_changes_list} = + Stage.single_multi(@runners, runner_to_changes_list, options) + + {[final_multi], final_remaining_runner_to_changes_list} + end +end diff --git a/apps/explorer/lib/explorer/prometheus/instrumenter.ex b/apps/explorer/lib/explorer/prometheus/instrumenter.ex index 397e5de88fca..7876bad933e5 100644 --- a/apps/explorer/lib/explorer/prometheus/instrumenter.ex +++ b/apps/explorer/lib/explorer/prometheus/instrumenter.ex @@ -13,6 +13,14 @@ defmodule Explorer.Prometheus.Instrumenter do help: "Block import stage, runner and step in runner processing time" ] + @histogram [ + name: :stats_import_stage_runner_duration_microseconds, + labels: [:stats_type], + buckets: [1000, 5000, 10000, 100_000], + duration_unit: :microseconds, + help: "Stats import stage runner duration microseconds" + ] + @histogram [ name: :media_processing_time, buckets: :default, @@ -85,6 +93,14 @@ defmodule Explorer.Prometheus.Instrumenter do result end + def stats_import_stage_runner(function, stats_type) do + {time, result} = :timer.tc(function) + + Histogram.observe([name: :stats_import_stage_runner_duration_microseconds, labels: [stats_type]], time) + + result + end + def success_transactions_number(number) do Gauge.set([name: :success_transactions_number, registry: :public], number) end diff --git a/apps/explorer/lib/explorer/sorting_helper.ex b/apps/explorer/lib/explorer/sorting_helper.ex index 637c78bb1934..8ee6bcca82c9 100644 --- a/apps/explorer/lib/explorer/sorting_helper.ex +++ b/apps/explorer/lib/explorer/sorting_helper.ex @@ -61,8 +61,14 @@ defmodule Explorer.SortingHelper do |> merge_sorting_params_with_defaults(default_sorting) |> do_page_with_sorting() |> case do - nil -> query - dynamic_where -> query |> where(^dynamic_where.(key)) + nil -> + query + + dynamic_where -> + case query.group_bys do + [] -> query |> where(^dynamic_where.(key)) + _ -> query |> having(^dynamic_where.(key)) + end end |> limit_query(page_size) end diff --git a/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex new file mode 100644 index 000000000000..16ff4c8a7d47 --- /dev/null +++ b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex @@ -0,0 +1,199 @@ +defmodule Explorer.Stats.HotSmartContracts do + @moduledoc """ + This module defines the HotSmartContracts schema and functions for aggregating and paginating hot contracts. + """ + use Explorer.Schema + + alias Explorer.{Chain, PagingOptions, SortingHelper} + alias Explorer.Chain.{Address, Block, Hash, Transaction} + alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral + alias Explorer.Helper, as: ExplorerHelper + + import Explorer.Chain.Address.Reputation, only: [reputation_association: 0] + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + + @primary_key false + typed_schema "hot_smart_contracts_daily" do + field(:date, :date, primary_key: true) + field(:contract_address_hash, Hash.Address, primary_key: true) + field(:transactions_count, :integer) + field(:total_gas_used, :decimal) + + belongs_to(:contract_address, Address, + foreign_key: :contract_address_hash, + references: :hash, + type: Hash.Address, + define_field: false + ) + + timestamps() + end + + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = struct, params \\ %{}) do + struct + |> cast(params, [:date, :contract_address_hash, :transactions_count, :total_gas_used]) + |> validate_required([:date, :contract_address_hash, :transactions_count, :total_gas_used]) + end + + @spec aggregate_hot_smart_contracts_for_date(Date.t(), keyword()) :: + {:ok, + [%{contract_address_hash: Hash.Address.t(), transactions_count: integer(), total_gas_used: Decimal.t()}]} + | {:error, any()} + def aggregate_hot_smart_contracts_for_date(date, options \\ []) do + with {:ok, from_date} <- DateTime.new(date, ~T[00:00:00], "Etc/UTC"), + {:ok, to_date} <- date |> Date.add(1) |> DateTime.new(~T[00:00:00], "Etc/UTC"), + {:ok, from_block} <- + BlockReaderGeneral.timestamp_to_block_number(from_date, :after, options[:api?] || false, true), + {:ok, to_block} <- + BlockReaderGeneral.timestamp_to_block_number(to_date, :before, options[:api?] || false, true) do + {:ok, + from_block + |> aggregate_hot_smart_contracts_for_block_interval_query(to_block) + |> Chain.select_repo(options).all() + |> Enum.map(&Map.put(&1, :date, date))} + else + error -> {:error, error} + end + end + + @spec aggregate_hot_smart_contracts_for_block_interval_query(Block.block_number(), Block.block_number()) :: + Ecto.Query.t() + defp aggregate_hot_smart_contracts_for_block_interval_query(from_block, to_block) do + from(transaction in Transaction, + as: :transaction, + where: transaction.block_number >= ^from_block and transaction.block_number <= ^to_block, + inner_join: address in assoc(transaction, :to_address), + as: :to_address, + where: not is_nil(address.contract_code), + group_by: transaction.to_address_hash, + select: %{ + contract_address_hash: selected_as(transaction.to_address_hash, :contract_address_hash), + transactions_count: selected_as(count(), :transactions_count), + total_gas_used: selected_as(sum(transaction.gas_used), :total_gas_used) + } + ) + end + + @spec indexed_dates(keyword()) :: [Date.t()] + def indexed_dates(options \\ []) do + __MODULE__ + |> select([hot_smart_contracts_daily], hot_smart_contracts_daily.date) + |> distinct(true) + |> Chain.select_repo(options).all() + end + + @spec delete_older_than(Date.t(), keyword()) :: {non_neg_integer(), nil} + def delete_older_than(date, options \\ []) do + __MODULE__ + |> where([hot_smart_contracts_daily], hot_smart_contracts_daily.date < ^date) + |> Chain.select_repo(options).delete_all(timeout: :infinity) + end + + @doc """ + Paginate hot contracts by scale. + + ## Parameters + - `scale`: Time interval to aggregate hot contracts on (5m, 1h, 3h, 1d, 7d, 30d). + - `options`: The options to pass to the paginated function. + + ## Returns + - A list of hot contracts. + """ + @spec paginated(String.t(), keyword()) :: [__MODULE__.t()] | {:error, :not_found} + def paginated(scale, options \\ []) do + case scale do + "5m" -> last_n_seconds_paginated(300, options) + "1h" -> last_n_seconds_paginated(3600, options) + "3h" -> last_n_seconds_paginated(10800, options) + "1d" -> last_n_days_paginated(1, options) + "7d" -> last_n_days_paginated(7, options) + "30d" -> last_n_days_paginated(30, options) + _ -> raise "Invalid scale: #{scale}" + end + end + + defp last_n_seconds_paginated(n, options) do + default_sorting = [ + {:dynamic, :transactions_count, :desc_nulls_last, transactions_count_on_transactions_dynamic()}, + {:dynamic, :total_gas_used, :desc_nulls_last, total_gas_used_on_transactions_dynamic()}, + {:asc, :to_address_hash, :transaction} + ] + + paging_options = Keyword.get(options, :paging_options, PagingOptions.default_paging_options()) + sorting_options = Keyword.get(options, :sorting, %{})[:aggregated_on_transactions] || [] + + preloads = + Keyword.get(options, :preloads, + contract_address: [:names, proxy_implementations_association(), reputation_association()] + ) + + now = DateTime.utc_now() + from_timestamp = DateTime.add(now, -n, :second) + + with {:ok, from_block} <- + BlockReaderGeneral.timestamp_to_block_number(from_timestamp, :after, options[:api?] || false, true), + {:ok, to_block} <- + BlockReaderGeneral.timestamp_to_block_number(now, :before, options[:api?] || false, true) do + from_block + |> aggregate_hot_smart_contracts_for_block_interval_query(to_block) + |> ExplorerHelper.maybe_hide_scam_addresses(:to_address_hash, options) + |> SortingHelper.apply_sorting(sorting_options, default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) + |> Chain.select_repo(options).all() + |> Enum.map(&struct(__MODULE__, &1)) + |> Chain.select_repo(options).preload(preloads) + end + end + + defp last_n_days_paginated(n, options) do + default_sorting = [ + {:dynamic, :transactions_count, :desc_nulls_last, transactions_count_dynamic()}, + {:dynamic, :total_gas_used, :desc_nulls_last, total_gas_used_dynamic()}, + asc: :contract_address_hash + ] + + paging_options = Keyword.get(options, :paging_options, PagingOptions.default_paging_options()) + sorting_options = Keyword.get(options, :sorting, %{})[:aggregated_on_hot_smart_contracts] || [] + + preloads = + Keyword.get(options, :preloads, + contract_address: [:names, proxy_implementations_association(), reputation_association()] + ) + + __MODULE__ + |> where([hot_smart_contracts_daily], hot_smart_contracts_daily.date >= ^Date.add(Date.utc_today(), -n)) + |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash, options) + |> group_by([hot_smart_contracts_daily], hot_smart_contracts_daily.contract_address_hash) + |> select([hot_smart_contracts_daily], %{ + contract_address_hash: hot_smart_contracts_daily.contract_address_hash, + transactions_count: sum(hot_smart_contracts_daily.transactions_count), + total_gas_used: sum(hot_smart_contracts_daily.total_gas_used) + }) + |> SortingHelper.apply_sorting(sorting_options, default_sorting) + |> SortingHelper.page_with_sorting(paging_options, sorting_options, default_sorting) + |> Chain.select_repo(options).all() + |> Enum.map(&struct(__MODULE__, &1)) + |> Chain.select_repo(options).preload(preloads) + end + + @spec transactions_count_dynamic() :: Ecto.Query.dynamic_expr() + def transactions_count_dynamic do + dynamic([hot_smart_contracts_daily], sum(hot_smart_contracts_daily.transactions_count)) + end + + @spec total_gas_used_dynamic() :: Ecto.Query.dynamic_expr() + def total_gas_used_dynamic do + dynamic([hot_smart_contracts_daily], sum(hot_smart_contracts_daily.total_gas_used)) + end + + @spec transactions_count_on_transactions_dynamic() :: Ecto.Query.dynamic_expr() + def transactions_count_on_transactions_dynamic do + dynamic([transaction], count()) + end + + @spec total_gas_used_on_transactions_dynamic() :: Ecto.Query.dynamic_expr() + def total_gas_used_on_transactions_dynamic do + dynamic([transaction], sum(transaction.gas_used)) + end +end diff --git a/apps/explorer/priv/repo/migrations/20251020144203_add_hot_smart_contracts_daily.exs b/apps/explorer/priv/repo/migrations/20251020144203_add_hot_smart_contracts_daily.exs new file mode 100644 index 000000000000..cb424d2653a8 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251020144203_add_hot_smart_contracts_daily.exs @@ -0,0 +1,24 @@ +defmodule Explorer.Repo.Migrations.AddHotContractsDaily do + use Ecto.Migration + + def change do + create table(:hot_smart_contracts_daily, primary_key: false) do + add(:date, :date, null: false, primary_key: true) + add(:contract_address_hash, :bytea, null: false, primary_key: true) + add(:transactions_count, :integer, null: false) + add(:total_gas_used, :numeric, precision: 100, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + end + + create( + index(:hot_smart_contracts_daily, ["date DESC", "total_gas_used DESC"], name: :idx_hot_smart_contracts_date_gas) + ) + + create( + index(:hot_smart_contracts_daily, ["date DESC", "transactions_count DESC"], + name: :idx_hot_smart_contracts_date_transactions_count + ) + ) + end +end diff --git a/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex new file mode 100644 index 000000000000..5958db408f91 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex @@ -0,0 +1,78 @@ +defmodule Indexer.Fetcher.Stats.HotSmartContracts do + @moduledoc """ + This module defines the HotSmartContracts fetcher for indexing hot contracts. + """ + use Indexer.Fetcher, restart: :permanent + + use GenServer + alias Explorer.Chain + alias Explorer.Stats.HotSmartContracts + + require Logger + + @retry_interval 10_000 + @max_days_ago 30 + + @impl GenServer + def init(opts) do + GenServer.cast(__MODULE__, :check_completeness) + + schedule_next_day_fetch() + + {:ok, opts} + end + + @spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()} + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl GenServer + def handle_info({:fetch_for_date, date, new_day?}, state) do + with {:ok, hot_smart_contracts} <- HotSmartContracts.aggregate_hot_smart_contracts_for_date(date), + {:ok, _} <- Chain.import(%{hot_smart_contracts_daily: %{params: hot_smart_contracts}, timeout: :infinity}) do + if new_day? do + schedule_next_day_fetch() + HotSmartContracts.delete_older_than(Date.add(date, 1 - @max_days_ago)) + end + + Logger.info("Hot contracts fetched for #{date}") + else + {:error, error} -> + Process.send_after(self(), {:fetch_for_date, date, new_day?}, @retry_interval) + Logger.error("Error fetching hot contracts for #{date}: #{inspect(error)}") + end + + {:noreply, state} + end + + @impl GenServer + def handle_info(_, state) do + {:noreply, state} + end + + @impl GenServer + def handle_cast(:check_completeness, state) do + indexed_dates = HotSmartContracts.indexed_dates() + + today = Date.utc_today() + + Enum.each(1..@max_days_ago, fn days_ago -> + date = Date.add(today, -days_ago) + + if date not in indexed_dates do + Process.send_after(self(), {:fetch_for_date, date, false}, @retry_interval) + end + end) + + {:noreply, state} + end + + defp schedule_next_day_fetch do + today = Date.utc_today() + now = DateTime.utc_now() + next_day = Date.utc_today() |> Date.add(1) |> DateTime.new!(~T[00:00:00], "Etc/UTC") + delay = DateTime.diff(next_day, now, :millisecond) + Process.send_after(self(), {:fetch_for_date, today, true}, delay) + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 90c6074a3176..aeeaa80b739b 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -30,6 +30,7 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue alias Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, as: MultichainSearchDbTokenInfoExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability + alias Indexer.Fetcher.Stats.HotSmartContracts alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry alias Indexer.Fetcher.TokenInstance.Sanitize, as: TokenInstanceSanitize @@ -298,7 +299,8 @@ defmodule Indexer.Supervisor do [name: BlockCatchup.Supervisor] ] ), - {Withdrawal.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]} + {Withdrawal.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, + configure(HotSmartContracts.Supervisor, [[memory_monitor: memory_monitor]]) ] |> List.flatten() diff --git a/config/runtime.exs b/config/runtime.exs index dd19cb692bed..9d0df840a908 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1100,6 +1100,10 @@ config :indexer, Indexer.Fetcher.MultichainSearchDb.CountersFetcher.Supervisor, is_nil(microservice_multichain_search_url) || !transactions_stats_enabled +config :indexer, Indexer.Fetcher.Stats.HotSmartContracts.Supervisor, + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_HOT_SMART_CONTRACTS_FETCHER"), + enabled: !ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_HOT_SMART_CONTRACTS_FETCHER") + config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE", 10), interval: ConfigHelper.parse_time_env_var("INDEXER_EMPTY_BLOCKS_SANITIZER_INTERVAL", "10s"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 33cd0e44e0f8..e594f005c071 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -414,6 +414,7 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_BEACON_DEPOSIT_STATUS_FETCHER_REFERENCE_TIMESTAMP # MISSING_BALANCE_OF_TOKENS_WINDOW_SIZE= # INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE= +# INDEXER_DISABLE_HOT_CONTRACTS_FETCHER= # WEBAPP_URL= # API_URL= # CHECKSUM_ADDRESS_HASHES=true From 9f1c80a3998b52e921e6897676c353e7b819e268 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:24:50 +0300 Subject: [PATCH 0482/1006] chore(deps): bump open_api_spex from 3.22.0 to 3.22.1 (#13619) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 39608f573693..2804622e4a8e 100644 --- a/mix.lock +++ b/mix.lock @@ -125,7 +125,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, - "open_api_spex": {:hex, :open_api_spex, "3.22.0", "fbf90dc82681dc042a4ee79853c8e989efbba73d9e87439085daf849bbf8bc20", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "dd751ddbdd709bb4a5313e9a24530da6e66594773c7242a0c2592cbd9f589063"}, + "open_api_spex": {:hex, :open_api_spex, "3.22.1", "38c99d8bf107dc7ffb112dc669f33e0a287396c6c9fd6bb7eeb27e3fe8dbba0e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "fa51ecd04ececbad89a8ede55ebd9db7aa9e55cc7ddbb46455522e0f3c098290"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, From 10c508422fd5ded54237a9ef5012abc7fa0e8747 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:25:07 +0300 Subject: [PATCH 0483/1006] chore(deps): bump plug_cowboy from 2.7.4 to 2.7.5 (#13617) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 2804622e4a8e..c35ea0b3ce89 100644 --- a/mix.lock +++ b/mix.lock @@ -139,7 +139,7 @@ "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.7.4", "729c752d17cf364e2b8da5bdb34fb5804f56251e88bb602aff48ae0bd8673d11", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9b85632bd7012615bae0a5d70084deb1b25d2bcbb32cab82d1e9a1e023168aa3"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, From c7181c54b23546099a2cc28590294b4f9a4d74e3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:40:16 +0300 Subject: [PATCH 0484/1006] chore(deps): bump absinthe from 1.8.0 to 1.9.0 (#13616) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c35ea0b3ce89..c74ddd4ddbf4 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "absinthe": {:hex, :absinthe, "1.8.0", "c2d941e951210ebd2494b4263d4354ca0a6d886d6dbf0cefaa21fe716a888a95", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0ac63d2f07db21913e58d662e4415768986e1bd0d2cd48e2417cfe200441161b"}, + "absinthe": {:hex, :absinthe, "1.9.0", "28f11753d01c0e8b6cb6e764a23cf4081e0e6cae88f53f4c9e4320912aee9c07", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "db65993420944ad90e932827663d4ab704262b007d4e3900cd69615f14ccc8ce"}, "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.6.0", "73590bdb0dcc192622f39fa11da6fca9df4e339b603c45ec7a93493eb94f1829", [:mix], [{:absinthe, ">= 1.7.10", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "32d6397a7af3fd02678ef9bc8e2f574487f14593cb3e4f9110fb1c695d4d2ac0"}, From e82c3cd7b51d00dcf9217c245658919f0d35f76a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:40:38 +0300 Subject: [PATCH 0485/1006] chore(deps): bump httpoison from 2.2.3 to 2.3.0 (#13614) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index c74ddd4ddbf4..75d031293ff7 100644 --- a/mix.lock +++ b/mix.lock @@ -87,7 +87,7 @@ "hammer_backend_redis": {:hex, :hammer_backend_redis, "7.1.0", "b1e3d5d9cb9a549d90109d17c12eb0c57ffe4658163e597542ad77c3c5464a46", [:mix], [{:hammer, "~> 7.0", [hex: :hammer, repo: "hexpm", optional: false]}, {:redix, "~> 1.5", [hex: :redix, repo: "hexpm", optional: false]}], "hexpm", "b0c22a0121a293002c09f415fc3a04e153c993db8702b4320b1aa9a6edd6de1a"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, - "httpoison": {:hex, :httpoison, "2.2.3", "a599d4b34004cc60678999445da53b5e653630651d4da3d14675fedc9dd34bd6", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "fa0f2e3646d3762fdc73edb532104c8619c7636a6997d20af4003da6cfc53e53"}, + "httpoison": {:hex, :httpoison, "2.3.0", "10eef046405bc44ba77dc5b48957944df8952cc4966364b3cf6aa71dce6de587", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d388ee70be56d31a901e333dbcdab3682d356f651f93cf492ba9f06056436a2c"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "image": {:hex, :image, "0.62.1", "1dd3d8d0d29d6562aa2141b5ef08c0f6a60e2a9f843fe475499b2f4f1ef60406", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "5a5a7acaf68cfaed8932d478b95152cd7d84071442cac558c59f2d31427e91ab"}, "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, From e2d31b0a29551152cc8b6ddfe569c7d9b2230f4c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 25 Nov 2025 10:41:24 +0300 Subject: [PATCH 0486/1006] chore: Drop token_instances_token_id_index index (#13598) --- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 3 +- apps/explorer/lib/explorer/application.ex | 4 ++ .../chain/cache/background_migrations.ex | 11 +++- .../migrator/heavy_db_index_operation.ex | 1 + .../drop_token_instances_token_id_index.ex | 60 +++++++++++++++++++ 6 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_token_instances_token_id_index.ex diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index ba8ad635c221..d4c7237ba75b 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -173,7 +173,8 @@ for index_operation <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex ] do config :explorer, index_operation, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 09dcd525fcd6..801692fd13e1 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -105,7 +105,8 @@ for migrator <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex ] do config :explorer, migrator, enabled: false end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 8b20286ef082..67d1c376c027 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -318,6 +318,10 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.CreateTransactionsOperatorFeeConstantIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 026dde330827..5fe698ead594 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -55,7 +55,8 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :heavy_indexes_drop_transactions_to_address_hash_with_pending_index_finished, key: :heavy_indexes_create_logs_deposits_withdrawals_index_finished, key: :heavy_indexes_create_addresses_transactions_count_desc_partial_index_finished, - key: :heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished + key: :heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished, + key: :heavy_indexes_drop_token_instances_token_id_index_finished @dialyzer :no_match @@ -88,6 +89,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do DropLogsAddressHashTransactionHashIndex, DropLogsBlockNumberAscIndexAscIndex, DropLogsIndexIndex, + DropTokenInstancesTokenIdIndex, DropTokenTransfersBlockNumberAscLogIndexAscIndex, DropTokenTransfersBlockNumberIndex, DropTokenTransfersFromAddressHashTransactionHashIndex, @@ -333,6 +335,13 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end + defp handle_fallback(:heavy_indexes_drop_token_instances_token_id_index_finished) do + set_and_return_migration_status( + DropTokenInstancesTokenIdIndex, + &set_heavy_indexes_drop_token_instances_token_id_index_finished/1 + ) + end + defp handle_fallback(:sanitize_verified_addresses_finished) do {:return, false} end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex index 230b6d9a99ab..2aa9fa8433fa 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex @@ -27,6 +27,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation do | :logs | :internal_transactions | :token_transfers + | :token_instances | :addresses | :smart_contracts | :arbitrum_batch_l2_blocks diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_token_instances_token_id_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_token_instances_token_id_index.ex new file mode 100644 index 000000000000..ff0fd4b43733 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_token_instances_token_id_index.ex @@ -0,0 +1,60 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex do + @moduledoc """ + Drops index "token_instances_token_id_index" btree (token_id). + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus} + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :token_instances + @index_name "token_instances_token_id_index" + @operation_type :drop + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations do + [] + end + + @impl HeavyDbIndexOperation + def db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + operation = HeavyDbIndexOperationHelper.drop_index_query_string(@index_name) + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_dropping_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache do + BackgroundMigrations.set_heavy_indexes_drop_token_instances_token_id_index_finished(true) + end +end From d9b8f48679214d5616fc41aa0728ccbd1123db8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:42:31 +0300 Subject: [PATCH 0487/1006] chore(deps): bump websockex from 0.4.3 to 0.5.0 (#13615) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/ethereum_jsonrpc/mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 4c5a080c17b4..aa2671ba63b2 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -88,7 +88,7 @@ defmodule EthereumJSONRPC.MixProject do {:poolboy, "~> 1.5.2"}, {:logger_json, "~> 5.1"}, {:utils, in_umbrella: true}, - {:websockex, "~> 0.4.3"} + {:websockex, "~> 0.5.0"} ] end end diff --git a/mix.lock b/mix.lock index 75d031293ff7..5eec863b4600 100644 --- a/mix.lock +++ b/mix.lock @@ -184,6 +184,6 @@ "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, - "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, + "websockex": {:hex, :websockex, "0.5.0", "3caac5f676a2c6ba2eab7bf2828991cd14f7281f936b03deccf041c8b948a5a2", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2f4b08ba6f9531df3debbd0a40a0c9a1336552eb10fabbdfa2d2e31b4e445896"}, "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"}, } From 1c193e7a97aef0a2edfea563a9f0011925a7bf9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:49:07 +0300 Subject: [PATCH 0488/1006] chore(deps): bump ex_cldr from 2.44.0 to 2.44.1 (#13612) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 5eec863b4600..6e37bb402783 100644 --- a/mix.lock +++ b/mix.lock @@ -55,7 +55,7 @@ "ex_aws": {:hex, :ex_aws, "2.6.0", "346e87e35e5df0b3c016a96fb30adf6001de102981a71648dfc3ce3ad04765af", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "30729ee9cbaacda674a4e4260d74206fa89bcd712267c4eaf42a0fc34592c0b3"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, - "ex_cldr": {:hex, :ex_cldr, "2.44.0", "19af18b3c9477ba8888cfd2b0874894dd007b649d323380d1094297d80394b10", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "58260baac6308156595cd0a2b81c09ac8e05dd64fd2a19c8b0ef309fdcdcd7ee"}, + "ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.36.0", "d0de502db2fa24eeae16749af1af0093f24fb823e8715c66076cd8c6a3a960e3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "17640b8daf2580a0a11317a720a26079e774d4c36f939d82f4e9f7075269897d"}, From 149003d46dabef4ad0d2e26f4a78ab5247fc7b97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:49:27 +0300 Subject: [PATCH 0489/1006] chore(deps): bump prometheus from 6.0.3 to 6.1.1 (#13618) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 6e37bb402783..4ec7dd70a5b5 100644 --- a/mix.lock +++ b/mix.lock @@ -144,7 +144,7 @@ "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, - "prometheus": {:hex, :prometheus, "6.0.3", "95302236124c0f919163a7762bf7d2b171b919b6ff6148d26eb38a5d2def7b81", [:rebar3], [{:ddskerl, "0.4.2", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "53554ecadac0354066801d514d1a244dd026175e4ee3a9a30192b71d530c8268"}, + "prometheus": {:hex, :prometheus, "6.1.1", "3c9d8e2d4fcf948450550693f6a82fae013d3665ba10aa55955b64c2875fadb3", [:rebar3], [{:ddskerl, "0.4.2", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "c3ecc7a35676948089e28ff4383ccb85dcef447f0982c01e5630a084008c456a"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:hex, :prometheus_ex, "5.0.0", "8e5514e385d5408b559792698dee2cfa60faed3b354d2a1046622923ecad4a68", [:mix], [{:prometheus, "~> 6.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "148bf80b110799362152e4bb154b508cba233456a171b5de6fe97e652bc90019"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, From ef67ae26eecbe3d84804a90e13ab53d315f97e5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 13:42:40 +0300 Subject: [PATCH 0490/1006] chore(deps): bump redix from 1.5.2 to 1.5.3 (#13613) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 4ec7dd70a5b5..6b80dae4a595 100644 --- a/mix.lock +++ b/mix.lock @@ -154,7 +154,7 @@ "que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"}, "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, "recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"}, - "redix": {:hex, :redix, "1.5.2", "ab854435a663f01ce7b7847f42f5da067eea7a3a10c0a9d560fa52038fd7ab48", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "78538d184231a5d6912f20567d76a49d1be7d3fca0e1aaaa20f4df8e1142dcb8"}, + "redix": {:hex, :redix, "1.5.3", "4eaae29c75e3285c0ff9957046b7c209aa7f72a023a17f0a9ea51c2a50ab5b0f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b06fb5246373af41f5826b03334dfa3f636347d4d5d98b4d455b699d425ae7e"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"}, From b323109db3e6b25b4b935fd1a87a8ff12123559a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 25 Nov 2025 14:41:07 +0300 Subject: [PATCH 0491/1006] feat: Indexer monitor Prometheus metrics (#13539) --- .../lib/block_scout_web/application.ex | 6 +- .../controllers/api/v2/config_controller.ex | 3 +- apps/explorer/lib/explorer/application.ex | 2 + .../explorer/chain/metrics/indexer_metrics.ex | 87 ++++++++ .../{metrics.ex => metrics/public_metrics.ex} | 10 +- .../chain/metrics/queries/indexer_metrics.ex | 211 ++++++++++++++++++ .../public_chain_metrics.ex} | 4 +- .../lib/explorer/prometheus/instrumenter.ex | 177 ++++++++++++++- apps/indexer/lib/indexer/application.ex | 2 + .../lib/indexer/block/catchup/fetcher.ex | 6 +- apps/indexer/lib/indexer/block/fetcher.ex | 4 +- .../lib/indexer/block/realtime/fetcher.ex | 8 +- .../lib/indexer/prometheus/instrumenter.ex | 123 ++++++---- config/runtime.exs | 17 +- docker-compose/envs/common-blockscout.env | 6 + 15 files changed, 588 insertions(+), 78 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex rename apps/explorer/lib/explorer/chain/{metrics.ex => metrics/public_metrics.ex} (88%) create mode 100644 apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex rename apps/explorer/lib/explorer/chain/metrics/{queries.ex => queries/public_chain_metrics.ex} (98%) diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index 78d3e3276cf2..cef8ca91acca 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -79,7 +79,8 @@ defmodule BlockScoutWeb.Application do } alias BlockScoutWeb.Utility.EventHandlersMetrics - alias Explorer.Chain.Metrics, as: ChainMetrics + alias Explorer.Chain.Metrics.IndexerMetrics + alias Explorer.Chain.Metrics.PublicMetrics, as: PublicChainMetrics Exporter.setup() PublicExporter.setup() @@ -103,7 +104,8 @@ defmodule BlockScoutWeb.Application do {BlocksIndexedCounter, name: BlocksIndexedCounter}, {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, {EventHandlersMetrics, []}, - {ChainMetrics, []}, + {PublicChainMetrics, []}, + {IndexerMetrics, []}, Hammer.child_for_supervisor() ], [ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index a39bfda1c026..9234fb5540fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -18,7 +18,8 @@ defmodule BlockScoutWeb.API.V2.ConfigController do end def public_metrics(conn, _params) do - public_metrics_update_period_hours = Application.get_env(:explorer, Explorer.Chain.Metrics)[:update_period_hours] + public_metrics_update_period_hours = + Application.get_env(:explorer, Explorer.Chain.Metrics.PublicMetrics)[:update_period_hours] conn |> put_status(200) diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 67d1c376c027..684cf2e6a65a 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -36,12 +36,14 @@ defmodule Explorer.Application do alias Explorer.Market.MarketHistoryCache alias Explorer.MicroserviceInterfaces.MultichainSearch + alias Explorer.Prometheus.Instrumenter alias Explorer.Repo.PrometheusLogger alias Explorer.Utility.Hammer @impl Application def start(_type, _args) do PrometheusLogger.setup() + Instrumenter.setup() :telemetry.attach( "prometheus-ecto", diff --git a/apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex b/apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex new file mode 100644 index 000000000000..7e8363639286 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex @@ -0,0 +1,87 @@ +defmodule Explorer.Chain.Metrics.IndexerMetrics do + @moduledoc """ + Module responsible for periodically setting indexer metrics. + """ + + use GenServer + + alias Explorer.Chain.Metrics.Queries.IndexerMetrics, as: IndexerMetricsQueries + alias Explorer.Prometheus.Instrumenter + + @interval :timer.hours(1) + @default_metrics_list [ + :missing_blocks_count, + :missing_internal_transactions_count, + :multichain_search_db_main_export_queue_count, + :multichain_search_db_export_balances_queue_count, + :multichain_search_db_export_counters_queue_count, + :multichain_search_db_export_token_info_queue_count + ] + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(_) do + if Application.get_env(:explorer, __MODULE__)[:enabled] do + send(self(), :set_metrics) + {:ok, %{}} + else + :ignore + end + end + + def handle_info(:set_metrics, state) do + schedule_next_run() + set_metrics() + + {:noreply, state} + end + + defp metrics_list do + additional_metrics = + Application.get_env(:explorer, __MODULE__)[:specific_metrics_enabled?] + |> Enum.filter(fn {_, enabled?} -> enabled? == true end) + |> Enum.map(fn {metric, _} -> metric end) + + @default_metrics_list ++ additional_metrics + end + + defp set_metrics do + metrics_list() + |> Enum.map(fn metric -> + Task.async(fn -> + set_handler_metric(metric) + end) + end) + |> Task.yield_many(:timer.hours(1)) + |> Enum.map(fn {_task, res} -> + case res do + {:ok, result} -> + result + + {:exit, reason} -> + raise "Query fetching indexer metrics terminated: #{inspect(reason)}" + + nil -> + raise "Query fetching indexer metrics timed out." + end + end) + end + + # sobelow_skip ["DOS.StringToAtom"] + defp set_handler_metric(metric) do + func = String.to_atom(to_string(metric)) + + items_count = + IndexerMetricsQueries + |> apply(func, []) + + apply(Instrumenter, metric, [items_count]) + end + + defp schedule_next_run do + Process.send_after(self(), :set_metrics, @interval) + end +end diff --git a/apps/explorer/lib/explorer/chain/metrics.ex b/apps/explorer/lib/explorer/chain/metrics/public_metrics.ex similarity index 88% rename from apps/explorer/lib/explorer/chain/metrics.ex rename to apps/explorer/lib/explorer/chain/metrics/public_metrics.ex index b90e925e42d0..5b5b33c22eda 100644 --- a/apps/explorer/lib/explorer/chain/metrics.ex +++ b/apps/explorer/lib/explorer/chain/metrics/public_metrics.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain.Metrics do +defmodule Explorer.Chain.Metrics.PublicMetrics do @moduledoc """ Module responsible for periodically setting current chain metrics. """ @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Metrics do import Explorer.Chain, only: [select_repo: 1] - alias Explorer.Chain.Metrics.Queries + alias Explorer.Chain.Metrics.Queries.PublicChainMetrics, as: PublicChainMetricsQueries alias Explorer.Prometheus.Instrumenter @interval :timer.hours(1) @@ -69,12 +69,12 @@ defmodule Explorer.Chain.Metrics do defp set_handler_metric(metric) do func = String.to_atom(to_string(metric) <> "_query") - transactions_count = - Queries + items_count = + PublicChainMetricsQueries |> apply(func, []) |> select_repo(@options).one() - apply(Instrumenter, metric, [transactions_count]) + apply(Instrumenter, metric, [items_count]) end defp schedule_next_run do diff --git a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex new file mode 100644 index 000000000000..30caccbfb3af --- /dev/null +++ b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex @@ -0,0 +1,211 @@ +defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do + @moduledoc """ + Module for DB queries to get indexer health metrics + """ + + import Ecto.Query + alias Ecto.Adapters.SQL + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue + alias Explorer.Chain.MultichainSearchDb.CountersExportQueue, as: MultichainSearchDbCountersExportQueue + alias Explorer.Chain.MultichainSearchDb.MainExportQueue, as: MultichainSearchDbMainExportQueue + alias Explorer.Chain.MultichainSearchDb.TokenInfoExportQueue, as: MultichainSearchDbTokenInfoExportQueue + alias Explorer.Chain.PendingBlockOperation + alias Explorer.Chain.Token.Instance + alias Explorer.Repo + + @doc """ + Query to get the number of missing block numbers in the DB + """ + # sobelow_skip ["SQL"] + @spec missing_blocks_count() :: integer() + def missing_blocks_count do + sql_string = """ + SELECT COUNT(DISTINCT b1.number) AS missing_blocks_count + FROM generate_series(0, (SELECT MAX(number) FROM blocks)) AS b1(number) + WHERE NOT EXISTS ( + SELECT 1 FROM blocks b2 + WHERE b2.number=b1.number + AND b2.consensus + ); + """ + + case SQL.query(Repo, sql_string) do + {:ok, %Postgrex.Result{command: :select, columns: ["missing_blocks_count"], rows: [[missing_blocks_count]]}} -> + missing_blocks_count + + _ -> + 0 + end + end + + @doc """ + Query to get the number of missing internal transactions in the DB + """ + @spec missing_internal_transactions_count() :: integer() + def missing_internal_transactions_count do + Repo.aggregate(PendingBlockOperation, :count, :block_hash) + end + + @doc """ + Query to get the count of current token balances with missing values + """ + # sobelow_skip ["SQL"] + @spec missing_current_token_balances_count() :: integer() + def missing_current_token_balances_count do + sql_string = + """ + SELECT COUNT(1) as missing_current_token_balances_count + FROM address_current_token_balances ctb + WHERE (ctb.value_fetched_at is NULL OR ctb.value is NULL) + AND NOT EXISTS( + SELECT 1 FROM address_token_balances tb + WHERE tb.address_hash = ctb.address_hash + AND tb.token_contract_address_hash = ctb.token_contract_address_hash + AND tb.retries_count is not null + ); + """ + + case SQL.query(Repo, sql_string) do + {:ok, + %Postgrex.Result{ + command: :select, + columns: ["missing_current_token_balances_count"], + rows: [[missing_current_token_balances_count]] + }} -> + missing_current_token_balances_count + + _ -> + 0 + end + end + + @doc """ + Query to get the count of archival token balances with missing values + """ + @spec missing_archival_token_balances_count() :: integer() + def missing_archival_token_balances_count do + query = + from( + token_balance in TokenBalance, + where: is_nil(token_balance.value_fetched_at) + ) + + query + |> Repo.aggregate(:count, :id) + end + + @doc """ + Query to get the count of token instances with failed metadata fetches + """ + @spec failed_token_instances_metadata_count() :: integer() + def failed_token_instances_metadata_count do + query = + from( + token_instance in Instance, + where: not is_nil(token_instance.error) + ) + + query + |> Repo.aggregate(:count, :token_id) + end + + @doc """ + Query to get the count of unfetched token instances + """ + # sobelow_skip ["SQL"] + @spec unfetched_token_instances_count() :: integer() + def unfetched_token_instances_count do + sql_string = + """ + SELECT COUNT(1) AS unfetched_token_instances_count + FROM ( + SELECT DISTINCT ON (s0."contract_address_hash", s0."token_id") s0."contract_address_hash", s0."token_id" + FROM ( + SELECT ss0."token_contract_address_hash" AS "contract_address_hash", ss0."token_id" AS "token_id" + FROM ( + SELECT sst0."token_contract_address_hash" AS "token_contract_address_hash", unnest(sst0."token_ids") AS "token_id" + FROM "token_transfers" AS sst0) AS ss0 INNER JOIN ( + SELECT sst0."contract_address_hash" AS "contract_address_hash" + FROM "tokens" AS sst0 + WHERE ((sst0."type" = 'ERC-1155') OR (sst0."type" = 'ERC-721')) + ) AS ss1 ON ss1."contract_address_hash" = ss0."token_contract_address_hash" + LEFT OUTER JOIN "token_instances" AS st2 ON (ss0."token_contract_address_hash" = st2."token_contract_address_hash") + AND (ss0."token_id" = st2."token_id") + WHERE (st2."token_id" IS NULL) + ) AS s0 + ) AS a; + """ + + case SQL.query(Repo, sql_string) do + {:ok, + %Postgrex.Result{ + command: :select, + columns: ["unfetched_token_instances_count"], + rows: [[unfetched_token_instances_count]] + }} -> + unfetched_token_instances_count + + _ -> + 0 + end + end + + @doc """ + Query to get the count of token instances not uploaded to CDN + """ + # sobelow_skip ["SQL"] + @spec token_instances_not_uploaded_to_cdn_count() :: integer() + def token_instances_not_uploaded_to_cdn_count do + sql_string = + """ + SELECT COUNT(1) AS token_instances_not_uploaded_to_cdn_count + FROM token_instances WHERE metadata IS NOT NULL AND thumbnails IS NULL AND cdn_upload_error IS NULL; + """ + + case SQL.query(Repo, sql_string) do + {:ok, + %Postgrex.Result{ + command: :select, + columns: ["token_instances_not_uploaded_to_cdn_count"], + rows: [[token_instances_not_uploaded_to_cdn_count]] + }} -> + token_instances_not_uploaded_to_cdn_count + + _ -> + 0 + end + end + + @doc """ + Query to get the count of multichain_search_db_export_balances_queue entries + """ + @spec multichain_search_db_export_balances_queue_count() :: integer() + def multichain_search_db_export_balances_queue_count do + Repo.aggregate(MultichainSearchDbBalancesExportQueue, :count, :id) + end + + @doc """ + Query to get the count of multichain_search_db_export_counters_queue entries + """ + @spec multichain_search_db_export_counters_queue_count() :: integer() + def multichain_search_db_export_counters_queue_count do + Repo.aggregate(MultichainSearchDbCountersExportQueue, :count, :timestamp) + end + + @doc """ + Query to get the count of multichain_search_db_export_token_info_queue entries + """ + @spec multichain_search_db_export_token_info_queue_count() :: integer() + def multichain_search_db_export_token_info_queue_count do + Repo.aggregate(MultichainSearchDbTokenInfoExportQueue, :count, :address_hash) + end + + @doc """ + Query to get the count of multichain_search_db_main_export_queue entries + """ + @spec multichain_search_db_main_export_queue_count() :: integer() + def multichain_search_db_main_export_queue_count do + Repo.aggregate(MultichainSearchDbMainExportQueue, :count, :hash) + end +end diff --git a/apps/explorer/lib/explorer/chain/metrics/queries.ex b/apps/explorer/lib/explorer/chain/metrics/queries/public_chain_metrics.ex similarity index 98% rename from apps/explorer/lib/explorer/chain/metrics/queries.ex rename to apps/explorer/lib/explorer/chain/metrics/queries/public_chain_metrics.ex index 927f762ce428..3b1baa59b607 100644 --- a/apps/explorer/lib/explorer/chain/metrics/queries.ex +++ b/apps/explorer/lib/explorer/chain/metrics/queries/public_chain_metrics.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain.Metrics.Queries do +defmodule Explorer.Chain.Metrics.Queries.PublicChainMetrics do @moduledoc """ Module for DB queries to get chain metrics exposed at /public-metrics endpoint """ @@ -268,6 +268,6 @@ defmodule Explorer.Chain.Metrics.Queries do end defp update_period_hours do - Application.get_env(:explorer, Explorer.Chain.Metrics)[:update_period_hours] + Application.get_env(:explorer, Explorer.Chain.Metrics.PublicMetrics)[:update_period_hours] end end diff --git a/apps/explorer/lib/explorer/prometheus/instrumenter.ex b/apps/explorer/lib/explorer/prometheus/instrumenter.ex index 7876bad933e5..41704a36cb6f 100644 --- a/apps/explorer/lib/explorer/prometheus/instrumenter.ex +++ b/apps/explorer/lib/explorer/prometheus/instrumenter.ex @@ -1,6 +1,6 @@ defmodule Explorer.Prometheus.Instrumenter do @moduledoc """ - Blocks fetch and import metrics for `Prometheus`. + Explorer metrics for `Prometheus`. """ use Prometheus.Metric @@ -21,12 +21,7 @@ defmodule Explorer.Prometheus.Instrumenter do help: "Stats import stage runner duration microseconds" ] - @histogram [ - name: :media_processing_time, - buckets: :default, - duration_unit: :seconds, - help: "Time in seconds taken for media resizing and uploading" - ] + # Public chain metrics exposed at /public_metrics endpoint @gauge [ name: :success_transactions_number, @@ -71,6 +66,31 @@ defmodule Explorer.Prometheus.Instrumenter do registry: :public ] + # metrics of indexing monitor + @gauge [name: :missing_blocks_count, help: "Number of blocks missing in the chain"] + @gauge [ + name: :missing_internal_transactions_count, + help: "Number of blocks with not yet fetched internal transactions" + ] + @gauge [name: :missing_current_token_balances_count, help: "Number of missing current token balances"] + @gauge [name: :missing_archival_token_balances_count, help: "Number of missing token balances in history"] + @gauge [name: :unfetched_token_instances_count, help: "Number of unfetched token instances"] + @gauge [name: :failed_token_instances_metadata_count, help: "Number of failed token instances metadata"] + @gauge [name: :token_instances_not_uploaded_to_cdn_count, help: "Token instances not uploaded to CDN"] + @gauge [name: :multichain_search_db_main_export_queue_count, help: "Size of the main multichain export queue"] + @gauge [name: :multichain_search_db_export_balances_queue_count, help: "Size of the balances export queue"] + @gauge [name: :multichain_search_db_export_counters_queue_count, help: "Size of the counters export queue"] + @gauge [name: :multichain_search_db_export_token_info_queue_count, help: "Size of the token info export queue"] + + # metrics of NFT media handler + + @histogram [ + name: :media_processing_time, + buckets: :default, + duration_unit: :seconds, + help: "Time in seconds taken for media resizing and uploading" + ] + @counter [ name: :successfully_uploaded_media_number, help: "Number of successfully uploaded media to CDN", @@ -85,6 +105,15 @@ defmodule Explorer.Prometheus.Instrumenter do @gauge [name: :batch_average_time, help: "L2 average batch time"] + def setup do + prepare_batch_metric([]) + end + + @doc """ + Defines the metric for the full processing time of a block (in microseconds). + """ + @spec block_import_stage_runner(function :: (-> any()), stage :: atom(), runner :: atom(), step :: atom()) :: + any() def block_import_stage_runner(function, stage, runner, step) do {time, result} = :timer.tc(function) @@ -93,6 +122,11 @@ defmodule Explorer.Prometheus.Instrumenter do result end + @doc """ + Defines the metric for the full processing time of a stats import stage runner + (in microseconds). + """ + @spec stats_import_stage_runner(function :: (-> any()), stats_type :: atom()) :: any() def stats_import_stage_runner(function, stats_type) do {time, result} = :timer.tc(function) @@ -101,50 +135,169 @@ defmodule Explorer.Prometheus.Instrumenter do result end + @doc """ + Defines the metric for the number of successful transactions in the period (default is 1 day). + """ + @spec success_transactions_number(number :: integer()) :: :ok def success_transactions_number(number) do Gauge.set([name: :success_transactions_number, registry: :public], number) end - def media_processing_time(seconds) do - Histogram.observe([name: :media_processing_time], seconds) - end - + @doc """ + Defines the metric for the number of weekly successful transactions. + """ + @spec weekly_success_transactions_number(number :: integer()) :: :ok def weekly_success_transactions_number(number) do Gauge.set([name: :weekly_success_transactions_number, registry: :public], number) end + @doc """ + Defines the metric for the number of deployed smart-contracts in the period (default is 1 day). + """ + @spec deployed_smart_contracts_number(number :: integer()) :: :ok def deployed_smart_contracts_number(number) do Gauge.set([name: :deployed_smart_contracts_number, registry: :public], number) end + @doc """ + Defines the metric for the number of verified smart-contracts in the period (default is 1 day). + """ + @spec verified_smart_contracts_number(number :: integer()) :: :ok def verified_smart_contracts_number(number) do Gauge.set([name: :verified_smart_contracts_number, registry: :public], number) end + @doc """ + Defines the metric for the number of new addresses in the period (default is 1 day). + """ + @spec new_addresses_number(number :: integer()) :: :ok def new_addresses_number(number) do Gauge.set([name: :new_addresses_number, registry: :public], number) end + @doc """ + Defines the metric for the number of new tokens in the period (default is 1 day). + """ + @spec new_tokens_number(number :: integer()) :: :ok def new_tokens_number(number) do Gauge.set([name: :new_tokens_number, registry: :public], number) end + @doc """ + Defines the metric for the number of new token transfers in the period (default is 1 day). + """ + @spec new_token_transfers_number(number :: integer()) :: :ok def new_token_transfers_number(number) do Gauge.set([name: :new_token_transfers_number, registry: :public], number) end + @doc """ + Defines the metric for the number of active EOA addresses in the period (default is 1 day). + """ + @spec simplified_active_addresses_number(number :: integer()) :: :ok def simplified_active_addresses_number(number) do Gauge.set([name: :active_addresses_number, registry: :public], number) end + @doc """ + Defines the metric for the number of blocks missing in the chain. + """ + @spec missing_blocks_count(integer()) :: :ok + def missing_blocks_count(value), do: Gauge.set([name: :missing_blocks_count], value) + + @doc """ + Defines the metric for the number of blocks with not yet fetched internal transactions. + """ + @spec missing_internal_transactions_count(integer()) :: :ok + def missing_internal_transactions_count(value), do: Gauge.set([name: :missing_internal_transactions_count], value) + + @doc """ + Defines the metric for the number of missing current token balances. + """ + @spec missing_current_token_balances_count(integer()) :: :ok + def missing_current_token_balances_count(value), + do: Gauge.set([name: :missing_current_token_balances_count], value) + + @doc """ + Defines the metric for the number of missing token balances in history. + """ + @spec missing_archival_token_balances_count(integer()) :: :ok + def missing_archival_token_balances_count(value), do: Gauge.set([name: :missing_archival_token_balances_count], value) + + @doc """ + Defines the metric for the number of unfetched token instances. + """ + @spec unfetched_token_instances_count(integer()) :: :ok + def unfetched_token_instances_count(value), + do: Gauge.set([name: :unfetched_token_instances_count], value) + + @doc """ + Defines the metric for the number of failed token instances metadata. + """ + @spec failed_token_instances_metadata_count(integer()) :: :ok + def failed_token_instances_metadata_count(value), + do: Gauge.set([name: :failed_token_instances_metadata_count], value) + + @doc """ + Defines the metric for the number of token instances not uploaded to CDN. + """ + @spec token_instances_not_uploaded_to_cdn_count(integer()) :: :ok + def token_instances_not_uploaded_to_cdn_count(value), + do: Gauge.set([name: :token_instances_not_uploaded_to_cdn_count], value) + + @doc """ + Defines the metric for the size of the main multichain export queue. + """ + @spec multichain_search_db_main_export_queue_count(integer()) :: :ok + def multichain_search_db_main_export_queue_count(value), + do: Gauge.set([name: :multichain_search_db_main_export_queue_count], value) + + @doc """ + Defines the metric for the size of the balances export queue. + """ + @spec multichain_search_db_export_balances_queue_count(integer()) :: :ok + def multichain_search_db_export_balances_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_balances_queue_count], value) + + @doc """ + Defines the metric for the size of the counters export queue. + """ + @spec multichain_search_db_export_counters_queue_count(integer()) :: :ok + def multichain_search_db_export_counters_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_counters_queue_count], value) + + @doc """ + Defines the metric for the size of the token info export queue. + """ + @spec multichain_search_db_export_token_info_queue_count(integer()) :: :ok + def multichain_search_db_export_token_info_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_token_info_queue_count], value) + + @doc """ + Defines the metric for time taken for media resizing and uploading (in seconds). + """ + @spec media_processing_time(number()) :: :ok + def media_processing_time(seconds) do + Histogram.observe([name: :media_processing_time], seconds) + end + + @doc """ + Increments the counter for successfully uploaded media to the CDN. + """ + @spec increment_successfully_uploaded_media_number() :: :ok def increment_successfully_uploaded_media_number do Counter.inc(name: :successfully_uploaded_media_number, registry: :public) end + @doc """ + Increments the counter for failed media uploads to the CDN. + """ + @spec increment_failed_uploading_media_number() :: :ok def increment_failed_uploading_media_number do Counter.inc(name: :failed_uploading_media_number, registry: :public) end + @spec batch_average_time(integer()) :: :ok defp batch_average_time(average_time) do Gauge.set([name: :batch_average_time], average_time) end @@ -201,6 +354,8 @@ defmodule Explorer.Prometheus.Instrumenter do def prepare_batch_metric(batches) do case batches do [] -> + batch_average_time(0) + {:error, :not_found} [batch] -> diff --git a/apps/indexer/lib/indexer/application.ex b/apps/indexer/lib/indexer/application.ex index 06ef190f0261..19ce93a01230 100644 --- a/apps/indexer/lib/indexer/application.ex +++ b/apps/indexer/lib/indexer/application.ex @@ -16,9 +16,11 @@ defmodule Indexer.Application do alias Indexer.Fetcher.TokenInstance.Refetch, as: TokenInstanceRefetch alias Indexer.Memory + alias Indexer.Prometheus.Instrumenter @impl Application def start(_type, _args) do + Instrumenter.setup() memory_monitor_name = Memory.Monitor json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index a6b0752522b4..3f113874bd50 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -179,7 +179,7 @@ defmodule Indexer.Block.Catchup.Fetcher do {fetch_duration, result} = :timer.tc(fn -> fetch_and_import_range(block_fetcher, range) end) - Prometheus.Instrumenter.block_full_process(fetch_duration, __MODULE__) + Prometheus.Instrumenter.set_block_full_process(fetch_duration, __MODULE__) case result do {:ok, %{errors: errors}} -> @@ -188,13 +188,13 @@ defmodule Indexer.Block.Catchup.Fetcher do {:ok, %{range: range, errors: valid_errors}} {:error, {:import = step, [%Changeset{} | _] = changesets}} = error -> - Prometheus.Instrumenter.import_errors() + Prometheus.Instrumenter.set_import_errors_count() Logger.error(fn -> ["failed to validate: ", inspect(changesets), ". Retrying."] end, step: step) error {:error, {:import = step, reason}} = error -> - Prometheus.Instrumenter.import_errors() + Prometheus.Instrumenter.set_import_errors_count() Logger.error(fn -> [inspect(reason), ". Retrying."] end, step: step) if reason == :timeout, do: add_range_to_massive_blocks(range) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index eb9719f9cf70..49d9fe537cb5 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -282,7 +282,7 @@ defmodule Indexer.Block.Fetcher do timeout: :infinity })} do inserted = Map.merge(inserted, inserted_transaction_actions) - Prometheus.Instrumenter.block_batch_fetch(fetch_time, callback_module) + Prometheus.Instrumenter.set_block_batch_fetch(fetch_time, callback_module) result = {:ok, %{inserted: inserted, errors: blocks_errors}} update_block_cache(inserted[:blocks]) update_transactions_cache(inserted[:transactions]) @@ -472,7 +472,7 @@ defmodule Indexer.Block.Fetcher do no_blocks_to_import = length(options_with_broadcast.blocks.params) if no_blocks_to_import != 0 do - Prometheus.Instrumenter.block_import(import_time / no_blocks_to_import, callback_module) + Prometheus.Instrumenter.set_block_import(import_time / no_blocks_to_import, callback_module) end result diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 5e64fca5c5e3..61b0d465f383 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -430,7 +430,7 @@ defmodule Indexer.Block.Realtime.Fetcher do {fetch_duration, result} = :timer.tc(fn -> fetch_and_import_range(block_fetcher, block_number_to_fetch..block_number_to_fetch) end) - Prometheus.Instrumenter.block_full_process(fetch_duration, __MODULE__) + Prometheus.Instrumenter.set_block_full_process(fetch_duration, __MODULE__) case result do {:ok, %{inserted: inserted, errors: []}} -> @@ -448,7 +448,7 @@ defmodule Indexer.Block.Realtime.Fetcher do end) {:error, {:import = step, [%Changeset{} | _] = changesets}} -> - Prometheus.Instrumenter.import_errors() + Prometheus.Instrumenter.set_import_errors_count() params = %{ changesets: changesets, @@ -473,7 +473,7 @@ defmodule Indexer.Block.Realtime.Fetcher do end {:error, {:import = step, reason}} -> - Prometheus.Instrumenter.import_errors() + Prometheus.Instrumenter.set_import_errors_count() Logger.error(fn -> inspect(reason) end, step: step) {:error, {step, reason}} -> @@ -504,7 +504,7 @@ defmodule Indexer.Block.Realtime.Fetcher do defp log_import_timings(%{blocks: [%{number: number, timestamp: timestamp}]}, fetch_duration, time_before) do node_delay = Timex.diff(time_before, timestamp, :seconds) - Prometheus.Instrumenter.node_delay(node_delay) + Prometheus.Instrumenter.set_json_rpc_node_delay(node_delay) Logger.debug("Block #{number} fetching duration: #{fetch_duration / 1_000_000}s. Node delay: #{node_delay}s.", fetcher: :block_import_timings diff --git a/apps/indexer/lib/indexer/prometheus/instrumenter.ex b/apps/indexer/lib/indexer/prometheus/instrumenter.ex index 47a71417fc6f..c8fc996b2a4a 100644 --- a/apps/indexer/lib/indexer/prometheus/instrumenter.ex +++ b/apps/indexer/lib/indexer/prometheus/instrumenter.ex @@ -4,7 +4,9 @@ defmodule Indexer.Prometheus.Instrumenter do """ use Prometheus.Metric - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + + alias EthereumJSONRPC.Utility.RangesHelper @rollups [:arbitrum, :zksync, :optimism, :polygon_zkevm, :scroll] @@ -32,8 +34,6 @@ defmodule Indexer.Prometheus.Instrumenter do help: "Block fetch batch request processing time" ] - @gauge [name: :missing_block_count, help: "Number of missing blocks in the database"] - @gauge [name: :delay_from_last_node_block, help: "Delay from the last block on the node in seconds"] @counter [name: :import_errors_count, help: "Number of database import errors"] @@ -44,39 +44,79 @@ defmodule Indexer.Prometheus.Instrumenter do @gauge [name: :latest_block_timestamp, help: "Latest block timestamp"] - def block_full_process(time, fetcher) do + @spec setup() :: :ok + def setup do + min_blockchain_block_number = + RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :block_ranges)) + + set_latest_block_number(min_blockchain_block_number) + set_latest_block_timestamp(0) + + if chain_type() in @rollups do + set_latest_batch_number(0) + set_latest_batch_timestamp(0) + end + + :ok + end + + @doc """ + Defines the metric for the full processing time of a block (in microseconds). + """ + @spec set_block_full_process(time :: integer(), fetcher :: atom()) :: :ok + def set_block_full_process(time, fetcher) do Histogram.observe([name: :block_full_processing_duration_microseconds, labels: [fetcher]], time) end - def block_import(time, fetcher) do + @doc """ + Defines the metric for the import time of a block (in microseconds). + """ + @spec set_block_import(time :: float(), fetcher :: atom()) :: :ok + def set_block_import(time, fetcher) do Histogram.observe([name: :block_import_duration_microseconds, labels: [fetcher]], time) end - def block_batch_fetch(time, fetcher) do + @doc """ + Defines the metric for the block batch fetch request time (in microseconds). + """ + @spec set_block_batch_fetch(time :: integer(), fetcher :: atom()) :: :ok + def set_block_batch_fetch(time, fetcher) do Histogram.observe([name: :block_batch_fetch_request_duration_microseconds, labels: [fetcher]], time) end - def missing_blocks(missing_block_count) do - Gauge.set([name: :missing_block_count], missing_block_count) - end - - def node_delay(delay) do + @doc """ + Defines the metric for JSON-RPC node response delay (in seconds) during block import. + """ + @spec set_json_rpc_node_delay(delay :: integer()) :: :ok + def set_json_rpc_node_delay(delay) do Gauge.set([name: :delay_from_last_node_block], delay) end - def import_errors(error_count \\ 1) do + @doc """ + Defines the metric for the number of import errors encountered during block processing. + """ + @spec set_import_errors_count(error_count :: integer()) :: :ok + def set_import_errors_count(error_count \\ 1) do Counter.inc([name: :import_errors_count], error_count) end + @doc """ + Defines the metric for memory consumed by a specific fetcher (in MB). + """ + @spec set_memory_consumed(fetcher :: nil | atom() | String.t(), memory :: float()) :: :ok + def set_memory_consumed(nil, _memory), do: :ok + def set_memory_consumed(fetcher, memory) do Gauge.set([name: :memory_consumed, labels: [fetcher]], memory) end - defp latest_block_number(number) do + @spec set_latest_block_number(number :: integer()) :: :ok + defp set_latest_block_number(number) do Gauge.set([name: :latest_block_number], number) end - defp latest_block_timestamp(timestamp) do + @spec set_latest_block_timestamp(timestamp :: integer()) :: :ok + defp set_latest_block_timestamp(timestamp) do Gauge.set([name: :latest_block_timestamp], timestamp) end @@ -90,47 +130,36 @@ defmodule Indexer.Prometheus.Instrumenter do """ @spec set_latest_block(number :: integer, timestamp :: DateTime.t()) :: :ok def set_latest_block(number, timestamp) do - latest_block_number(number) - latest_block_timestamp(DateTime.to_unix(timestamp)) + set_latest_block_number(number) + set_latest_block_timestamp(DateTime.to_unix(timestamp)) end - if @chain_type in @rollups do - @gauge [name: :latest_batch_number, help: "L2 latest batch number"] - - @gauge [name: :latest_batch_timestamp, help: "L2 latest batch timestamp"] - - defp latest_batch_number(number) do - Gauge.set([name: :latest_batch_number], number) - end + @gauge [name: :latest_batch_number, help: "L2 latest batch number"] - defp latest_batch_timestamp(timestamp) do - Gauge.set([name: :latest_batch_timestamp], timestamp) - end + @gauge [name: :latest_batch_timestamp, help: "L2 latest batch timestamp"] - @doc """ - Generates the latest batch number and timestamp Prometheus metrics. + defp set_latest_batch_number(number) do + Gauge.set([name: :latest_batch_number], number) + end - ## Parameters + defp set_latest_batch_timestamp(timestamp) do + Gauge.set([name: :latest_batch_timestamp], timestamp) + end - - `number`: The batch number to set. - - `timestamp`: The timestamp of the batch as a `DateTime` struct. - """ - @spec set_latest_batch(number :: integer, timestamp :: DateTime.t()) :: :ok - def set_latest_batch(number, timestamp) do - latest_batch_number(number) - latest_batch_timestamp(DateTime.to_unix(timestamp)) - end - else - @doc """ - Generates the latest batch number and timestamp Prometheus metrics. + @doc """ + Generates the latest batch number and timestamp Prometheus metrics. - ## Parameters + ## Parameters - - `number`: The batch number to set. - - `timestamp`: The timestamp of the batch as a `DateTime` struct. - """ - @spec set_latest_batch(number :: integer, timestamp :: DateTime.t()) :: :ok - def set_latest_batch(_number, _timestamp) do + - `number`: The batch number to set. + - `timestamp`: The timestamp of the batch as a `DateTime` struct. + """ + @spec set_latest_batch(number :: integer, timestamp :: DateTime.t()) :: :ok + def set_latest_batch(number, timestamp) do + if chain_type() in @rollups do + set_latest_batch_number(number) + set_latest_batch_timestamp(DateTime.to_unix(timestamp)) + else :ok end end diff --git a/config/runtime.exs b/config/runtime.exs index 9d0df840a908..4f66a2c4be15 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -856,10 +856,25 @@ config :explorer, Explorer.Chain.TokenTransfer, whitelisted_weth_contracts: ConfigHelper.parse_list_env_var("WHITELISTED_WETH_CONTRACTS", ""), weth_token_transfers_filtering_enabled: ConfigHelper.parse_bool_env_var("WETH_TOKEN_TRANSFERS_FILTERING_ENABLED") -config :explorer, Explorer.Chain.Metrics, +config :explorer, Explorer.Chain.Metrics.PublicMetrics, enabled: ConfigHelper.parse_bool_env_var("PUBLIC_METRICS_ENABLED", "false"), update_period_hours: ConfigHelper.parse_integer_env_var("PUBLIC_METRICS_UPDATE_PERIOD_HOURS", 24) +config :explorer, Explorer.Chain.Metrics.IndexerMetrics, + enabled: ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED", "true"), + specific_metrics_enabled?: %{ + token_instances_not_uploaded_to_cdn_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT", "false"), + failed_token_instances_metadata_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT", "true"), + unfetched_token_instances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT", "true"), + missing_current_token_balances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT", "true"), + missing_archival_token_balances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT", "true") + } + config :explorer, Explorer.Chain.Filecoin.NativeAddress, network_prefix: ConfigHelper.parse_catalog_value("FILECOIN_NETWORK_PREFIX", ["f", "t"], true, "f") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index e594f005c071..714eb49872b5 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -594,6 +594,12 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # WHITELISTED_WETH_CONTRACTS= # PUBLIC_METRICS_ENABLED= # PUBLIC_METRICS_UPDATE_PERIOD_HOURS= +# INDEXER_METRICS_ENABLED= +# INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT= +# INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT= +# INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT= +# INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT= +# INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT= # CSV_EXPORT_LIMIT= # SHRINK_INTERNAL_TRANSACTIONS_ENABLED= NFT_MEDIA_HANDLER_ENABLED=true From c6f63310bece834ab441fb0ffd2dcd7a796b4710 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Tue, 25 Nov 2025 17:27:39 +0400 Subject: [PATCH 0492/1006] feat: Support ZRC-2 tokens for `zilliqa` chain type (#13443) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../account/api/v2/user_controller.ex | 96 +-- .../account/watchlist_address_controller.ex | 17 +- .../controllers/api/v2/address_controller.ex | 11 +- .../api/v2/advanced_filter_controller.ex | 11 +- .../controllers/api/v2/token_controller.ex | 14 +- .../tokens/instance/holder_controller.ex | 7 +- .../tokens/instance/metadata_controller.ex | 3 +- .../tokens/instance/transfer_controller.ex | 7 +- .../controllers/tokens/instance_controller.ex | 3 +- .../lib/block_scout_web/paging_helper.ex | 9 +- .../block_scout_web/schemas/api/v2/general.ex | 35 +- .../block_scout_web/schemas/api/v2/token.ex | 17 +- .../views/account/api/v2/user_view.ex | 31 +- .../views/api/rpc/address_view.ex | 6 + .../views/api/v2/token_view.ex | 6 + .../views/api/v2/zilliqa_view.ex | 25 + .../block_scout_web/views/tokens/helper.ex | 59 +- .../views/tokens/holder_view.ex | 4 + .../block_scout_web/views/transaction_view.ex | 1 + .../account/api/v2/user_controller_test.exs | 13 +- .../lib/explorer/account/notifier/email.ex | 3 + .../lib/explorer/account/notifier/notify.ex | 2 + .../lib/explorer/account/notifier/summary.ex | 2 +- .../lib/explorer/account/watchlist_address.ex | 113 ++- apps/explorer/lib/explorer/chain.ex | 52 +- apps/explorer/lib/explorer/chain/address.ex | 29 + .../explorer/chain/address/token_balance.ex | 3 +- apps/explorer/lib/explorer/chain/block.ex | 6 + .../explorer/chain/import/runner/blocks.ex | 36 +- .../runner/zilliqa/zrc2/token_adapters.ex | 79 ++ .../runner/zilliqa/zrc2/token_transfers.ex | 110 +++ .../chain/import/stage/chain_type_specific.ex | 4 +- apps/explorer/lib/explorer/chain/log.ex | 40 +- apps/explorer/lib/explorer/chain/token.ex | 19 + .../lib/explorer/chain/transaction.ex | 12 + .../chain/transaction/state_change.ex | 2 +- .../chain/zilliqa/zrc2/token_adapter.ex | 97 +++ .../chain/zilliqa/zrc2/token_transfer.ex | 220 ++++++ apps/explorer/lib/explorer/etherscan.ex | 30 +- ...15930_account_watchlist_zrc2_addresses.exs | 17 + ...0251003112047_create_zrc2_token_tables.exs | 39 + apps/explorer/test/support/factory.ex | 59 +- apps/indexer/lib/indexer/block/fetcher.ex | 37 + .../indexer/fetcher/zilliqa/zrc2_tokens.ex | 697 ++++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 1 + .../lib/indexer/transform/addresses.ex | 36 + config/runtime.exs | 2 + cspell.json | 2 + 48 files changed, 1959 insertions(+), 165 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_adapters.ex create mode 100644 apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_transfers.ex create mode 100644 apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_adapter.ex create mode 100644 apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_transfer.ex create mode 100644 apps/explorer/priv/account/migrations/20251029115930_account_watchlist_zrc2_addresses.exs create mode 100644 apps/explorer/priv/zilliqa/migrations/20251003112047_create_zrc2_token_tables.exs create mode 100644 apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex index 7e493ae4322a..8aca96eca49c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/user_controller.ex @@ -2,6 +2,8 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do alias Explorer.ThirdPartyIntegrations.Auth0 use BlockScoutWeb, :controller + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import BlockScoutWeb.Chain, @@ -102,32 +104,22 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do def create_watchlist(conn, %{ "address_hash" => address_hash, "name" => name, - "notification_settings" => %{ - "native" => %{ - "incoming" => watch_coin_input, - "outcoming" => watch_coin_output - }, - "ERC-20" => %{ - "incoming" => watch_erc_20_input, - "outcoming" => watch_erc_20_output - }, - "ERC-721" => %{ - "incoming" => watch_erc_721_input, - "outcoming" => watch_erc_721_output - }, - # "ERC-1155" => %{ - # "incoming" => watch_erc_1155_input, - # "outcoming" => watch_erc_1155_output - # }, - "ERC-404" => %{ - "incoming" => watch_erc_404_input, - "outcoming" => watch_erc_404_output - } - }, + "notification_settings" => notification_settings, "notification_methods" => %{ "email" => notify_email } }) do + watch_coin_input = notification_settings["native"]["incoming"] + watch_coin_output = notification_settings["native"]["outcoming"] + watch_erc_20_input = notification_settings["ERC-20"]["incoming"] + watch_erc_20_output = notification_settings["ERC-20"]["outcoming"] + watch_erc_721_input = notification_settings["ERC-721"]["incoming"] + watch_erc_721_output = notification_settings["ERC-721"]["outcoming"] + # watch_erc_1155_input = notification_settings["ERC-1155"]["incoming"] + # watch_erc_1155_output = notification_settings["ERC-1155"]["outcoming"] + watch_erc_404_input = notification_settings["ERC-404"]["incoming"] + watch_erc_404_output = notification_settings["ERC-404"]["outcoming"] + watchlist_params = %{ name: name, watch_coin_input: watch_coin_input, @@ -144,12 +136,23 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do address_hash: address_hash } + watchlist_params_extended = + if chain_type() == :zilliqa do + zrc2_notification_settings = Map.get(notification_settings, "ZRC-2", %{"incoming" => true, "outcoming" => true}) + + watchlist_params + |> Map.put(:watch_zrc_2_input, zrc2_notification_settings["incoming"]) + |> Map.put(:watch_zrc_2_output, zrc2_notification_settings["outcoming"]) + else + watchlist_params + end + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, {:watchlist, %{watchlists: [watchlist | _]}} <- {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, {:ok, watchlist_address} <- - WatchlistAddress.create(Map.put(watchlist_params, :watchlist_id, watchlist.id)) do + WatchlistAddress.create(Map.put(watchlist_params_extended, :watchlist_id, watchlist.id)) do conn |> put_status(200) |> render(:watchlist_address, %{ @@ -163,32 +166,22 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do "id" => watchlist_address_id, "address_hash" => address_hash, "name" => name, - "notification_settings" => %{ - "native" => %{ - "incoming" => watch_coin_input, - "outcoming" => watch_coin_output - }, - "ERC-20" => %{ - "incoming" => watch_erc_20_input, - "outcoming" => watch_erc_20_output - }, - "ERC-721" => %{ - "incoming" => watch_erc_721_input, - "outcoming" => watch_erc_721_output - }, - # "ERC-1155" => %{ - # "incoming" => watch_erc_1155_input, - # "outcoming" => watch_erc_1155_output - # }, - "ERC-404" => %{ - "incoming" => watch_erc_404_input, - "outcoming" => watch_erc_404_output - } - }, + "notification_settings" => notification_settings, "notification_methods" => %{ "email" => notify_email } }) do + watch_coin_input = notification_settings["native"]["incoming"] + watch_coin_output = notification_settings["native"]["outcoming"] + watch_erc_20_input = notification_settings["ERC-20"]["incoming"] + watch_erc_20_output = notification_settings["ERC-20"]["outcoming"] + watch_erc_721_input = notification_settings["ERC-721"]["incoming"] + watch_erc_721_output = notification_settings["ERC-721"]["outcoming"] + # watch_erc_1155_input = notification_settings["ERC-1155"]["incoming"] + # watch_erc_1155_output = notification_settings["ERC-1155"]["outcoming"] + watch_erc_404_input = notification_settings["ERC-404"]["incoming"] + watch_erc_404_output = notification_settings["ERC-404"]["outcoming"] + watchlist_params = %{ id: watchlist_address_id, name: name, @@ -206,12 +199,23 @@ defmodule BlockScoutWeb.Account.API.V2.UserController do address_hash: address_hash } + zrc2_notification_settings = Map.get(notification_settings, "ZRC-2") + + watchlist_params_extended = + if chain_type() == :zilliqa and not is_nil(zrc2_notification_settings) do + watchlist_params + |> Map.put(:watch_zrc_2_input, zrc2_notification_settings["incoming"]) + |> Map.put(:watch_zrc_2_output, zrc2_notification_settings["outcoming"]) + else + watchlist_params + end + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(uid)}, {:watchlist, %{watchlists: [watchlist | _]}} <- {:watchlist, Repo.account_repo().preload(identity, :watchlists)}, {:ok, watchlist_address} <- - WatchlistAddress.update(Map.put(watchlist_params, :watchlist_id, watchlist.id)) do + WatchlistAddress.update(Map.put(watchlist_params_extended, :watchlist_id, watchlist.id)) do conn |> put_status(200) |> render(:watchlist_address, %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex index 55236895a427..6cd44551d2d2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/watchlist_address_controller.ex @@ -1,6 +1,8 @@ defmodule BlockScoutWeb.Account.WatchlistAddressController do use BlockScoutWeb, :controller + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + alias Explorer.Account.WatchlistAddress import BlockScoutWeb.Account.AuthController, only: [authenticate!: 1] @@ -71,10 +73,10 @@ defmodule BlockScoutWeb.Account.WatchlistAddressController do "watch_erc_721_input" => watch_nft_input, "watch_erc_721_output" => watch_nft_output, "notify_email" => notify_email - }, + } = params, watchlist_id ) do - %{ + attributes = %{ address_hash: address_hash, name: name, watch_coin_input: watch_coin_input, @@ -88,5 +90,16 @@ defmodule BlockScoutWeb.Account.WatchlistAddressController do notify_email: notify_email, watchlist_id: watchlist_id } + + if chain_type() == :zilliqa do + watch_zrc_2_input = Map.get(params, "watch_zrc_2_input", true) + watch_zrc_2_output = Map.get(params, "watch_zrc_2_output", true) + + attributes + |> Map.put(:watch_zrc_2_input, watch_zrc_2_input) + |> Map.put(:watch_zrc_2_output, watch_zrc_2_output) + else + attributes + end end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 9f644f85fd48..084296fddd1e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -1,6 +1,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do use BlockScoutWeb, :controller - use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] + use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity], chain_type: [:explorer, :chain_type] use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] use OpenApiSpex.ControllerSpecs @@ -284,10 +284,15 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + if @chain_type == :zilliqa do + @token_balances_operation_description "Retrieves all token balances held by a specific address, including ERC-20, ERC-721, ERC-1155, ERC-404, and ZRC-2 tokens." + else + @token_balances_operation_description "Retrieves all token balances held by a specific address, including ERC-20, ERC-721, ERC-1155, and ERC-404 tokens." + end + operation :token_balances, summary: "List all token balances held by a specific address", - description: - "Retrieves all token balances held by a specific address, including ERC-20, ERC-721, ERC-1155 and ERC-404 tokens.", + description: @token_balances_operation_description, parameters: [address_hash_param() | base_params()], responses: [ ok: diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 3ff0aaa6a30d..2f08a88f82dc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do use BlockScoutWeb, :controller + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] import BlockScoutWeb.Chain, only: [split_list_by_page: 1, next_page_params: 5, fetch_scam_token_toggle: 2] import Explorer.PagingOptions, only: [default_paging_options: 0] @@ -234,7 +235,15 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do ] end - @allowed_transaction_types ~w(COIN_TRANSFER CONTRACT_INTERACTION CONTRACT_CREATION ERC-20 ERC-404 ERC-721 ERC-1155) + @default_allowed_transaction_types ~w(COIN_TRANSFER CONTRACT_INTERACTION CONTRACT_CREATION ERC-20 ERC-404 ERC-721 ERC-1155) + + if @chain_type == :zilliqa do + @chain_type_allowed_transaction_types ~w(ZRC-2) + else + @chain_type_allowed_transaction_types ~w() + end + + @allowed_transaction_types @default_allowed_transaction_types ++ @chain_type_allowed_transaction_types defp prepare_transaction_types(transaction_types) when is_binary(transaction_types) do transaction_types diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 26e61388a74e..79733c327414 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -268,7 +268,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {:ok, holder_address_hash}} <- {:format, Chain.string_to_address_hash(holder_address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(holder_address_hash_string, params) do holder_address_with_proxy_implementations = @@ -356,7 +356,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @token_options)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)}, {:ok, token_instance} <- Instance.nft_instance_by_token_id_and_token_address(token_id, address_hash, @api_true) do @@ -416,7 +416,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do paging_options = paging_options(params) @@ -470,7 +470,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do paging_options = paging_options(params) @@ -520,7 +520,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)} do conn |> put_status(200) @@ -677,7 +677,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)}, + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)}, {:format, {token_id, ""}} <- {:format, Integer.parse(token_id_string)}, {:ok, token_instance} <- Instance.nft_instance_by_token_id_and_token_address(token_id, address_hash, @api_true) do @@ -721,7 +721,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, - {:not_found, false} <- {:not_found, Chain.erc_20_token?(token)} do + {:not_found, false} <- {:not_found, Chain.erc_20_token?(token) or Token.zrc_2_token?(token)} do NFTCollectionMetadataRefetchOnDemand.trigger_refetch(ip, token) conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex index e23f27835d4c..c0f8eda41f2d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/holder_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do alias BlockScoutWeb.Tokens.HolderView alias BlockScoutWeb.Tokens.Instance.Helper alias Explorer.Chain - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Token} alias Explorer.Chain.Token.Instance alias Phoenix.View @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(address_hash), - false <- Chain.erc_20_token?(token), + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token), {token_id, ""} <- Integer.parse(token_id_string), token_holders <- Chain.fetch_token_holders_from_token_hash_and_token_id(address_hash, token_id, paging_options(params)) do @@ -54,12 +54,13 @@ defmodule BlockScoutWeb.Tokens.Instance.HolderController do end end + # credo:disable-for-next-line Credo.Check.Design.DuplicatedCode def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.erc_20_token?(token), + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token), {token_id, ""} <- Integer.parse(token_id_string) do case Instance.nft_instance_by_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex index 55d6846f1cca..5d66eb18d349 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.Tokens.Instance.MetadataController do alias BlockScoutWeb.Tokens.Instance.Helper alias Explorer.Chain + alias Explorer.Chain.Token alias Explorer.Chain.Token.Instance def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do @@ -10,7 +11,7 @@ defmodule BlockScoutWeb.Tokens.Instance.MetadataController do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.erc_20_token?(token), + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token), {token_id, ""} <- Integer.parse(token_id_string), {:ok, token_instance} <- Instance.nft_instance_by_token_id_and_token_address(token_id, hash) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex index ef95c9f21d2e..2c437333b4d9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do alias BlockScoutWeb.Tokens.Instance.Helper alias BlockScoutWeb.Tokens.TransferView alias Explorer.Chain - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Token} alias Explorer.Chain.Token.Instance alias Phoenix.View @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string, "type" => "JSON"} = params) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash), - false <- Chain.erc_20_token?(token), + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token), {token_id, ""} <- Integer.parse(token_id_string), token_transfers <- Chain.fetch_token_transfers_from_token_hash_and_token_id(hash, token_id, paging_options(params)) do @@ -57,12 +57,13 @@ defmodule BlockScoutWeb.Tokens.Instance.TransferController do end end + # credo:disable-for-next-line Credo.Check.Design.DuplicatedCode def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id_string}) do options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash, options), - false <- Chain.erc_20_token?(token), + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token), {token_id, ""} <- Integer.parse(token_id_string) do case Instance.nft_instance_by_token_id_and_token_address(token_id, hash) do {:ok, token_instance} -> Helper.render(conn, token_instance, hash, token_id, token) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex index fd222f9294e6..601bc0a0d629 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex @@ -3,11 +3,12 @@ defmodule BlockScoutWeb.Tokens.InstanceController do alias BlockScoutWeb.Controller alias Explorer.Chain + alias Explorer.Chain.Token def show(conn, %{"token_id" => token_address_hash, "id" => token_id}) do with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash), {:ok, token} <- Chain.token_from_address_hash(hash), - false <- Chain.erc_20_token?(token) do + false <- Chain.erc_20_token?(token) or Token.zrc_2_token?(token) do token_instance_transfer_path = conn |> token_instance_transfer_path(:index, token_address_hash, token_id) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index b81e38abdce6..3e53bda1a14c 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -17,8 +17,15 @@ defmodule BlockScoutWeb.PagingHelper do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @allowed_filter_labels ["validated", "pending"] + @allowed_base_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + if @chain_type == :zilliqa do + @allowed_chain_type_token_transfer_type_labels ["ZRC-2"] + else + @allowed_chain_type_token_transfer_type_labels [] + end - @allowed_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + @allowed_token_transfer_type_labels @allowed_base_token_transfer_type_labels ++ + @allowed_chain_type_token_transfer_type_labels @allowed_nft_type_labels ["ERC-721", "ERC-1155", "ERC-404"] @allowed_chain_id [1, 56, 99] @allowed_stability_validators_states ["active", "probation", "inactive"] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 5b809bbe3269..487301e1af42 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -27,7 +27,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @address_hash_pattern ~r"^0x([A-Fa-f0-9]{40})$" @full_hash_pattern ~r"^0x([A-Fa-f0-9]{64})$" @hex_string_pattern ~r"^0x([A-Fa-f0-9]*)$" - @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*\]?$/i + + if @chain_type == :zilliqa do + @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2)(,(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2))*\]?$/i + else + @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*\]?$/i + end + # Matches ISO-like datetime strings where separators between time fields can be ':' or percent-encoded '%3A'. # Accepts examples like: # - "2025-10-12T09" @@ -457,6 +463,23 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @token_type_param_description """ + Filter by token type. Comma-separated list of: + * ERC-20 - Fungible tokens + * ERC-721 - Non-fungible tokens + * ERC-1155 - Multi-token standard + * ERC-404 - Hybrid fungible/non-fungible tokens + #{if @chain_type == :zilliqa do + """ + * ZRC-2 - Fungible tokens on Zilliqa + """ + else + "" + end} + + Example: `ERC-20,ERC-721` to show both fungible and NFT transfers + """ + @doc """ Returns a parameter definition for filtering by token type. """ @@ -474,15 +497,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do ] }, required: false, - description: """ - Filter by token type. Comma-separated list of: - * ERC-20 - Fungible tokens - * ERC-721 - Non-fungible tokens - * ERC-1155 - Multi-token standard - * ERC-404 - Hybrid fungible/non-fungible tokens - - Example: `ERC-20,ERC-721` to show both fungible and NFT transfers - """, + description: @token_type_param_description, name: :type } end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index 227ce67ca5fb..04fb6a3a0c80 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -107,13 +107,18 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.Type do """ require OpenApiSpex + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + @token_types ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + + if @chain_type == :zilliqa do + @chain_type_token_types ["ZRC-2"] + else + @chain_type_token_types [] + end + OpenApiSpex.schema(%{ type: :string, - enum: [ - "ERC-20", - "ERC-721", - "ERC-1155", - "ERC-404" - ] + enum: @token_types ++ @chain_type_token_types }) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex index 360a188dcab4..2c92484f2084 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex @@ -97,14 +97,8 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do @spec prepare_watchlist_address(WatchlistAddress.t(), Chain.Address.t(), map()) :: map defp prepare_watchlist_address(watchlist, address, exchange_rate) do - %{ - "id" => watchlist.id, - "address" => Helper.address_with_info(nil, address, watchlist.address_hash, false), - "address_hash" => watchlist.address_hash, - "name" => watchlist.name, - "address_balance" => if(address && address.fetched_coin_balance, do: address.fetched_coin_balance.value), - "exchange_rate" => exchange_rate.fiat_value, - "notification_settings" => %{ + notification_settings = + %{ "native" => %{ "incoming" => watchlist.watch_coin_input, "outcoming" => watchlist.watch_coin_output @@ -125,7 +119,26 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do "incoming" => watchlist.watch_erc_404_input, "outcoming" => watchlist.watch_erc_404_output } - }, + } + + notification_settings_extended = + if not is_nil(Map.get(watchlist, :watch_zrc_2_input)) and not is_nil(Map.get(watchlist, :watch_zrc_2_output)) do + Map.put(notification_settings, "ZRC-2", %{ + "incoming" => watchlist.watch_zrc_2_input, + "outcoming" => watchlist.watch_zrc_2_output + }) + else + notification_settings + end + + %{ + "id" => watchlist.id, + "address" => Helper.address_with_info(nil, address, watchlist.address_hash, false), + "address_hash" => watchlist.address_hash, + "name" => watchlist.name, + "address_balance" => if(address && address.fetched_coin_balance, do: address.fetched_coin_balance.value), + "exchange_rate" => exchange_rate.fiat_value, + "notification_settings" => notification_settings_extended, "notification_methods" => %{ "email" => watchlist.notify_email }, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index a6eba00e8b1d..94d24120f93f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -211,6 +211,12 @@ defmodule BlockScoutWeb.API.RPC.AddressView do |> Map.put_new(:value, to_string(token_transfer.amount)) end + defp prepare_token_transfer(%{token_type: "ZRC-2"} = token_transfer, max_block_number, decoded_input) do + token_transfer + |> prepare_common_token_transfer(max_block_number, decoded_input) + |> Map.put_new(:value, to_string(token_transfer.amount)) + end + defp prepare_token_transfer(token_transfer, max_block_number, decoded_input) do prepare_common_token_transfer(token_transfer, max_block_number, decoded_input) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex index e52a04a91613..60738088df68 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex @@ -153,6 +153,12 @@ defmodule BlockScoutWeb.API.V2.TokenView do BlockScoutWeb.API.V2.FilecoinView.put_filecoin_robust_address(result, params) end + :zilliqa -> + defp chain_type_fields(result, params) do + # credo:disable-for-next-line Credo.Check.Design.AliasUsage + BlockScoutWeb.API.V2.ZilliqaView.extend_token_json_response(result, params.address) + end + _ -> defp chain_type_fields(result, _params) do result diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex index ee02d462a88a..69c54925594e 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/zilliqa_view.ex @@ -9,6 +9,9 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do alias Ecto.Association.NotLoaded alias Explorer.Chain.{Address, Block, Transaction} alias Explorer.Chain.Zilliqa.{AggregateQuorumCertificate, QuorumCertificate} + alias Explorer.Chain.Zilliqa.Zrc2.TokenAdapter + + @api_true [api?: true] @doc """ Extends the JSON output with a sub-map containing information related to Zilliqa, @@ -53,6 +56,28 @@ defmodule BlockScoutWeb.API.V2.ZilliqaView do }) end + @doc """ + Extends the JSON output with a sub-map containing information related to Zilliqa, + such as ZRC-2 contract address. + + ## Parameters + - `out_json`: A map defining the output JSON which will be extended. + - `adapter_address`: The adapter contract address bound with the ZRC-2 contract address. + + ## Returns + - A map extended with data related to Zilliqa. + """ + @spec extend_token_json_response(map(), Address.t()) :: map() + def extend_token_json_response(%{"type" => "ZRC-2"} = out_json, %Address{} = adapter_address) do + Map.put(out_json, :zilliqa, %{ + zrc2_address_hash: TokenAdapter.adapter_address_hash_to_zrc2_address_hash(adapter_address.hash, @api_true) + }) + end + + def extend_token_json_response(out_json, _) do + out_json + end + @doc """ Extends the JSON output with a sub-map containing information related to Zilliqa, such as if the address is a Scilla smart contract. diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex index b8757f444499..629ea6c5fec2 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helper.ex @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.Tokens.Helper do @doc """ Returns the token transfers' amount according to the token's type and decimals. - When the token's type is ERC-20, then we are going to format the amount according to the token's + When the token's type is ERC-20 or ZRC-2, then we are going to format the amount according to the token's decimals considering 0 when the decimals is nil. Case the amount is nil, this function will return the symbol `--`. @@ -57,6 +57,33 @@ defmodule BlockScoutWeb.Tokens.Helper do {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} end + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ZRC-2"}, nil, nil, nil, _token_ids) do + {:ok, "--"} + end + + defp do_token_transfer_amount(_token, "ZRC-2", nil, nil, _token_ids) do + {:ok, "--"} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ZRC-2", decimals: nil}, nil, amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} + end + + defp do_token_transfer_amount(%Token{decimals: nil}, "ZRC-2", amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, Decimal.new(0))} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount(%Token{type: "ZRC-2", decimals: decimals}, nil, amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} + end + + defp do_token_transfer_amount(%Token{decimals: decimals}, "ZRC-2", amount, _amounts, _token_ids) do + {:ok, CurrencyHelper.format_according_to_decimals(amount, decimals)} + end + # TODO: remove this clause along with token transfer denormalization defp do_token_transfer_amount(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do {:ok, :erc721_instance} @@ -133,6 +160,36 @@ defmodule BlockScoutWeb.Tokens.Helper do {:ok, amount, decimals} end + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api(%Token{type: "ZRC-2"}, nil, nil, nil, _token_ids) do + {:ok, nil} + end + + defp do_token_transfer_amount_for_api(_token, "ZRC-2", nil, nil, _token_ids) do + {:ok, nil} + end + + # TODO: remove this clause along with token transfer denormalization + defp do_token_transfer_amount_for_api( + %Token{type: "ZRC-2", decimals: decimals}, + nil, + amount, + _amounts, + _token_ids + ) do + {:ok, amount, decimals} + end + + defp do_token_transfer_amount_for_api( + %Token{decimals: decimals}, + "ZRC-2", + amount, + _amounts, + _token_ids + ) do + {:ok, amount, decimals} + end + # TODO: remove this clause along with token transfer denormalization defp do_token_transfer_amount_for_api(%Token{type: "ERC-721"}, nil, _amount, _amounts, _token_ids) do {:ok, :erc721_instance} diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex index e916a69fe2b7..46ad1648ba82 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex @@ -66,6 +66,10 @@ defmodule BlockScoutWeb.Tokens.HolderView do format_according_to_decimals(value, decimals) end + def format_token_balance_value(value, _id, %Token{type: "ZRC-2", decimals: decimals}) do + format_according_to_decimals(value, decimals) + end + def format_token_balance_value(value, id, %Token{type: "ERC-1155", decimals: decimals}) do to_string(format_according_to_decimals(value, decimals)) <> " TokenID " <> to_string(id) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index e763da414218..605bc257f30d 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -224,6 +224,7 @@ defmodule BlockScoutWeb.TransactionView do :erc721 -> gettext("ERC-721 ") :erc1155 -> gettext("ERC-1155 ") :erc404 -> gettext("ERC-404 ") + :zrc2 -> gettext("ZRC-2 ") _ -> "" end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs index 25b1861470e1..450bf1b73bde 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do use BlockScoutWeb.ConnCase + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] alias Explorer.Account.{ Identity, @@ -1090,12 +1091,22 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do } } + notification_settings_extended = + if chain_type() == :zilliqa do + Map.put(notification_settings, "ZRC-2", %{ + "incoming" => watchlist.watch_zrc_2_input, + "outcoming" => watchlist.watch_zrc_2_output + }) + else + notification_settings + end + assert json["address_hash"] == to_string(watchlist.address_hash) assert json["name"] == watchlist.name assert json["id"] == watchlist.id assert json["address"]["hash"] == Address.checksum(watchlist.address_hash) assert json["notification_methods"]["email"] == watchlist.notify_email - assert json["notification_settings"] == notification_settings + assert json["notification_settings"] == notification_settings_extended end defp check_paginated_response(first_page_resp, second_page_resp, list) do diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex index 46abfc7519cf..3c9c443b0fcb 100644 --- a/apps/explorer/lib/explorer/account/notifier/email.ex +++ b/apps/explorer/lib/explorer/account/notifier/email.ex @@ -55,6 +55,9 @@ defmodule Explorer.Account.Notifier.Email do "ERC-20" -> amount + "ZRC-2" -> + amount + "ERC-721" -> "Token ID: " <> subject <> " of " diff --git a/apps/explorer/lib/explorer/account/notifier/notify.ex b/apps/explorer/lib/explorer/account/notifier/notify.ex index 8e9289629b23..72f250909dbc 100644 --- a/apps/explorer/lib/explorer/account/notifier/notify.ex +++ b/apps/explorer/lib/explorer/account/notifier/notify.ex @@ -149,6 +149,8 @@ defmodule Explorer.Account.Notifier.Notify do {"ERC-1155", :outgoing} -> address.watch_erc_1155_output {"ERC-404", :incoming} -> address.watch_erc_404_input {"ERC-404", :outgoing} -> address.watch_erc_404_output + {"ZRC-2", :incoming} -> Map.get(address, :watch_zrc_2_input) + {"ZRC-2", :outgoing} -> Map.get(address, :watch_zrc_2_output) end end diff --git a/apps/explorer/lib/explorer/account/notifier/summary.ex b/apps/explorer/lib/explorer/account/notifier/summary.ex index 5ccfef8e6a1b..36987d8ad087 100644 --- a/apps/explorer/lib/explorer/account/notifier/summary.ex +++ b/apps/explorer/lib/explorer/account/notifier/summary.ex @@ -100,7 +100,7 @@ defmodule Explorer.Account.Notifier.Summary do %Chain.TokenTransfer{} = transfer ) do case transfer.token.type do - "ERC-20" -> + type when type in ["ERC-20", "ZRC-2"] -> %Summary{ transaction_hash: transaction.hash, method: method(transfer), diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex index 356734713057..58800975abaa 100644 --- a/apps/explorer/lib/explorer/account/watchlist_address.ex +++ b/apps/explorer/lib/explorer/account/watchlist_address.ex @@ -1,9 +1,82 @@ +defmodule Explorer.Account.WatchlistAddress.Schema do + @moduledoc """ + Models account's addresses watchlist. + """ + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + alias Explorer.Account.Watchlist + alias Explorer.Chain.Wei + + @chain_type_fields (case @chain_type do + :zilliqa -> + elem( + quote do + field(:watch_zrc_2_input, :boolean, default: true, null: false) + field(:watch_zrc_2_output, :boolean, default: true, null: false) + end, + 2 + ) + + _ -> + [] + end) + + @doc """ + Generates the typed schema for watchlist addresses. + + This macro dynamically creates the schema definition including both common fields and chain-type-specific + fields (e.g., ZRC-2 fields for Zilliqa). Should be invoked in the module that requires the schema definition. + + ## Returns + - The typed_schema block with all necessary fields. + """ + defmacro generate do + quote do + typed_schema "account_watchlist_addresses" do + field(:address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil + field(:name, Explorer.Encrypted.Binary, null: false) + field(:address_hash, Explorer.Encrypted.AddressHash, null: false) + field(:user_created, :boolean, null: false, default: true) + + belongs_to(:watchlist, Watchlist, null: false) + + field(:watch_coin_input, :boolean, default: true, null: false) + field(:watch_coin_output, :boolean, default: true, null: false) + field(:watch_erc_20_input, :boolean, default: true, null: false) + field(:watch_erc_20_output, :boolean, default: true, null: false) + field(:watch_erc_721_input, :boolean, default: true, null: false) + field(:watch_erc_721_output, :boolean, default: true, null: false) + field(:watch_erc_1155_input, :boolean, default: true, null: false) + field(:watch_erc_1155_output, :boolean, default: true, null: false) + field(:watch_erc_404_input, :boolean, default: true, null: false) + field(:watch_erc_404_output, :boolean, default: true, null: false) + field(:notify_email, :boolean, default: true, null: false) + field(:notify_epns, :boolean) + field(:notify_feed, :boolean) + field(:notify_inapp, :boolean) + + field(:fetched_coin_balance, Wei, virtual: true) + field(:tokens_fiat_value, :decimal, virtual: true) + field(:tokens_count, :integer, virtual: true) + field(:tokens_overflow, :boolean, virtual: true) + + timestamps() + + unquote_splicing(@chain_type_fields) + end + end + end +end + defmodule Explorer.Account.WatchlistAddress do @moduledoc """ WatchlistAddress entity """ + require Explorer.Account.WatchlistAddress.Schema + use Explorer.Schema + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] import Ecto.Changeset @@ -11,44 +84,22 @@ defmodule Explorer.Account.WatchlistAddress do alias Explorer.Account.Notifier.ForbiddenAddress alias Explorer.Account.Watchlist alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.Address import Explorer.Chain, only: [hash_to_lower_case_string: 1] @watchlist_not_found "Watchlist not found" - typed_schema "account_watchlist_addresses" do - field(:address_hash_hash, Cloak.Ecto.SHA256) :: binary() | nil - field(:name, Explorer.Encrypted.Binary, null: false) - field(:address_hash, Explorer.Encrypted.AddressHash, null: false) - field(:user_created, :boolean, null: false, default: true) - - belongs_to(:watchlist, Watchlist, null: false) - - field(:watch_coin_input, :boolean, default: true, null: false) - field(:watch_coin_output, :boolean, default: true, null: false) - field(:watch_erc_20_input, :boolean, default: true, null: false) - field(:watch_erc_20_output, :boolean, default: true, null: false) - field(:watch_erc_721_input, :boolean, default: true, null: false) - field(:watch_erc_721_output, :boolean, default: true, null: false) - field(:watch_erc_1155_input, :boolean, default: true, null: false) - field(:watch_erc_1155_output, :boolean, default: true, null: false) - field(:watch_erc_404_input, :boolean, default: true, null: false) - field(:watch_erc_404_output, :boolean, default: true, null: false) - field(:notify_email, :boolean, default: true, null: false) - field(:notify_epns, :boolean) - field(:notify_feed, :boolean) - field(:notify_inapp, :boolean) - - field(:fetched_coin_balance, Wei, virtual: true) - field(:tokens_fiat_value, :decimal, virtual: true) - field(:tokens_count, :integer, virtual: true) - field(:tokens_overflow, :boolean, virtual: true) - - timestamps() + @attrs_base ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output watch_erc_404_input watch_erc_404_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a + if @chain_type == :zilliqa do + @attrs_chain_type ~w(watch_zrc_2_input watch_zrc_2_output)a + else + @attrs_chain_type ~w()a end - @attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output watch_erc_404_input watch_erc_404_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a + @attrs @attrs_base ++ @attrs_chain_type + + Explorer.Account.WatchlistAddress.Schema.generate() def changeset do %__MODULE__{} diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index c43975016ea4..8a4848aefca1 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3200,18 +3200,30 @@ defmodule Explorer.Chain do @spec data() :: Dataloader.Ecto.t() def data, do: DataloaderEcto.new(Repo) + @doc """ + Determines token transfer type by its transaction. + + ## Parameters + - `transaction`: The transaction which token transfer type we need to determine. + + ## Returns + - A token transfer type which can be one of the following cases: + :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 | :token_transfer + The `:token_transfer` means the token transfer of unknown type. + - `nil` if this transaction is not related to any token transfer. + """ @spec transaction_token_transfer_type(Transaction.t()) :: - :erc20 | :erc721 | :erc1155 | :erc404 | :token_transfer | nil + :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 | :token_transfer | nil def transaction_token_transfer_type( %Transaction{ status: :ok, created_contract_address_hash: nil, - input: input, + input: _input, value: value } = transaction ) do zero_wei = %Wei{value: Decimal.new(0)} - result = find_token_transfer_type(transaction, input, value) + result = find_token_transfer_type(transaction) if is_nil(result) && not Enum.empty?(transaction.token_transfers) && value == zero_wei, do: :token_transfer, @@ -3222,11 +3234,21 @@ defmodule Explorer.Chain do def transaction_token_transfer_type(_), do: nil - defp find_token_transfer_type(transaction, input, value) do + # Determines token transfer type by its transaction. + # + # ## Parameters + # - `transaction`: The transaction which token transfer type we need to determine. + # + # ## Returns + # - A token transfer type which can be one of the following cases: + # :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 + # - `nil` if this transaction has unknown transfer type or not related to any token transfer. + @spec find_token_transfer_type(Transaction.t()) :: :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 | nil + defp find_token_transfer_type(transaction) do zero_wei = %Wei{value: Decimal.new(0)} # https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC721/ERC721.sol#L35 - case {to_string(input), value} do + case {to_string(transaction.input), transaction.value} do # transferFrom(address,address,uint256) {"0x23b872dd" <> params, ^zero_wei} -> types = [:address, :address, {:uint, 256}] @@ -3262,7 +3284,7 @@ defmodule Explorer.Chain do find_erc1155_token_transfer(transaction.token_transfers, {from_address, to_address}) - # check for ERC-20 or for old ERC-721, ERC-1155, ERC-404 token versions + # check for ERC-20, ZRC-2 or for old ERC-721, ERC-1155, ERC-404 token versions {unquote(TokenTransfer.transfer_function_signature()) <> params, ^zero_wei} -> types = [:address, {:uint, 256}] @@ -3270,7 +3292,7 @@ defmodule Explorer.Chain do decimal_value = Decimal.new(value) - find_known_token_transfer(transaction.token_transfers, {address, decimal_value}) + find_known_token_transfer(transaction.token_transfers, address, decimal_value) _ -> nil @@ -3295,7 +3317,20 @@ defmodule Explorer.Chain do if token_transfer, do: :erc1155 end - defp find_known_token_transfer(token_transfers, {address, decimal_value}) do + # Finds token transfer type by the given list of token transfers in a transaction. + # To filter transaction's token transfers, the `to_address_hash` and `amount` fields are used. + # + # ## Parameters + # - `token_transfers`: The list of transaction's token transfers. + # - `address`: The destination address of the transfer. + # - `decimal_value`: The decimal amount of the transfer. + # + # ## Returns + # - A token transfer type which can be one of the following cases: + # :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 + # - `nil` if the corresponding transaction has unknown transfer type. + @spec find_known_token_transfer(list(), binary(), Decimal.t()) :: :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2 | nil + defp find_known_token_transfer(token_transfers, address, decimal_value) do token_transfer = Enum.find(token_transfers, fn token_transfer -> token_transfer.to_address_hash.bytes == address && token_transfer.amount == decimal_value @@ -3307,6 +3342,7 @@ defmodule Explorer.Chain do %Token{type: "ERC-721"} -> :erc721 %Token{type: "ERC-1155"} -> :erc1155 %Token{type: "ERC-404"} -> :erc404 + %Token{type: "ZRC-2"} -> :zrc2 _ -> nil end else diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index bb3b0775a5b1..98416bfac481 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -62,6 +62,35 @@ defmodule Explorer.Chain.Address.Schema do ] end + :zilliqa -> + alias Explorer.Chain.Zilliqa.Zrc2.TokenAdapter, as: Zrc2TokenAdapter + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer + + quote do + [ + has_one(:zilliqa_zrc2_token_contract, Zrc2TokenAdapter, + foreign_key: :zrc2_address_hash, + references: :hash + ), + has_one(:zilliqa_zrc2_token_adapter, Zrc2TokenAdapter, + foreign_key: :adapter_address_hash, + references: :hash + ), + has_many(:zilliqa_zrc2_token_transfers_from, Zrc2TokenTransfer, + foreign_key: :from_address_hash, + references: :hash + ), + has_many(:zilliqa_zrc2_token_transfers_to, Zrc2TokenTransfer, + foreign_key: :to_address_hash, + references: :hash + ), + has_many(:zilliqa_zrc2_token_transfers_contract, Zrc2TokenTransfer, + foreign_key: :zrc2_address_hash, + references: :hash + ) + ] + end + :zksync -> quote do [ diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 29f4e77696c2..d1abecf13f5a 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -80,6 +80,7 @@ defmodule Explorer.Chain.Address.TokenBalance do tb in TokenBalance, where: ((tb.address_hash != ^@burn_address_hash and tb.token_type == "ERC-721") or tb.token_type == "ERC-20" or + tb.token_type == "ZRC-2" or tb.token_type == "ERC-1155" or tb.token_type == "ERC-404") and (is_nil(tb.value_fetched_at) or is_nil(tb.value)) and (is_nil(tb.refetch_after) or tb.refetch_after < ^Timex.now()) @@ -90,7 +91,7 @@ defmodule Explorer.Chain.Address.TokenBalance do join: t in Token, on: tb.token_contract_address_hash == t.contract_address_hash, where: - ((tb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or + ((tb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or t.type == "ZRC-2" or t.type == "ERC-1155" or t.type == "ERC-404") and (is_nil(tb.value_fetched_at) or is_nil(tb.value)) and (is_nil(tb.refetch_after) or tb.refetch_after < ^Timex.now()) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index a569a8fcc83a..2867a95579e3 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -27,6 +27,7 @@ defmodule Explorer.Chain.Block.Schema do alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Zilliqa.AggregateQuorumCertificate, as: ZilliqaAggregateQuorumCertificate alias Explorer.Chain.Zilliqa.QuorumCertificate, as: ZilliqaQuorumCertificate + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer alias Explorer.Chain.ZkSync.BatchBlock, as: ZkSyncBatchBlock @chain_type_fields (case @chain_type do @@ -116,6 +117,11 @@ defmodule Explorer.Chain.Block.Schema do foreign_key: :block_hash, references: :hash ) + + has_many(:zilliqa_zrc2_token_transfers, Zrc2TokenTransfer, + foreign_key: :block_hash, + references: :hash + ) end, 2 ) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 1c529bb57f90..c9f9fdf2984f 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -33,6 +33,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances alias Explorer.Chain.Import.Runner.{Addresses, TokenInstances, Tokens} alias Explorer.Chain.InternalTransaction.DeleteQueue, as: InternalTransactionDeleteQueue + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator @@ -525,10 +526,25 @@ defmodule Explorer.Chain.Import.Runner.Blocks do |> Enum.map(& &1.number) end - defp lose_consensus(repo, changes_list, %{ - timeout: timeout, - timestamps: %{updated_at: updated_at} - }) do + # Handles block consensus loss. + # + # ## Parameters + # - `repo`: The `t:Explorer.Repo.t/0` or db replica instance. + # - `changes_list`: List of block changes to process. + # - `_opts`: The options containing timeout and `updated_at` timestamp for db operations. + # + # ## Returns + # - `{:ok, removed_consensus_blocks}` tuple with the list of `{block_number, block_hash}` + # tuples for the blocks that lost consensus. + # - `{:error, %{exception: postgrex_error}}` in case of database error. + defp lose_consensus( + repo, + changes_list, + %{ + timeout: timeout, + timestamps: %{updated_at: updated_at} + } = _opts + ) do hashes = Enum.map(changes_list, & &1.hash) consensus_block_numbers = consensus_block_numbers(changes_list) @@ -597,6 +613,18 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) + if Application.get_env(:explorer, :chain_type) == :zilliqa do + repo.delete_all( + from( + zrc2_token_transfer in Zrc2TokenTransfer, + join: s in subquery(acquire_query), + on: zrc2_token_transfer.block_number == s.number, + where: zrc2_token_transfer.block_hash not in ^hashes + ), + timeout: timeout + ) + end + removed_consensus_block_numbers |> Enum.reject(&Enum.member?(consensus_block_numbers, &1)) |> MissingRangesManipulator.add_ranges_by_block_numbers() diff --git a/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_adapters.ex b/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_adapters.ex new file mode 100644 index 000000000000..9a7b9c154dd5 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_adapters.ex @@ -0,0 +1,79 @@ +defmodule Explorer.Chain.Import.Runner.Zilliqa.Zrc2.TokenAdapters do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Zilliqa.Zrc2.TokenAdapter.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Zilliqa.Zrc2.TokenAdapter + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [TokenAdapter.t()] + + @impl Import.Runner + def ecto_schema_module, do: TokenAdapter + + @impl Import.Runner + def option_key, do: :zilliqa_zrc2_token_adapters + + @impl Import.Runner + @spec imported_table_row() :: %{:value_description => binary(), :value_type => binary()} + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + @spec run(Multi.t(), list(), map()) :: Multi.t() + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_zilliqa_zrc2_token_adapters, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :zilliqa_zrc2_token_adapters, + :zilliqa_zrc2_token_adapters + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [TokenAdapter.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = _options) when is_list(changes_list) do + # Enforce TokenAdapter ShareLocks order (see docs: sharelock.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.adapter_address_hash) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + for: TokenAdapter, + returning: true, + timeout: timeout, + timestamps: timestamps, + conflict_target: :adapter_address_hash, + on_conflict: :nothing + ) + + {:ok, inserted} + end +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_transfers.ex b/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_transfers.ex new file mode 100644 index 000000000000..c68390dc8b51 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/zilliqa/zrc2/token_transfers.ex @@ -0,0 +1,110 @@ +defmodule Explorer.Chain.Import.Runner.Zilliqa.Zrc2.TokenTransfers do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Zilliqa.Zrc2.TokenTransfer.t/0`. + """ + + require Ecto.Query + + import Ecto.Query, only: [from: 2] + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.Import + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer + alias Explorer.Prometheus.Instrumenter + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [TokenTransfer.t()] + + @impl Import.Runner + def ecto_schema_module, do: TokenTransfer + + @impl Import.Runner + def option_key, do: :zilliqa_zrc2_token_transfers + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :insert_zilliqa_zrc2_token_transfers, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :zilliqa_zrc2_token_transfers, + :zilliqa_zrc2_token_transfers + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{required(:timeout) => timeout(), required(:timestamps) => Import.timestamps()}) :: + {:ok, [TokenTransfer.t()]} + | {:error, [Changeset.t()]} + def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md) + ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.log_index}) + + {:ok, inserted} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:transaction_hash, :log_index, :block_hash], + on_conflict: on_conflict, + for: TokenTransfer, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + + {:ok, inserted} + end + + defp default_on_conflict do + from( + token_transfer in TokenTransfer, + update: [ + set: [ + # Don't update `transaction_hash` as it is part of the composite primary key and used for the conflict target. + # Don't update `log_index` as it is part of the composite primary key and used for the conflict target. + # Don't update `block_hash` as it is part of the composite primary key and used for the conflict target. + from_address_hash: fragment("EXCLUDED.from_address_hash"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + amount: fragment("EXCLUDED.amount"), + zrc2_address_hash: fragment("EXCLUDED.zrc2_address_hash"), + block_number: fragment("EXCLUDED.block_number"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token_transfer.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token_transfer.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.from_address_hash, EXCLUDED.to_address_hash, EXCLUDED.amount, EXCLUDED.zrc2_address_hash, EXCLUDED.block_number) IS DISTINCT FROM (?, ?, ?, ?, ?)", + token_transfer.from_address_hash, + token_transfer.to_address_hash, + token_transfer.amount, + token_transfer.zrc2_address_hash, + token_transfer.block_number + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex index 01448381142a..ae42f2172bf7 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/chain_type_specific.ex @@ -62,7 +62,9 @@ defmodule Explorer.Chain.Import.Stage.ChainTypeSpecific do zilliqa: [ Runner.Zilliqa.AggregateQuorumCertificates, Runner.Zilliqa.NestedQuorumCertificates, - Runner.Zilliqa.QuorumCertificates + Runner.Zilliqa.QuorumCertificates, + Runner.Zilliqa.Zrc2.TokenAdapters, + Runner.Zilliqa.Zrc2.TokenTransfers ], stability: [ Runner.Stability.Validators diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex index 70ba284ae7af..a60b5de3473d 100644 --- a/apps/explorer/lib/explorer/chain/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -88,7 +88,7 @@ defmodule Explorer.Chain.Log do alias ABI.{Event, FunctionSelector} alias Explorer.{Chain, Repo} - alias Explorer.Chain.{ContractMethod, Hash, Log, TokenTransfer, Transaction} + alias Explorer.Chain.{Block, ContractMethod, Hash, Log, TokenTransfer, Transaction} alias Explorer.SmartContract.SigProviderInterface @required_attrs ~w(address_hash data block_hash index)a @@ -585,4 +585,42 @@ defmodule Explorer.Chain.Log do log.first_topic not in [^TokenTransfer.weth_deposit_signature(), ^TokenTransfer.weth_withdrawal_signature()] ) end + + @doc """ + Returns the first (min) known consensus block number found in the `logs` table. + + ## Returns + - An integer block number if found. + - `nil` if not found. + """ + @spec first_known_block_number() :: non_neg_integer() | nil + def first_known_block_number do + Repo.aggregate(get_known_block_number_query(), :min, :block_number) + end + + @doc """ + Returns the last (max) known consensus block number found in the `logs` table. + + ## Returns + - An integer block number if found. + - `nil` if not found. + """ + @spec last_known_block_number() :: non_neg_integer() | nil + def last_known_block_number do + Repo.aggregate(get_known_block_number_query(), :max, :block_number) + end + + # Returns a query to get all logs with consensus blocks. + # + # ## Returns + # - A query to be used by other Repo functions. + @spec get_known_block_number_query() :: Ecto.Query.t() + defp get_known_block_number_query do + from( + log in __MODULE__, + inner_join: block in Block, + on: block.hash == log.block_hash and block.consensus == true, + select: log + ) + end end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 57e716584a5c..1eba3e00e03a 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -67,6 +67,7 @@ defmodule Explorer.Chain.Token do * ERC-721 * ERC-1155 * ERC-404 + * ZRC-2 (for Zilliqa chain type) ## Token Specifications @@ -75,6 +76,7 @@ defmodule Explorer.Chain.Token do * [ERC-777](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-777.md) * [ERC-1155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1155.md) * [ERC-404](https://github.com/Pandora-Labs-Org/erc404) + * [ZRC-2](https://github.com/Zilliqa/ZRC/blob/main/zrcs/zrc-2.md) """ use Explorer.Schema @@ -515,4 +517,21 @@ defmodule Explorer.Chain.Token do Chain.select_repo(options).exists?(query) end + + @doc """ + Checks if the given token is ZRC-2 token. + + ## Parameters + - `token`: The token to check the type of. + + ## Returns + - `true` if this is ZRC-2 token, `false` otherwise. + """ + @spec zrc_2_token?(__MODULE__.t()) :: bool + def zrc_2_token?(token) do + case Map.get(token, :type) do + "ZRC-2" -> true + _ -> false + end + end end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index d95041cf2404..becd1f61c690 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -179,6 +179,18 @@ defmodule Explorer.Chain.Transaction.Schema do 2 ) + :zilliqa -> + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer + + quote do + [ + has_many(:zilliqa_zrc2_token_transfers, Zrc2TokenTransfer, + foreign_key: :transaction_hash, + references: :hash + ) + ] + end + _ -> [] end) diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex index 3acf6eadc92a..16ef194776eb 100644 --- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex +++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex @@ -301,7 +301,7 @@ defmodule Explorer.Chain.Transaction.StateChange do balance_diff = Decimal.sub(balance, balance_before) transfer = elem(List.first(transfers), 1) - if transfer.token.type != "ERC-20" or has_diff?(balance_diff) do + if transfer.token.type not in ["ERC-20", "ZRC-2"] or has_diff?(balance_diff) do %StateChange{ coin_or_token_transfers: transfers, address: address, diff --git a/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_adapter.ex b/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_adapter.ex new file mode 100644 index 000000000000..958c1bb2c579 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_adapter.ex @@ -0,0 +1,97 @@ +defmodule Explorer.Chain.Zilliqa.Zrc2.TokenAdapter do + @moduledoc """ + Represents a list of `ERC-20 adapter contract address <-> ZRC-2 token contract address` pairs. + + Changes in the schema should be reflected in the bulk import module: + - `Explorer.Chain.Import.Runner.Zilliqa.Zrc2.TokenAdapters` + """ + use Explorer.Schema + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash} + + @required_attrs ~w(zrc2_address_hash adapter_address_hash)a + + @typedoc """ + * `zrc2_address_hash` - The ZRC-2 token contract address hash. + * `zrc2_address` - An instance of `Explorer.Chain.Address` referenced by `zrc2_address_hash`. + * `adapter_address_hash` - The ERC-20 adapter contract address hash. + * `adapter_address` - An instance of `Explorer.Chain.Address` referenced by `adapter_address_hash`. + """ + @primary_key false + typed_schema "zilliqa_zrc2_token_adapters" do + belongs_to(:zrc2_address, Address, + foreign_key: :zrc2_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + belongs_to(:adapter_address, Address, + foreign_key: :adapter_address_hash, + primary_key: true, + references: :hash, + type: Hash.Address, + null: false + ) + + timestamps() + end + + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = pair, attrs) do + pair + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:zrc2_address_hash) + |> foreign_key_constraint(:adapter_address_hash) + |> unique_constraint(:adapter_address_hash) + end + + @doc """ + Builds `%{zrc2_address_hash => adapter_address_hash}` map for the given ZRC-2 address hashes. + If adapter address is not found for some ZRC-2 address, the corresponding key will be absent in the map. + + ## Parameters + - `zrc2_address_hashes`: The list of ZRC-2 address hashes. + + ## Returns + - `%{zrc2_address_hash => adapter_address_hash}` map. The map can be empty. + """ + @spec adapter_address_hash_by_zrc2_address_hash([Hash.t()]) :: %{Hash.t() => Hash.t()} + def adapter_address_hash_by_zrc2_address_hash([]), do: %{} + + def adapter_address_hash_by_zrc2_address_hash(zrc2_address_hashes) do + query = + from(a in __MODULE__, + select: {a.zrc2_address_hash, a.adapter_address_hash}, + where: a.zrc2_address_hash in ^zrc2_address_hashes + ) + + query + |> Repo.all(timeout: :infinity) + |> Enum.into(%{}) + end + + @doc """ + Returns ZRC-2 address hash for the given adapter address hash. + + ## Parameters + - `adapter_address_hash`: The adapter address hash. + - `options`: A keyword list of options that may include whether to use a replica database. + + ## Returns + - ZRC-2 address hash for the adapter address hash. + - `nil` if the ZRC-2 address hash is not found. + """ + @spec adapter_address_hash_to_zrc2_address_hash(Hash.t(), [Chain.api?()]) :: Hash.t() | nil + def adapter_address_hash_to_zrc2_address_hash(adapter_address_hash, options \\ []) do + query = + from(a in __MODULE__, + select: a.zrc2_address_hash, + where: a.adapter_address_hash == ^adapter_address_hash + ) + + Chain.select_repo(options).one(query) + end +end diff --git a/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_transfer.ex b/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_transfer.ex new file mode 100644 index 000000000000..01fb8e08f05b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/zilliqa/zrc2/token_transfer.ex @@ -0,0 +1,220 @@ +defmodule Explorer.Chain.Zilliqa.Zrc2.TokenTransfer do + @moduledoc """ + Represents a token transfer between addresses for a given ZRC-2 token with unknown ERC-20 adapter contract address yet. + + Changes in the schema should be reflected in the bulk import module: + - Explorer.Chain.Import.Runner.Zilliqa.Zrc2.TokenTransfers + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.Chain.{Address, Block, Data, Hash, Log, Transaction} + alias Explorer.Chain.Zilliqa.Zrc2.TokenAdapter + alias Explorer.Repo + + @typedoc """ + * `:transaction_hash` - Transaction foreign key. + * `:transaction` - The `t:Explorer.Chain.Transaction.t/0` ledger. + * `:log_index` - Index of the corresponding `t:Explorer.Chain.Log.t/0` in the block. + * `:from_address_hash` - Address hash foreign key. + * `:from_address` - The `t:Explorer.Chain.Address.t/0` that sent the tokens. + * `:to_address_hash` - Address hash foreign key. + * `:to_address` - The `t:Explorer.Chain.Address.t/0` that received the tokens. + * `:amount` - The token transferred amount. + * `:zrc2_address_hash` - Address hash foreign key. + * `:zrc2_address` - The `t:Explorer.Chain.Address.t/0` of the token's contract. + * `:block_number` - The block number that the transfer took place in. + * `:block_hash` - The hash of the block. + """ + @primary_key false + typed_schema "zilliqa_zrc2_token_transfers" do + belongs_to(:transaction, Transaction, + foreign_key: :transaction_hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) + + field(:log_index, :integer, primary_key: true, null: false) + + belongs_to(:from_address, Address, + foreign_key: :from_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + belongs_to(:to_address, Address, + foreign_key: :to_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + field(:amount, :decimal) + + belongs_to( + :zrc2_address, + Address, + foreign_key: :zrc2_address_hash, + references: :hash, + type: Hash.Address, + null: false + ) + + field(:block_number, :integer) + + belongs_to(:block, Block, + foreign_key: :block_hash, + primary_key: true, + references: :hash, + type: Hash.Full, + null: false + ) + + timestamps() + end + + @required_attrs ~w(transaction_hash log_index from_address_hash to_address_hash amount zrc2_address_hash block_number block_hash)a + + @spec changeset(Ecto.Schema.t(), map()) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = transfer, attrs) do + transfer + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> foreign_key_constraint(:transaction_hash) + |> unique_constraint([:transaction_hash, :log_index, :block_hash]) + end + + @doc """ + Gets all transfer logs from the `logs` table for the given block. + This function is only used by the historic handler. + + ## Parameters + - `block_number`: The block number. + - `transfer_events`: The list of transfer logs (their signatures) needed to be found. + + ## Returns + - A list of found log maps for the given block. + Each returned log can have the corresponding non-empty `adapter_address_hash` field. + """ + @spec read_block_logs(non_neg_integer(), [String.t()]) :: [ + %{ + first_topic: Hash.t(), + data: Data.t(), + address_hash: Hash.t(), + transaction_hash: Hash.t(), + index: non_neg_integer(), + block_number: non_neg_integer(), + block_hash: Hash.t(), + adapter_address_hash: Hash.t() | nil + } + ] + def read_block_logs(block_number, transfer_events) do + Repo.all( + from( + l in Log, + inner_join: b in Block, + on: b.hash == l.block_hash and b.consensus == true, + left_join: a in TokenAdapter, + on: a.zrc2_address_hash == l.address_hash, + where: l.block_number == ^block_number and l.first_topic in ^transfer_events, + select: %{ + first_topic: l.first_topic, + data: l.data, + address_hash: l.address_hash, + transaction_hash: l.transaction_hash, + index: l.index, + block_number: l.block_number, + block_hash: l.block_hash, + adapter_address_hash: a.adapter_address_hash + } + ), + timeout: :infinity + ) + end + + @doc """ + Gets a list of transactions with necessary data (such as transaction hash, input, and `to` address hash) + by the list of logs prepared with the `read_block_logs` function. This function is only used by the + historic handler. + + We only need the transactions having the `TransferSuccess` event with unknown ZRC-2 adapter address. + + ## Parameters + - `logs`: The list of logs. + - `zrc2_transfer_success_event`: The signature of the `TransferSuccess` event. + + ## Returns + - A list of transaction maps for the given list of logs. + """ + @spec read_transfer_transactions( + [%{first_topic: Hash.t(), transaction_hash: Hash.t(), adapter_address_hash: Hash.t() | nil}], + String.t() + ) :: [%{hash: Hash.t(), input: Data.t(), to_address_hash: Hash.t()}] + def read_transfer_transactions(logs, zrc2_transfer_success_event) do + transaction_hashes = + logs + |> Enum.filter( + &(Hash.to_string(&1.first_topic) == zrc2_transfer_success_event and is_nil(&1.adapter_address_hash)) + ) + |> Enum.map(& &1.transaction_hash) + + Repo.all( + from( + t in Transaction, + where: t.hash in ^transaction_hashes, + select: %{ + hash: t.hash, + input: t.input, + to_address_hash: t.to_address_hash + } + ), + timeout: :infinity + ) + end + + @doc """ + Scans the `zilliqa_zrc2_token_transfers` table for the rows that have corresponding + adapter addresses in the `zilliqa_zrc2_token_adapters` table and returns the found rows. + + ## Returns + - The list of found rows. The list can be empty. + """ + @spec zrc2_token_transfers_having_adapter() :: [ + %{ + transaction_hash: Hash.t(), + log_index: non_neg_integer(), + from_address_hash: Hash.t(), + to_address_hash: Hash.t(), + amount: Decimal.t(), + adapter_address_hash: Hash.t(), + block_number: non_neg_integer(), + block_hash: Hash.t() + } + ] + def zrc2_token_transfers_having_adapter do + query = + from( + ztt in __MODULE__, + inner_join: a in TokenAdapter, + on: a.zrc2_address_hash == ztt.zrc2_address_hash, + select: %{ + transaction_hash: ztt.transaction_hash, + log_index: ztt.log_index, + from_address_hash: ztt.from_address_hash, + to_address_hash: ztt.to_address_hash, + amount: ztt.amount, + adapter_address_hash: a.adapter_address_hash, + block_number: ztt.block_number, + block_hash: ztt.block_hash + } + ) + + query + |> Repo.all(timeout: :infinity) + end +end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index 02fc5f4cec6a..aed462e27f59 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -263,7 +263,7 @@ defmodule Explorer.Etherscan do Retrieves token transfers filtered by token standard type with optional address and contract filtering. This function queries token transfers based on the specified token standard - (ERC-20, ERC-721, ERC-1155, or ERC-404) and applies optional filtering by + (ERC-20, ERC-721, ERC-1155, ERC-404, or ZRC-2) and applies optional filtering by address and contract address. The function merges provided options with default settings for pagination, ordering, and block range filtering. @@ -273,7 +273,7 @@ defmodule Explorer.Etherscan do ## Parameters - `token_transfers_type`: The token standard type (`:erc20`, `:erc721`, - `:erc1155`, or `:erc404`) + `:erc1155`, `:erc404`, or `:zrc2`) - `address_hash`: Optional address hash to filter transfers involving this address as sender or recipient (filters by `from_address_hash` or `to_address_hash`) @@ -289,7 +289,7 @@ defmodule Explorer.Etherscan do and `index_in_batch` fields """ @spec list_token_transfers( - :erc20 | :erc721 | :erc1155 | :erc404, + :erc20 | :erc721 | :erc1155 | :erc404 | :zrc2, Hash.Address.t() | nil, Hash.Address.t() | nil, map() @@ -309,6 +309,9 @@ defmodule Explorer.Etherscan do :erc404 -> list_erc404_token_transfers(address_hash, contract_address_hash, options) + + :zrc2 -> + list_zrc2_token_transfers(address_hash, contract_address_hash, options) end end @@ -535,6 +538,27 @@ defmodule Explorer.Etherscan do "ERC-20" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.replica().all() end + # Retrieves token transfers filtered by ZRC-2 type with optional address and contract filtering. + # + # This function queries token transfers based on the ZRC-2 token standard + # and applies optional filtering by address and contract address. + # + # ## Parameters + # - `address_hash`: Optional address hash to filter transfers involving this + # address as sender or recipient (filters by `from_address_hash` or `to_address_hash`). + # - `contract_address_hash`: Optional contract address hash to filter transfers + # for a specific token contract. + # - `options`: Map of query options that gets merged with default options + # including pagination (`page_number`, `page_size`), ordering + # (`order_by_direction`), and block range filtering (`startblock`, `endblock`). + # + # ## Returns + # - A list of `TokenTransfer` structs matching the specified criteria. + @spec list_zrc2_token_transfers(Hash.Address.t() | nil, Hash.Address.t() | nil, map()) :: [TokenTransfer.t()] + defp list_zrc2_token_transfers(address_hash, contract_address_hash, options) do + "ZRC-2" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.replica().all() + end + defp list_nft_transfers(address_hash, contract_address_hash, options) do "ERC-721" |> base_token_transfers_query(address_hash, contract_address_hash, options) |> Repo.replica().all() end diff --git a/apps/explorer/priv/account/migrations/20251029115930_account_watchlist_zrc2_addresses.exs b/apps/explorer/priv/account/migrations/20251029115930_account_watchlist_zrc2_addresses.exs new file mode 100644 index 000000000000..719a26613f8e --- /dev/null +++ b/apps/explorer/priv/account/migrations/20251029115930_account_watchlist_zrc2_addresses.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Account.Migrations.AccountWatchlistZRC2Addresses do + use Ecto.Migration + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + + if @chain_type == :zilliqa do + def change do + alter table(:account_watchlist_addresses) do + add(:watch_zrc_2_input, :boolean, default: true) + add(:watch_zrc_2_output, :boolean, default: true) + end + end + else + def change do + # does nothing for other chain types + end + end +end diff --git a/apps/explorer/priv/zilliqa/migrations/20251003112047_create_zrc2_token_tables.exs b/apps/explorer/priv/zilliqa/migrations/20251003112047_create_zrc2_token_tables.exs new file mode 100644 index 000000000000..499c75691649 --- /dev/null +++ b/apps/explorer/priv/zilliqa/migrations/20251003112047_create_zrc2_token_tables.exs @@ -0,0 +1,39 @@ +defmodule Explorer.Repo.Zilliqa.Migrations.CreateZRC2TokenTables do + use Ecto.Migration + + def up do + create table(:zilliqa_zrc2_token_adapters, primary_key: false) do + add(:zrc2_address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:adapter_address_hash, references(:addresses, column: :hash, type: :bytea), primary_key: true) + timestamps(type: :utc_datetime_usec) + end + + create(unique_index(:zilliqa_zrc2_token_adapters, :zrc2_address_hash)) + + create table(:zilliqa_zrc2_token_transfers, primary_key: false) do + add( + :transaction_hash, + references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), + primary_key: true + ) + + add(:log_index, :integer, primary_key: true) + add(:from_address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:to_address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:amount, :decimal, null: false) + add(:zrc2_address_hash, references(:addresses, column: :hash, type: :bytea), null: false) + add(:block_number, :integer, null: false) + add(:block_hash, references(:blocks, column: :hash, type: :bytea), primary_key: true) + + timestamps(type: :utc_datetime_usec) + end + + create(index(:zilliqa_zrc2_token_transfers, :zrc2_address_hash)) + create(index(:zilliqa_zrc2_token_transfers, [:block_number, :block_hash])) + end + + def down do + drop_if_exists(table(:zilliqa_zrc2_token_transfers)) + drop_if_exists(table(:zilliqa_zrc2_token_adapters)) + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 0ffee5775752..ffb9a736a13a 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1,6 +1,7 @@ defmodule Explorer.Factory do use ExMachina.Ecto, repo: Explorer.Repo use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] require Ecto.Query @@ -147,27 +148,36 @@ defmodule Explorer.Factory do end def watchlist_address_factory do + notification_settings = %{ + "native" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + }, + "ERC-20" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + }, + "ERC-721" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + }, + "ERC-404" => %{ + "incoming" => random_bool(), + "outcoming" => random_bool() + } + } + + notification_settings_extended = + if chain_type() == :zilliqa do + Map.put(notification_settings, "ZRC-2", %{"incoming" => random_bool(), "outcoming" => random_bool()}) + else + notification_settings + end + %{ "address_hash" => to_string(build(:address).hash), "name" => sequence("test"), - "notification_settings" => %{ - "native" => %{ - "incoming" => random_bool(), - "outcoming" => random_bool() - }, - "ERC-20" => %{ - "incoming" => random_bool(), - "outcoming" => random_bool() - }, - "ERC-721" => %{ - "incoming" => random_bool(), - "outcoming" => random_bool() - }, - "ERC-404" => %{ - "incoming" => random_bool(), - "outcoming" => random_bool() - } - }, + "notification_settings" => notification_settings_extended, "notification_methods" => %{ "email" => random_bool() } @@ -177,7 +187,7 @@ defmodule Explorer.Factory do def watchlist_address_db_factory(%{wl_id: id}) do hash = insert(:address).hash - %WatchlistAddress{ + watchlist_address = %WatchlistAddress{ name: sequence("test"), watchlist_id: id, address_hash: hash, @@ -194,6 +204,17 @@ defmodule Explorer.Factory do watch_erc_404_output: random_bool(), notify_email: random_bool() } + + watchlist_address_extended = + if chain_type() == :zilliqa do + watchlist_address + |> Map.put(:watch_zrc_2_input, random_bool()) + |> Map.put(:watch_zrc_2_output, random_bool()) + else + watchlist_address + end + + watchlist_address_extended end def custom_abi_factory do diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 49d9fe537cb5..d6b8c9b26d9a 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -31,6 +31,7 @@ defmodule Indexer.Block.Fetcher do alias Indexer.Fetcher.Filecoin.AddressInfo, as: FilecoinAddressInfo alias Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens, as: PolygonZkevmBridgeL1Tokens alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime + alias Indexer.Fetcher.Zilliqa.Zrc2Tokens alias Indexer.{Prometheus, TokenBalances, Tracer} @@ -292,6 +293,12 @@ defmodule Indexer.Block.Fetcher do async_match_arbitrum_messages_to_l2(arbitrum_transactions_for_further_handling) + if chain_type() == :zilliqa do + inserted_logs = Map.get(inserted, :logs, []) + inserted_transactions = Map.get(inserted, :transactions, []) + Zrc2Tokens.fetch_zrc2_token_transfers_and_adapters(inserted_logs, inserted_transactions, range, callback_module) + end + result else {step, {:error, reason}} -> {:error, {step, reason}} @@ -538,6 +545,20 @@ defmodule Indexer.Block.Fetcher do def async_import_internal_transactions(_, _), do: :ok + @doc """ + Triggers async import of tokens just inserted into the database by the realtime or catchup indexer. + + ## Parameters + - `%{tokens: tokens}`: A map returned by the `Chain.import` function containing the list of inserted tokens. + - `realtime?`: A boolean flag indicating whether to insert the tokens to the beginning (true) + or to the end (false) of the import queue. + + ## Returns + - :ok + """ + @spec async_import_tokens(%{:tokens => list(), optional(any()) => any()}, boolean()) :: :ok + def async_import_tokens(%{tokens: []}, _realtime?), do: :ok + def async_import_tokens(%{tokens: tokens}, realtime?) do tokens |> Enum.map(& &1.contract_address_hash) @@ -546,6 +567,22 @@ defmodule Indexer.Block.Fetcher do def async_import_tokens(_, _), do: :ok + @doc """ + Triggers async import of token balances just inserted into the database by the realtime or catchup indexer + or internal transactions fetcher. + + ## Parameters + - `%{address_token_balances: token_balances}`: A map returned by the `Chain.import` function containing + the list of inserted token balances. + - `realtime?`: A boolean flag indicating whether to insert the items to the beginning (true) + or to the end (false) of the import queue. + + ## Returns + - :ok + """ + @spec async_import_token_balances(%{:address_token_balances => list(), optional(any()) => any()}, boolean()) :: :ok + def async_import_token_balances(%{address_token_balances: []}, _realtime?), do: :ok + def async_import_token_balances(%{address_token_balances: token_balances}, realtime?) do TokenBalance.async_fetch(token_balances, realtime?) end diff --git a/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex b/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex new file mode 100644 index 000000000000..a22a23e9f46d --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex @@ -0,0 +1,697 @@ +defmodule Indexer.Fetcher.Zilliqa.Zrc2Tokens do + @moduledoc """ + Fills `zilliqa_zrc2_token_transfers` (or `token_transfers`) and `zilliqa_zrc2_token_adapters` table + (with accompanying tables like `tokens`, `address_token_balances`, etc.). + + The `zilliqa_zrc2_token_transfers` table is used for temporary storing ZRC-2 transfers that have unknown adapter contract address. + Once the token adapter address becomes known, the corresponding transfers are moved from + the `zilliqa_zrc2_token_transfers` to `token_transfers` table (and the adapter address is used for the `token_contract_address_hash` field). + + This fetcher is responsible for: + - handling historic ZRC-2 transfer events (`TransferSuccess`, `TransferFromSuccess`, `Minted`, `Burnt`) and + filling the token transfer and token adapter tables. + - periodically checking and moving ZRC-2 token transfers from the `zilliqa_zrc2_token_transfers` + to the `token_transfers` table. + + Also, this fetcher contains a common handling function which is also called by the realtime and catchup block fetchers + (see `fetch_zrc2_token_transfers_and_adapters` code) to handle the ZRC-2 transfer events in catchup and realtime mode. + + The historic handler goes through the `logs` and `transactions` database tables (in the reverse block number order) + and fills the `zilliqa_zrc2_token_transfers` and `zilliqa_zrc2_token_adapters` tables. If ZRC-2 token adapter is already known + (exists in the `zilliqa_zrc2_token_adapters` table), the fetcher instead writes the corresponding rows directly into the `token_transfers` table + setting `token_contract_address_hash` field to the adapter's address. + + Periodically checking and moving ZRC-2 token transfers from the `zilliqa_zrc2_token_transfers` to the `token_transfers` table + is implemented in the `move_zrc2_token_transfers_to_token_transfers` function. It's called after every + `fetch_zrc2_token_transfers_and_adapters` call. After the historic handler finishes its work (reaches the FIRST_BLOCK), + the `move_zrc2_token_transfers_to_token_transfers` function continues to be called periodically (once per a few seconds) + to check and move token transfers created by the realtime (or catchup) block fetcher. This call isn't invoked in the block + fetcher itself as it can take a lot of time (if there are a lot of rows in the `zilliqa_zrc2_token_transfers` table) and lead to + block fetcher delays. It's called asynchronously by this fetcher instead. + + The handled token transfers are added with "ZRC-2" token type. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + import Ecto.Query + import Explorer.Helper, only: [decode_data: 2] + + import Indexer.Block.Fetcher, + only: [ + async_import_token_balances: 2, + async_import_tokens: 2 + ] + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.{Data, Hash, Log, SmartContract, TokenTransfer} + alias Explorer.Chain.Zilliqa.Zrc2.TokenAdapter + alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer + alias Indexer.Block.Fetcher, as: BlockFetcher + alias Indexer.TokenBalances + alias Indexer.Transform.{Addresses, AddressTokenBalances} + + @counter_type "zilliqa_zrc2_tokens_fetcher_last_block_number" + @logging_max_block_range 100 + + @zrc2_transfer_success_event "0xa5901fdb53ef45260c18811f35461e0eda2b6133d807dabbfb65314dd4fc2fac" + @zrc2_transfer_from_success_event "0x96acecb2152edcc0681aa27d354d55d64192e86489ff8d5d903d63ef266755a1" + @zrc2_minted_event "0x845020906442ad0651b44a75d9767153912bfa586416784cf8ead41b37b1dbf5" + @zrc2_burnt_event "0x92b328e20a23b6dc6c50345c8a05b555446cbde2e9e1e2ee91dab47bd5204d07" + + @check_zrc2_token_transfers_interval :timer.seconds(10) + + def child_spec(start_link_arguments) do + spec = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments}, + restart: :transient, + type: :worker + } + + Supervisor.child_spec(spec, []) + end + + def start_link(args, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) + end + + @impl GenServer + def init(_args) do + {:ok, %{}, {:continue, nil}} + end + + @impl GenServer + def handle_continue(_, _state) do + Logger.metadata(fetcher: __MODULE__) + + # two seconds pause needed to avoid exceeding Supervisor restart intensity when DB issues + :timer.sleep(2000) + + # get the starting block number from the cache to start from the same point + # we stopped before the instance was down. If this is the first run, + # get the latest known block number from the `logs` table + block_number_to_analyze = + case LastFetchedCounter.get(@counter_type, nullable: true) do + nil -> Log.last_known_block_number() + block_number -> Decimal.to_integer(block_number) + end + + if is_nil(block_number_to_analyze) do + # this is the first run of the instance from scratch, so the events will be handled by the realtime and catchup indexers + # and we only need to call the `move_zrc2_token_transfers_to_token_transfers` periodically + Logger.info( + "There are no known consensus blocks in the database, so #{__MODULE__} will only periodically check the `zilliqa_zrc2_token_transfers` table." + ) + + LastFetchedCounter.upsert(%{counter_type: @counter_type, value: 0}) + Process.send_after(self(), :continue, @check_zrc2_token_transfers_interval) + + {:noreply, %{block_number_to_analyze: 0, is_initial_block: true, first_block_number: 0}} + else + # we continue handling after instance restart + first_block_number = + if block_number_to_analyze > 0 do + max(Application.get_env(:indexer, :first_block), Log.first_known_block_number()) + else + 0 + end + + Process.send(self(), :continue, []) + + {:noreply, + %{ + block_number_to_analyze: block_number_to_analyze, + is_initial_block: true, + first_block_number: first_block_number + }} + end + end + + @doc """ + Implements the handler loop. + + If we didn't reach the FIRST_BLOCK, get the logs and transactions of the `block_number_to_analyze` block, + call the `fetch_zrc2_token_transfers_and_adapters` function for this block, call the + `move_zrc2_token_transfers_to_token_transfers` function, and go to the next block (in the reverse order). + + If we reached the FIRST_BLOCK, we just call the `move_zrc2_token_transfers_to_token_transfers` function + and go to the next iteration (and so on indefinitely). + + ## Parameters + - `:continue`: The GenServer message. + - `%{:block_number_to_analyze, :is_initial_block, :first_block_number}`: A map with the current module state where: + - `block_number_to_analyze` is the current block number we need to handle. Must be zero if we only need to call + `move_zrc2_token_transfers_to_token_transfers`. + - `is_initial_block` must be `true` for the first iteration (for correct logging). + - `first_block_number` is the block number which should be processed last. + + ## Returns + - `{:noreply, %{:block_number_to_analyze, :is_initial_block, :first_block_number}}` map with the changed state. + """ + @impl GenServer + def handle_info(:continue, %{ + block_number_to_analyze: block_number_to_analyze, + is_initial_block: is_initial_block, + first_block_number: first_block_number + }) do + if block_number_to_analyze > 0 and block_number_to_analyze >= first_block_number do + cond do + is_initial_block -> + Logger.info( + "Handling blocks #{block_number_to_analyze}..#{max(div(block_number_to_analyze, @logging_max_block_range) * @logging_max_block_range + 1, first_block_number)}..." + ) + + rem(block_number_to_analyze, @logging_max_block_range) == 0 -> + Logger.info( + "Handling blocks #{block_number_to_analyze}..#{max(block_number_to_analyze - @logging_max_block_range + 1, first_block_number)}..." + ) + + true -> + :ok + end + + transfer_events = [ + @zrc2_transfer_success_event, + @zrc2_transfer_from_success_event, + @zrc2_minted_event, + @zrc2_burnt_event, + TokenTransfer.constant() + ] + + logs = Zrc2TokenTransfer.read_block_logs(block_number_to_analyze, transfer_events) + transactions = Zrc2TokenTransfer.read_transfer_transactions(logs, @zrc2_transfer_success_event) + + block_numbers_to_analyze = Range.new(block_number_to_analyze, block_number_to_analyze) + + fetch_zrc2_token_transfers_and_adapters(logs, transactions, block_numbers_to_analyze, __MODULE__) + move_zrc2_token_transfers_to_token_transfers() + + LastFetchedCounter.upsert(%{counter_type: @counter_type, value: block_number_to_analyze - 1}) + + # little pause to unload cpu + Process.send_after(self(), :continue, 10) + else + Logger.info("Checking zilliqa_zrc2_token_transfers table and hanging adapters...") + move_zrc2_token_transfers_to_token_transfers() + remove_hanging_adapters() + Process.send_after(self(), :continue, @check_zrc2_token_transfers_interval) + end + + {:noreply, + %{ + block_number_to_analyze: max(block_number_to_analyze - 1, 0), + is_initial_block: false, + first_block_number: first_block_number + }} + end + + @impl GenServer + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + @doc """ + Fetches ZRC-2 token transfers and adapter contract addresses for ZRC-2 tokens + by the given list of logs and transactions from a certain block range. + + ## Parameters + - `logs`: The list of logs to filter them by ZRC-2 events. + - `transactions`: The list of transactions to find ZRC-2 adapter addresses. + - `block_numbers`: The block range for logging purposes. + - `calling_module`: The calling module for logging purposes and for prioritization + of async import of tokens and balances. + + ## Returns + - Nothing. + """ + @spec fetch_zrc2_token_transfers_and_adapters( + [ + %{ + :first_topic => Hash.t(), + :data => Data.t(), + :address_hash => Hash.t(), + :transaction_hash => Hash.t(), + :index => non_neg_integer(), + :block_number => non_neg_integer(), + :block_hash => Hash.t(), + optional(:adapter_address_hash) => Hash.t() | nil + } + ], + [ + %{ + :hash => Hash.t(), + :input => Data.t(), + :to_address_hash => Hash.t() + } + ], + Range.t(), + module() + ) :: no_return() + def fetch_zrc2_token_transfers_and_adapters([], _transactions, _block_numbers, _calling_module), do: nil + + def fetch_zrc2_token_transfers_and_adapters(logs, transactions, block_numbers, calling_module) do + zrc2_logs = filter_zrc2_logs(logs) + + adapter_address_hash_by_zrc2_address_hash = + zrc2_logs + |> Enum.reject(&Map.has_key?(&1, :adapter_address_hash)) + |> Enum.map(& &1.address_hash) + |> Enum.uniq() + |> TokenAdapter.adapter_address_hash_by_zrc2_address_hash() + + {zrc2_token_transfers, token_transfers} = + zrc2_logs + |> Enum.reduce({[], []}, fn log, {zrc2_token_transfers_acc, token_transfers_acc} -> + first_topic = Hash.to_string(log.first_topic) + params = zrc2_event_params(log.data) + + {from_address_hash, to_address_hash, amount} = + try do + cond do + first_topic in [@zrc2_transfer_success_event, @zrc2_transfer_from_success_event] -> + {params.sender, params.recipient, Decimal.new(params.amount)} + + first_topic == @zrc2_minted_event -> + {SmartContract.burn_address_hash_string(), params.recipient, Decimal.new(params.amount)} + + first_topic == @zrc2_burnt_event -> + {params.burn_account, SmartContract.burn_address_hash_string(), Decimal.new(params.amount)} + end + rescue + _ -> + {nil, nil, nil} + end + + if Enum.all?([from_address_hash, to_address_hash, amount], &is_nil(&1)) do + # the event is not supported as has incorrect parameters (doesn't relate to ZRC-2) + {zrc2_token_transfers_acc, token_transfers_acc} + else + adapter_address_hash = zrc2_log_adapter_address_hash(log, adapter_address_hash_by_zrc2_address_hash) + + token_transfer = %{ + transaction_hash: log.transaction_hash, + log_index: log.index, + from_address_hash: from_address_hash, + to_address_hash: to_address_hash, + amount: amount, + block_number: log.block_number, + block_hash: log.block_hash + } + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if is_nil(adapter_address_hash) do + # the adapter address is unknown yet, so place the token transfer to the `zilliqa_zrc2_token_transfers` table + {[Map.put(token_transfer, :zrc2_address_hash, Hash.to_string(log.address_hash)) | zrc2_token_transfers_acc], + token_transfers_acc} + else + # the adapter address is already known, so place the token transfer directly to the `token_transfers` table + {zrc2_token_transfers_acc, + [ + token_transfer + |> Map.put(:token_contract_address_hash, Hash.to_string(adapter_address_hash)) + |> Map.put(:token_type, "ZRC-2") + |> Map.put(:block_consensus, true) + |> Map.put(:token_ids, nil) + | token_transfers_acc + ]} + end + end + end) + + if token_transfers != [] do + Logger.info( + "Found #{Enum.count(token_transfers)} ZRC-2 token transfer(s) with known adapter address in the block(s) #{block_range_printable(block_numbers)}.", + fetcher: calling_module + ) + end + + if zrc2_token_transfers != [] do + Logger.info( + "Found #{Enum.count(zrc2_token_transfers)} ZRC-2 token transfer(s) with unknown adapter address in the block(s) #{block_range_printable(block_numbers)}.", + fetcher: calling_module + ) + end + + addresses = + Addresses.extract_addresses(%{ + token_transfers: token_transfers, + zilliqa_zrc2_token_transfers: zrc2_token_transfers + }) + + {tokens, address_token_balances, address_current_token_balances} = + prepare_tokens_and_balances(token_transfers) + + {:ok, imported} = + Chain.import(%{ + addresses: %{params: addresses}, + address_token_balances: %{params: address_token_balances}, + address_current_token_balances: %{params: address_current_token_balances}, + tokens: %{params: tokens}, + token_transfers: %{params: token_transfers}, + zilliqa_zrc2_token_transfers: %{params: zrc2_token_transfers}, + timeout: :infinity + }) + + async_import_tokens(imported, calling_module == Indexer.Block.Realtime.Fetcher) + async_import_token_balances(imported, calling_module == Indexer.Block.Realtime.Fetcher) + + fetch_zrc2_token_adapters( + logs, + transactions, + block_numbers, + adapter_address_hash_by_zrc2_address_hash, + calling_module + ) + end + + # Filters the given logs to get ZRC-2 logs. + # + # ## Parameters + # - `logs`: The unfiltered list of logs. + # + # ## Returns + # - The filtered list of logs. + @spec filter_zrc2_logs(list()) :: list() + defp filter_zrc2_logs(logs) do + logs + |> Enum.filter( + &(!is_nil(&1.first_topic) and + Hash.to_string(&1.first_topic) in [ + @zrc2_transfer_success_event, + @zrc2_transfer_from_success_event, + @zrc2_minted_event, + @zrc2_burnt_event + ]) + ) + end + + # Fetches ZRC-2 token adapter contract addresses for ZRC-2 tokens + # by the given list of logs and transactions from a certain block range. + # + # For `TransferSuccess` event, we look into the corresponding transaction input: if that's the `transfer` method call + # and the transaction doesn't have the corresponding `Transfer` event emitted by the `to` address, the `to` address + # is the adapter address we searching for, and the address emitted the `TransferSuccess` event is the corresponding + # ZRC-2 address. + # + # ## Parameters + # - `logs`: The list of logs to filter them by `TransferSuccess` and `Transfer` events. + # - `transactions`: The list of transactions to find ZRC-2 adapter addresses. + # - `block_numbers`: The block range for logging purposes. + # - `adapter_address_hash_by_zrc2_address_hash`: A `%{zrc2_address_hash => adapter_address_hash}` map with + # the existing relations to avoid searching of already found adapter addresses. + # - `calling_module`: The calling module for logging purposes. + # + # ## Returns + # - Nothing. + @spec fetch_zrc2_token_adapters( + [ + %{ + :first_topic => Hash.t(), + :data => Data.t(), + :address_hash => Hash.t(), + :transaction_hash => Hash.t(), + :index => non_neg_integer(), + :block_number => non_neg_integer(), + :block_hash => Hash.t(), + optional(:adapter_address_hash) => Hash.t() | nil + } + ], + [ + %{ + :hash => Hash.t(), + :input => Data.t(), + :to_address_hash => Hash.t() + } + ], + Range.t(), + %{Hash.t() => Hash.t()}, + module() + ) :: no_return() + defp fetch_zrc2_token_adapters( + logs, + transactions, + block_numbers, + adapter_address_hash_by_zrc2_address_hash, + calling_module + ) do + transaction_by_hash = + transactions + |> Enum.map(&{&1.hash, &1}) + |> Enum.into(%{}) + + zrc2_token_adapters = + logs + |> Enum.filter(fn log -> + with false <- is_nil(log.first_topic), + true <- Hash.to_string(log.first_topic) == @zrc2_transfer_success_event, + # only ZRC-2 is supported + params = zrc2_event_params(log.data), + true <- Map.has_key?(params, :sender) && Map.has_key?(params, :recipient) && Map.has_key?(params, :amount), + true <- is_nil(zrc2_log_adapter_address_hash(log, adapter_address_hash_by_zrc2_address_hash)) do + transaction_input = transaction_by_hash[log.transaction_hash].input.bytes + + method_id = + if byte_size(transaction_input) >= 4 do + <> = transaction_input + "0x" <> Base.encode16(method_id, case: :lower) + end + + method_id == TokenTransfer.transfer_function_signature() + else + _ -> false + end + end) + |> Enum.reduce([], fn log, acc -> + transaction_hash = log.transaction_hash + to_address_hash = transaction_by_hash[transaction_hash].to_address_hash + + # are there any `Transfer` logs emitted by the `to_address_hash` in this transaction? + erc20_transfer_event_found = + logs + |> Enum.filter(&(&1.transaction_hash == transaction_hash)) + |> Enum.any?( + &(!is_nil(&1.first_topic) and Hash.to_string(&1.first_topic) == TokenTransfer.constant() and + &1.address_hash == to_address_hash) + ) + + if erc20_transfer_event_found do + acc + else + # if the `Transfer` log is not found, this is ERC-20 adapter contract address + [ + %{ + adapter_address_hash: Hash.to_string(to_address_hash), + zrc2_address_hash: Hash.to_string(log.address_hash) + } + | acc + ] + end + end) + |> Enum.uniq() + + if zrc2_token_adapters != [] do + Logger.info( + "Found #{Enum.count(zrc2_token_adapters)} new ERC-20 adapter(s) for ZRC-2 token(s) in the block(s) #{block_range_printable(block_numbers)}.", + fetcher: calling_module + ) + + {:ok, _} = + Chain.import(%{ + addresses: %{params: Addresses.extract_addresses(%{zilliqa_zrc2_token_adapters: zrc2_token_adapters})}, + zilliqa_zrc2_token_adapters: %{params: zrc2_token_adapters}, + timeout: :infinity + }) + end + end + + # Scans the `zilliqa_zrc2_token_transfers` table for the rows that have corresponding + # adapter addresses in the `zilliqa_zrc2_token_adapters` table. The found rows are inserted into the `token_transfers` + # table (with the `token_contract_address_hash` == `adapter_address_hash` and `token_type` == "ZRC-2"), and then + # removed from the `zilliqa_zrc2_token_transfers` table. + # + # For the found token transfers, the function prepares and asynchronously imports the corresponding tokens + # and their balances. + # + # ## Returns + # - :ok + @spec move_zrc2_token_transfers_to_token_transfers() :: :ok + defp move_zrc2_token_transfers_to_token_transfers do + zrc2_token_transfers = Zrc2TokenTransfer.zrc2_token_transfers_having_adapter() + + token_transfers = + zrc2_token_transfers + |> Enum.map(fn token_transfer -> + token_transfer + |> Map.put(:from_address_hash, Hash.to_string(token_transfer.from_address_hash)) + |> Map.put(:to_address_hash, Hash.to_string(token_transfer.to_address_hash)) + |> Map.put(:token_contract_address_hash, Hash.to_string(token_transfer.adapter_address_hash)) + |> Map.put(:token_type, "ZRC-2") + |> Map.put(:block_consensus, true) + |> Map.put(:token_ids, nil) + |> Map.delete(:adapter_address_hash) + end) + + if token_transfers != [] do + {tokens, address_token_balances, address_current_token_balances} = + prepare_tokens_and_balances(token_transfers) + + Logger.info("Moving #{Enum.count(token_transfers)} ZRC-2 token transfer(s) to the token_transfers table...") + + {:ok, imported} = + Chain.import(%{ + address_token_balances: %{params: address_token_balances}, + address_current_token_balances: %{params: address_current_token_balances}, + tokens: %{params: tokens}, + token_transfers: %{params: token_transfers}, + timeout: :infinity + }) + + async_import_tokens(imported, false) + async_import_token_balances(imported, false) + end + + Enum.each(token_transfers, fn tt -> + Repo.delete_all( + from( + ztt in Zrc2TokenTransfer, + where: + ztt.transaction_hash == ^tt.transaction_hash and ztt.log_index == ^tt.log_index and + ztt.block_hash == ^tt.block_hash + ) + ) + end) + end + + # Checks if there are adapter addresses not bound to any consensus token transfers: if there are, + # they are removed from the `zilliqa_zrc2_token_adapters` table. Also, the corresponding rows are + # removed from the `zilliqa_zrc2_token_transfers` table by the `zrc2_address_hash` address. + # The hanging adapters can appear due to reorgs. + @spec remove_hanging_adapters() :: any() + defp remove_hanging_adapters do + hanging_adapters = + TokenAdapter + |> Repo.all() + |> Enum.reduce([], fn adapter, acc -> + query = + from(tt in TokenTransfer, + where: tt.token_contract_address_hash == ^adapter.adapter_address_hash and tt.block_consensus == true, + limit: 1 + ) + + case Repo.one(query) do + nil -> [adapter | acc] + _ -> acc + end + end) + + if hanging_adapters != [] do + adapter_address_hashes = + hanging_adapters + |> Enum.map(& &1.adapter_address_hash) + + zrc2_address_hashes = + hanging_adapters + |> Enum.map(& &1.zrc2_address_hash) + + Repo.delete_all(from(a in TokenAdapter, where: a.adapter_address_hash in ^adapter_address_hashes)) + Repo.delete_all(from(ztt in Zrc2TokenTransfer, where: ztt.zrc2_address_hash in ^zrc2_address_hashes)) + end + end + + # Prepares tokens, token balances, and current token balances to be imported + # to the database by the given token transfers. + # + # ## Parameters + # - `token_transfers`: The list of token transfers details. + # + # ## Returns + # - `{tokens, address_token_balances, address_current_token_balances}` tuple with the lists + # ready to be imported to the database. + @spec prepare_tokens_and_balances([map()]) :: {list(), list(), list()} + defp prepare_tokens_and_balances(token_transfers) do + tokens = + token_transfers + |> Enum.map(&%{contract_address_hash: &1.token_contract_address_hash, type: &1.token_type}) + |> Enum.uniq() + + address_token_balances = + %{token_transfers_params: BlockFetcher.token_transfers_merge_token(token_transfers, tokens)} + |> AddressTokenBalances.params_set() + |> MapSet.to_list() + + address_current_token_balances = + address_token_balances + |> TokenBalances.to_address_current_token_balances() + + {tokens, address_token_balances, address_current_token_balances} + end + + # Gets ZRC-2 adapter address hash from the corresponding ZRC-2 log + # or (if not found) from the `%{zrc2_address_hash => adapter_address_hash}` map. + # + # ## Parameters + # - `log`: The ZRC-2 log map with or without the `adapter_address_hash` key. + # - `adapter_address_hash_by_zrc2_address_hash`: The `%{zrc2_address_hash => adapter_address_hash}` map + # with already existing relations. + # + # ## Returns + # - The adapter address hash. + # - `nil` if not found. + @spec zrc2_log_adapter_address_hash( + %{optional(:adapter_address_hash) => Hash.t() | nil, :address_hash => Hash.t()}, + %{Hash.t() => Hash.t()} + ) :: Hash.t() | nil + defp zrc2_log_adapter_address_hash(log, adapter_address_hash_by_zrc2_address_hash) do + case Map.fetch(log, :adapter_address_hash) do + {:ok, adapter_address_hash} -> adapter_address_hash + :error -> Map.get(adapter_address_hash_by_zrc2_address_hash, log.address_hash) + end + end + + # Converts ZRC-2 event string parameter into a map. + # + # Example: + # {"address":"0x818ca2e217e060ad17b7bd0124a483a1f66930a9","_eventname":"TransferSuccess","params":[{"vname":"sender","value":"0x90696d5bea3f11feb2e304718d17564d90c3d780","type":"ByStr20"},{"vname":"recipient","value":"0x4afc249b706560766a3c6ab8db2825102aa224fc","type":"ByStr20"},{"vname":"amount","value":"1000000000","type":"Uint128"}]} + # will be converted to + # %{sender: "0x90696d5bea3f11feb2e304718d17564d90c3d780", recipient: "0x4afc249b706560766a3c6ab8db2825102aa224fc", amount: "1000000000"} + # + # ## Parameters + # - `log_data`: The event's data field. + # + # ## Returns + # - A map with the parameters from the event. + @spec zrc2_event_params(Data.t()) :: map() + defp zrc2_event_params(log_data) do + log_data + |> decode_data([:string]) + |> Enum.at(0) + |> Jason.decode!() + |> Map.get("params", nil) + |> Enum.map(fn param -> {String.to_atom(param["vname"]), param["value"]} end) + |> Enum.into(%{}) + end + + # Makes the given range human readable (and prints only one value if the range contains only one item). + # + # ## Parameters + # - `range`: The range to make printable. + # + # ## Returns + # - A string of the range. + @spec block_range_printable(Range.t()) :: String.t() + defp block_range_printable(range) do + first..last//_step = range + + if Range.size(range) == 1 do + to_string(first) + else + "#{first}..#{last}" + end + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index aeeaa80b739b..59a4df4583d8 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -263,6 +263,7 @@ defmodule Indexer.Supervisor do ] ]}, {Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, [[memory_monitor: memory_monitor]]}, + {Indexer.Fetcher.Zilliqa.Zrc2Tokens.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.Beacon.Blob.Supervisor, [[memory_monitor: memory_monitor]]}, {Indexer.Fetcher.Beacon.Deposit.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]}, {Indexer.Fetcher.Beacon.Deposit.Status.Supervisor, []}, diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index dbe6399c91c1..737418e5c29f 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -137,6 +137,28 @@ defmodule Indexer.Transform.Addresses do %{from: :token_contract_address_hash, to: :hash} ] ], + zilliqa_zrc2_token_transfers: [ + [ + %{from: :block_number, to: :fetched_coin_balance_block_number}, + %{from: :from_address_hash, to: :hash} + ], + [ + %{from: :block_number, to: :fetched_coin_balance_block_number}, + %{from: :to_address_hash, to: :hash} + ], + [ + %{from: :block_number, to: :fetched_coin_balance_block_number}, + %{from: :zrc2_address_hash, to: :hash} + ] + ], + zilliqa_zrc2_token_adapters: [ + [ + %{from: :zrc2_address_hash, to: :hash} + ], + [ + %{from: :adapter_address_hash, to: :hash} + ] + ], mint_transfers: [ [ %{from: :block_number, to: :fetched_coin_balance_block_number}, @@ -480,6 +502,20 @@ defmodule Indexer.Transform.Addresses do required(:block_number) => non_neg_integer() } ], + optional(:zilliqa_zrc2_token_transfers) => [ + %{ + required(:from_address_hash) => String.t(), + required(:to_address_hash) => String.t(), + required(:zrc2_address_hash) => String.t(), + required(:block_number) => non_neg_integer() + } + ], + optional(:zilliqa_zrc2_token_adapters) => [ + %{ + required(:zrc2_address_hash) => String.t(), + required(:adapter_address_hash) => String.t() + } + ], optional(:transaction_actions) => [ %{ required(:data) => map() diff --git a/config/runtime.exs b/config/runtime.exs index 4f66a2c4be15..e381b7f8c89f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1640,6 +1640,8 @@ config :nft_media_handler, Indexer.NFTMediaHandler.Backfiller, config :indexer, Indexer.Fetcher.Zilliqa.ScillaSmartContracts.Supervisor, disabled?: ConfigHelper.chain_type() != :zilliqa +config :indexer, Indexer.Fetcher.Zilliqa.Zrc2Tokens.Supervisor, disabled?: ConfigHelper.chain_type() != :zilliqa + config :libcluster, topologies: [ k8sDNS: [ diff --git a/cspell.json b/cspell.json index 2dbb71364ded..f4fd70c2b161 100644 --- a/cspell.json +++ b/cspell.json @@ -246,6 +246,7 @@ "etimedout", "eveem", "evenodd", + "eventname", "evision", "evmversion", "exitor", @@ -729,6 +730,7 @@ "verifyproxycontract", "verifysourcecode", "viewerjs", + "vname", "volumefrom", "volumeto", "vyper", From e1a7f0c816d2201959da709c13522d43e8b3b877 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 25 Nov 2025 17:08:01 +0300 Subject: [PATCH 0493/1006] fix: Fix dialyzer test for filecoin chain type (#13623) --- apps/explorer/lib/explorer/chain/search.ex | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index cc84d79fb231..a542b1eb461c 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -279,7 +279,15 @@ defmodule Explorer.Chain.Search do | {:text, binary()} | nil - @spec match_search_result(map(), binary()) :: base_search_results() + case @chain_type do + :filecoin -> + @type search_results :: base_search_results() | {:filecoin, any()} + + _ -> + @type search_results :: base_search_results() + end + + @spec match_search_result(map(), binary()) :: search_results() defp match_search_result(%{address_hash_result: {:ok, address_hash}}, _prepared_term), do: {:address_hash, address_hash} @@ -305,20 +313,11 @@ defmodule Explorer.Chain.Search do defp match_search_result(_, _), do: nil - if @chain_type == :filecoin do - @spec prepare_search_query(binary(), {:some, binary()} | :none) :: base_search_results() | {:filecoin, any()} - defp prepare_search_query(query, {:some, prepared_term}) do - results = prepare_results(query) - - match_search_result(results, prepared_term) - end - else - @spec prepare_search_query(binary(), {:some, binary()} | :none) :: base_search_results() - defp prepare_search_query(query, {:some, prepared_term}) do - results = prepare_results(query) + @spec prepare_search_query(binary(), {:some, binary()} | :none) :: search_results() + defp prepare_search_query(query, {:some, prepared_term}) do + results = prepare_results(query) - match_search_result(results, prepared_term) - end + match_search_result(results, prepared_term) end defp prepare_search_query(_query, _) do @@ -356,7 +355,7 @@ defmodule Explorer.Chain.Search do defp run_ens_task_if_first_page(_, _query_string, _options), do: nil @spec balanced_unpaginated_search_result( - base_search_results(), + search_results(), PagingOptions.t(), binary(), [Chain.api?() | Chain.show_scam_tokens?()] From 46928d8ece7a437478f85cb5ea60ebea5b48aee5 Mon Sep 17 00:00:00 2001 From: Nick Zenchik <65654967+nzenchik@users.noreply.github.com> Date: Tue, 25 Nov 2025 18:22:43 +0400 Subject: [PATCH 0494/1006] chore: Using own runner for build (#13624) --- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/public-release.yml | 20 +++++++++---------- .../publish-docker-image-custom-build.yml | 4 ++-- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 4 ++-- .../publish-docker-image-for-celo.yml | 5 ++--- .../publish-docker-image-for-core.yml | 4 ++-- .../publish-docker-image-for-eth-sepolia.yml | 4 ++-- .../publish-docker-image-for-eth.yml | 4 ++-- .../publish-docker-image-for-filecoin.yml | 4 ++-- .../publish-docker-image-for-fuse.yml | 4 ++-- .../publish-docker-image-for-gnosis-chain.yml | 4 ++-- .../publish-docker-image-for-l2-staging.yml | 4 ++-- .../publish-docker-image-for-lukso.yml | 4 ++-- ...h-docker-image-for-optimism-worldchain.yml | 5 ++--- .../publish-docker-image-for-optimism.yml | 5 ++--- .../publish-docker-image-for-rootstock.yml | 4 ++-- .../publish-docker-image-for-scroll.yml | 5 ++--- .../publish-docker-image-for-zetachain.yml | 4 ++-- .../publish-docker-image-for-zilliqa.yml | 6 ++---- .../publish-docker-image-for-zkevm.yml | 5 ++--- .../publish-docker-image-for-zksync.yml | 5 ++--- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 3 +-- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 3 +-- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 3 +-- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 3 +-- .github/workflows/release-rootstock.yml | 3 +-- .github/workflows/release-scroll.yml | 3 +-- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 4 +--- .github/workflows/release-zksync.yml | 2 +- 51 files changed, 79 insertions(+), 95 deletions(-) diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 848133b694b9..d5f6f99c4e16 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 379762b01455..50251f287a01 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index cb3a62481591..a6f664835633 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 25b93a779972..cb51492706c3 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index bb7302ac5fa6..9cdbc7f7178d 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index 67f1372a7ada..af8421e492cf 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index ebf56ff30a1b..72b9c97752bd 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index e5bfc51f7b24..4a98bda61744 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index b0d2859957d9..4c4cf34d5b72 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 34fdc5dc6f71..12fbdb8fcbbc 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index ca8999bdac09..4666f2493e0d 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index ad4384d6f5e7..0a6282db6382 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index cd932d6746b6..2ba719dda421 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -19,7 +19,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/public-release.yml b/.github/workflows/public-release.yml index 243ae60a2e50..4ad1c303b603 100644 --- a/.github/workflows/public-release.yml +++ b/.github/workflows/public-release.yml @@ -10,11 +10,11 @@ on: jobs: copy-bundle: - runs-on: ubuntu-latest + runs-on: build permissions: contents: read packages: write - + steps: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -40,7 +40,7 @@ jobs: - name: Copy chain-specific images from private to public repositories run: | RELEASE_NUMBER="${{ github.event.inputs.release_number }}" - + # Array of chain-specific repositories CHAINS=( "blockscout" @@ -58,21 +58,21 @@ jobs: "blockscout-zilliqa" "blockscout-zksync" ) - + # Copy each chain-specific image for CHAIN in "${CHAINS[@]}"; do echo "Copying ${CHAIN}..." - + # Copy with the specific release number tag docker buildx imagetools create \ --tag ghcr.io/blockscout/${CHAIN}:${RELEASE_NUMBER} \ ghcr.io/blockscout/${CHAIN}-private:${RELEASE_NUMBER} - + # Also copy with latest tag docker buildx imagetools create \ --tag ghcr.io/blockscout/${CHAIN}:latest \ ghcr.io/blockscout/${CHAIN}-private:latest - + echo "✅ Completed copying ${CHAIN}" done @@ -82,7 +82,7 @@ jobs: echo "🎉 All bundles successfully copied from private to public repositories" echo "" echo "🔗 All repositories copied:" - + CHAINS=( "blockscout" "blockscout-arbitrum" @@ -99,11 +99,11 @@ jobs: "blockscout-zilliqa" "blockscout-zksync" ) - + for CHAIN in "${CHAINS[@]}"; do echo " ✅ ${CHAIN}-private -> ${CHAIN} (tags: ${RELEASE_NUMBER}, latest)" done - + echo "" echo "🔍 Inspecting main repository images:" docker buildx imagetools inspect ghcr.io/blockscout/blockscout:${TARGET_TAG} diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index d611f2e4bc2e..810fea2195c6 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -25,7 +25,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 22bb365556bb..cb14bda0ce4f 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -20,7 +20,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 027c0cc95f15..54536eb6e7ed 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: arbitrum @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index fbeadebcd2cf..9af3f85d2e4a 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism-celo @@ -27,7 +27,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image for CELO (indexer + API) uses: docker/build-push-action@v6 with: @@ -62,4 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=${{ env.DOCKER_CHAIN_NAME }} - diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index bb93181a8fc2..1041c1b4d5a4 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: poa @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index a33b682c9d11..2f8538dbeb77 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: eth-sepolia @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 48799bc527f5..ac02731804fa 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: ethereum @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index d7ed77faa437..178596481857 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -11,7 +11,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: filecoin @@ -25,7 +25,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image for Filecoin (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index aa1187e3cff3..edc5dcf91bbc 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: fuse @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 76dfe75049d5..66aa400af280 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: xdai @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index f4f9a9d0870f..88976b6c6636 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism-l2-advanced @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 56ab0211a99a..c3a310d21e84 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: lukso @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index cec976c9666b..600edab111c5 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -59,4 +59,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=optimism - diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index fe8948235b46..ea544d4f0038 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: optimism @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -59,4 +59,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=optimism - diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index f2490e9c1dd0..48eb2975100f 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: rsk @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index a7237878dede..e183f176af1b 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: scroll @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -59,4 +59,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=scroll - diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index ad4bd4726fdb..c0d4e3e73d04 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zetachain @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image uses: docker/build-push-action@v6 with: diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index ddd94cb766e1..5bca7d319212 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zilliqa @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -59,5 +59,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=${{ env.DOCKER_CHAIN_NAME }} - - diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 35eae8cf8468..f2c8d41300af 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -12,7 +12,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zkevm @@ -26,7 +26,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -59,4 +59,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=polygon_zkevm - diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index cd361ec5f5c0..bac29c965387 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -11,7 +11,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 DOCKER_CHAIN_NAME: zksync @@ -25,7 +25,7 @@ jobs: docker-remote-multi-platform: true docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} - + - name: Build and push Docker image (indexer + API) uses: docker/build-push-action@v6 with: @@ -58,4 +58,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=zksync - diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 93f5c67f519a..c6c2a77df141 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -14,7 +14,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 3dc4250aee30..be9a4c503ea2 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -21,7 +21,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index f381ed3f53c7..27386dfd4a6b 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -14,7 +14,7 @@ permissions: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build steps: - uses: actions/checkout@v5 - name: Setup repo @@ -68,4 +68,3 @@ jobs: AMPLITUDE_API_KEY= BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} - diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 5c76b75e9369..ba17ab56de03 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index 54c5718698f3..a9df88603646 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 @@ -65,4 +65,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=optimism-celo - diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 24c5aabb6330..e7627dfcf414 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 09367af296ca..bab71a34a080 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -62,4 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=ethereum - diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index c89a7c137bbb..0fb1fb8b0dc3 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index fe353ebc35c7..e144a483db34 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index b513b3c3d7e7..b50018ee61ed 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 6e8b700792c9..0b159478ffd8 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index c49b8e8c64a4..2e451a9c1c61 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -62,4 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=polygon_zkevm - diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 4c73f56d285f..0202d2bab00b 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -62,4 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=rsk - diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 43afa0bac809..ba5f14520767 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -62,4 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=scroll - diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 2fd4ad024dad..d426b1a838c2 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index eca5696a465c..7c8d8964d577 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: @@ -62,5 +62,3 @@ jobs: BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }} RELEASE_VERSION=${{ env.RELEASE_VERSION }} CHAIN_TYPE=zilliqa - - diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index a66309ae7985..9dd51b3960ce 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -16,7 +16,7 @@ env: jobs: push_to_registry: name: Push Docker image to GitHub Container Registry - runs-on: ubuntu-latest + runs-on: build env: RELEASE_VERSION: 9.2.2 steps: From e8060ef6e0ac812c42fbedc677ec810d728ab89f Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:03:54 +0300 Subject: [PATCH 0495/1006] feat: Add metadata to eth bytecode DB lookup request (#13625) --- .../controllers/api/v2/import_controller.ex | 10 ++++++---- ...ook_up_smart_contract_sources_on_demand.ex | 9 +++++---- .../eth_bytecode_db_interface.ex | 18 +++++++++-------- .../lib/explorer/smart_contract/helper.ex | 4 ++-- .../rust_verifier_interface_behaviour.ex | 20 ++++++++----------- 5 files changed, 31 insertions(+), 30 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex index ba7b6bf002e4..59eaac18738b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.API.V2.ImportController do alias Explorer.SmartContract.EthBytecodeDBInterface alias Indexer.Fetcher.TokenUpdater - import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, contract_creation_input: 1] + import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, fetch_data_for_verification: 2] require Logger @api_true [api?: true] @@ -126,14 +126,16 @@ defmodule BlockScoutWeb.API.V2.ImportController do {:already_verified, not (is_nil(address.smart_contract) or address.smart_contract.partially_verified)} do - creation_transaction_input = contract_creation_input(address.hash) + {creation_transaction_input, deployed_bytecode, verifier_metadata} = + fetch_data_for_verification(address_hash_string, Data.to_string(address.contract_code)) with {:ok, %{"sourceType" => type} = source} <- %{} - |> prepare_bytecode_for_microservice(creation_transaction_input, Data.to_string(address.contract_code)) + |> prepare_bytecode_for_microservice(creation_transaction_input, deployed_bytecode) |> EthBytecodeDBInterface.search_contract_in_eth_bytecode_internal_db( address_hash_string, - params_to_contract_search_options(params) + params_to_contract_search_options(params), + verifier_metadata ), {:ok, _} <- LookUpSmartContractSourcesOnDemand.process_contract_source(type, source, address.hash) do conn diff --git a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex index eb18e5f01318..5d187b54a48c 100644 --- a/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex +++ b/apps/explorer/lib/explorer/chain/fetcher/look_up_smart_contract_sources_on_demand.ex @@ -31,7 +31,7 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do alias Explorer.SmartContract.Vyper.Publisher, as: VyperPublisher alias Explorer.Utility.RateLimiter - import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, contract_creation_input: 1] + import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, fetch_data_for_verification: 2] @cache_name :smart_contracts_sources_fetching @@ -93,12 +93,13 @@ defmodule Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand do defp fetch_sources(address_hash_string, address_contract_code, only_full?) do Publisher.broadcast(%{eth_bytecode_db_lookup_started: [address_hash_string]}, :on_demand) - creation_transaction_input = contract_creation_input(address_hash_string) + {creation_transaction_input, deployed_bytecode, verifier_metadata} = + fetch_data_for_verification(address_hash_string, Data.to_string(address_contract_code)) with {:ok, %{"sourceType" => type, "matchType" => match_type} = source} <- %{} - |> prepare_bytecode_for_microservice(creation_transaction_input, Data.to_string(address_contract_code)) - |> EthBytecodeDBInterface.search_contract(address_hash_string), + |> prepare_bytecode_for_microservice(creation_transaction_input, deployed_bytecode) + |> EthBytecodeDBInterface.search_contract(address_hash_string, verifier_metadata), :ok <- check_match_type(match_type, only_full?), {:ok, _} <- process_contract_source(type, source, address_hash_string) do Publisher.broadcast(%{smart_contract_was_verified: [address_hash_string]}, :on_demand) diff --git a/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex b/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex index ace02a4d746e..e142ce4a2cee 100644 --- a/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/eth_bytecode_db_interface.ex @@ -3,18 +3,19 @@ defmodule Explorer.SmartContract.EthBytecodeDBInterface do Adapter for interaction with https://github.com/blockscout/blockscout-rs/tree/main/eth-bytecode-db """ - def search_contract(%{"bytecode" => _, "bytecodeType" => _} = body, address_hash) do + def search_contract(%{"bytecode" => _, "bytecodeType" => _} = body, address_hash, metadata) do if chain_id = Application.get_env(:block_scout_web, :chain_id) do http_post_request( bytecode_search_all_sources_url(), - Map.merge(body, %{ + body + |> Map.merge(%{ "chain" => to_string(chain_id), "address" => to_string(address_hash) - }), - false + }) + |> append_metadata(metadata) ) else - http_post_request(bytecode_search_sources_url(), body, false) + http_post_request(bytecode_search_sources_url(), append_metadata(body, metadata)) end end @@ -22,11 +23,12 @@ defmodule Explorer.SmartContract.EthBytecodeDBInterface do Function to search smart contracts in eth-bytecode-db, similar to `search_contract/2` but this function uses only `/api/v2/bytecodes/sources:search` method """ - @spec search_contract_in_eth_bytecode_internal_db(map(), binary(), keyword()) :: {:error, any} | {:ok, any} + @spec search_contract_in_eth_bytecode_internal_db(map(), binary(), keyword(), map()) :: {:error, any} | {:ok, any} def search_contract_in_eth_bytecode_internal_db( %{"bytecode" => _, "bytecodeType" => _} = body, address_hash_string, - options + options, + metadata ) do chain_id = Application.get_env(:block_scout_web, :chain_id) @@ -51,7 +53,7 @@ defmodule Explorer.SmartContract.EthBytecodeDBInterface do })} end - http_post_request(url, body, false, options) + http_post_request(url, body |> append_metadata(metadata), options) end def process_verifier_response( diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index 9b3231443793..d7a956382cfc 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -198,8 +198,8 @@ defmodule Explorer.SmartContract.Helper do Metadata will be sent to a verifier microservice """ @spec fetch_data_for_verification(binary() | Hash.t()) :: {binary() | nil, binary(), map()} - def fetch_data_for_verification(address_hash) do - deployed_bytecode = Chain.smart_contract_bytecode(address_hash) + def fetch_data_for_verification(address_hash, deployed_bytecode \\ nil) do + deployed_bytecode = deployed_bytecode || Chain.smart_contract_bytecode(address_hash) metadata = %{ "contractAddress" => to_string(address_hash), diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex index ff9774911a28..a5ccc5d7d4bb 100644 --- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex +++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex @@ -24,7 +24,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do } = body, metadata ) do - http_post_request(solidity_multiple_files_verification_url(), append_metadata(body, metadata), true) + http_post_request(solidity_multiple_files_verification_url(), append_metadata(body, metadata)) end def verify_standard_json_input( @@ -36,7 +36,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do } = body, metadata ) do - http_post_request(solidity_standard_json_verification_url(), append_metadata(body, metadata), true) + http_post_request(solidity_standard_json_verification_url(), append_metadata(body, metadata)) end def zksync_verify_standard_json_input( @@ -48,7 +48,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do } = body, metadata ) do - http_post_request(solidity_standard_json_verification_url(), append_metadata(body, metadata), true) + http_post_request(solidity_standard_json_verification_url(), append_metadata(body, metadata)) end def vyper_verify_multipart( @@ -60,7 +60,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do } = body, metadata ) do - http_post_request(vyper_multiple_files_verification_url(), append_metadata(body, metadata), true) + http_post_request(vyper_multiple_files_verification_url(), append_metadata(body, metadata)) end def vyper_verify_standard_json( @@ -72,15 +72,13 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do } = body, metadata ) do - http_post_request(vyper_standard_json_verification_url(), append_metadata(body, metadata), true) + http_post_request(vyper_standard_json_verification_url(), append_metadata(body, metadata)) end - def http_post_request(url, body, is_verification_request?, options \\ []) do + def http_post_request(url, body, options \\ []) do headers = [{"Content-Type", "application/json"}] - case HttpClient.post(url, Jason.encode!(body), maybe_put_api_key_header(headers, is_verification_request?), - recv_timeout: @post_timeout - ) do + case HttpClient.post(url, Jason.encode!(body), put_api_key_header(headers), recv_timeout: @post_timeout) do {:ok, %{body: body, status_code: _}} -> process_verifier_response(body, options) @@ -100,9 +98,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do end end - defp maybe_put_api_key_header(headers, false), do: headers - - defp maybe_put_api_key_header(headers, true) do + defp put_api_key_header(headers) do api_key = Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:api_key] if api_key do From 02b7c09d13f0ec69c676409f57453651236eee35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:06:32 +0300 Subject: [PATCH 0496/1006] chore(deps): bump logger_json from 5.1.4 to 7.0.4 (#12783) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Baranov --- apps/block_scout_web/config/config.exs | 28 ++------- apps/block_scout_web/config/dev.exs | 6 +- apps/block_scout_web/config/prod.exs | 3 - apps/block_scout_web/config/test.exs | 4 +- apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/config/config.exs | 6 +- apps/ethereum_jsonrpc/config/dev.exs | 4 +- apps/ethereum_jsonrpc/config/prod.exs | 1 - apps/ethereum_jsonrpc/config/test.exs | 4 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/config/config.exs | 6 +- apps/explorer/config/dev.exs | 6 +- apps/explorer/config/prod.exs | 3 - apps/explorer/config/test.exs | 4 +- .../lib/explorer/smart_contract/helper.ex | 10 ---- apps/explorer/mix.exs | 4 +- apps/indexer/config/config.exs | 6 +- apps/indexer/config/dev.exs | 11 +--- apps/indexer/config/prod.exs | 8 --- apps/indexer/config/test.exs | 7 +-- apps/indexer/mix.exs | 2 +- config/config.exs | 36 +++-------- config/config_helper.exs | 60 ++++++++++++------- config/dev.exs | 12 +--- config/prod.exs | 12 +--- config/runtime.exs | 12 ++++ config/test.exs | 9 +-- mix.lock | 2 +- 28 files changed, 83 insertions(+), 187 deletions(-) diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 01d273f77049..e3e905924674 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -49,33 +49,13 @@ config :block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, # days history_size: 30 -config :ex_cldr, - default_locale: "en", - default_backend: BlockScoutWeb.Cldr - config :logger, :block_scout_web, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, + metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :block_scout_web] -config :logger, :api, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, - metadata_filter: [application: :api] - -config :logger, :api_v2, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, - metadata_filter: [application: :api_v2] +config :ex_cldr, + default_locale: "en", + default_backend: BlockScoutWeb.Cldr config :prometheus, BlockScoutWeb.Prometheus.PublicExporter, path: "/public-metrics", diff --git a/apps/block_scout_web/config/dev.exs b/apps/block_scout_web/config/dev.exs index d69502e2d5bd..5c1e216afaa1 100644 --- a/apps/block_scout_web/config/dev.exs +++ b/apps/block_scout_web/config/dev.exs @@ -50,17 +50,13 @@ config :block_scout_web, BlockScoutWeb.Endpoint, config :block_scout_web, BlockScoutWeb.Tracer, env: "dev", disabled?: true -config :logger, :block_scout_web, - level: :debug, - path: Path.absname("logs/dev/block_scout_web.log") +config :logger, :block_scout_web, path: Path.absname("logs/dev/block_scout_web.log") config :logger, :api, - level: :debug, path: Path.absname("logs/dev/api.log"), metadata_filter: [application: :api] config :logger, :api_v2, - level: :debug, path: Path.absname("logs/dev/api_v2.log"), metadata_filter: [application: :api_v2] diff --git a/apps/block_scout_web/config/prod.exs b/apps/block_scout_web/config/prod.exs index 2efcfd72068f..916e45932d79 100644 --- a/apps/block_scout_web/config/prod.exs +++ b/apps/block_scout_web/config/prod.exs @@ -20,18 +20,15 @@ config :block_scout_web, BlockScoutWeb.Endpoint, config :block_scout_web, BlockScoutWeb.Tracer, env: "production", disabled?: true config :logger, :block_scout_web, - level: :info, path: Path.absname("logs/prod/block_scout_web.log"), rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :api, - level: :debug, path: Path.absname("logs/prod/api.log"), metadata_filter: [application: :api], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :api_v2, - level: :debug, path: Path.absname("logs/prod/api_v2.log"), metadata_filter: [application: :api_v2], rotate: %{max_bytes: 52_428_800, keep: 19} diff --git a/apps/block_scout_web/config/test.exs b/apps/block_scout_web/config/test.exs index f7327da90a86..c5a40aa68bde 100644 --- a/apps/block_scout_web/config/test.exs +++ b/apps/block_scout_web/config/test.exs @@ -13,9 +13,7 @@ config :block_scout_web, BlockScoutWeb.Endpoint, config :block_scout_web, BlockScoutWeb.Tracer, disabled?: false -config :logger, :block_scout_web, - level: :warn, - path: Path.absname("logs/test/block_scout_web.log") +config :logger, :block_scout_web, path: Path.absname("logs/test/block_scout_web.log") # Configure wallaby config :wallaby, screenshot_on_failure: true, driver: Wallaby.Chrome, js_errors: false diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 2f22397dec21..3acc56b6b3f2 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -118,6 +118,7 @@ defmodule BlockScoutWeb.Mixfile do {:junit_formatter, ">= 0.0.0", only: [:test], runtime: false}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, + {:logger_json, "~> 7.0"}, {:math, "~> 0.7.0"}, {:mock, "~> 0.3.0", only: [:test], runtime: false}, {:number, "~> 1.0.1"}, @@ -160,7 +161,6 @@ defmodule BlockScoutWeb.Mixfile do {:ueberauth_auth0, "~> 2.0"}, {:utils, in_umbrella: true}, {:bureaucrat, "~> 0.2.9", only: :test}, - {:logger_json, "~> 5.1"}, {:open_api_spex, "~> 3.21"}, {:ymlr, "~> 5.1"} ] diff --git a/apps/ethereum_jsonrpc/config/config.exs b/apps/ethereum_jsonrpc/config/config.exs index 7df95245ee45..9f8334f12554 100644 --- a/apps/ethereum_jsonrpc/config/config.exs +++ b/apps/ethereum_jsonrpc/config/config.exs @@ -22,11 +22,7 @@ config :ethereum_jsonrpc, EthereumJSONRPC.Tracer, trace_key: :blockscout config :logger, :ethereum_jsonrpc, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, + metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :ethereum_jsonrpc] config :tesla, adapter: Tesla.Adapter.Mint diff --git a/apps/ethereum_jsonrpc/config/dev.exs b/apps/ethereum_jsonrpc/config/dev.exs index da39fcf0802c..2493947caaf7 100644 --- a/apps/ethereum_jsonrpc/config/dev.exs +++ b/apps/ethereum_jsonrpc/config/dev.exs @@ -2,6 +2,4 @@ import Config config :ethereum_jsonrpc, EthereumJSONRPC.Tracer, env: "dev", disabled?: true -config :logger, :ethereum_jsonrpc, - level: :debug, - path: Path.absname("logs/dev/ethereum_jsonrpc.log") +config :logger, :ethereum_jsonrpc, path: Path.absname("logs/dev/ethereum_jsonrpc.log") diff --git a/apps/ethereum_jsonrpc/config/prod.exs b/apps/ethereum_jsonrpc/config/prod.exs index f11e4e7a728c..fc811410212c 100644 --- a/apps/ethereum_jsonrpc/config/prod.exs +++ b/apps/ethereum_jsonrpc/config/prod.exs @@ -3,6 +3,5 @@ import Config config :ethereum_jsonrpc, EthereumJSONRPC.Tracer, env: "production", disabled?: true config :logger, :ethereum_jsonrpc, - level: :info, path: Path.absname("logs/prod/ethereum_jsonrpc.log"), rotate: %{max_bytes: 52_428_800, keep: 19} diff --git a/apps/ethereum_jsonrpc/config/test.exs b/apps/ethereum_jsonrpc/config/test.exs index f04b52869c67..0f84d4698042 100644 --- a/apps/ethereum_jsonrpc/config/test.exs +++ b/apps/ethereum_jsonrpc/config/test.exs @@ -19,6 +19,4 @@ config :ethereum_jsonrpc, EthereumJSONRPC.Tracer, disabled?: false config :tesla, adapter: Explorer.Mock.TeslaAdapter -config :logger, :ethereum_jsonrpc, - level: :warn, - path: Path.absname("logs/test/ethereum_jsonrpc.log") +config :logger, :ethereum_jsonrpc, path: Path.absname("logs/test/ethereum_jsonrpc.log") diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index aa2671ba63b2..ab655560dcaf 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -67,6 +67,7 @@ defmodule EthereumJSONRPC.MixProject do {:jason, "~> 1.3"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, + {:logger_json, "~> 7.0"}, # Mocking `EthereumJSONRPC.Transport` and `EthereumJSONRPC.HTTP` so we avoid hitting real chains for local testing {:mox, "~> 1.1.0", only: [:test]}, {:prometheus_ex, "~> 5.0.0", override: true}, @@ -86,7 +87,6 @@ defmodule EthereumJSONRPC.MixProject do {:decorator, "~> 1.4"}, {:hackney, "~> 1.18"}, {:poolboy, "~> 1.5.2"}, - {:logger_json, "~> 5.1"}, {:utils, in_umbrella: true}, {:websockex, "~> 0.5.0"} ] diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index d4c7237ba75b..c321b04223fe 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -211,11 +211,7 @@ config :explorer, :http_client, Explorer.HttpClient.Tesla config :explorer, Explorer.Chain.BridgedToken, enabled: ConfigHelper.parse_bool_env_var("BRIDGED_TOKENS_ENABLED") config :logger, :explorer, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, + metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :explorer] config :spandex_ecto, SpandexEcto.EctoLogger, diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs index ef5177a7c822..715741583d1a 100644 --- a/apps/explorer/config/dev.exs +++ b/apps/explorer/config/dev.exs @@ -39,16 +39,12 @@ end config :explorer, Explorer.Tracer, env: "dev", disabled?: true -config :logger, :explorer, - level: :debug, - path: Path.absname("logs/dev/explorer.log") +config :logger, :explorer, path: Path.absname("logs/dev/explorer.log") config :logger, :reading_token_functions, - level: :debug, path: Path.absname("logs/dev/explorer/tokens/reading_functions.log"), metadata_filter: [fetcher: :token_functions] config :logger, :token_instances, - level: :debug, path: Path.absname("logs/dev/explorer/tokens/token_instances.log"), metadata_filter: [fetcher: :token_instances] diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs index 848311e6dfc5..8371ee6c6d7e 100644 --- a/apps/explorer/config/prod.exs +++ b/apps/explorer/config/prod.exs @@ -45,18 +45,15 @@ end config :explorer, Explorer.Tracer, env: "production", disabled?: true config :logger, :explorer, - level: :info, path: Path.absname("logs/prod/explorer.log"), rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :reading_token_functions, - level: :debug, path: Path.absname("logs/prod/explorer/tokens/reading_functions.log"), metadata_filter: [fetcher: :token_functions], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :token_instances, - level: :debug, path: Path.absname("logs/prod/explorer/tokens/token_instances.log"), metadata_filter: [fetcher: :token_instances], rotate: %{max_bytes: 52_428_800, keep: 19} diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index a3b6c11f7f1b..dc452c183dde 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -109,9 +109,7 @@ config :explorer, Explorer.Repo.PolygonZkevm, timeout: :timer.seconds(60), queue_target: 1000 -config :logger, :explorer, - level: :warn, - path: Path.absname("logs/test/explorer.log") +config :logger, :explorer, path: Path.absname("logs/test/explorer.log") config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: false diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index d7a956382cfc..769502b0d6a0 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -9,7 +9,6 @@ defmodule Explorer.SmartContract.Helper do alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.{Reader, Writer} - alias Phoenix.HTML @api_true [api?: true] @@ -84,15 +83,6 @@ defmodule Explorer.SmartContract.Helper do end end - def sanitize_input(nil), do: nil - - def sanitize_input(input) do - input - |> HTML.html_escape() - |> HTML.safe_to_string() - |> String.trim() - end - @doc """ Escapes only <, > and & symbols """ diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 15e11769dba2..b873945aa7a8 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -86,9 +86,10 @@ defmodule Explorer.Mixfile do {:httpoison, "~> 2.0"}, {:jason, "~> 1.3"}, {:junit_formatter, ">= 0.0.0", only: [:test], runtime: false}, + {:libcluster, "~> 3.5"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, - {:libcluster, "~> 3.5"}, + {:logger_json, "~> 7.0"}, {:math, "~> 0.7.0"}, {:mock, "~> 0.3.0", only: [:test], runtime: false}, {:mox, "~> 1.1.0"}, @@ -119,7 +120,6 @@ defmodule Explorer.Mixfile do {:cloak_ecto, "~> 1.3.0"}, {:redix, "~> 1.1"}, {:hammer_backend_redis, "~> 7.0"}, - {:logger_json, "~> 5.1"}, {:typed_ecto_schema, "~> 0.4.1"}, {:ueberauth, "~> 0.7"}, {:recon, "~> 2.5"}, diff --git a/apps/indexer/config/config.exs b/apps/indexer/config/config.exs index dc97d7881075..0b3d40c7aef7 100644 --- a/apps/indexer/config/config.exs +++ b/apps/indexer/config/config.exs @@ -15,11 +15,7 @@ config :indexer, Indexer.Block.Catchup.MissingRangesCollector, future_check_inte config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, enabled: true config :logger, :indexer, - # keep synced with `config/config.exs` - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, + metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :indexer] config :os_mon, diff --git a/apps/indexer/config/dev.exs b/apps/indexer/config/dev.exs index 28f928baa1ae..bd3644a7bc24 100644 --- a/apps/indexer/config/dev.exs +++ b/apps/indexer/config/dev.exs @@ -2,41 +2,32 @@ import Config config :indexer, Indexer.Tracer, env: "dev", disabled?: true -config :logger, :indexer, - level: :debug, - path: Path.absname("logs/dev/indexer.log") +config :logger, :indexer, path: Path.absname("logs/dev/indexer.log") config :logger, :indexer_token_balances, - level: :debug, path: Path.absname("logs/dev/indexer/token_balances/error.log"), metadata_filter: [fetcher: :token_balances] config :logger, :failed_contract_creations, - level: :debug, path: Path.absname("logs/dev/indexer/failed_contract_creations.log"), metadata_filter: [fetcher: :failed_created_addresses] config :logger, :addresses_without_code, - level: :debug, path: Path.absname("logs/dev/indexer/addresses_without_code.log"), metadata_filter: [fetcher: :addresses_without_code] config :logger, :pending_transactions_to_refetch, - level: :debug, path: Path.absname("logs/dev/indexer/pending_transactions_to_refetch.log"), metadata_filter: [fetcher: :pending_transactions_to_refetch] config :logger, :empty_blocks_to_refetch, - level: :debug, path: Path.absname("logs/dev/indexer/empty_blocks_to_refetch.log"), metadata_filter: [fetcher: :empty_blocks_to_refetch] config :logger, :block_import_timings, - level: :debug, path: Path.absname("logs/dev/indexer/block_import_timings.log"), metadata_filter: [fetcher: :block_import_timings] config :logger, :withdrawal, - level: :debug, path: Path.absname("logs/dev/indexer/withdrawal.log"), metadata_filter: [fetcher: :withdrawal] diff --git a/apps/indexer/config/prod.exs b/apps/indexer/config/prod.exs index 8e81a078f4a5..f34c981a8d40 100644 --- a/apps/indexer/config/prod.exs +++ b/apps/indexer/config/prod.exs @@ -3,48 +3,40 @@ import Config config :indexer, Indexer.Tracer, env: "production", disabled?: true config :logger, :indexer, - level: :info, path: Path.absname("logs/prod/indexer.log"), rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :indexer_token_balances, - level: :debug, path: Path.absname("logs/prod/indexer/token_balances/error.log"), metadata_filter: [fetcher: :token_balances], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :failed_contract_creations, - level: :debug, path: Path.absname("logs/prod/indexer/failed_contract_creations.log"), metadata_filter: [fetcher: :failed_created_addresses], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :addresses_without_code, - level: :debug, path: Path.absname("logs/prod/indexer/addresses_without_code.log"), metadata_filter: [fetcher: :addresses_without_code], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :pending_transactions_to_refetch, - level: :debug, path: Path.absname("logs/prod/indexer/pending_transactions_to_refetch.log"), metadata_filter: [fetcher: :pending_transactions_to_refetch], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :empty_blocks_to_refetch, - level: :info, path: Path.absname("logs/prod/indexer/empty_blocks_to_refetch.log"), metadata_filter: [fetcher: :empty_blocks_to_refetch], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :block_import_timings, - level: :debug, path: Path.absname("logs/prod/indexer/block_import_timings.log"), metadata_filter: [fetcher: :block_import_timings], rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :withdrawal, - level: :info, path: Path.absname("logs/prod/indexer/withdrawal.log"), metadata_filter: [fetcher: :withdrawal], rotate: %{max_bytes: 52_428_800, keep: 19} diff --git a/apps/indexer/config/test.exs b/apps/indexer/config/test.exs index c3f053a1365e..ed8adef277fc 100644 --- a/apps/indexer/config/test.exs +++ b/apps/indexer/config/test.exs @@ -6,21 +6,16 @@ config :indexer, Indexer.Block.Catchup.MissingRangesCollector, future_check_inte config :indexer, Indexer.Migrator.RecoveryWETHTokenTransfers, enabled: false -config :logger, :indexer, - level: :warn, - path: Path.absname("logs/test/indexer.log") +config :logger, :indexer, path: Path.absname("logs/test/indexer.log") config :logger, :indexer_token_balances, - level: :debug, path: Path.absname("logs/test/indexer/token_balances/error.log"), metadata_filter: [fetcher: :token_balances] config :logger, :failed_contract_creations, - level: :debug, path: Path.absname("logs/test/indexer/failed_contract_creations.log"), metadata_filter: [fetcher: :failed_created_addresses] config :logger, :addresses_without_code, - level: :debug, path: Path.absname("logs/test/indexer/addresses_without_code.log"), metadata_filter: [fetcher: :addresses_without_code] diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index cae5d8cf52ca..65f7637b9625 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -63,6 +63,7 @@ defmodule Indexer.MixProject do {:ex_secp256k1, "~> 0.7.0"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, + {:logger_json, "~> 7.0"}, # Mocking `EthereumJSONRPC.Transport`, so we avoid hitting real chains for local testing {:mox, "~> 1.1.0"}, {:prometheus_ex, "~> 5.0.0", override: true}, @@ -70,7 +71,6 @@ defmodule Indexer.MixProject do {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog {:spandex_datadog, "~> 1.0"}, - {:logger_json, "~> 5.1"}, {:varint, "~> 1.4"}, {:utils, in_umbrella: true}, {:cachex, "~> 4.0"} diff --git a/config/config.exs b/config/config.exs index 2f45fe9d8ec6..9bc9a39c16d0 100644 --- a/config/config.exs +++ b/config/config.exs @@ -13,35 +13,13 @@ end config :phoenix, :json_library, Jason -config :logger_json, :backend, - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id duration status unit endpoint method)a, - json_encoder: Jason, - formatter: LoggerJSON.Formatters.BasicLogger - -config :logger, :console, - # Use same format for all loggers, even though the level should only ever be `:error` for `:error` backend - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a - -config :logger, :ecto, - # Use same format for all loggers, even though the level should only ever be `:error` for `:error` backend - format: "$dateT$time $metadata[$level] $message\n", - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a, - metadata_filter: [application: :ecto] - -config :logger, :error, - # Use same format for all loggers, even though the level should only ever be `:error` for `:error` backend - format: "$dateT$time $metadata[$level] $message\n", - level: :error, - metadata: - ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count - block_number step count error_count shrunk import_id transaction_id)a +config :logger, :default_formatter, format: "$dateT$time $metadata[$level] $message\n" + +config :logger, :console, metadata: ConfigHelper.logger_metadata() + +config :logger, :error, metadata: ConfigHelper.logger_metadata() + +config :logger, :block_scout_web, metadata: ConfigHelper.logger_metadata() # todo: migrate from deprecated usages config :tesla, disable_deprecated_builder_warning: true diff --git a/config/config_helper.exs b/config/config_helper.exs index 98611f611083..48cd6a9e67bb 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -55,37 +55,53 @@ defmodule ConfigHelper do @spec logger_backends() :: list() def logger_backends do base_logger_backends = [ - :console, - LoggerJSON + :console ] - file_logger_backends = - [ - {LoggerFileBackend, :error}, - {LoggerFileBackend, :ecto}, - {LoggerFileBackend, :block_scout_web}, - {LoggerFileBackend, :ethereum_jsonrpc}, - {LoggerFileBackend, :explorer}, - {LoggerFileBackend, :indexer}, - {LoggerFileBackend, :indexer_token_balances}, - {LoggerFileBackend, :token_instances}, - {LoggerFileBackend, :reading_token_functions}, - {LoggerFileBackend, :pending_transactions_to_refetch}, - {LoggerFileBackend, :empty_blocks_to_refetch}, - {LoggerFileBackend, :withdrawal}, - {LoggerFileBackend, :api}, - {LoggerFileBackend, :block_import_timings}, - {LoggerFileBackend, :account}, - {LoggerFileBackend, :api_v2} - ] - if parse_bool_env_var("DISABLE_FILE_LOGGING") do base_logger_backends else + file_logger_backends = + [ + {LoggerFileBackend, :error}, + {LoggerFileBackend, :block_scout_web}, + {LoggerFileBackend, :ethereum_jsonrpc}, + {LoggerFileBackend, :explorer}, + {LoggerFileBackend, :indexer}, + {LoggerFileBackend, :indexer_token_balances}, + {LoggerFileBackend, :token_instances}, + {LoggerFileBackend, :reading_token_functions}, + {LoggerFileBackend, :pending_transactions_to_refetch}, + {LoggerFileBackend, :empty_blocks_to_refetch}, + {LoggerFileBackend, :withdrawal}, + {LoggerFileBackend, :api}, + {LoggerFileBackend, :block_import_timings}, + {LoggerFileBackend, :account}, + {LoggerFileBackend, :api_v2} + ] + base_logger_backends ++ file_logger_backends end end + @doc """ + Returns the list of metadata fields to be included in logger output. + """ + @spec logger_metadata() :: list() + def logger_metadata() do + ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count + block_number step count error_count shrunk import_id transaction_id)a + end + + @doc """ + Returns the list of metadata fields to be included in logger backend output. + """ + @spec logger_backend_metadata() :: list() + def logger_backend_metadata() do + ~w(application fetcher request_id first_block_number last_block_number missing_block_range_count missing_block_count + block_number step count error_count shrunk import_id transaction_id duration status unit endpoint method)a + end + @spec http_options(non_neg_integer()) :: list() def http_options(default_timeout \\ 1) do http_timeout = timeout(default_timeout) diff --git a/config/dev.exs b/config/dev.exs index 27154824a939..19ffbc7d4eb9 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -1,17 +1,9 @@ import Config -# DO NOT make it `:debug` or all Ecto logs will be shown for indexer -config :logger, :console, level: :info +config :logger, level: :info -config :logger_json, :backend, level: :none - -config :logger, :ecto, - level: :debug, - path: Path.absname("logs/dev/ecto.log") - -config :logger, :error, path: Path.absname("logs/dev/error.log") +config :logger, :error, path: Path.absname("logs/dev/error.log"), level: :error config :logger, :account, - level: :debug, path: Path.absname("logs/dev/account.log"), metadata_filter: [fetcher: :account] diff --git a/config/prod.exs b/config/prod.exs index 2e8c9db24d97..dc33ab1fb491 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,22 +1,14 @@ import Config # Do not print debug messages in production - -config :logger, :console, level: :none - -config :logger_json, :backend, level: :info - -config :logger, :ecto, - level: :info, - path: Path.absname("logs/prod/ecto.log"), - rotate: %{max_bytes: 52_428_800, keep: 19} +config :logger, level: :info config :logger, :error, + level: :error, path: Path.absname("logs/prod/error.log"), rotate: %{max_bytes: 52_428_800, keep: 19} config :logger, :account, - level: :info, path: Path.absname("logs/prod/account.log"), rotate: %{max_bytes: 52_428_800, keep: 19}, metadata_filter: [fetcher: :account] diff --git a/config/runtime.exs b/config/runtime.exs index e381b7f8c89f..57d09b07f080 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -7,6 +7,18 @@ import Config config :logger, backends: ConfigHelper.logger_backends() +config :logger, :default_handler, + formatter: + (if Mix.env() == :prod do + LoggerJSON.Formatters.Basic.new(metadata: ConfigHelper.logger_backend_metadata()) + else + Logger.Formatter.new() + end) + +config :logger, :api, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api] + +config :logger, :api_v2, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api_v2] + microservice_multichain_search_url = System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL") transactions_stats_enabled = ConfigHelper.parse_bool_env_var("TXS_STATS_ENABLED", "true") diff --git a/config/test.exs b/config/test.exs index 279c1e5cd5b8..2b9a7bc630d8 100644 --- a/config/test.exs +++ b/config/test.exs @@ -1,13 +1,6 @@ import Config # Print only warnings and errors during test - -config :logger, :console, level: :warn - -config :logger_json, :backend, level: :none - -config :logger, :ecto, - level: :warn, - path: Path.absname("logs/test/ecto.log") +config :logger, level: :warn config :logger, :error, path: Path.absname("logs/test/error.log") diff --git a/mix.lock b/mix.lock index 6b80dae4a595..025f706177de 100644 --- a/mix.lock +++ b/mix.lock @@ -100,7 +100,7 @@ "junit_formatter": {:hex, :junit_formatter, "3.4.0", "d0e8db6c34dab6d3c4154c3b46b21540db1109ae709d6cf99ba7e7a2ce4b1ac2", [:mix], [], "hexpm", "bb36e2ae83f1ced6ab931c4ce51dd3dbef1ef61bb4932412e173b0cfa259dacd"}, "libcluster": {:hex, :libcluster, "3.5.0", "5ee4cfde4bdf32b2fef271e33ce3241e89509f4344f6c6a8d4069937484866ba", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ebf6561fcedd765a4cd43b4b8c04b1c87f4177b5fb3cbdfe40a780499d72f743"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"}, - "logger_json": {:hex, :logger_json, "5.1.4", "9e30a4f2e31a8b9e402bdc20bd37cf9b67d3a31f19d0b33082a19a06b4c50f6d", [:mix], [{:ecto, "~> 2.1 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.5.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "3f20eea58e406a33d3eb7814c7dff5accb503bab2ee8601e84da02976fa3934c"}, + "logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, From 414cb5aa955309c373502182a28fdeb3b9edb292 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 26 Nov 2025 16:03:57 +0300 Subject: [PATCH 0497/1006] chore: Internal transaction, Token transfer, Withdrawal, Smart-contracts, Main Page, Stats, Config and Search controllers OpenAPI specs (#13557) --- .../lib/block_scout_web/api_spec.ex | 3 +- .../lib/block_scout_web/chain.ex | 42 +- .../controllers/api/v2/address_controller.ex | 2 +- .../controllers/api/v2/config_controller.ex | 80 ++++ .../api/v2/internal_transaction_controller.ex | 38 +- .../api/v2/main_page_controller.ex | 106 ++++- .../controllers/api/v2/optimism_controller.ex | 63 ++- .../api/v2/proxy/solidity_scan_controller.ex | 38 ++ .../controllers/api/v2/search_controller.ex | 84 +++- .../api/v2/smart_contract_controller.ex | 386 ++++++++++------- .../controllers/api/v2/stats_controller.ex | 78 +++- .../controllers/api/v2/token_controller.ex | 50 ++- .../api/v2/token_transfer_controller.ex | 37 +- .../api/v2/transaction_controller.ex | 12 +- .../api/v2/withdrawal_controller.ex | 32 ++ .../lib/block_scout_web/paging_helper.ex | 17 + .../lib/block_scout_web/routers/api_router.ex | 5 +- .../routers/smart_contracts_api_v2_router.ex | 8 +- .../block_scout_web/schemas/api/v2/general.ex | 392 +++++++++++++----- .../schemas/api/v2/search/results.ex | 19 + .../schemas/api/v2/smart_contract.ex | 136 ++++++ .../api/v2/smart_contract/audit_report.ex | 15 + .../schemas/api/v2/smart_contract/counters.ex | 16 + .../schemas/api/v2/smart_contract/language.ex | 8 + .../schemas/api/v2/stats/response.ex | 120 ++++++ .../schemas/api/v2/withdrawal/counter.ex | 20 + .../lib/block_scout_web/schemas/helper.ex | 30 +- .../api/v2/config_controller_test.exs | 43 ++ .../internal_transaction_controller_test.exs | 188 +++++---- .../api/v2/smart_contract_controller_test.exs | 44 +- .../routers/chain_type_scope_test.exs | 16 +- apps/explorer/lib/explorer/chain/search.ex | 67 +-- .../lib/explorer/chain/smart_contract.ex | 21 +- 33 files changed, 1716 insertions(+), 500 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/solidity_scan_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/results.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/audit_report.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/counters.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/language.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/response.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal/counter.ex diff --git a/apps/block_scout_web/lib/block_scout_web/api_spec.ex b/apps/block_scout_web/lib/block_scout_web/api_spec.ex index 91ec0265d4e0..a153f60a61b5 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_spec.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_spec.ex @@ -2,7 +2,7 @@ defmodule BlockScoutWeb.ApiSpec do @moduledoc """ This module defines the API specification for the BlockScoutWeb application. """ - alias BlockScoutWeb.Routers.{ApiRouter, TokensApiV2Router} + alias BlockScoutWeb.Routers.{ApiRouter, SmartContractsApiV2Router, TokensApiV2Router} alias OpenApiSpex.{Contact, Info, OpenApi, Paths, Server} alias Utils.Helper @@ -25,6 +25,7 @@ defmodule BlockScoutWeb.ApiSpec do ApiRouter |> Paths.from_router() |> Map.merge(Paths.from_routes(routes_with_prefix(TokensApiV2Router, "/v2/tokens"))) + |> Map.merge(Paths.from_routes(routes_with_prefix(SmartContractsApiV2Router, "/v2/smart-contracts"))) } |> OpenApiSpex.resolve_schema_modules() end diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index ec39a2269c9a..30d51474bc5b 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -205,6 +205,31 @@ defmodule BlockScoutWeb.Chain do """ @spec paging_options(any) :: [{:paging_options, Explorer.PagingOptions.t()}, ...] | Explorer.PagingOptions.t() + # todo: function clause for the old UI, to be removed later + def paging_options(%{ + "hash" => hash_string, + "fetched_coin_balance" => fetched_coin_balance_string, + "transactions_count" => transactions_count_string + }) + when is_binary(hash_string) do + case string_to_address_hash(hash_string) do + {:ok, address_hash} -> + [ + paging_options: %{ + @default_paging_options + | key: %{ + fetched_coin_balance: decimal_parse(fetched_coin_balance_string), + hash: address_hash, + transactions_count: parse_integer(transactions_count_string) + } + } + ] + + _ -> + [paging_options: @default_paging_options] + end + end + def paging_options(%{ hash: hash_string, fetched_coin_balance: fetched_coin_balance_string, @@ -557,6 +582,9 @@ defmodule BlockScoutWeb.Chain do def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}), do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}] + def paging_options(%{token_name: name, token_type: type, token_inserted_at: inserted_at}), + do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}] + def paging_options(%{"value" => value, "address_hash" => address_hash}) do [paging_options: %{@default_paging_options | key: {value, address_hash}}] end @@ -605,6 +633,10 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{value: value, id: id}) do + [paging_options: %{@default_paging_options | key: {nil, value, id}}] + end + def paging_options(%{"items_count" => items_count_string, "state_changes" => _}) when is_binary(items_count_string) do case Integer.parse(items_count_string) do {count, ""} -> [paging_options: %{@default_paging_options | key: {count}}] @@ -921,7 +953,7 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%Transaction{block_number: nil, inserted_at: inserted_at, hash: hash}) do - %{"inserted_at" => DateTime.to_iso8601(inserted_at), "hash" => hash} + %{inserted_at: DateTime.to_iso8601(inserted_at), hash: hash} end defp paging_params(%Transaction{block_number: block_number, index: index}) do @@ -947,7 +979,7 @@ defmodule BlockScoutWeb.Chain do end defp paging_params(%SmartContract{address: %NotLoaded{}} = smart_contract) do - %{"smart_contract_id" => smart_contract.id} + %{smart_contract_id: smart_contract.id} end defp paging_params(%OptimismDeposit{l1_block_number: l1_block_number, l2_transaction_hash: l2_transaction_hash}) do @@ -964,9 +996,9 @@ defmodule BlockScoutWeb.Chain do defp paging_params(%SmartContract{} = smart_contract) do %{ - "smart_contract_id" => smart_contract.id, - "transactions_count" => smart_contract.address.transactions_count, - "coin_balance" => + smart_contract_id: smart_contract.id, + transactions_count: smart_contract.address.transactions_count, + coin_balance: smart_contract.address.fetched_coin_balance && Wei.to(smart_contract.address.fetched_coin_balance, :wei) } end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 084296fddd1e..e78a7dca05dd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -1480,7 +1480,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do ] @doc """ - Handles `api/v2/addresses/:address_hash/beacon/deposits` endpoint. + Handles `api/v2/addresses/:address_hash_param/beacon/deposits` endpoint. Fetches beacon deposits for a given address with pagination support. This endpoint retrieves all beacon deposits originating from the specified diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 9234fb5540fb..f86f428baccc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -1,5 +1,24 @@ defmodule BlockScoutWeb.API.V2.ConfigController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs + + alias Explorer.Chain.SmartContract + alias OpenApiSpex.Schema + + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["config"]) + + operation :backend_version, + summary: "Get backend version", + description: "Returns application backend version string.", + parameters: base_params(), + responses: [ + ok: + {"Backend version", "application/json", + %Schema{type: :object, properties: %{backend_version: %Schema{type: :string, nullable: true}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] def backend_version(conn, _params) do backend_version = Application.get_env(:block_scout_web, :version) @@ -9,6 +28,16 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> render(:backend_version, %{version: backend_version}) end + operation :csv_export, + summary: "CSV export limits", + description: "Returns configured limits for CSV export endpoints.", + parameters: base_params(), + responses: [ + ok: + {"CSV export limits", "application/json", %Schema{type: :object, properties: %{limit: %Schema{type: :integer}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + def csv_export(conn, _params) do limit = Application.get_env(:explorer, :csv_export_limit) @@ -17,6 +46,17 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> json(%{limit: limit}) end + operation :public_metrics, + summary: "Public metrics configuration", + description: "Returns update period / configuration for public metrics.", + parameters: base_params(), + responses: [ + ok: + {"Public metrics config", "application/json", + %Schema{type: :object, properties: %{update_period_hours: %Schema{type: :integer}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + def public_metrics(conn, _params) do public_metrics_update_period_hours = Application.get_env(:explorer, Explorer.Chain.Metrics.PublicMetrics)[:update_period_hours] @@ -26,6 +66,17 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> json(%{update_period_hours: public_metrics_update_period_hours}) end + operation :celo, + summary: "Celo chain configuration", + description: "Returns Celo-specific configuration (l2 migration block).", + parameters: base_params(), + responses: [ + ok: + {"Celo config", "application/json", + %Schema{type: :object, properties: %{l2_migration_block: %Schema{type: :integer, nullable: true}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + def celo(conn, _params) do config = Application.get_env(:explorer, :celo) @@ -33,4 +84,33 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> put_status(200) |> json(%{l2_migration_block: config[:l2_migration_block]}) end + + operation :languages_list, + summary: "Smart contract languages list", + description: "Returns list of smart contract languages supported by the database schema.", + parameters: base_params(), + responses: [ + ok: + {"Smart contract languages", "application/json", + %OpenApiSpex.Schema{ + type: :object, + properties: %{ + languages: %OpenApiSpex.Schema{ + type: :array, + items: Schemas.SmartContract.Language + } + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/config/smart-contracts/languages` endpoint. + """ + @spec languages_list(Plug.Conn.t(), map()) :: Plug.Conn.t() + def languages_list(conn, _params) do + conn + |> put_status(200) + |> json(%{languages: SmartContract.language_strings()}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index df2fb742cc3b..99a92e7b94c6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -1,6 +1,9 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do use BlockScoutWeb, :controller - alias Explorer.{Chain, Helper, PagingOptions} + use OpenApiSpex.ControllerSpecs + + alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.InternalTransaction @@ -15,8 +18,35 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["internal_transactions"]) + @api_true [api?: true] + operation :internal_transactions, + summary: "List internal transactions", + description: "Retrieves internal transactions with optional filtering and pagination.", + parameters: + base_params() ++ + [query_transaction_hash_param(), limit_param()] ++ + define_paging_params(["index", "block_number", "transaction_index", "items_count"]), + responses: [ + ok: + {"Internal transactions", "application/json", + paginated_response( + items: Schemas.InternalTransaction, + next_page_params_example: %{ + "index" => 50, + "transaction_index" => 68, + "block_number" => 22_133_247, + "items_count" => 50 + }, + title_prefix: "InternalTransactions" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/internal-transactions` endpoint. """ @@ -27,7 +57,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do transaction_hash = transaction_hash_from_params(params), false <- transaction_hash == :invalid do paging_options = paging_options(params) - options = options(paging_options, %{transaction_hash: transaction_hash, limit: params["limit"]}) + options = options(paging_options, %{transaction_hash: transaction_hash, limit: params[:limit]}) result = options @@ -66,14 +96,14 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - maybe_parsed_limit = Helper.parse_integer(params["limit"]) + maybe_parsed_limit = params[:limit] %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(@api_true) end defp transaction_hash_from_params(params) do - with transaction_hash_string when not is_nil(transaction_hash_string) <- params["transaction_hash"], + with transaction_hash_string when not is_nil(transaction_hash_string) <- params[:transaction_hash], {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string) do transaction_hash else diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 5bb96e30d04b..e37b0a2a0065 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -4,10 +4,10 @@ defmodule BlockScoutWeb.API.V2.MainPageController do use Utils.CompileTimeEnvHelper, chain_identity: [:explorer, :chain_identity] - alias BlockScoutWeb.API.V2.{BlockView, OptimismView, TransactionView} - alias Explorer.{Chain, PagingOptions} - alias Explorer.{Chain, Repo} - alias Explorer.Chain.Optimism.Deposit + use OpenApiSpex.ControllerSpecs + + alias BlockScoutWeb.API.V2.{BlockView, TransactionView} + alias Explorer.{Chain, PagingOptions, Repo} import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] @@ -39,6 +39,29 @@ defmodule BlockScoutWeb.API.V2.MainPageController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["main_page"]) + + operation :blocks, + summary: "Last 4 blocks on the main page", + description: "Retrieves last 4 blocks list.", + parameters: base_params(), + responses: [ + ok: + {"Blocks List on the main page.", "application/json", + %Schema{ + type: :array, + items: Schemas.Block.Response, + nullable: false, + additionalProperties: false + }} + ] + + @doc """ + Returns the last 4 blocks for display on the main page. + """ + @spec blocks(Plug.Conn.t(), map()) :: Plug.Conn.t() def blocks(conn, _params) do blocks = [paging_options: %PagingOptions{page_size: 4}, api?: true] @@ -55,19 +78,26 @@ defmodule BlockScoutWeb.API.V2.MainPageController do |> render(:blocks, %{blocks: blocks |> maybe_preload_metadata()}) end - def optimism_deposits(conn, _params) do - recent_deposits = - Deposit.list( - paging_options: %PagingOptions{page_size: 6}, - api?: true - ) - - conn - |> put_status(200) - |> put_view(OptimismView) - |> render(:optimism_deposits, %{deposits: recent_deposits}) - end - + operation :transactions, + summary: "Last 6 transactions on the main page", + description: "Retrieves last 6 transactions list.", + parameters: base_params(), + responses: [ + ok: + {"Transactions list on the main page.", "application/json", + %Schema{ + type: :array, + items: Schemas.Transaction.Response, + nullable: false, + additionalProperties: false + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Returns the last 6 transactions for display on the main page. + """ + @spec transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() def transactions(conn, _params) do recent_transactions = Chain.recent_collated_transactions(false, @transactions_options) @@ -77,6 +107,26 @@ defmodule BlockScoutWeb.API.V2.MainPageController do |> render(:transactions, %{transactions: recent_transactions |> maybe_preload_metadata()}) end + operation :watchlist_transactions, + summary: "Last 6 transactions from the current user's watchlist", + description: "Retrieves a list of last 6 transactions from the current user's watchlist.", + parameters: base_params(), + responses: [ + ok: + {"List of watchlist transactions", "application/json", + %Schema{ + type: :array, + items: Schemas.Transaction.Response, + nullable: false, + additionalProperties: false + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Returns the last 6 watchlist transactions for display on the main page. + """ + @spec watchlist_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() def watchlist_transactions(conn, _params) do with {:auth, %{watchlist_id: watchlist_id}} <- {:auth, current_user(conn)} do {watchlist_names, transactions} = Chain.fetch_watchlist_transactions(watchlist_id, @transactions_options) @@ -91,6 +141,28 @@ defmodule BlockScoutWeb.API.V2.MainPageController do end end + operation :indexing_status, + summary: "Get indexing status", + description: "Returns current indexing progress and ratios for blocks and internal transactions.", + parameters: base_params(), + responses: [ + ok: + {"Indexing status", "application/json", + %Schema{ + type: :object, + properties: %{ + finished_indexing_blocks: %Schema{type: :boolean}, + finished_indexing: %Schema{type: :boolean}, + indexed_blocks_ratio: %Schema{type: :number, format: :float}, + indexed_internal_transactions_ratio: %Schema{type: :number, format: :float, nullable: true} + } + }} + ] + + @doc """ + Lists the indexing status of blocks and transactions. + """ + @spec indexing_status(Plug.Conn.t(), map()) :: Plug.Conn.t() def indexing_status(conn, _params) do indexed_ratio_blocks = Chain.indexed_ratio_blocks() finished_indexing_blocks = Chain.finished_indexing_from_ratio?(indexed_ratio_blocks) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 4b4daa4edb4c..4b2e201f5514 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -12,8 +12,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do import Explorer.Helper, only: [hash_to_binary: 1] - alias BlockScoutWeb.API.V2.ApiView - alias Explorer.Chain + alias BlockScoutWeb.API.V2.{ApiView, OptimismView} + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Data, Hash, Token, Transaction} @@ -35,8 +35,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do @api_true [api?: true] @doc """ - Function to handle GET requests to `/api/v2/optimism/txn-batches` and - `/api/v2/optimism/txn-batches/:l2_block_range_start/:l2_block_range_end` endpoints. + Function to handle GET requests to `/api/v2/optimism/txn-batches` and + `/api/v2/optimism/txn-batches/:l2_block_range_start/:l2_block_range_end` endpoints. """ @spec transaction_batches(Plug.Conn.t(), map()) :: Plug.Conn.t() def transaction_batches(conn, params) do @@ -60,7 +60,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/txn-batches/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/txn-batches/count` endpoint. """ @spec transaction_batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def transaction_batches_count(conn, _params) do @@ -68,7 +68,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/batches` endpoint. + Function to handle GET requests to `/api/v2/optimism/batches` endpoint. """ @spec batches(Plug.Conn.t(), map()) :: Plug.Conn.t() def batches(conn, params) do @@ -122,7 +122,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/batches/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/batches/count` endpoint. """ @spec batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def batches_count(conn, _params) do @@ -130,7 +130,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/batches/da/celestia/:height/:commitment` endpoint. + Function to handle GET requests to `/api/v2/optimism/batches/da/celestia/:height/:commitment` endpoint. """ @spec batch_by_celestia_blob(Plug.Conn.t(), map()) :: Plug.Conn.t() def batch_by_celestia_blob(conn, %{"height" => height, "commitment" => commitment}) do @@ -155,7 +155,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/batches/:number` endpoint. + Function to handle GET requests to `/api/v2/optimism/batches/:number` endpoint. """ @spec batch_by_number(Plug.Conn.t(), map()) :: Plug.Conn.t() def batch_by_number(conn, %{"number" => number}) do @@ -173,7 +173,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/output-roots` endpoint. + Function to handle GET requests to `/api/v2/optimism/output-roots` endpoint. """ @spec output_roots(Plug.Conn.t(), map()) :: Plug.Conn.t() def output_roots(conn, params) do @@ -195,7 +195,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/output-roots/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/output-roots/count` endpoint. """ @spec output_roots_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def output_roots_count(conn, _params) do @@ -203,7 +203,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/games` endpoint. + Function to handle GET requests to `/api/v2/optimism/games` endpoint. """ @spec games(Plug.Conn.t(), map()) :: Plug.Conn.t() def games(conn, params) do @@ -225,7 +225,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/games/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/games/count` endpoint. """ @spec games_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def games_count(conn, _params) do @@ -237,7 +237,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/deposits` endpoint. + Function to handle GET requests to `/api/v2/optimism/deposits` endpoint. """ @spec deposits(Plug.Conn.t(), map()) :: Plug.Conn.t() def deposits(conn, params) do @@ -259,7 +259,24 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/deposits/count` endpoint. + Function to handle GET requests to `/api/v2/main-page/optimism-deposits` endpoint. + """ + @spec main_page_deposits(Plug.Conn.t(), map()) :: Plug.Conn.t() + def main_page_deposits(conn, _params) do + recent_deposits = + Deposit.list( + paging_options: %PagingOptions{page_size: 6}, + api?: true + ) + + conn + |> put_status(200) + |> put_view(OptimismView) + |> render(:optimism_deposits, %{deposits: recent_deposits}) + end + + @doc """ + Function to handle GET requests to `/api/v2/optimism/deposits/count` endpoint. """ @spec deposits_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def deposits_count(conn, _params) do @@ -271,7 +288,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/interop/messages/:unique_id` endpoint. + Function to handle GET requests to `/api/v2/optimism/interop/messages/:unique_id` endpoint. """ @spec interop_message(Plug.Conn.t(), map()) :: Plug.Conn.t() def interop_message(conn, params) do @@ -392,7 +409,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/interop/messages/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/interop/messages/count` endpoint. """ @spec interop_messages_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def interop_messages_count(conn, _params) do @@ -402,7 +419,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/withdrawals` endpoint. + Function to handle GET requests to `/api/v2/optimism/withdrawals` endpoint. """ @spec withdrawals(Plug.Conn.t(), map()) :: Plug.Conn.t() def withdrawals(conn, params) do @@ -424,7 +441,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/withdrawals/count` endpoint. + Function to handle GET requests to `/api/v2/optimism/withdrawals/count` endpoint. """ @spec withdrawals_count(Plug.Conn.t(), map()) :: Plug.Conn.t() def withdrawals_count(conn, _params) do @@ -432,7 +449,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle GET requests to `/api/v2/optimism/interop/public-key` endpoint. + Function to handle GET requests to `/api/v2/optimism/interop/public-key` endpoint. """ @spec interop_public_key(Plug.Conn.t(), map()) :: Plug.Conn.t() def interop_public_key(conn, _params) do @@ -455,9 +472,9 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end @doc """ - Function to handle POST request to `/api/v2/import/optimism/interop/` endpoint. - Accepts `init` part of the interop message from the source instance or - `relay` part of the interop message from the target instance. + Function to handle POST request to `/api/v2/import/optimism/interop/` endpoint. + Accepts `init` part of the interop message from the source instance or + `relay` part of the interop message from the target instance. """ @spec interop_import(Plug.Conn.t(), map()) :: Plug.Conn.t() def interop_import( diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/solidity_scan_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/solidity_scan_controller.ex new file mode 100644 index 000000000000..59433940dbac --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/solidity_scan_controller.ex @@ -0,0 +1,38 @@ +defmodule BlockScoutWeb.API.V2.Proxy.SolidityScanController do + use BlockScoutWeb, :controller + + alias BlockScoutWeb.AccessHelper + alias Explorer.Chain + alias Explorer.Chain.{Address, SmartContract} + alias Explorer.ThirdPartyIntegrations.SolidityScan + + @api_true [api?: true] + + @doc """ + /api/v2/proxy/3rdparty/solidityscan/smart-contracts/:address_hash_string/report logic + """ + @spec solidityscan_report(Plug.Conn.t(), map()) :: + {:address, {:error, :not_found}} + | {:format_address, :error} + | {:is_empty_response, true} + | {:is_smart_contract, false | nil} + | {:restricted_access, true} + | {:is_verified_smart_contract, false} + | {:language, :vyper} + | Plug.Conn.t() + def solidityscan_report(conn, %{"address_hash" => address_hash_string} = params) do + with {:format_address, {:ok, address_hash}} <- {:format_address, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:address, {:ok, address}} <- {:address, Chain.hash_to_address(address_hash)}, + {:is_smart_contract, true} <- {:is_smart_contract, Address.smart_contract?(address)}, + smart_contract = SmartContract.address_hash_to_smart_contract(address_hash, @api_true), + {:is_verified_smart_contract, true} <- {:is_verified_smart_contract, !is_nil(smart_contract)}, + {:language, language} when language != :vyper <- {:language, SmartContract.language(smart_contract)}, + response = SolidityScan.solidityscan_request(address_hash_string), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(200) + |> json(response) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex index 14db522f73aa..06c6b93dd73e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex @@ -1,16 +1,49 @@ defmodule BlockScoutWeb.API.V2.SearchController do - use Phoenix.Controller, namespace: BlockScoutWeb + use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Chain, only: [from_param: 1, fetch_scam_token_toggle: 2] import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens_info_to_search_results: 1] alias Explorer.Chain.Search alias Explorer.PagingOptions + alias OpenApiSpex.Schema + + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["search"]) @api_true [api?: true] @min_query_length 3 - def search(conn, %{"q" => query} = params) do + operation :search, + summary: "Search across blocks, transactions and addresses", + description: "Performs a joint search for blocks, transactions, addresses and other resources.", + parameters: + [q_param() | base_params()] ++ + define_search_paging_params([ + "next_page_params_type", + "label", + "token", + "contract", + "tac_operation", + "metadata_tag", + "block", + "blob", + "user_operation", + "address", + "ens_domain" + ]), + responses: [ + ok: {"Search results", "application/json", Schemas.Search.Results}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Performs a joint search for blocks, transactions, addresses and other resources. + """ + @spec search(Plug.Conn.t(), map()) :: Plug.Conn.t() + def search(conn, %{q: query} = params) do [paging_options: paging_options] = Search.parse_paging_options(params) options = @api_true |> fetch_scam_token_toggle(conn) @@ -25,7 +58,33 @@ defmodule BlockScoutWeb.API.V2.SearchController do }) end - def check_redirect(conn, %{"q" => query}) do + operation :check_redirect, + summary: "Check redirect target for a query", + description: "Check whether the query resolves to a single redirect target (eg. an address or tx) and return it.", + parameters: [q_param() | base_params()], + responses: [ + ok: + {"Redirect check result", "application/json", + %Schema{ + type: :object, + properties: %{ + parameter: %Schema{type: :string, nullable: true}, + redirect: %Schema{type: :boolean, nullable: true}, + type: %Schema{ + type: :string, + enum: ["address", "block", "transaction", "user_operation", "blob"], + nullable: true + } + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Check redirect target for a query. + """ + @spec check_redirect(Plug.Conn.t(), map()) :: Plug.Conn.t() + def check_redirect(conn, %{q: query}) do result = query |> String.trim() @@ -36,13 +95,28 @@ defmodule BlockScoutWeb.API.V2.SearchController do |> render(:search_results, %{result: result}) end - def quick_search(conn, %{"q" => query}) when byte_size(query) < @min_query_length do + operation :quick_search, + summary: "Quick (unpaginated) search", + description: "Performs a quick, unpaginated search for short queries.", + parameters: [q_param() | base_params()], + responses: [ + ok: + {"Quick search results", "application/json", + %Schema{type: :array, items: %Schema{type: :object}, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Performs a quick, unpaginated search for short queries. + """ + @spec quick_search(Plug.Conn.t(), map()) :: Plug.Conn.t() + def quick_search(conn, %{q: query}) when byte_size(query) < @min_query_length do conn |> put_status(200) |> render(:search_results, %{search_results: []}) end - def quick_search(conn, %{"q" => query}) do + def quick_search(conn, %{q: query}) do options = @api_true |> fetch_scam_token_toggle(conn) search_results = Search.balanced_unpaginated_search(%PagingOptions{page_size: 50}, query, options) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 0a68e666adb1..b419d26e6b36 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] @@ -34,46 +35,28 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do alias Explorer.Chain.SmartContract.AuditReport alias Explorer.SmartContract.Helper, as: SmartContractHelper alias Explorer.SmartContract.Solidity.PublishHelper - alias Explorer.ThirdPartyIntegrations.SolidityScan - @api_true [api?: true] + action_fallback(BlockScoutWeb.API.V2.FallbackController) - @spec contract_creation_transaction_associations() :: [keyword()] - defp contract_creation_transaction_associations do - case chain_type() do - :filecoin -> - Address.contract_creation_transaction_with_from_address_associations() + @api_true [api?: true] - _ -> - Address.contract_creation_transaction_associations() - end - end + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) - @spec smart_contract_address_options() :: keyword() - defp smart_contract_address_options do - [ - necessity_by_association: %{ - [smart_contract: :smart_contract_additional_sources] => :optional, - contract_creation_transaction_associations() => :optional - } - ] - |> Keyword.merge(@api_true) - end + tags(["smart_contracts"]) - @spec verified_smart_contract_addresses_options() :: keyword() - defp verified_smart_contract_addresses_options do - [ - necessity_by_association: %{ - [:token, :names, :proxy_implementations] => :optional, - contract_creation_transaction_associations() => :optional - } + operation :smart_contract, + summary: "Smart contract", + description: "Returns details for a smart contract address.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Smart contract", "application/json", Schemas.SmartContract} ] - |> Keyword.merge(@api_true) - end - - action_fallback(BlockScoutWeb.API.V2.FallbackController) - def smart_contract(conn, %{"address_hash" => address_hash_string} = params) do + @doc """ + GET /api/v2/smart-contracts/:address_hash_param + """ + @spec smart_contract(Plug.Conn.t(), map()) :: Plug.Conn.t() + def smart_contract(conn, %{address_hash_param: address_hash_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), _ <- PublishHelper.sourcify_check(address_hash_string), @@ -87,34 +70,49 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end end + operation :smart_contracts_list, + summary: "Smart contracts list", + description: + "Returns a paginated list of verified smart contract addresses. Supports search, filtering and pagination keys.", + parameters: + base_params() ++ + [ + sort_param(["balance", "transactions_count"]), + order_param(), + q_param(), + %OpenApiSpex.Parameter{ + name: :filter, + in: :query, + schema: Schemas.SmartContract.Language, + required: false, + description: "Filter to apply" + } + ] ++ + define_paging_params([ + "smart_contract_id", + "coin_balance", + "address_hash", + "transactions_count", + "items_count" + ]), + responses: [ + ok: + {"Smart contracts", "application/json", + paginated_response( + items: Schemas.SmartContract, + next_page_params_example: %{ + "smart_contract_id" => 1_947_801, + "items_count" => 50 + }, + title_prefix: "SmartContracts" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ - /api/v2/smart-contracts/:address_hash_string/solidityscan-report logic + Function to handle GET requests to `/api/v2/smart-contracts` endpoint. """ - @spec solidityscan_report(Plug.Conn.t(), map()) :: - {:address, {:error, :not_found}} - | {:format_address, :error} - | {:is_empty_response, true} - | {:is_smart_contract, false | nil} - | {:restricted_access, true} - | {:is_verified_smart_contract, false} - | {:language, :vyper} - | Plug.Conn.t() - def solidityscan_report(conn, %{"address_hash" => address_hash_string} = params) do - with {:format_address, {:ok, address_hash}} <- {:format_address, Chain.string_to_address_hash(address_hash_string)}, - {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), - {:address, {:ok, address}} <- {:address, Chain.hash_to_address(address_hash)}, - {:is_smart_contract, true} <- {:is_smart_contract, Address.smart_contract?(address)}, - smart_contract = SmartContract.address_hash_to_smart_contract(address_hash, @api_true), - {:is_verified_smart_contract, true} <- {:is_verified_smart_contract, !is_nil(smart_contract)}, - {:language, language} when language != :vyper <- {:language, SmartContract.language(smart_contract)}, - response = SolidityScan.solidityscan_request(address_hash_string), - {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do - conn - |> put_status(200) - |> json(response) - end - end - + @spec smart_contracts_list(Plug.Conn.t(), map()) :: Plug.Conn.t() def smart_contracts_list(conn, params) do full_options = verified_smart_contract_addresses_options() @@ -156,6 +154,152 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do }) end + operation :smart_contracts_counters, + summary: "Smart contracts counters", + description: "Returns counts for smart contracts and related metrics.", + parameters: base_params(), + responses: [ + ok: {"Smart contracts counters", "application/json", Schemas.SmartContract.Counters}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/smart-contracts/counters` endpoint. + """ + @spec smart_contracts_counters(Plug.Conn.t(), map()) :: Plug.Conn.t() + def smart_contracts_counters(conn, _params) do + conn + |> json(%{ + smart_contracts: Chain.count_contracts_from_cache(@api_true), + new_smart_contracts_24h: Chain.count_new_contracts_from_cache(@api_true), + verified_smart_contracts: Chain.count_verified_contracts_from_cache(@api_true), + new_verified_smart_contracts_24h: Chain.count_new_verified_contracts_from_cache(@api_true) + }) + end + + operation :audit_reports_list, + summary: "Audit reports list", + description: "Returns audit reports for a given smart contract address.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: + {"Audit reports", "application/json", + %Schema{ + description: "List of smart-contract's audit reports", + type: :object, + properties: %{ + items: %Schema{ + type: :array, + items: Schemas.SmartContract.AuditReport, + nullable: false + }, + next_page_params: %Schema{type: :object, nullable: true} + }, + required: [:items, :next_page_params], + additionalProperties: false + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + GET /api/v2/smart-contracts/{address_hash}/audit-reports + """ + @spec audit_reports_list(Plug.Conn.t(), map()) :: + {:format, :error} + | {:not_found, nil | Explorer.Chain.SmartContract.t()} + | {:restricted_access, true} + | Plug.Conn.t() + def audit_reports_list(conn, %{address_hash_param: address_hash_string} = params) do + with {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string) do + reports = AuditReport.get_audit_reports_by_smart_contract_address_hash(address_hash, @api_true) + + conn + |> render(:audit_reports, %{reports: reports}) + end + end + + operation :audit_report_submission, + summary: "Submit audit report", + description: "Submits an audit report for a given smart contract address.", + parameters: [address_hash_param() | base_params()], + request_body: audit_report_request_body(), + responses: [ + ok: + {"OK", "application/json", + %OpenApiSpex.Schema{ + type: :object, + properties: %{ + message: %Schema{type: :string} + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + POST /api/v2/smart-contracts/{address_hash}/audit-reports + """ + @spec audit_report_submission(Plug.Conn.t(), map()) :: + {:error, Ecto.Changeset.t()} + | {:format, :error} + | {:not_found, nil | Explorer.Chain.SmartContract.t()} + | {:recaptcha, any()} + | {:restricted_access, true} + | Plug.Conn.t() + def audit_report_submission(conn, %{address_hash_param: address_hash_string} = params) do + with {:disabled, true} <- {:disabled, Application.get_env(:explorer, :air_table_audit_reports)[:enabled]}, + {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string), + audit_report_params <- %{ + address_hash: address_hash, + submitter_name: params[:submitter_name], + submitter_email: params[:submitter_email], + is_project_owner: params[:is_project_owner], + project_name: params[:project_name], + project_url: params[:project_url], + audit_company_name: params[:audit_company_name], + audit_report_url: params[:audit_report_url], + audit_publish_date: params[:audit_publish_date], + comment: params[:comment] + }, + {:ok, _} <- AuditReport.create(audit_report_params) do + conn + |> put_status(200) + |> json(%{message: "OK"}) + end + end + + @spec contract_creation_transaction_associations() :: [keyword()] + defp contract_creation_transaction_associations do + case chain_type() do + :filecoin -> + Address.contract_creation_transaction_with_from_address_associations() + + _ -> + Address.contract_creation_transaction_associations() + end + end + + @spec smart_contract_address_options() :: keyword() + defp smart_contract_address_options do + [ + necessity_by_association: %{ + [smart_contract: :smart_contract_additional_sources] => :optional, + contract_creation_transaction_associations() => :optional + } + ] + |> Keyword.merge(@api_true) + end + + @spec verified_smart_contract_addresses_options() :: keyword() + defp verified_smart_contract_addresses_options do + [ + necessity_by_association: %{ + [:token, :names, :proxy_implementations] => :optional, + contract_creation_transaction_associations() => :optional + } + ] + |> Keyword.merge(@api_true) + end + @doc """ Builds paging options for smart contract addresses based on request parameters. @@ -167,30 +311,30 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do sorting. Otherwise, returns default paging options. ## Examples - iex> smart_contract_addresses_paging_options(%{"hash" => "0x123...", "transactions_count" => "100", "coin_balance" => "1000"}) + iex> smart_contract_addresses_paging_options(%{hash: "0x123...", transactions_count: "100", coin_balance: "1000"}) [paging_options: %{key: %{hash: ..., transactions_count: 100, fetched_coin_balance: 1000}}] - iex> smart_contract_addresses_paging_options(%{"smart_contract_id" => "42"}) + iex> smart_contract_addresses_paging_options(%{smart_contract_id: "42"}) [paging_options: %{key: %{id: 42}}] iex> smart_contract_addresses_paging_options(%{}) [paging_options: %{}] """ - @spec smart_contract_addresses_paging_options(%{required(String.t()) => String.t()}) :: + @spec smart_contract_addresses_paging_options(%{required(atom()) => String.t()}) :: [paging_options: map()] def smart_contract_addresses_paging_options(params) do options = do_smart_contract_addresses_paging_options(params) [paging_options: default_paging_options() |> Map.merge(options)] end - @spec do_smart_contract_addresses_paging_options(%{required(String.t()) => String.t()}) :: map() - defp do_smart_contract_addresses_paging_options(%{"hash" => hash_string} = params) do + @spec do_smart_contract_addresses_paging_options(%{required(atom()) => String.t()}) :: map() + defp do_smart_contract_addresses_paging_options(%{hash: hash_string} = params) do hash_string |> Chain.string_to_address_hash() |> case do {:ok, address_hash} -> - transactions_count = parse_integer(params["transactions_count"]) - coin_balance = parse_integer(params["coin_balance"]) + transactions_count = parse_integer(params[:transactions_count]) + coin_balance = parse_integer(params[:coin_balance]) %{ key: %{ @@ -205,7 +349,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end end - defp do_smart_contract_addresses_paging_options(%{"smart_contract_id" => smart_contract_id}) do + defp do_smart_contract_addresses_paging_options(%{smart_contract_id: smart_contract_id}) + when is_integer(smart_contract_id) do + %{key: %{id: smart_contract_id}} + end + + # todo: remove this function clause when all controllers are covered with OpenAPI spec + defp do_smart_contract_addresses_paging_options(%{smart_contract_id: smart_contract_id}) do smart_contract_id |> safe_parse_non_negative_integer() |> case do @@ -216,37 +366,35 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do defp do_smart_contract_addresses_paging_options(_params), do: %{} - @doc """ - Extracts pagination parameters from an Address struct for use in the next page - URL. + # Extracts pagination parameters from an Address struct for use in the next page + # URL. - ## Returns - A map with string keys that can be used as query parameters. + # ## Returns + # A map with string keys that can be used as query parameters. - ## Examples - iex> address = %Explorer.Chain.Address{hash: "0x123...", transactions_count: 100, fetched_coin_balance: 1000} - iex> smart_contract_addresses_paging_params(address) - %{"hash" => "0x123...", "transactions_count" => 100, "coin_balance" => 1000} - """ + # ## Examples + # iex> address = %Explorer.Chain.Address{hash: "0x123...", transactions_count: 100, fetched_coin_balance: 1000} + # iex> smart_contract_addresses_paging_params(address) + # %{hash: "0x123...", transactions_count: 100, coin_balance: 1000} @spec smart_contract_addresses_paging_params(Explorer.Chain.Address.t()) :: %{ - required(String.t()) => any() + required(atom()) => any() } - def smart_contract_addresses_paging_params(%Explorer.Chain.Address{ - hash: address_hash, - transactions_count: transactions_count, - fetched_coin_balance: coin_balance - }) do + defp smart_contract_addresses_paging_params(%Explorer.Chain.Address{ + hash: address_hash, + transactions_count: transactions_count, + fetched_coin_balance: coin_balance + }) do %{ - "hash" => address_hash, - "transactions_count" => transactions_count, - "coin_balance" => coin_balance + hash: address_hash, + transactions_count: transactions_count, + coin_balance: coin_balance } end - @spec smart_contract_addresses_sorting(%{required(String.t()) => String.t()}) :: [ + @spec smart_contract_addresses_sorting(%{required(atom()) => String.t()}) :: [ {:sorting, list()} ] - defp smart_contract_addresses_sorting(%{"sort" => sort_field, "order" => order}) do + defp smart_contract_addresses_sorting(%{sort: sort_field, order: order}) do {sort_field, order} |> case do {"balance", "asc"} -> {:ok, [{:asc_nulls_first, :fetched_coin_balance}]} @@ -266,68 +414,6 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do defp smart_contract_addresses_sorting(_), do: [] - @doc """ - POST /api/v2/smart-contracts/{address_hash}/audit-reports - """ - @spec audit_report_submission(Plug.Conn.t(), map()) :: - {:error, Ecto.Changeset.t()} - | {:format, :error} - | {:not_found, nil | Explorer.Chain.SmartContract.t()} - | {:recaptcha, any()} - | {:restricted_access, true} - | Plug.Conn.t() - def audit_report_submission(conn, %{"address_hash" => address_hash_string} = params) do - with {:disabled, true} <- {:disabled, Application.get_env(:explorer, :air_table_audit_reports)[:enabled]}, - {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string), - audit_report_params <- %{ - address_hash: address_hash, - submitter_name: params["submitter_name"], - submitter_email: params["submitter_email"], - is_project_owner: params["is_project_owner"], - project_name: params["project_name"], - project_url: params["project_url"], - audit_company_name: params["audit_company_name"], - audit_report_url: params["audit_report_url"], - audit_publish_date: params["audit_publish_date"], - comment: params["comment"] - }, - {:ok, _} <- AuditReport.create(audit_report_params) do - conn - |> put_status(200) - |> json(%{message: "OK"}) - end - end - - @doc """ - GET /api/v2/smart-contracts/{address_hash}/audit-reports - """ - @spec audit_reports_list(Plug.Conn.t(), map()) :: - {:format, :error} - | {:not_found, nil | Explorer.Chain.SmartContract.t()} - | {:restricted_access, true} - | Plug.Conn.t() - def audit_reports_list(conn, %{"address_hash" => address_hash_string} = params) do - with {:ok, address_hash, _smart_contract} <- validate_smart_contract(params, address_hash_string) do - reports = AuditReport.get_audit_reports_by_smart_contract_address_hash(address_hash, @api_true) - - conn - |> render(:audit_reports, %{reports: reports}) - end - end - - def smart_contracts_counters(conn, _params) do - conn - |> json(%{ - smart_contracts: Chain.count_contracts_from_cache(@api_true), - new_smart_contracts_24h: Chain.count_new_contracts_from_cache(@api_true), - verified_smart_contracts: Chain.count_verified_contracts_from_cache(@api_true), - new_verified_smart_contracts_24h: Chain.count_new_verified_contracts_from_cache(@api_true) - }) - end - - def prepare_args(list) when is_list(list), do: list - def prepare_args(other), do: [other] - defp validate_smart_contract(params, address_hash_string) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 0bdd396a1182..a86da1e4deb2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -1,11 +1,11 @@ defmodule BlockScoutWeb.API.V2.StatsController do + use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs + use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type], chain_identity: [:explorer, :chain_identity] - use BlockScoutWeb, :controller - use OpenApiSpex.ControllerSpecs - import BlockScoutWeb.PagingHelper, only: [hot_smart_contracts_sorting: 1, delete_items_count_from_next_page_params: 1] import BlockScoutWeb.Chain, @@ -18,6 +18,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + alias OpenApiSpex.Schema + alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.Chain.MarketHistoryChartController alias Explorer.{Chain, Market} @@ -29,14 +31,25 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias Plug.Conn alias Timex.Duration - @api_true [api?: true] - plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) - tags ["stats"] + tags(["stats"]) + + @api_true [api?: true] - operation :stats, false + operation :stats, + summary: "Get indexing and chain stats", + description: "Returns current indexing progress, chain stats and market data used on the UI.", + parameters: base_params(), + responses: [ + ok: {"Stats", "application/json", Schemas.Stats.Response}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ + Returns current indexing progress, chain stats and market data used on the UI. + """ + @spec stats(Plug.Conn.t(), map()) :: Plug.Conn.t() def stats(conn, _params) do market_cap_type = case Application.get_env(:explorer, :supply) do @@ -118,8 +131,21 @@ defmodule BlockScoutWeb.API.V2.StatsController do else: gas_used |> Decimal.div(gas_limit) |> Decimal.mult(100) |> Decimal.to_float() end - operation :transactions_chart, false + operation :transactions_chart, + summary: "Transactions chart data", + description: "Returns transaction counts by date for charting.", + parameters: base_params(), + responses: [ + ok: + {"Transactions chart data", "application/json", + %Schema{type: :object, properties: %{chart_data: %Schema{type: :array, items: %Schema{type: :object}}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ + Returns transaction counts by date chart. + """ + @spec transactions_chart(Plug.Conn.t(), map()) :: Plug.Conn.t() def transactions_chart(conn, _params) do [{:history_size, history_size}] = Application.get_env(:block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController, [{:history_size, 30}]) @@ -141,8 +167,27 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end - operation :market_chart, false + operation :market_chart, + summary: "Market history chart data", + description: "Returns market history (price, market cap, tvl) for charting.", + parameters: base_params(), + responses: [ + ok: + {"Market chart data", "application/json", + %Schema{ + type: :object, + properties: %{ + chart_data: %Schema{type: :array, items: %Schema{type: :object}}, + available_supply: %Schema{anyOf: [Schemas.General.FloatString, %Schema{type: :integer}]} + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ + Returns market history (price, market cap, tvl) for charting. + """ + @spec market_chart(Plug.Conn.t(), map()) :: Plug.Conn.t() def market_chart(conn, _params) do exchange_rate = Market.get_coin_exchange_rate() @@ -176,8 +221,21 @@ defmodule BlockScoutWeb.API.V2.StatsController do }) end - operation :secondary_coin_market_chart, false + operation :secondary_coin_market_chart, + summary: "Secondary coin market history chart data", + description: "Returns market history for the secondary coin used for charting.", + parameters: base_params(), + responses: [ + ok: + {"Secondary coin market chart data", "application/json", + %Schema{type: :object, properties: %{chart_data: %Schema{type: :array, items: %Schema{type: :object}}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ + Returns market history for the secondary coin used for charting. + """ + @spec secondary_coin_market_chart(Plug.Conn.t(), map()) :: Plug.Conn.t() def secondary_coin_market_chart(conn, _params) do recent_market_history = Market.fetch_recent_history(true) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 79733c327414..2bdc2842a4e2 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse - alias Explorer.{Chain, Helper, PagingOptions} + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, BridgedToken, Token, Token.Instance} alias Explorer.Migrator.BackfillMetadataURL alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand @@ -59,7 +59,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param` endpoint. """ @spec token(Plug.Conn.t(), map()) :: Plug.Conn.t() def token(conn, %{address_hash_param: address_hash_string} = params) do @@ -109,7 +109,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param/counters` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param/counters` endpoint. """ @spec counters(Plug.Conn.t(), map()) :: Plug.Conn.t() def counters(conn, %{address_hash_param: address_hash_string} = params) do @@ -156,7 +156,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param/transfers` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param/transfers` endpoint. """ @spec transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() def transfers(conn, %{address_hash_param: address_hash_string} = params) do @@ -212,7 +212,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param/holders` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param/holders` endpoint. """ @spec holders(Plug.Conn.t(), map()) :: Plug.Conn.t() def holders(conn, %{address_hash_param: address_hash_string} = params) do @@ -257,7 +257,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param/instances` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param/instances` endpoint. """ @spec instances(Plug.Conn.t(), map()) :: Plug.Conn.t() @@ -349,7 +349,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ] @doc """ - Handles GET requests to `/api/v2/tokens/:token_address_param/instances/:token_id_param` endpoint. + Handles GET requests to `/api/v2/tokens/:address_hash_param/instances/:token_id_param` endpoint. """ @spec instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def instance(conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params) do @@ -409,6 +409,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do not_found: NotFoundResponse.response() ] + @doc """ + Handles GET requests to `/api/v2/tokens/:address_hash_param/instances/:token_id_param/transfers` endpoint. + """ + @spec transfers_by_instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def transfers_by_instance( conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params @@ -466,6 +470,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do not_found: NotFoundResponse.response() ] + @doc """ + Handles GET requests to `/api/v2/tokens/:address_hash_param/instances/:token_id_param/holders` endpoint. + """ + @spec holders_by_instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def holders_by_instance(conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), @@ -508,11 +516,15 @@ defmodule BlockScoutWeb.API.V2.TokenController do responses: [ ok: {"Transfer count for the instance.", "application/json", - %OpenApiSpex.Schema{type: :object, properties: %{transfers_count: %Schema{type: :integer}}}}, + %Schema{type: :object, properties: %{transfers_count: %Schema{type: :integer}}}}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] + @doc """ + Handles GET requests to `/api/v2/tokens/:address_hash_param/instances/:token_id_param/transfers-count` endpoint. + """ + @spec transfers_count_by_instance(Plug.Conn.t(), map()) :: Plug.Conn.t() def transfers_count_by_instance( conn, %{address_hash_param: address_hash_string, token_id_param: token_id_string} = params @@ -570,6 +582,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do unprocessable_entity: JsonErrorResponse.response() ] + @doc """ + Handles GET requests to `/api/v2/tokens` endpoint. + """ + @spec tokens_list(Plug.Conn.t(), map()) :: Plug.Conn.t() def tokens_list(conn, params) do filter = params[:q] @@ -579,7 +595,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - maybe_parsed_limit = Helper.parse_integer(params[:limit]) + maybe_parsed_limit = params[:limit] %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(token_transfers_types_options(params)) @@ -630,6 +646,10 @@ defmodule BlockScoutWeb.API.V2.TokenController do unprocessable_entity: JsonErrorResponse.response() ] + @doc """ + Handles GET requests to `/api/v2/tokens/bridged` endpoint. + """ + @spec bridged_tokens_list(Plug.Conn.t(), map()) :: Plug.Conn.t() def bridged_tokens_list(conn, params) do filter = params[:q] @@ -662,11 +682,15 @@ defmodule BlockScoutWeb.API.V2.TokenController do responses: [ ok: {"Metadata refetch triggered.", "application/json", - %OpenApiSpex.Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, + %Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] + @doc """ + Handles PATCH requests to `/api/v2/tokens/:address_hash_param/instances/:token_id_param/refetch-metadata` endpoint. + """ + @spec refetch_metadata(Plug.Conn.t(), map()) :: Plug.Conn.t() def refetch_metadata( conn, params @@ -702,11 +726,15 @@ defmodule BlockScoutWeb.API.V2.TokenController do responses: [ ok: {"NFT collection metadata refetch triggered.", "application/json", - %OpenApiSpex.Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, + %Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] + @doc """ + Handles PATCH requests to `/api/v2/tokens/:address_hash_param/instances/refetch-metadata` endpoint. + """ + @spec trigger_nft_collection_metadata_refetch(Plug.Conn.t(), map()) :: Plug.Conn.t() def trigger_nft_collection_metadata_refetch( conn, params diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index 8e57ab2d1b08..d4e30986a23d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -1,6 +1,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do use BlockScoutWeb, :controller - alias Explorer.{Chain, Helper, PagingOptions} + use OpenApiSpex.ControllerSpecs + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{TokenTransfer, Transaction} import BlockScoutWeb.Chain, @@ -24,8 +25,40 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["token_transfers"]) + @api_true [api?: true] + operation :token_transfers, + summary: "List token transfers", + description: "Retrieves token transfers with optional filtering and pagination.", + parameters: + base_params() ++ + [token_type_param(), limit_param()] ++ + define_paging_params([ + "index", + "block_number", + "batch_log_index", + "batch_block_hash", + "batch_transaction_hash", + "index_in_batch" + ]), + responses: [ + ok: + {"Token transfers", "application/json", + paginated_response( + items: Schemas.TokenTransfer, + next_page_params_example: %{ + "index" => 50, + "block_number" => 22_133_247 + }, + title_prefix: "TokenTransfers" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/token-transfers` endpoint. """ @@ -38,7 +71,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> - maybe_parsed_limit = Helper.parse_integer(params["limit"]) + maybe_parsed_limit = params[:limit] %PagingOptions{paging_options | page_size: min(page_size, maybe_parsed_limit && abs(maybe_parsed_limit))} end) |> Keyword.merge(token_transfers_types_options(params)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index df52c2ebfb9a..9222fb6fdcfa 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -143,7 +143,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "Retrieves detailed information for a specific transaction, including token transfers, internal transactions, and metadata.", parameters: [transaction_hash_param() | base_params()], responses: [ - ok: {"Transaction details", "application/json", BlockScoutWeb.Schemas.API.V2.Transaction.Response}, + ok: {"Transaction details", "application/json", Schemas.Transaction.Response}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() ] @@ -631,7 +631,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "Retrieves the raw trace for a specific transaction, including internal operations and execution details.", parameters: [transaction_hash_param() | base_params()], responses: [ - ok: {"Raw trace for transaction", "application/json", BlockScoutWeb.Schemas.API.V2.Transaction.RawTrace}, + ok: {"Raw trace for transaction", "application/json", Schemas.Transaction.RawTrace}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() ] @@ -943,8 +943,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do {"Transaction summary", "application/json", %Schema{ anyOf: [ - BlockScoutWeb.Schemas.API.V2.Transaction.Summary, - BlockScoutWeb.Schemas.API.V2.Transaction.SummaryJustRequestBody + Schemas.Transaction.Summary, + Schemas.Transaction.SummaryJustRequestBody ] }}, not_found: NotFoundResponse.response(), @@ -1015,7 +1015,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do title: "TransactionBlobs", type: :object, properties: %{ - items: %Schema{type: :array, items: BlockScoutWeb.Schemas.API.V2.Blob.Response} + items: %Schema{type: :array, items: Schemas.Blob.Response} }, nullable: false, additionalProperties: false @@ -1097,7 +1097,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ok: {"Beacon deposits for transaction", "application/json", paginated_response( - items: BlockScoutWeb.Schemas.API.V2.Beacon.Deposit.Response, + items: Schemas.Beacon.Deposit.Response, next_page_params_example: %{ "index" => 2_287_943, "items_count" => 50 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index 83b009cee4bc..08601469113d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] @@ -9,6 +10,28 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do alias Explorer.Chain + tags(["withdrawals"]) + + operation :withdrawals_list, + summary: "List withdrawals", + description: "Retrieves withdrawals with optional filtering and pagination.", + parameters: + base_params() ++ + define_paging_params(["index", "items_count"]), + responses: [ + ok: + {"Withdrawals", "application/json", + paginated_response( + items: Schemas.Withdrawal, + next_page_params_example: %{ + "index" => 50, + "items_count" => 50 + }, + title_prefix: "Withdrawals" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + def withdrawals_list(conn, params) do full_options = [ @@ -33,6 +56,15 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do }) end + operation :withdrawals_counters, + summary: "Withdrawals counters", + description: "Returns total withdrawals count and sum from cache.", + parameters: base_params(), + responses: [ + ok: {"Withdrawals counters", "application/json", Schemas.Withdrawal.Counter}, + unprocessable_entity: JsonErrorResponse.response() + ] + def withdrawals_counters(conn, _params) do conn |> json(%{ diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 3e53bda1a14c..c66d27cd4db3 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -299,6 +299,7 @@ defmodule BlockScoutWeb.PagingHelper do def delete_items_count_from_next_page_params(other), do: other + # todo: it is used in the old UI only, consider removing it later def current_filter(%{"filter" => language_string}) do SmartContract.language_string_to_atom() |> Map.fetch(language_string) @@ -308,6 +309,15 @@ defmodule BlockScoutWeb.PagingHelper do end end + def current_filter(%{filter: language_string}) do + SmartContract.language_string_to_atom() + |> Map.fetch(language_string) + |> case do + {:ok, language} -> [filter: language] + :error -> [] + end + end + def current_filter(_), do: [] def search_query(%{"search" => ""}), do: [] @@ -316,12 +326,19 @@ defmodule BlockScoutWeb.PagingHelper do [search: search_string] end + # todo: it is used in the old UI only, consider removing it later def search_query(%{"q" => ""}), do: [] def search_query(%{"q" => search_string}) do [search: search_string] end + def search_query(%{q: ""}), do: [] + + def search_query(%{q: search_string}) do + [search: search_string] + end + def search_query(_), do: [] @spec tokens_sorting(%{required(String.t()) => String.t()}) :: [{:sorting, SortingHelper.sorting_params()}] diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index aa3e9feb85cd..5f301bb94b8d 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -141,6 +141,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/backend-version", V2.ConfigController, :backend_version) get("/csv-export", V2.ConfigController, :csv_export) get("/public-metrics", V2.ConfigController, :public_metrics) + get("/smart-contracts/languages", V2.ConfigController, :languages_list) if @chain_identity == {:optimism, :celo} do get("/celo", V2.ConfigController, :celo) @@ -269,7 +270,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/indexing-status", V2.MainPageController, :indexing_status) if @chain_type == :optimism do - get("/optimism-deposits", V2.MainPageController, :optimism_deposits) + get("/optimism-deposits", V2.OptimismController, :main_page_deposits) end if @chain_type == :polygon_zkevm do @@ -387,7 +388,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end scope "/solidityscan" do - get("/smart-contracts/:address_hash/report", V2.SmartContractController, :solidityscan_report) + get("/smart-contracts/:address_hash/report", V2.Proxy.SolidityScanController, :solidityscan_report) end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex index 37e1750f7542..033ccca6cac2 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex @@ -25,6 +25,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end pipeline :api_v2_no_forgery_protect do @@ -41,6 +42,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(:accepts, ["json"]) plug(CheckApiV2) plug(:fetch_session) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) end scope "/", as: :api_v2 do @@ -48,8 +50,8 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do get("/", V2.SmartContractController, :smart_contracts_list) get("/counters", V2.SmartContractController, :smart_contracts_counters) - get("/:address_hash", V2.SmartContractController, :smart_contract) - get("/:address_hash/audit-reports", V2.SmartContractController, :audit_reports_list) + get("/:address_hash_param", V2.SmartContractController, :smart_contract) + get("/:address_hash_param/audit-reports", V2.SmartContractController, :audit_reports_list) get("/verification/config", V2.VerificationController, :config) end @@ -57,7 +59,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do scope "/", as: :api_v2 do pipe_through(:api_v2_no_forgery_protect) - post("/:address_hash/audit-reports", V2.SmartContractController, :audit_report_submission) + post("/:address_hash_param/audit-reports", V2.SmartContractController, :audit_report_submission) end scope "/:address_hash/verification/via", as: :api_v2 do diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 487301e1af42..ab3563711503 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -68,11 +68,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :address_hash_param, in: :path, schema: AddressHash, - required: true + required: true, + description: "Address hash in the path" } end # todo: It should be removed when the frontend stops sending the address_id parameter with the request + # https://github.com/blockscout/frontend/issues/3090 @doc """ Returns a parameter definition for an address hash in the path. """ @@ -82,7 +84,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :address_id, in: :query, schema: AddressHash, - required: false + required: false, + description: "Address hash in the query" } end @@ -154,7 +157,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do %Parameter{ name: :limit, in: :query, - schema: %Schema{type: :string, nullable: true}, + schema: %Schema{type: :integer, nullable: true}, required: false, description: "Limit result items in the response" } @@ -189,7 +192,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do end @doc """ - Returns a parameter definition for a holder address hash in the query. + Returns a parameter definition for a token holder address hash in the query. """ @spec holder_address_hash_param() :: Parameter.t() def holder_address_hash_param do @@ -197,7 +200,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :holder_address_hash, in: :query, schema: AddressHash, - required: false + required: false, + description: "Token holder address hash in the query" } end @@ -210,7 +214,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :execution_node_hash_param, in: :path, schema: AddressHash, - required: true + required: true, + description: "Execution node hash in the path" } end @@ -223,7 +228,22 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :transaction_hash_param, in: :path, schema: FullHash, - required: true + required: true, + description: "Transaction hash in the path" + } + end + + @doc """ + Returns a parameter definition for a transaction hash in the query. + """ + @spec query_transaction_hash_param() :: Parameter.t() + def query_transaction_hash_param do + %Parameter{ + name: :transaction_hash, + in: :query, + schema: FullHash, + required: false, + description: "Transaction hash in the query" } end @@ -236,7 +256,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :block_hash_or_number_param, in: :path, schema: %Schema{anyOf: [%Schema{type: :integer}, FullHash]}, - required: true + required: true, + description: "Block hash or number in the path" } end @@ -249,7 +270,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do name: :block_number_param, in: :path, schema: %Schema{type: :integer, minimum: 0}, - required: true + required: true, + description: "Block number in the path" } end @@ -306,6 +328,43 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a reusable OpenApiSpex.RequestBody for audit report submission. + """ + @spec audit_report_request_body() :: OpenApiSpex.RequestBody.t() + def audit_report_request_body do + %OpenApiSpex.RequestBody{ + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: %OpenApiSpex.Schema{ + type: :object, + properties: %{ + submitter_name: %Schema{type: :string}, + submitter_email: %Schema{type: :string}, + is_project_owner: %Schema{type: :boolean}, + project_name: %Schema{type: :string}, + project_url: %Schema{type: :string}, + audit_company_name: %Schema{type: :string}, + audit_report_url: %Schema{type: :string}, + audit_publish_date: %Schema{type: :string, format: :date}, + comment: %Schema{type: :string, nullable: true} + }, + required: [ + :submitter_name, + :submitter_email, + :is_project_owner, + :project_name, + :project_url, + :audit_company_name, + :audit_report_url, + :audit_publish_date + ] + } + } + } + } + end + @doc """ Returns a parameter definition for a batch number in the path. """ @@ -486,6 +545,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec token_type_param() :: Parameter.t() def token_type_param do %Parameter{ + name: :type, in: :query, schema: %Schema{ anyOf: [ @@ -497,8 +557,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do ] }, required: false, - description: @token_type_param_description, - name: :type + description: @token_type_param_description } end @@ -508,6 +567,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec nft_token_type_param() :: Parameter.t() def nft_token_type_param do %Parameter{ + name: :type, in: :query, schema: %Schema{ anyOf: [ @@ -526,8 +586,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do * ERC-404 - Hybrid fungible/non-fungible tokens Example: `ERC-721,ERC-1155` to show both NFT and multi-token transfers - """, - name: :type + """ } end @@ -537,11 +596,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec topic_param() :: Parameter.t() def topic_param do %Parameter{ + name: :topic, in: :query, schema: HexString, required: false, - description: "Filter logs by topic", - name: :topic + description: "Log topic param in the query" } end @@ -551,11 +610,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec token_filter_param() :: Parameter.t() def token_filter_param do %Parameter{ + name: :token, in: :query, schema: AddressHash, required: false, - description: "Filter token transfers by token contract address.", - name: :token + description: "Filter token transfers by token contract address." } end @@ -621,11 +680,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec api_key_param() :: Parameter.t() def api_key_param do %Parameter{ + name: :apikey, in: :query, schema: %Schema{type: :string}, required: false, - description: "API key for rate limiting or for sensitive endpoints", - name: :apikey + description: "API key for rate limiting or for sensitive endpoints" } end @@ -635,11 +694,11 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @spec key_param() :: Parameter.t() def key_param do %Parameter{ + name: :key, in: :query, schema: %Schema{type: :string}, required: false, - description: "Secret key for getting access to restricted resources", - name: :key + description: "Secret key for getting access to restricted resources" } end @@ -697,293 +756,315 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @paging_params %{ "block_number" => %Parameter{ + name: :block_number, in: :query, schema: %Schema{type: :integer, minimum: 0}, required: false, - description: "Block number for paging", - name: :block_number + description: "Block number for paging" }, "block_number_nullable" => %Parameter{ + name: :block_number, in: :query, schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, required: false, - description: "Block number for paging", - name: :block_number + description: "Block number for paging" }, "block_number_no_casting" => %Parameter{ + name: :block_number, in: :query, schema: IntegerString, required: false, - description: "Block number for paging", - name: :block_number + description: "Block number for paging" }, "epoch_number" => %Parameter{ + name: :epoch_number, in: :query, schema: IntegerString, required: false, - description: "Epoch number for paging", - name: :epoch_number + description: "Epoch number for paging" }, "index" => %Parameter{ + name: :index, in: :query, schema: %Schema{type: :integer}, required: false, - description: "Transaction index for paging", - name: :index + description: "Transaction index for paging" }, "index_nullable" => %Parameter{ + name: :index, in: :query, schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, required: false, - description: "Transaction index for paging", - name: :index + description: "Transaction index for paging" }, "block_index" => %Parameter{ + name: :block_index, in: :query, schema: %Schema{type: :integer}, required: false, - description: "Block index for paging", - name: :block_index + description: "Block index for paging" }, "inserted_at" => %Parameter{ + name: :inserted_at, in: :query, schema: %Schema{type: :string, format: :"date-time"}, required: false, - description: "Inserted at timestamp for paging (ISO8601)", - name: :inserted_at + description: "Inserted at timestamp for paging (ISO8601)" }, "hash" => %Parameter{ + name: :hash, in: :query, schema: FullHash, required: false, - description: "Transaction hash for paging", - name: :hash + description: "Transaction hash for paging" }, # TODO: consider refactoring, to avoid ambiguity with hash param (the same name) "address_hash" => %Parameter{ + name: :hash, in: :query, schema: AddressHash, required: false, - description: "Address hash for paging", - name: :hash + description: "Address hash for paging" }, "address_hash_param" => %Parameter{ + name: :address_hash, in: :query, schema: AddressHash, required: false, - description: "Address hash for paging", - name: :address_hash + description: "Address hash for paging" }, "contract_address_hash" => %Parameter{ + name: :contract_address_hash, in: :query, schema: AddressHashNullable, required: false, - description: "Contract address hash for paging", - name: :contract_address_hash + description: "Contract address hash for paging" }, "value" => %Parameter{ + name: :value, in: :query, schema: IntegerString, required: false, - description: "Transaction value for paging", - name: :value + description: "Transaction value for paging" }, "fiat_value" => %Parameter{ + name: :fiat_value, in: :query, schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, required: false, - description: "Fiat value for paging", - name: :fiat_value + description: "Fiat value for paging" }, "fee" => %Parameter{ + name: :fee, in: :query, schema: IntegerString, required: false, - description: "Transaction fee for paging", - name: :fee + description: "Transaction fee for paging" }, "items_count" => %Parameter{ + name: :items_count, in: :query, schema: %Schema{type: :integer, minimum: 1, maximum: 50}, required: false, - description: "Number of items returned per page", - name: :items_count + description: "Number of items returned per page" }, "holders_count" => %Parameter{ + name: :holders_count, in: :query, schema: %Schema{anyOf: [IntegerString, EmptyString, NullString]}, required: false, - description: "Number of holders returned per page", - name: :holders_count + description: "Number of holders returned per page" }, "is_name_null" => %Parameter{ + name: :is_name_null, in: :query, schema: %Schema{type: :boolean}, required: false, - description: "Is name null for paging", - name: :is_name_null + description: "Is name null for paging" }, "market_cap" => %Parameter{ + name: :market_cap, in: :query, schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, required: false, - description: "Market cap for paging", - name: :market_cap + description: "Market cap for paging" }, "name" => %Parameter{ + name: :name, in: :query, schema: %Schema{type: :string}, required: false, - description: "Name for paging", - name: :name + description: "Name for paging" }, "batch_log_index" => %Parameter{ + name: :batch_log_index, in: :query, schema: %Schema{type: :integer}, required: false, - description: "Batch log index for paging", - name: :batch_log_index + description: "Batch log index for paging" }, "batch_block_hash" => %Parameter{ + name: :batch_block_hash, in: :query, schema: FullHash, required: false, - description: "Batch block hash for paging", - name: :batch_block_hash + description: "Batch block hash for paging" }, "batch_transaction_hash" => %Parameter{ + name: :batch_transaction_hash, in: :query, schema: FullHash, required: false, - description: "Batch transaction hash for paging", - name: :batch_transaction_hash + description: "Batch transaction hash for paging" }, "index_in_batch" => %Parameter{ + name: :index_in_batch, in: :query, schema: %Schema{type: :integer}, required: false, - description: "Index in batch for paging", - name: :index_in_batch + description: "Index in batch for paging" }, "transaction_index" => %Parameter{ + name: :transaction_index, in: :query, schema: %Schema{type: :integer}, required: false, - description: "Transaction index for paging", - name: :transaction_index + description: "Transaction index for paging" }, "fiat_value_nullable" => %Parameter{ + name: :fiat_value, in: :query, schema: %Schema{anyOf: [FloatString, EmptyString, NullString]}, required: false, - description: "Fiat value for paging", - name: :fiat_value + description: "Fiat value for paging" }, "id" => %Parameter{ + name: :id, in: :query, schema: %Schema{type: :integer}, required: false, - description: "ID for paging", - name: :id + description: "ID for paging" + }, + "smart_contract_id" => %Parameter{ + name: :smart_contract_id, + in: :query, + schema: %Schema{type: :integer}, + required: false, + description: "Smart-contract ID for paging" }, "fetched_coin_balance" => %Parameter{ + name: :fetched_coin_balance, in: :query, schema: IntegerStringNullable, required: false, - description: "Fetched coin balance for paging", - name: :fetched_coin_balance + description: "Fetched coin balance for paging" }, "transactions_count" => %Parameter{ + name: :transactions_count, in: :query, schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, required: false, - description: "Transactions count for paging", - name: :transactions_count + description: "Transactions count for paging" }, "token_contract_address_hash" => %Parameter{ + name: :token_contract_address_hash, in: :query, schema: AddressHash, required: false, - description: "Token contract address hash for paging", - name: :token_contract_address_hash + description: "Token contract address hash for paging" }, "token_id" => %Parameter{ + name: :token_id, in: :query, schema: IntegerStringNullable, required: false, - description: "Token ID for paging", - name: :token_id + description: "Token ID for paging" }, # todo: eliminate in favour token_id "unique_token" => %Parameter{ + name: :unique_token, in: :query, schema: IntegerStringNullable, required: false, - description: "Token ID for paging", - name: :unique_token + description: "Token ID for paging" }, "token_type" => %Parameter{ + name: :token_type, in: :query, schema: TokenType, required: false, - description: "Token type for paging", - name: :token_type + description: "Token type for paging" }, "amount" => %Parameter{ + name: :amount, in: :query, schema: IntegerStringNullable, required: false, - description: "Amount for paging", - name: :amount + description: "Amount for paging" }, "associated_account_address_hash" => %Parameter{ + name: :associated_account_address_hash, in: :query, schema: AddressHash, required: false, - description: "Associated account address hash for paging", - name: :associated_account_address_hash + description: "Associated account address hash for paging" }, "type" => %Parameter{ + name: :type, in: :query, schema: CeloElectionRewardType.schema(), required: false, - description: "Type for paging", - name: :type + description: "Type for paging" }, "filter" => %Parameter{ + name: :filter, in: :query, schema: %Schema{type: :string}, required: false, - description: "Filter for paging", - name: :filter + description: "Filter for paging" }, "deposit_index" => %Parameter{ + name: :index, in: :query, schema: %Schema{type: :integer, minimum: 0, maximum: 9_223_372_036_854_775_807}, required: false, - description: "Deposit index for paging", - name: :index + description: "Deposit index for paging" }, "total_gas_used" => %Parameter{ + name: :total_gas_used, in: :query, schema: %Schema{type: :integer, minimum: 0}, required: false, - description: "Total gas used for paging", - name: :total_gas_used + description: "Total gas used for paging" }, "contract_address_hash_not_nullable" => %Parameter{ + name: :contract_address_hash, in: :query, schema: AddressHash, required: false, - description: "Contract address hash for paging", - name: :contract_address_hash + description: "Contract address hash for paging" }, "transactions_count_positive" => %Parameter{ + name: :transactions_count, in: :query, schema: %Schema{type: :integer, minimum: 1}, required: false, - description: "Transactions count for paging", - name: :transactions_count + description: "Transactions count for paging" + }, + "coin_balance" => %Parameter{ + name: :coin_balance, + in: :query, + schema: %Schema{anyOf: [%Schema{type: :integer}, EmptyString, NullString]}, + required: false, + description: "Coin balance for paging" + }, + # todo: remove in the future as this param is unused in the pagination of state changes + "state_changes" => %Parameter{ + name: :state_changes, + in: :query, + schema: %Schema{type: :integer, nullable: true}, + required: false, + description: "State changes for paging" } } @@ -991,19 +1072,106 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do # "items_count" is used for pagination for the list of transactions's state changes and it can be higher than 50. # Thus, we extracted it to a separate map. "items_count" => %Parameter{ + name: :items_count, in: :query, schema: %Schema{type: :integer, minimum: 1}, required: false, - description: "Cumulative number of items to skip for keyset-based pagination of state changes", - name: :items_count + description: "Cumulative number of items to skip for keyset-based pagination of state changes" }, # todo: remove in the future as this param is unused in the pagination of state changes "state_changes" => %Parameter{ + name: :state_changes, in: :query, schema: %Schema{type: :string, nullable: true}, required: false, - description: "State changes for paging", - name: :state_changes + description: "State changes for paging" + } + } + + @search_paging_params %{ + "q" => %Parameter{ + name: :q, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Search query for paging" + }, + "next_page_params_type" => %Parameter{ + name: :next_page_params_type, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Next page params type for paging" + }, + "label" => %Parameter{ + name: :label, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Label for paging in the search results" + }, + "token" => %Parameter{ + name: :token, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Token for paging in the search results" + }, + "tac_operation" => %Parameter{ + name: :tac_operation, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "TAC operation for paging in the search results" + }, + "contract" => %Parameter{ + name: :contract, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Contract for paging in the search results" + }, + "metadata_tag" => %Parameter{ + name: :metadata_tag, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Metadata tag for paging in the search results" + }, + "block" => %Parameter{ + name: :block, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Block for paging in the search results" + }, + "blob" => %Parameter{ + name: :blob, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Blob for paging in the search results" + }, + "user_operation" => %Parameter{ + name: :user_operation, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "User operation for paging in the search results" + }, + "address" => %Parameter{ + name: :address, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "Address for paging in the search results" + }, + "ens_domain" => %Parameter{ + name: :ens_domain, + in: :query, + schema: %Schema{type: :object}, + required: false, + description: "ENS domain for paging in the search results" } } @@ -1027,6 +1195,16 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do end) end + @doc """ + Returns a list of pagination parameters for `/api/v2/search` API endpoint + """ + @spec define_search_paging_params([String.t()]) :: [Parameter.t()] + def define_search_paging_params(fields) do + Enum.map(fields, fn field -> + Map.get(@search_paging_params, field) || raise "Unknown paging param: #{field}" + end) + end + @doc """ Returns the list of allowed transaction type labels based on the configured chain type. """ diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/results.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/results.ex new file mode 100644 index 000000000000..ac6dd9dd93df --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/search/results.ex @@ -0,0 +1,19 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Search.Results do + @moduledoc """ + This module defines the schema for search results response. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "SearchResult", + description: "Search results containing blocks, transactions, and addresses", + type: :object, + properties: %{ + items: %Schema{type: :array, items: %Schema{type: :object}}, + next_page_params: %Schema{type: :object, nullable: true} + }, + required: [] + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex new file mode 100644 index 000000000000..b5008603e631 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex @@ -0,0 +1,136 @@ +defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.ChainTypeCustomizations do + @moduledoc false + alias BlockScoutWeb.Schemas.Helper + alias OpenApiSpex.Schema + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + # credo:disable-for-next-line + def chain_type_fields(schema) do + case Application.get_env(:explorer, :chain_type) do + :arbitrum -> + schema + |> Helper.extend_schema( + properties: %{ + package_name: %Schema{type: :string, nullable: true}, + github_repository_metadata: %Schema{type: :object, nullable: true} + }, + required: [] + ) + + :zksync -> + schema + |> Helper.extend_schema( + properties: %{ + zk_compiler_version: %Schema{type: :string, nullable: true} + }, + required: [] + ) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.SmartContract do + @moduledoc """ + OpenAPI schema for a smart contract response. + + Matches the API response shape returned by the `SmartContractController.smart_contract/2` action. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.API.V2.SmartContract.ChainTypeCustomizations + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %{ + description: "Smart contract", + type: :object, + properties: %{ + file_path: %Schema{type: :string, nullable: true}, + creation_status: %Schema{type: :string, nullable: true}, + source_code: %Schema{type: :string, nullable: true}, + deployed_bytecode: %Schema{type: :string, nullable: true}, + address: BlockScoutWeb.Schemas.API.V2.Address, + coin_balance: %Schema{type: :string, nullable: true}, + compiler_version: %Schema{type: :string, nullable: true}, + has_constructor_args: %Schema{type: :boolean, nullable: true}, + language: %Schema{type: :string, nullable: true}, + license_type: %Schema{type: :string, nullable: true}, + market_cap: %Schema{type: :string, nullable: true}, + optimization_enabled: %Schema{type: :boolean, nullable: true}, + reputation: %Schema{type: :string, nullable: true}, + transactions_count: %Schema{type: :integer, nullable: true}, + verified_at: %Schema{type: :string, format: :"date-time", nullable: true}, + verification_metadata: %Schema{type: :object, nullable: true}, + verified_twin_address_hash: %Schema{type: :string, nullable: true}, + compiler_settings: %Schema{type: :object, nullable: true}, + optimization_runs: %Schema{type: :integer, nullable: true}, + sourcify_repo_url: %Schema{type: :string, nullable: true}, + decoded_constructor_args: %Schema{ + type: :array, + items: %Schema{type: :array, items: %Schema{anyOf: [%Schema{type: :object}, %Schema{type: :string}]}}, + nullable: true + }, + is_verified_via_verifier_alliance: %Schema{type: :boolean, nullable: true}, + implementations: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + address_hash: General.AddressHash, + name: %Schema{type: :string, nullable: true} + } + }, + nullable: true + }, + proxy_type: %Schema{type: :string, nullable: true}, + external_libraries: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{name: %Schema{type: :string, nullable: true}, address_hash: General.AddressHash} + }, + nullable: true + }, + creation_bytecode: %Schema{type: :string, nullable: true}, + name: %Schema{type: :string, nullable: true}, + is_blueprint: %Schema{type: :boolean, nullable: true}, + is_verified: %Schema{type: :boolean, nullable: true}, + is_fully_verified: %Schema{type: :boolean, nullable: true}, + is_verified_via_eth_bytecode_db: %Schema{type: :boolean, nullable: true}, + evm_version: %Schema{type: :string, nullable: true}, + can_be_visualized_via_sol2uml: %Schema{type: :boolean, nullable: true}, + is_verified_via_sourcify: %Schema{type: :boolean, nullable: true}, + additional_sources: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{file_path: %Schema{type: :string}, source_code: %Schema{type: :string}} + }, + nullable: true + }, + certified: %Schema{type: :boolean}, + conflicting_implementations: %Schema{type: :array, nullable: true, items: %Schema{type: :object}}, + abi: %Schema{type: :array, nullable: true, items: %Schema{type: :object}}, + is_changed_bytecode: %Schema{type: :boolean, nullable: true}, + is_partially_verified: %Schema{type: :boolean, nullable: true}, + constructor_args: %Schema{type: :string, nullable: true} + }, + required: [], + additionalProperties: false + } + |> ChainTypeCustomizations.chain_type_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/audit_report.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/audit_report.ex new file mode 100644 index 000000000000..37872d6f9fb6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/audit_report.ex @@ -0,0 +1,15 @@ +defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.AuditReport do + @moduledoc false + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Smart contract audit report item", + type: :object, + properties: %{ + audit_company_name: %Schema{type: :string, nullable: true}, + audit_publish_date: %Schema{type: :string, format: :date, nullable: true}, + audit_report_url: %Schema{type: :string, nullable: true} + } + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/counters.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/counters.ex new file mode 100644 index 000000000000..dc6fb493d69d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/counters.ex @@ -0,0 +1,16 @@ +defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.Counters do + @moduledoc false + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + description: "Smart contracts counters", + type: :object, + properties: %{ + smart_contracts: General.IntegerString, + new_smart_contracts_24h: General.IntegerString, + verified_smart_contracts: General.IntegerString, + new_verified_smart_contracts_24h: General.IntegerString + } + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/language.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/language.ex new file mode 100644 index 000000000000..1facce6274ac --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract/language.ex @@ -0,0 +1,8 @@ +defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.Language do + @moduledoc false + require OpenApiSpex + + alias Explorer.Chain.SmartContract + + OpenApiSpex.schema(%{type: :string, enum: SmartContract.language_strings()}) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/response.ex new file mode 100644 index 000000000000..d63201ad197f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/stats/response.ex @@ -0,0 +1,120 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Stats.Response.ChainTypeCustomizations do + @moduledoc false + require OpenApiSpex + + use Utils.RuntimeEnvHelper, + chain_type: [:explorer, :chain_type], + chain_identity: [:explorer, :chain_identity] + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.Helper + alias OpenApiSpex.Schema + + @doc """ + Applies chain-specific field customizations to the given schema based on the configured chain type. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain-specific properties added based on the current chain type configuration + """ + @spec chain_type_fields(map()) :: map() + def chain_type_fields(schema) do + case chain_type() do + :rsk -> + schema + |> Helper.extend_schema( + properties: %{ + rootstock_locked_btc: %Schema{type: :string, nullable: true, description: "Present on RSK chains only"} + } + ) + + :optimism -> + schema + |> Helper.extend_schema( + properties: %{ + last_output_root_size: %Schema{ + type: :string, + nullable: true, + description: "Present on Optimism chains only" + } + } + ) + + _ -> + schema + end + end + + @doc """ + Applies chain identity-specific field customizations to the given schema based on the configured chain identity. + + ## Parameters + - `schema`: The base schema map to be customized + + ## Returns + - The schema map with chain identity-specific properties added based on the current chain identity configuration + """ + @spec chain_identity_fields(map()) :: map() + def chain_identity_fields(schema) do + case chain_identity() do + {:optimism, :celo} -> + schema + |> Helper.extend_schema( + properties: %{ + celo: %Schema{ + type: :object, + nullable: true, + properties: %{epoch_number: %Schema{type: :integer, nullable: true}} + } + } + ) + + _ -> + schema + end + end +end + +defmodule BlockScoutWeb.Schemas.API.V2.Stats.Response do + @moduledoc """ + This module defines the schema for response from /api/v2/stats. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.API.V2.Stats.Response.ChainTypeCustomizations + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + %Schema{ + type: :object, + title: "StatsResponse", + description: "Stats response", + properties: %{ + average_block_time: %Schema{type: :number, format: :float}, + coin_image: %Schema{type: :string, nullable: true}, + coin_price: General.FloatStringNullable, + coin_price_change_percentage: %Schema{type: :number, format: :float, nullable: true}, + gas_price_updated_at: General.TimestampNullable, + gas_prices: %Schema{type: :object, nullable: true}, + gas_prices_update_in: %Schema{type: :integer, nullable: true}, + gas_used_today: %Schema{anyOf: [General.IntegerStringNullable, %Schema{type: :integer}]}, + market_cap: General.FloatString, + network_utilization_percentage: %Schema{type: :number, nullable: true}, + secondary_coin_image: %Schema{type: :string, nullable: true}, + secondary_coin_price: General.FloatStringNullable, + static_gas_price: General.IntegerStringNullable, + total_addresses: General.IntegerString, + total_blocks: General.IntegerString, + total_gas_used: General.IntegerString, + total_transactions: General.IntegerString, + transactions_today: General.IntegerString, + tvl: General.IntegerStringNullable + } + } + |> ChainTypeCustomizations.chain_type_fields() + |> ChainTypeCustomizations.chain_identity_fields() + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal/counter.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal/counter.ex new file mode 100644 index 000000000000..a637e8be80c4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/withdrawal/counter.ex @@ -0,0 +1,20 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Withdrawal.Counter do + @moduledoc """ + This module defines the schema for the Withdrawal counters struct. + """ + require OpenApiSpex + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + withdrawals_count: General.IntegerString, + withdrawals_sum: General.IntegerString + }, + required: [ + :withdrawals_count, + :withdrawals_sum + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex index 85f69e09c602..2c6082a2ac1d 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/helper.ex @@ -8,15 +8,26 @@ defmodule BlockScoutWeb.Schemas.Helper do """ @spec extend_schema(Schema.t() | map(), Keyword.t()) :: Schema.t() | map() def extend_schema(schema, options) do - updated_schema = - schema - |> Map.update!(:properties, &Map.merge(&1, Keyword.get(options, :properties, %{}))) - |> Map.update!(:required, &(&1 ++ Keyword.get(options, :required, []))) + updated_schema_with_properties = + if Keyword.has_key?(options, :properties), + do: + Map.update!(schema, :properties, fn properties -> + Map.merge(properties, Keyword.get(options, :properties, %{})) + end), + else: schema + + updated_schema_with_required = + if Keyword.has_key?(options, :required), + do: + Map.update!(updated_schema_with_properties, :required, fn required -> + required ++ Keyword.get(options, :required, []) + end), + else: updated_schema_with_properties updated_schema_with_title = if Keyword.has_key?(options, :title), - do: Map.put(updated_schema, :title, Keyword.get(options, :title)), - else: updated_schema + do: Map.put(updated_schema_with_required, :title, Keyword.get(options, :title)), + else: updated_schema_with_required updated_schema_with_description = if Keyword.has_key?(options, :description), @@ -28,6 +39,11 @@ defmodule BlockScoutWeb.Schemas.Helper do do: Map.put(updated_schema_with_description, :nullable, Keyword.get(options, :nullable)), else: updated_schema_with_description - updated_schema_with_nullable + updated_schema_with_enum = + if Keyword.has_key?(options, :enum), + do: Map.put(updated_schema_with_nullable, :enum, Keyword.get(options, :enum)), + else: updated_schema_with_nullable + + updated_schema_with_enum end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs index eb55d1225b1b..438694e048b9 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -19,4 +19,47 @@ defmodule BlockScoutWeb.API.V2.ConfigControllerTest do assert %{"backend_version" => nil} = json_response(request, 200) end end + + describe "/config/public-metrics" do + test "returns configured update period hours", %{conn: conn} do + # save existing configuration and set test value + prev = Application.get_env(:explorer, Explorer.Chain.Metrics.PublicMetrics) + Application.put_env(:explorer, Explorer.Chain.Metrics.PublicMetrics, update_period_hours: 7) + + on_exit(fn -> Application.put_env(:explorer, Explorer.Chain.Metrics.PublicMetrics, prev) end) + + request = get(conn, "/api/v2/config/public-metrics") + assert %{"update_period_hours" => 7} = json_response(request, 200) + end + end + + describe "/config/smart-contracts/languages" do + @base_languages ["solidity", "vyper", "yul", "geas"] + + case Application.compile_env(:explorer, :chain_type) do + :arbitrum -> + test "gets smart-contract languages", %{conn: conn} do + request = get(conn, "/api/v2/config/smart-contracts/languages") + response = json_response(request, 200) + + assert response == %{"languages" => @base_languages ++ ["stylus_rust"]} + end + + :zilliqa -> + test "gets smart-contract languages", %{conn: conn} do + request = get(conn, "/api/v2/config/smart-contracts/languages") + response = json_response(request, 200) + + assert response == %{"languages" => @base_languages ++ ["scilla"]} + end + + _ -> + test "gets smart-contract languages", %{conn: conn} do + request = get(conn, "/api/v2/config/smart-contracts/languages") + response = json_response(request, 200) + + assert response == %{"languages" => @base_languages} + end + end + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs index cee0215b1919..35cdb17074a3 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -2,93 +2,103 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do use BlockScoutWeb.ConnCase alias Explorer.Chain.{Address, InternalTransaction} - - # todo: enable when /internal-transactions API endpoint will be enabled - # describe "/internal-transactions" do - # test "empty list", %{conn: conn} do - # request = get(conn, "/api/v2/internal-transactions") - - # assert response = json_response(request, 200) - # assert response["items"] == [] - # assert response["next_page_params"] == nil - # end - - # test "non empty list", %{conn: conn} do - # tx = - # :transaction - # |> insert() - # |> with_block() - - # insert(:internal_transaction, - # transaction: tx, - # block_hash: tx.block_hash, - # index: 0, - # block_index: 0 - # ) - - # request = get(conn, "/api/v2/internal-transactions") - - # assert response = json_response(request, 200) - # assert Enum.count(response["items"]) == 1 - # assert response["next_page_params"] == nil - # end - - # test "internal transactions with next_page_params", %{conn: conn} do - # transaction = insert(:transaction) |> with_block() - - # internal_transaction = - # insert(:internal_transaction, - # transaction: transaction, - # transaction_index: 0, - # block_number: transaction.block_number, - # block_hash: transaction.block_hash, - # index: 0, - # block_index: 0 - # ) - - # transaction_2 = insert(:transaction) |> with_block() - - # internal_transactions = - # for i <- 0..49 do - # insert(:internal_transaction, - # transaction: transaction_2, - # transaction_index: 0, - # block_number: transaction_2.block_number, - # block_hash: transaction_2.block_hash, - # index: i, - # block_index: i - # ) - # end - - # internal_transactions = [internal_transaction | internal_transactions] - - # request = get(conn, "/api/v2/internal-transactions") - # assert response = json_response(request, 200) - - # request_2nd_page = get(conn, "/api/v2/internal-transactions", response["next_page_params"]) - # assert response_2nd_page = json_response(request_2nd_page, 200) - - # check_paginated_response(response, response_2nd_page, internal_transactions) - # end - # end - - # defp compare_item(%InternalTransaction{} = internal_transaction, json) do - # assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] - # assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] - # assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] - # assert internal_transaction.block_number == json["block_number"] - # assert internal_transaction.block_index == json["block_index"] - # end - - # defp check_paginated_response(first_page_resp, second_page_resp, internal_transactions) do - # assert Enum.count(first_page_resp["items"]) == 50 - # assert first_page_resp["next_page_params"] != nil - # compare_item(Enum.at(internal_transactions, 50), Enum.at(first_page_resp["items"], 0)) - - # compare_item(Enum.at(internal_transactions, 1), Enum.at(first_page_resp["items"], 49)) - - # assert Enum.count(second_page_resp["items"]) == 1 - # assert second_page_resp["next_page_params"] == nil - # compare_item(Enum.at(internal_transactions, 0), Enum.at(second_page_resp["items"], 0)) - # end + alias Explorer.Chain.Cache.BackgroundMigrations + + describe "/internal-transactions" do + setup do + BackgroundMigrations.set_heavy_indexes_create_internal_transactions_block_number_desc_transaction_index_desc_index_desc_index_finished( + true + ) + + :ok + end + + test "empty list", %{conn: conn} do + request = get(conn, "/api/v2/internal-transactions") + + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "non empty list", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: 0, + block_index: 0 + ) + + request = get(conn, "/api/v2/internal-transactions") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + end + + test "internal transactions with next_page_params", %{conn: conn} do + transaction = insert(:transaction) |> with_block() + + internal_transaction = + insert(:internal_transaction, + transaction: transaction, + transaction_index: 0, + block_number: transaction.block_number, + block_hash: transaction.block_hash, + index: 0, + block_index: 0 + ) + + transaction_2 = insert(:transaction) |> with_block() + + internal_transactions = + for i <- 0..49 do + insert(:internal_transaction, + transaction: transaction_2, + transaction_index: 0, + block_number: transaction_2.block_number, + block_hash: transaction_2.block_hash, + index: i, + block_index: i + ) + end + + internal_transactions = [internal_transaction | internal_transactions] + + request = get(conn, "/api/v2/internal-transactions") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/internal-transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, internal_transactions) + end + end + + defp compare_item(%InternalTransaction{} = internal_transaction, json) do + assert Address.checksum(internal_transaction.from_address_hash) == json["from"]["hash"] + assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] + assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] + assert internal_transaction.block_number == json["block_number"] + assert internal_transaction.block_index == json["block_index"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, internal_transactions) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(internal_transactions, 50), Enum.at(first_page_resp["items"], 0)) + + compare_item(Enum.at(internal_transactions, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(internal_transactions, 0), Enum.at(second_page_resp["items"], 0)) + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs index e21d97f2c70e..199114df18eb 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/smart_contract_controller_test.exs @@ -53,7 +53,15 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do test "get 422 on invalid address", %{conn: conn} do request = get(conn, "/api/v2/smart-contracts/0x") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/", + "source" => %{"pointer" => "/address_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "get unverified smart-contract info", %{conn: conn} do @@ -770,7 +778,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do old_chain_id = Application.get_env(:block_scout_web, :chain_id) - {:ok, pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) + {:ok, _pid} = Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand.start_link([]) bypass = Bypass.open() Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) @@ -1778,24 +1786,30 @@ defmodule BlockScoutWeb.API.V2.SmartContractControllerTest do check_paginated_response(response, response_2nd_page, smart_contracts) end - test "ignores wrong ordering params", %{conn: conn} do - smart_contracts = - for _ <- 0..50 do - insert(:smart_contract) - end - |> Enum.sort_by(& &1.id) + test "Returns error with wrong ordering params", %{conn: conn} do + for _ <- 0..50 do + insert(:smart_contract) + end + |> Enum.sort_by(& &1.id) ordering_params = %{"sort" => "foo", "order" => "bar"} request = get(conn, "/api/v2/smart-contracts", ordering_params) - assert response = json_response(request, 200) - request_2nd_page = - get(conn, "/api/v2/smart-contracts", ordering_params |> Map.merge(response["next_page_params"])) - - assert response_2nd_page = json_response(request_2nd_page, 200) - - check_paginated_response(response, response_2nd_page, smart_contracts) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid value for enum", + "source" => %{"pointer" => "/sort"}, + "title" => "Invalid value" + }, + %{ + "detail" => "Invalid value for enum", + "source" => %{"pointer" => "/order"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "can order by balance ascending", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs index cc0f6ac215a3..ea2d2fe0314a 100644 --- a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs +++ b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs @@ -36,15 +36,13 @@ defmodule BlockScoutWeb.Routers.ChainTypeScopeTest do %{conn: conn} do Application.put_env(:explorer, :chain_type, :blackfort) - assert response = - conn - |> get("/api/v2/validators/blackfort/counters") - |> json_response(200) - - assert response = - conn - |> get("/api/v2/validators/stability/counters") - |> json_response(404) + assert conn + |> get("/api/v2/validators/blackfort/counters") + |> json_response(200) + + assert conn + |> get("/api/v2/validators/stability/counters") + |> json_response(404) end end end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index a542b1eb461c..5142c77e4a55 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -131,8 +131,8 @@ defmodule Explorer.Chain.Search do %{items: metadata_tags, next_page_params: metadata_next_page_params} = maybe_fetch_metadata_tags( query_string, - parse_possible_nil(paging_options.key["metadata_tag"]["metadata_next_page_params"]), - ExplorerHelper.parse_boolean(paging_options.key["metadata_tag"]["end_of_tags"]) + parse_possible_nil(paging_options.key[:metadata_tag]["metadata_next_page_params"]), + ExplorerHelper.parse_boolean(paging_options.key[:metadata_tag]["end_of_tags"]) ) paginated_metadata_tags = page_metadata_tags(metadata_tags, paging_options) @@ -250,7 +250,7 @@ defmodule Explorer.Chain.Search do end defp page_metadata_tags(tags, paging_options) do - case (paging_options.key || %{})["metadata_tag"] do + case (paging_options.key || %{})[:metadata_tag] do %{"addresses_index" => addresses_index} -> Enum.drop(tags, ExplorerHelper.parse_integer(addresses_index)) @@ -798,7 +798,7 @@ defmodule Explorer.Chain.Search do query, %PagingOptions{ key: %{ - "label" => %{ + label: %{ "name" => name, "inserted_at" => inserted_at } @@ -825,7 +825,7 @@ defmodule Explorer.Chain.Search do query, %PagingOptions{ key: %{ - "contract" => %{ + contract: %{ "certified" => certified, "name" => name, "inserted_at" => inserted_at @@ -858,7 +858,7 @@ defmodule Explorer.Chain.Search do query, %PagingOptions{ key: %{ - "token" => %{ + token: %{ "circulating_market_cap" => circulating_market_cap, "fiat_value" => fiat_value, "is_verified_via_admin_panel" => is_verified_via_admin_panel, @@ -977,8 +977,8 @@ defmodule Explorer.Chain.Search do defp search_tac_operations(search_query, paging_options) do case paging_options do - %PagingOptions{key: %{"tac_operation" => nil}} -> {:ok, %{items: [], next_page_params: nil}} - %PagingOptions{key: %{"tac_operation" => page_params}} -> do_search_tac_operations(search_query, page_params) + %PagingOptions{key: %{tac_operation: nil}} -> {:ok, %{items: [], next_page_params: nil}} + %PagingOptions{key: %{tac_operation: page_params}} -> do_search_tac_operations(search_query, page_params) _ -> do_search_tac_operations(search_query, nil) end end @@ -1121,7 +1121,18 @@ defmodule Explorer.Chain.Search do end end - @paginated_types ["label", "contract", "token", "metadata_tag", "tac_operation"] + @paginated_types [ + "label", + "contract", + "token", + "metadata_tag", + "tac_operation", + :label, + :contract, + :token, + :metadata_tag, + :tac_operation + ] defp trim_list_and_prepare_next_page_params( items, @@ -1141,7 +1152,7 @@ defmodule Explorer.Chain.Search do base_params = Map.merge( - %{"next_page_params_type" => "search", "q" => query}, + %{next_page_params_type: "search", q: query}, prev_options ) @@ -1191,8 +1202,8 @@ defmodule Explorer.Chain.Search do inserted_at_datetime = DateTime.to_iso8601(inserted_at) %{ - "name" => name, - "inserted_at" => inserted_at_datetime + name: name, + inserted_at: inserted_at_datetime } end @@ -1211,12 +1222,12 @@ defmodule Explorer.Chain.Search do inserted_at_datetime = DateTime.to_iso8601(inserted_at) %{ - "circulating_market_cap" => circulating_market_cap, - "fiat_value" => exchange_rate, - "is_verified_via_admin_panel" => is_verified_via_admin_panel, - "holder_count" => holders_count, - "name" => name, - "inserted_at" => inserted_at_datetime + circulating_market_cap: circulating_market_cap, + fiat_value: exchange_rate, + is_verified_via_admin_panel: is_verified_via_admin_panel, + holder_count: holders_count, + name: name, + inserted_at: inserted_at_datetime } end @@ -1232,9 +1243,9 @@ defmodule Explorer.Chain.Search do inserted_at_datetime = DateTime.to_iso8601(inserted_at) %{ - "certified" => certified, - "name" => name, - "inserted_at" => inserted_at_datetime + certified: certified, + name: name, + inserted_at: inserted_at_datetime } end @@ -1257,14 +1268,14 @@ defmodule Explorer.Chain.Search do ) %{ - "address_hash" => address_hash, - "metadata_next_page_params" => + address_hash: address_hash, + metadata_next_page_params: if(metadata_tag?(first_element_of_the_next_page), do: metadata_tag_to_paging_params(first_element_of_the_next_page), else: metadata_next_page_params ), - "end_of_tags" => is_nil(next_page_params), - "addresses_index" => + end_of_tags: is_nil(next_page_params), + addresses_index: if(metadata_tag?(first_element_of_the_next_page) && first_element_of_the_next_page[:metadata]["slug"] == slug, do: addresses_index + 1, else: 0 @@ -1276,11 +1287,11 @@ defmodule Explorer.Chain.Search do %{type: "tac_operation", tac_operation: %{"timestamp" => timestamp}}, _ ) do - %{"page_token" => timestamp |> DateTime.from_iso8601() |> elem(1) |> DateTime.to_unix()} + %{page_token: timestamp |> DateTime.from_iso8601() |> elem(1) |> DateTime.to_unix()} end defp metadata_tag_to_paging_params(%{metadata: metadata}) do - %{"page_token" => "#{metadata["ordinal"]},#{metadata["slug"]},#{metadata["tagType"]}", "page_size" => 50} + %{page_token: "#{metadata["ordinal"]},#{metadata["slug"]},#{metadata["tagType"]}", page_size: 50} end defp metadata_tag?(%{type: "metadata_tag"}), do: true @@ -1298,7 +1309,7 @@ defmodule Explorer.Chain.Search do A keyword list with paging options, where key is the map with the parsed paging options. """ @spec parse_paging_options(map()) :: [paging_options: PagingOptions.t()] - def parse_paging_options(%{"next_page_params_type" => "search"} = paging_params) do + def parse_paging_options(%{next_page_params_type: "search"} = paging_params) do key = Enum.reduce(@paginated_types, %{}, fn type, acc -> if Map.has_key?(paging_params, type) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 2aee6a0f54c6..dc0195912741 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -211,8 +211,11 @@ defmodule Explorer.Chain.SmartContract do end) @languages_enum @default_languages ++ @chain_type_languages - @language_string_to_atom @languages_enum - |> Enum.map(&elem(&1, 0)) + @language_atoms @languages_enum + |> Enum.map(&elem(&1, 0)) + @language_strings @language_atoms + |> Enum.map(&to_string(&1)) + @language_string_to_atom @language_atoms |> Map.new(&{to_string(&1), &1}) @type base_language :: :solidity | :vyper | :yul | :geas @@ -229,7 +232,15 @@ defmodule Explorer.Chain.SmartContract do end @doc """ - Returns list of languages supported by the database schema. + Returns list of languages (as strings) supported by the database schema. + """ + @spec language_strings() :: [String.t()] + def language_strings do + @language_strings + end + + @doc """ + Returns list of languages as map(string to atom) supported by the database schema. """ @spec language_string_to_atom() :: %{String.t() => atom()} def language_string_to_atom do @@ -237,7 +248,7 @@ defmodule Explorer.Chain.SmartContract do end @doc """ - Returns burn address hash + Returns burn address hash """ @spec burn_address_hash_string() :: EthereumJSONRPC.address() def burn_address_hash_string do @@ -245,7 +256,7 @@ defmodule Explorer.Chain.SmartContract do end @doc """ - Returns dead address hash + Returns dead address hash """ @spec dead_address_hash_string() :: EthereumJSONRPC.address() def dead_address_hash_string do From f5ff3f472334234ac856c10f86ab140c3b81734c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 26 Nov 2025 19:30:18 +0300 Subject: [PATCH 0498/1006] fix: Fix filecoin web tests (#13634) --- .../lib/block_scout_web/schemas/api/v2/address.ex | 6 ++++++ .../block_scout_web/schemas/api/v2/address/response.ex | 10 ++-------- .../implementation/chain_type_customizations.ex | 9 ++------- .../block_scout_web/schemas/api/v2/smart_contract.ex | 10 ++++++++++ .../lib/block_scout_web/schemas/api/v2/token.ex | 9 ++------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex index dcc50d4ac6d4..5056915b265b 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address.ex @@ -14,6 +14,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations do nullable: true } + @doc """ + OpenAPI schema for Filecoin robust address. + """ + @spec filecoin_robust_address_schema() :: Schema.t() + def filecoin_robust_address_schema, do: @filecoin_robust_address_schema + def chain_type_fields(schema) do case Application.get_env(:explorer, :chain_type) do :filecoin -> diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex index 7ea622783edb..fd3a147d0dc4 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/address/response.ex @@ -1,19 +1,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations do @moduledoc false require OpenApiSpex + import BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations, only: [filecoin_robust_address_schema: 0] alias BlockScoutWeb.Schemas.{API.V2.Address, Helper} alias OpenApiSpex.Schema use Utils.RuntimeEnvHelper, chain_identity: [:explorer, :chain_identity] - @filecoin_robust_address_schema %Schema{ - type: :string, - description: "Robust f0/f1/f2/f3/f4 Filecoin address", - example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", - nullable: true - } - @zilliqa_schema %Schema{ type: :object, properties: %{ @@ -84,7 +78,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Address.Response.ChainTypeCustomizations {:filecoin, nil} -> schema |> Helper.extend_schema( - properties: %{creator_filecoin_robust_address: @filecoin_robust_address_schema}, + properties: %{creator_filecoin_robust_address: filecoin_robust_address_schema()}, required: [:creator_filecoin_robust_address] ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex index 8e0db25315d6..0d81dbda73af 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/implementation/chain_type_customizations.ex @@ -1,6 +1,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.General.Implementation.ChainTypeCustomizations do @moduledoc false - alias OpenApiSpex.Schema + import BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations, only: [filecoin_robust_address_schema: 0] @doc """ Applies chain-specific field customizations to the given schema based on the configured chain type. @@ -14,18 +14,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General.Implementation.ChainTypeCustomiza @spec chain_type_fields(map()) :: map() def chain_type_fields(schema) do alias BlockScoutWeb.Schemas.Helper - alias OpenApiSpex.Schema case Application.get_env(:explorer, :chain_type) do :filecoin -> schema |> Helper.extend_schema( properties: %{ - filecoin_robust_address: %Schema{ - type: :string, - example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", - nullable: true - } + filecoin_robust_address: filecoin_robust_address_schema() }, required: [:filecoin_robust_address] ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex index b5008603e631..8b4fd059fc1a 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/smart_contract.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.ChainTypeCustomizations do @moduledoc false + import BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations, only: [filecoin_robust_address_schema: 0] alias BlockScoutWeb.Schemas.Helper alias OpenApiSpex.Schema @@ -35,6 +36,15 @@ defmodule BlockScoutWeb.Schemas.API.V2.SmartContract.ChainTypeCustomizations do required: [] ) + :filecoin -> + schema + |> Helper.extend_schema( + properties: %{ + verified_twin_filecoin_robust_address: filecoin_robust_address_schema() + }, + required: [] + ) + _ -> schema end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index 04fb6a3a0c80..accf82d4ef2c 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -1,24 +1,19 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.ChainTypeCustomizations do @moduledoc false require OpenApiSpex + import BlockScoutWeb.Schemas.API.V2.Address.ChainTypeCustomizations, only: [filecoin_robust_address_schema: 0] alias BlockScoutWeb.Schemas.API.V2.General alias BlockScoutWeb.Schemas.Helper alias Explorer.Chain.BridgedToken alias OpenApiSpex.Schema - @filecoin_robust_address_schema %Schema{ - type: :string, - example: "f25nml2cfbljvn4goqtclhifepvfnicv6g7mfmmvq", - nullable: true - } - def chain_type_fields(schema) do case Application.get_env(:explorer, :chain_type) do :filecoin -> schema |> Helper.extend_schema( - properties: %{filecoin_robust_address: @filecoin_robust_address_schema}, + properties: %{filecoin_robust_address: filecoin_robust_address_schema()}, required: [:filecoin_robust_address] ) From 8cab8a145431270c74aabc5a8db43941fbc07fd8 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 27 Nov 2025 14:21:51 +0400 Subject: [PATCH 0499/1006] chore: Update devcontainer image to use Elixir 1.19 (#13637) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- .devcontainer/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 447440aa245a..eff1dbfb125d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # Since this is a copy of https://github.com/blockscout/devcontainer-elixir/blob/main/Dockerfile # So after successful testing this file, the original one must be updated as well. -ARG VARIANT="1.17.3-erlang-27.1.3-debian-bookworm-20250811" +ARG VARIANT="1.19.3-erlang-27.3.4.5-debian-bookworm-20251117" FROM hexpm/elixir:${VARIANT} # ARGs declared before FROM are not persisted beyond the FROM instruction. diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7d20dfdabd33..646609a86a4a 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: elixir: - image: ghcr.io/blockscout/devcontainer-elixir:1.17.3-erlang-27.1.3 + image: ghcr.io/blockscout/devcontainer-elixir:1.19.3-erlang-27.3.4.5 # Uncomment next lines to use test Dockerfile with new Elixir version # build: From 87305ab13ce40a73cd05f9d86d4000ec9281d1f0 Mon Sep 17 00:00:00 2001 From: Fedor Ivanov Date: Thu, 27 Nov 2025 16:04:20 +0300 Subject: [PATCH 0500/1006] fix: celo aggregated election rewards migrator test (#13639) --- .../migrator/celo_aggregated_election_rewards_test.exs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs b/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs index 0ed08b45462d..74a647de848c 100644 --- a/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs +++ b/apps/explorer/test/explorer/migrator/celo_aggregated_election_rewards_test.exs @@ -340,10 +340,13 @@ defmodule Explorer.Migrator.CeloAggregatedElectionRewardsTest do ) # Run migration first time - start_supervised!(CeloAggregatedElectionRewards) + {:ok, pid} = CeloAggregatedElectionRewards.start_link([]) Process.sleep(500) + # Verify process has stopped after migration completed + refute Process.alive?(pid) + # Verify data was created (only 1 type has data, so only 1 aggregate) assert Repo.aggregate(AggregatedElectionReward, :count) == 1 @@ -355,10 +358,13 @@ defmodule Explorer.Migrator.CeloAggregatedElectionRewardsTest do MigrationStatus.set_status("celo_aggregated_election_rewards", nil) # Run migration second time - start_supervised!(CeloAggregatedElectionRewards) + {:ok, pid} = CeloAggregatedElectionRewards.start_link([]) Process.sleep(500) + # Verify second process has also stopped + refute Process.alive?(pid) + # Verify data was not duplicated assert Repo.aggregate(AggregatedElectionReward, :count) == 1 From c81871ade7f4d93df7cf7bfd18a71e75497b0e20 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 28 Nov 2025 08:30:55 +0300 Subject: [PATCH 0501/1006] fix: `Mix.env()` in `runtime.exs` (#13641) --- config/runtime.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/runtime.exs b/config/runtime.exs index 57d09b07f080..54eeac3b9404 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -9,7 +9,7 @@ config :logger, config :logger, :default_handler, formatter: - (if Mix.env() == :prod do + (if config_env() == :prod do LoggerJSON.Formatters.Basic.new(metadata: ConfigHelper.logger_backend_metadata()) else Logger.Formatter.new() From 4102f9ba74ed4269bd4696da41d708c9efd5c3d7 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 28 Nov 2025 12:27:42 +0300 Subject: [PATCH 0502/1006] chore: Elixir 1.19.3 -> 1.19.4 (#13643) --- .github/workflows/config.yml | 2 +- .tool-versions | 4 ++-- docker/Dockerfile | 4 ++-- docker/oldUI.Dockerfile | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 2447c266ce97..cdd53359e2e1 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -32,7 +32,7 @@ on: env: MIX_ENV: test OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ github.ref_name == '13566/merge' && '1.19.3' || vars.ELIXIR_VERSION }} + ELIXIR_VERSION: ${{ github.ref_name == '13566/merge' && '1.19.4' || vars.ELIXIR_VERSION }} ACCOUNT_AUTH0_DOMAIN: "blockscoutcom.us.auth0.com" jobs: diff --git a/.tool-versions b/.tool-versions index 043e0817eb10..45ce6d515c65 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ -elixir 1.19.3-otp-27 -erlang 27.3.4.5 +elixir 1.19.4-otp-27 +erlang 27.3.4.6 nodejs 20.17.0 diff --git a/docker/Dockerfile b/docker/Dockerfile index bf3b5a77a673..cfb6f78a4d33 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 AS builder-deps +FROM hexpm/elixir:1.19.4-erlang-27.3.4.6-alpine-3.22.2 AS builder-deps WORKDIR /app @@ -49,7 +49,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 +FROM hexpm/elixir:1.19.4-erlang-27.3.4.6-alpine-3.22.2 WORKDIR /app diff --git a/docker/oldUI.Dockerfile b/docker/oldUI.Dockerfile index 9cb00f832ad7..661bb04827aa 100644 --- a/docker/oldUI.Dockerfile +++ b/docker/oldUI.Dockerfile @@ -1,4 +1,4 @@ -FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 AS builder-deps +FROM hexpm/elixir:1.19.4-erlang-27.3.4.6-alpine-3.22.2 AS builder-deps WORKDIR /app @@ -65,7 +65,7 @@ RUN mkdir -p /opt/release && \ mv _build/${MIX_ENV}/rel/blockscout /opt/release ############################################################## -FROM hexpm/elixir:1.19.3-erlang-27.2.1-alpine-3.21.5 +FROM hexpm/elixir:1.19.4-erlang-27.3.4.6-alpine-3.22.2 WORKDIR /app From c59707d537ebba5b270d1e039dc4cea6f4aa5dec Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Fri, 28 Nov 2025 15:04:39 +0400 Subject: [PATCH 0503/1006] chore: Update devcontainer image to use Elixir 1.19.4 (#13645) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- .devcontainer/docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index eff1dbfb125d..324a6d2eb818 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # Since this is a copy of https://github.com/blockscout/devcontainer-elixir/blob/main/Dockerfile # So after successful testing this file, the original one must be updated as well. -ARG VARIANT="1.19.3-erlang-27.3.4.5-debian-bookworm-20251117" +ARG VARIANT="1.19.4-erlang-27.3.4.6-debian-bookworm-20251117" FROM hexpm/elixir:${VARIANT} # ARGs declared before FROM are not persisted beyond the FROM instruction. diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 646609a86a4a..b0126db4cc9d 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,6 +1,6 @@ services: elixir: - image: ghcr.io/blockscout/devcontainer-elixir:1.19.3-erlang-27.3.4.5 + image: ghcr.io/blockscout/devcontainer-elixir:1.19.4-erlang-27.3.4.6 # Uncomment next lines to use test Dockerfile with new Elixir version # build: From c67a2673381843a634db21a07320ff8a2a0cca86 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 28 Nov 2025 15:47:58 +0300 Subject: [PATCH 0504/1006] doc: update API endpoints descriptions in OpenAPI (#13647) --- .../controllers/api/v2/address_controller.ex | 2 +- .../controllers/api/v2/block_controller.ex | 21 +++--- .../controllers/api/v2/config_controller.ex | 11 +-- .../api/v2/internal_transaction_controller.ex | 7 +- .../api/v2/main_page_controller.ex | 18 ++--- .../controllers/api/v2/search_controller.ex | 18 +++-- .../api/v2/smart_contract_controller.ex | 25 ++++--- .../controllers/api/v2/stats_controller.ex | 28 ++++---- .../controllers/api/v2/token_controller.ex | 64 +++++++++-------- .../api/v2/token_transfer_controller.ex | 6 +- .../api/v2/transaction_controller.ex | 72 ++++++++++--------- .../api/v2/withdrawal_controller.ex | 9 +-- .../block_scout_web/schemas/api/v2/general.ex | 24 +++---- cspell.json | 1 + 14 files changed, 164 insertions(+), 142 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index e78a7dca05dd..925f6952b652 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -237,7 +237,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "Retrieves count statistics for an address, including transactions, token transfers, gas usage, and validations.", parameters: [address_hash_param() | base_params()], responses: [ - ok: {"Count statistics for the specified address", "application/json", Schemas.Address.Counters}, + ok: {"Count statistics for the specified address.", "application/json", Schemas.Address.Counters}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index fd0861b239b3..6dc2d5fd5bdd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -145,7 +145,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do tags(["blocks"]) operation :block, - summary: "Retrieve detailed information about a specific block", + summary: "Retrieves detailed information for a specific block identified by its number or hash.", description: "Retrieves detailed information for a specific block, including transactions, internal transactions, and metadata.", parameters: [block_hash_or_number_param() | base_params()], @@ -185,15 +185,15 @@ defmodule BlockScoutWeb.API.V2.BlockController do end operation :blocks, - summary: "List blocks with optional filtering and sorting", - description: "Retrieves a paginated list of blocks with optional filtering by block type and sorting options.", + summary: "List blocks with optional filtering by block type", + description: "Retrieves a paginated list of blocks with optional filtering by block type.", parameters: base_params() ++ [block_type_param()] ++ define_paging_params(["block_number", "items_count"]), responses: [ ok: - {"List of blocks with pagination.", "application/json", + {"List of blocks with pagination information.", "application/json", paginated_response( items: Schemas.Block, next_page_params_example: %{ @@ -376,15 +376,15 @@ defmodule BlockScoutWeb.API.V2.BlockController do end operation :transactions, - summary: "List transactions in a specific block", - description: "Retrieves transactions included in a specific block with optional filtering and sorting.", + summary: "List transactions and tx details included in a specific block", + description: "Retrieves transactions included in a specific block, ordered by transaction index.", parameters: base_params() ++ [block_hash_or_number_param(), transaction_type_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"Transactions in the specified block.", "application/json", + {"Transactions in the specified block, with pagination.", "application/json", paginated_response( items: Schemas.Transaction, next_page_params_example: %{ @@ -499,15 +499,16 @@ defmodule BlockScoutWeb.API.V2.BlockController do end operation :withdrawals, - summary: "List withdrawals in a specific block", - description: "Retrieves validator withdrawals included in a specific block.", + summary: "List validator withdrawals including amounts, index and receiver details processed in a specific block", + description: "Retrieves withdrawals processed in a specific block (typically for proof-of-stake networks).", parameters: base_params() ++ [block_hash_or_number_param()] ++ define_paging_params(["index", "items_count"]), responses: [ ok: - {"Withdrawals in the specified block.", "application/json", + {"Withdrawals in the specified block, with pagination. Note that block_number and timestamp fields are not included in this endpoint.", + "application/json", paginated_response( items: Schemas.Withdrawal, next_page_params_example: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index f86f428baccc..187816378e74 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -15,7 +15,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do parameters: base_params(), responses: [ ok: - {"Backend version", "application/json", + {"Backend version.", "application/json", %Schema{type: :object, properties: %{backend_version: %Schema{type: :string, nullable: true}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -34,7 +34,8 @@ defmodule BlockScoutWeb.API.V2.ConfigController do parameters: base_params(), responses: [ ok: - {"CSV export limits", "application/json", %Schema{type: :object, properties: %{limit: %Schema{type: :integer}}}}, + {"CSV export limits.", "application/json", + %Schema{type: :object, properties: %{limit: %Schema{type: :integer}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -52,7 +53,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do parameters: base_params(), responses: [ ok: - {"Public metrics config", "application/json", + {"Public metrics config.", "application/json", %Schema{type: :object, properties: %{update_period_hours: %Schema{type: :integer}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -72,7 +73,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do parameters: base_params(), responses: [ ok: - {"Celo config", "application/json", + {"Celo config.", "application/json", %Schema{type: :object, properties: %{l2_migration_block: %Schema{type: :integer, nullable: true}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -91,7 +92,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do parameters: base_params(), responses: [ ok: - {"Smart contract languages", "application/json", + {"Smart contract languages.", "application/json", %OpenApiSpex.Schema{ type: :object, properties: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 99a92e7b94c6..6864942cf0d9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -25,15 +25,16 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do @api_true [api?: true] operation :internal_transactions, - summary: "List internal transactions", - description: "Retrieves internal transactions with optional filtering and pagination.", + summary: "List internal transactions generated during smart contract execution", + description: + "Retrieves a paginated list of internal transactions. Internal transactions are generated during contract execution and not directly recorded on the blockchain.", parameters: base_params() ++ [query_transaction_hash_param(), limit_param()] ++ define_paging_params(["index", "block_number", "transaction_index", "items_count"]), responses: [ ok: - {"Internal transactions", "application/json", + {"List of internal transactions with pagination information.", "application/json", paginated_response( items: Schemas.InternalTransaction, next_page_params_example: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index e37b0a2a0065..51a32ec2d2e1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -44,12 +44,12 @@ defmodule BlockScoutWeb.API.V2.MainPageController do tags(["main_page"]) operation :blocks, - summary: "Last 4 blocks on the main page", - description: "Retrieves last 4 blocks list.", + summary: "Retrieve recent blocks as displayed on Blockscout homepage", + description: "Retrieves a limited set of recent blocks for display on the main page or dashboard.", parameters: base_params(), responses: [ ok: - {"Blocks List on the main page.", "application/json", + {"List of recent blocks on the home page.", "application/json", %Schema{ type: :array, items: Schemas.Block.Response, @@ -79,12 +79,12 @@ defmodule BlockScoutWeb.API.V2.MainPageController do end operation :transactions, - summary: "Last 6 transactions on the main page", - description: "Retrieves last 6 transactions list.", + summary: "Retrieve recent transactions as displayed on Blockscout homepage", + description: "Retrieves a limited set of recent transactions displayed on the home page.", parameters: base_params(), responses: [ ok: - {"Transactions list on the main page.", "application/json", + {"List of recent transactions on the home page.", "application/json", %Schema{ type: :array, items: Schemas.Transaction.Response, @@ -142,12 +142,12 @@ defmodule BlockScoutWeb.API.V2.MainPageController do end operation :indexing_status, - summary: "Get indexing status", - description: "Returns current indexing progress and ratios for blocks and internal transactions.", + summary: "Check if indexing is finished with indexing ratio", + description: "Retrieves the current status of blockchain data indexing by the BlockScout instance.", parameters: base_params(), responses: [ ok: - {"Indexing status", "application/json", + {"Current blockchain indexing status.", "application/json", %Schema{ type: :object, properties: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex index 06c6b93dd73e..5f0512be2bfe 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/search_controller.ex @@ -17,8 +17,9 @@ defmodule BlockScoutWeb.API.V2.SearchController do @min_query_length 3 operation :search, - summary: "Search across blocks, transactions and addresses", - description: "Performs a joint search for blocks, transactions, addresses and other resources.", + summary: "Search for tokens, addresses, contracts, blocks, or transactions by identifier", + description: + "Performs a unified search across multiple blockchain entity types including tokens, addresses, contracts, blocks, transactions and other resources.", parameters: [q_param() | base_params()] ++ define_search_paging_params([ @@ -35,7 +36,10 @@ defmodule BlockScoutWeb.API.V2.SearchController do "ens_domain" ]), responses: [ - ok: {"Search results", "application/json", Schemas.Search.Results}, + ok: + {"Successful search response containing matched items and pagination information. + Results are ordered by relevance and limited to 50 items per page.", "application/json", + Schemas.Search.Results}, unprocessable_entity: JsonErrorResponse.response() ] @@ -59,12 +63,12 @@ defmodule BlockScoutWeb.API.V2.SearchController do end operation :check_redirect, - summary: "Check redirect target for a query", - description: "Check whether the query resolves to a single redirect target (eg. an address or tx) and return it.", + summary: "Check if search query should redirect to a specific entity page", + description: "Checks if a search query redirects to a specific entity page rather than showing search results.", parameters: [q_param() | base_params()], responses: [ ok: - {"Redirect check result", "application/json", + {"Response indicating whether the query should redirect to a specific entity page.", "application/json", %Schema{ type: :object, properties: %{ @@ -101,7 +105,7 @@ defmodule BlockScoutWeb.API.V2.SearchController do parameters: [q_param() | base_params()], responses: [ ok: - {"Quick search results", "application/json", + {"Quick search results.", "application/json", %Schema{type: :array, items: %Schema{type: :object}, nullable: false}}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index b419d26e6b36..169dd23ae5bd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -45,11 +45,13 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do tags(["smart_contracts"]) operation :smart_contract, - summary: "Smart contract", - description: "Returns details for a smart contract address.", + summary: "Retrieve detailed information about a verified smart contract", + description: + "Retrieves detailed information about a specific verified smart contract, including source code, ABI, and deployment details.", parameters: [address_hash_param() | base_params()], responses: [ - ok: {"Smart contract", "application/json", Schemas.SmartContract} + ok: + {"Detailed information about the specified verified smart contract.", "application/json", Schemas.SmartContract} ] @doc """ @@ -71,9 +73,9 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end operation :smart_contracts_list, - summary: "Smart contracts list", + summary: "List verified smart contracts with optional filtering options", description: - "Returns a paginated list of verified smart contract addresses. Supports search, filtering and pagination keys.", + "Retrieves a paginated list of verified smart contracts with optional filtering by proxy status or programming language.", parameters: base_params() ++ [ @@ -97,7 +99,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do ]), responses: [ ok: - {"Smart contracts", "application/json", + {"List of verified smart contracts matching the filter criteria, with pagination.", "application/json", paginated_response( items: Schemas.SmartContract, next_page_params_example: %{ @@ -155,11 +157,12 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end operation :smart_contracts_counters, - summary: "Smart contracts counters", - description: "Returns counts for smart contracts and related metrics.", + summary: "Get count statistics (new & newly verified) for deployed smart contracts", + description: + "Retrieves count statistics for smart contracts, including total contracts, verified contracts, and new contracts in the last 24 hours.", parameters: base_params(), responses: [ - ok: {"Smart contracts counters", "application/json", Schemas.SmartContract.Counters}, + ok: {"Count statistics for smart contracts.", "application/json", Schemas.SmartContract.Counters}, unprocessable_entity: JsonErrorResponse.response() ] @@ -183,7 +186,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do parameters: [address_hash_param() | base_params()], responses: [ ok: - {"Audit reports", "application/json", + {"Audit reports.", "application/json", %Schema{ description: "List of smart-contract's audit reports", type: :object, @@ -225,7 +228,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do request_body: audit_report_request_body(), responses: [ ok: - {"OK", "application/json", + {"Audit report submission is successful.", "application/json", %OpenApiSpex.Schema{ type: :object, properties: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index a86da1e4deb2..ff7919421731 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -38,11 +38,12 @@ defmodule BlockScoutWeb.API.V2.StatsController do @api_true [api?: true] operation :stats, - summary: "Get indexing and chain stats", - description: "Returns current indexing progress, chain stats and market data used on the UI.", + summary: "Retrieve blockchain network statistics and metrics", + description: + "Retrieves blockchain network statistics including total blocks, transactions, addresses, average block time, market data, and network utilization.", parameters: base_params(), responses: [ - ok: {"Stats", "application/json", Schemas.Stats.Response}, + ok: {"Blockchain network statistics.", "application/json", Schemas.Stats.Response}, unprocessable_entity: JsonErrorResponse.response() ] @@ -132,12 +133,12 @@ defmodule BlockScoutWeb.API.V2.StatsController do end operation :transactions_chart, - summary: "Transactions chart data", - description: "Returns transaction counts by date for charting.", + summary: "Get daily transaction counts", + description: "Retrieves time series data of daily transaction counts for rendering charts.", parameters: base_params(), responses: [ ok: - {"Transactions chart data", "application/json", + {"Time series data for transaction count charts.", "application/json", %Schema{type: :object, properties: %{chart_data: %Schema{type: :array, items: %Schema{type: :object}}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -168,12 +169,13 @@ defmodule BlockScoutWeb.API.V2.StatsController do end operation :market_chart, - summary: "Market history chart data", - description: "Returns market history (price, market cap, tvl) for charting.", + summary: "Get daily closing price and market cap for native coin", + description: + "Retrieves time series data of market information (daily closing price, market cap) for rendering charts.", parameters: base_params(), responses: [ ok: - {"Market chart data", "application/json", + {"Time series data for market charts and available token supply.", "application/json", %Schema{ type: :object, properties: %{ @@ -227,7 +229,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do parameters: base_params(), responses: [ ok: - {"Secondary coin market chart data", "application/json", + {"Secondary coin market chart data.", "application/json", %Schema{type: :object, properties: %{chart_data: %Schema{type: :array, items: %Schema{type: :object}}}}}, unprocessable_entity: JsonErrorResponse.response() ] @@ -249,8 +251,8 @@ defmodule BlockScoutWeb.API.V2.StatsController do end operation :hot_smart_contracts, - summary: "Retrieve hot contracts", - description: "Retrieves hot contracts", + summary: "Retrieve hot smart-contracts", + description: "Retrieves paginated list of hot smart-contracts", parameters: base_params() ++ [sort_param(["transactions_count", "total_gas_used"]), order_param(), hot_smart_contracts_scale_param()] ++ @@ -262,7 +264,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do ]), responses: [ ok: - {"Hot contracts.", "application/json", + {"Paginated list of hot smart-contracts.", "application/json", paginated_response( items: Schemas.Stats.HotContract, next_page_params_example: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 2bdc2842a4e2..f386d05f54a0 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -50,7 +50,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do operation :token, summary: "Retrieve detailed information about a specific token", - description: "Retrieves detailed information for a specific token, including metadata and statistics.", + description: "Retrieves detailed information for a specific token identified by its contract address.", parameters: [address_hash_param() | base_params()], responses: [ ok: {"Detailed information about the specified token.", "application/json", Schemas.Token.Response}, @@ -99,11 +99,11 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :counters, - summary: "Get activity count stats for a specific token", - description: "Retrieves count statistics for a token, including transfers, holders, and other metrics.", + summary: "Get holder and transfer count statistics for a specific token", + description: "Retrieves count statistics for a specific token, including holders count and transfers count.", parameters: [address_hash_param() | base_params()], responses: [ - ok: {"Count statistics for the specified token", "application/json", Schemas.Token.Counters}, + ok: {"Count statistics for the specified token.", "application/json", Schemas.Token.Counters}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] @@ -123,8 +123,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :transfers, - summary: "List token transfers for a specific token", - description: "Retrieves token transfers for a specific token, with optional filtering and pagination.", + summary: "List ownership transfer history for a specific NFT", + description: "Retrieves transfer history for a specific NFT instance, showing ownership changes over time.", parameters: base_params() ++ [address_hash_param()] ++ @@ -138,7 +138,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ]), responses: [ ok: - {"List of token transfers.", "application/json", + {"Transfers of the specified token, with pagination.", "application/json", paginated_response( items: Schemas.TokenTransfer, next_page_params_example: %{ @@ -189,15 +189,16 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :holders, - summary: "List holders for a specific token", - description: "Retrieves holders for a specific token, with optional filtering and pagination.", + summary: "List addresses holding a specific token sorted by balance", + description: + "Retrieves addresses holding a specific token, sorted by balance. Useful for analyzing token distribution.", parameters: base_params() ++ [address_hash_param()] ++ define_paging_params(["address_hash_param", "value", "items_count"]), responses: [ ok: - {"List of token holders.", "application/json", + {"Holders of the specified token, with pagination.", "application/json", paginated_response( items: Schemas.Token.Holder, next_page_params_example: %{ @@ -236,15 +237,16 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :instances, - summary: "List all instances of a specific token", - description: "Retrieves all instances of a specific token, including holders and balances.", + summary: "List individual NFT instances for a token contract", + description: + "Retrieves instances of NFTs for a specific token contract. This endpoint is primarily for ERC-721 and ERC-1155 tokens.", parameters: base_params() ++ [address_hash_param(), holder_address_hash_param()] ++ define_paging_params(["unique_token"]), responses: [ ok: - {"List of token instances.", "application/json", + {"NFT instances for the specified token contract, with pagination.", "application/json", paginated_response( items: Schemas.TokenInstance, next_page_params_example: %{ @@ -334,8 +336,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :instance, - summary: "Get details for a specific token instance", - description: "Retrieves details for a specific token instance by token address and token ID.", + summary: "Retrieve detailed information about a specific NFT", + description: + "Retrieves detailed information about a specific NFT instance, identified by its token contract address and token ID.", parameters: base_params() ++ [ @@ -343,7 +346,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do token_id_param() ], responses: [ - ok: {"Details for the token instance.", "application/json", Schemas.TokenInstance}, + ok: {"Detailed information about the specified NFT instance.", "application/json", Schemas.TokenInstance}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() ] @@ -395,7 +398,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do define_paging_params(["index", "block_number", "token_id"]), responses: [ ok: - {"List of token transfers for the instance.", "application/json", + {"Transfer history for the specified NFT instance, with pagination.", "application/json", paginated_response( items: Schemas.TokenTransfer, next_page_params_example: %{ @@ -447,15 +450,16 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :holders_by_instance, - summary: "List holders for a specific token instance", - description: "Retrieves holders for a specific token instance (by token address and token ID).", + summary: "List current holders of a specific NFT", + description: + "Retrieves current holders of a specific NFT instance. For ERC-721, this will typically be a single address. For ERC-1155, multiple addresses may hold the same token ID.", parameters: base_params() ++ [address_hash_param(), token_id_param()] ++ define_paging_params(["address_hash_param", "items_count", "token_id", "value"]), responses: [ ok: - {"List of token holders for the instance.", "application/json", + {"Current holders of the specified NFT instance, with pagination.", "application/json", paginated_response( items: Schemas.Token.Holder, next_page_params_example: %{ @@ -505,8 +509,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :transfers_count_by_instance, - summary: "Get transfer count for a specific token instance", - description: "Retrieves the number of transfers for a specific token instance (by token address and token ID).", + summary: "Get total number of ownership transfers for a specific NFT", + description: + "Retrieves the total number of transfers for a specific NFT instance. Useful for determining how frequently an NFT has changed hands.", parameters: base_params() ++ [ @@ -515,7 +520,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ], responses: [ ok: - {"Transfer count for the instance.", "application/json", + {"Total number of transfers for the specified NFT instance.", "application/json", %Schema{type: :object, properties: %{transfers_count: %Schema{type: :integer}}}}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -543,8 +548,8 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :tokens_list, - summary: "List tokens with optional filtering and sorting", - description: "Retrieves a paginated list of tokens with optional filtering and sorting.", + summary: "List tokens with optional filtering by name, symbol, or type", + description: "Retrieves a paginated list of tokens with optional filtering by name, symbol, or type.", parameters: base_params() ++ [ @@ -565,7 +570,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ]), responses: [ ok: - {"List of tokens.", "application/json", + {"List of tokens matching the filter criteria, with pagination.", "application/json", paginated_response( items: Schemas.Token, next_page_params_example: %{ @@ -670,8 +675,9 @@ defmodule BlockScoutWeb.API.V2.TokenController do end operation :refetch_metadata, - summary: "Trigger metadata refetch for a specific token instance", - description: "Triggers a metadata refetch for a specific token instance (by token address and token ID).", + summary: "Trigger a refresh of metadata for a specific NFT", + description: + "Triggers a refresh of metadata for a specific NFT instance. Useful when the NFT's metadata has been updated but is not yet reflected in the BlockScout database.", parameters: base_params() ++ [ @@ -681,7 +687,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do ], responses: [ ok: - {"Metadata refetch triggered.", "application/json", + {"Metadata refresh has been successfully initiated.", "application/json", %Schema{type: :object, properties: %{message: %Schema{type: :string}}}}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index d4e30986a23d..e91ed2f9ec1e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -32,8 +32,8 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do @api_true [api?: true] operation :token_transfers, - summary: "List token transfers", - description: "Retrieves token transfers with optional filtering and pagination.", + summary: "List token transfers across all token types (ERC-20, ERC-721, ERC-1155)", + description: "Retrieves a paginated list of token transfers across all token types (ERC-20, ERC-721, ERC-1155).", parameters: base_params() ++ [token_type_param(), limit_param()] ++ @@ -47,7 +47,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do ]), responses: [ ok: - {"Token transfers", "application/json", + {"List of token transfers with pagination information.", "application/json", paginated_response( items: Schemas.TokenTransfer, next_page_params_example: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 9222fb6fdcfa..0ad364358ac9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -139,11 +139,10 @@ defmodule BlockScoutWeb.API.V2.TransactionController do operation :transaction, summary: "Retrieve detailed information about a specific transaction", - description: - "Retrieves detailed information for a specific transaction, including token transfers, internal transactions, and metadata.", + description: "Retrieves detailed information for a specific transaction identified by its hash.", parameters: [transaction_hash_param() | base_params()], responses: [ - ok: {"Transaction details", "application/json", Schemas.Transaction.Response}, + ok: {"Detailed information about the specified transaction.", "application/json", Schemas.Transaction.Response}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() ] @@ -215,15 +214,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :transactions, - summary: "List transactions with optional filtering and sorting", - description: "Retrieves a paginated list of transactions with optional filtering and sorting options.", + summary: "List blockchain transactions with filtering options for status, type, and method", + description: "Retrieves a paginated list of transactions with optional filtering by status, type, and method.", parameters: base_params() ++ [transaction_filter_param()] ++ define_paging_params(["block_number", "index", "items_count", "hash", "inserted_at"]), responses: [ ok: - {"List of transactions", "application/json", + {"List of transactions with pagination information.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -272,7 +271,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do parameters: [batch_number_param() | base_params()], responses: [ ok: - {"Polygon ZkEVM batch transactions", "application/json", + {"Polygon ZkEVM batch transactions.", "application/json", %Schema{ title: "PolygonZkEVMBatchTransactions", type: :object, @@ -317,7 +316,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"ZkSync batch transactions", "application/json", + {"ZkSync batch transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -347,7 +346,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"Arbitrum batch transactions", "application/json", + {"Arbitrum batch transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -376,7 +375,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do parameters: [transaction_hash_param() | base_params()], responses: [ ok: - {"Linked external transactions", "application/json", + {"Linked external transactions.", "application/json", %Schema{type: :array, items: %Schema{type: :string}, nullable: false}}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() @@ -418,7 +417,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"Optimism batch transactions", "application/json", + {"Optimism batch transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -453,7 +452,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [batch_number_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"Scroll batch transactions", "application/json", + {"Scroll batch transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -583,7 +582,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [execution_node_hash_param() | base_params()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"List of transactions", "application/json", + {"List of transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -626,12 +625,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :raw_trace, - summary: "Get raw trace for a transaction", + summary: "Get step-by-step execution trace for a specific transaction", description: - "Retrieves the raw trace for a specific transaction, including internal operations and execution details.", + "Retrieves the raw execution trace for a transaction, showing the step-by-step execution path and all contract interactions.", parameters: [transaction_hash_param() | base_params()], responses: [ - ok: {"Raw trace for transaction", "application/json", Schemas.Transaction.RawTrace}, + ok: {"Raw execution trace for the specified transaction.", "application/json", Schemas.Transaction.RawTrace}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() ] @@ -664,8 +663,9 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :token_transfers, - summary: "List token transfers for a transaction", - description: "Retrieves token transfers for a specific transaction.", + summary: "List token transfers within a specific transaction", + description: + "Retrieves token transfers that occurred within a specific transaction, with optional filtering by token type.", parameters: base_params() ++ [transaction_hash_param(), token_type_param()] ++ @@ -679,7 +679,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ]), responses: [ ok: - {"Token transfers for transaction", "application/json", + {"Token transfers within the specified transaction, with pagination.", "application/json", paginated_response( items: Schemas.TokenTransfer, next_page_params_example: %{ @@ -730,14 +730,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :internal_transactions, - summary: "List internal transactions for a transaction", - description: "Retrieves internal transactions for a specific transaction.", + summary: "List internal transactions triggered during a specific transaction", + description: + "Retrieves internal transactions generated during the execution of a specific transaction. Useful for analyzing contract interactions and debugging failed transactions.", parameters: [transaction_hash_param() | base_params()] ++ define_paging_params(["index", "block_number", "transaction_index", "items_count"]), responses: [ ok: - {"Internal transactions for transaction", "application/json", + {"Internal transactions for the specified transaction, with pagination.", "application/json", paginated_response( items: Schemas.InternalTransaction, next_page_params_example: %{ @@ -782,14 +783,15 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :logs, - summary: "List logs for a transaction", - description: "Retrieves logs for a specific transaction.", + summary: "List event logs emitted during a specific transaction", + description: + "Retrieves event logs emitted during the execution of a specific transaction. Logs contain information about contract events and state changes.", parameters: [transaction_hash_param() | base_params()] ++ define_paging_params(["index", "block_number", "items_count"]), responses: [ ok: - {"Logs for transaction", "application/json", + {"Event logs for the specified transaction, with pagination.", "application/json", paginated_response( items: Schemas.Log, next_page_params_example: %{ @@ -837,13 +839,13 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :state_changes, - summary: "List state changes for a transaction", - description: "Retrieves state changes for a specific transaction.", + summary: "Get on-chain state changes caused by a specific transaction", + description: "Retrieves state changes (balance changes, token transfers) caused by a specific transaction.", parameters: [transaction_hash_param() | base_params()] ++ define_state_changes_paging_params(["state_changes", "items_count"]), responses: [ ok: { - "State changes for transaction", + "State changes caused by the specified transaction, with pagination.", "application/json", paginated_response( items: Schemas.Transaction.StateChange, @@ -891,7 +893,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do parameters: base_params() ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: - {"Watchlist transactions", "application/json", + {"Watchlist transactions.", "application/json", paginated_response( items: Schemas.Transaction.Response, next_page_params_example: %{ @@ -935,12 +937,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do end operation :summary, - summary: "Get interpreted summary for a transaction", - description: "Retrieves interpreted summary for a specific transaction.", + summary: "Get a human-readable, LLM-based transaction summary", + description: "Retrieves a human-readable summary of what a transaction did, presented in natural language.", parameters: base_params() ++ [transaction_hash_param(), just_request_body_param()], responses: [ ok: - {"Transaction summary", "application/json", + {"Human-readable summary of the specified transaction.", "application/json", %Schema{ anyOf: [ Schemas.Transaction.Summary, @@ -1010,7 +1012,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do parameters: [transaction_hash_param() | base_params()], responses: [ ok: - {"Blobs for transaction", "application/json", + {"Blobs for transaction.", "application/json", %Schema{ title: "TransactionBlobs", type: :object, @@ -1046,7 +1048,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do description: "Retrieves statistics for transactions, including counts and fee summaries for the last 24 hours.", responses: [ ok: - {"Transaction statistics", "application/json", + {"Transaction statistics.", "application/json", %Schema{ type: :object, properties: %{ @@ -1095,7 +1097,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do parameters: [transaction_hash_param() | base_params()] ++ define_paging_params(["index", "items_count"]), responses: [ ok: - {"Beacon deposits for transaction", "application/json", + {"Beacon deposits for transaction.", "application/json", paginated_response( items: Schemas.Beacon.Deposit.Response, next_page_params_example: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index 08601469113d..0d36c1d8f10f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -13,14 +13,15 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do tags(["withdrawals"]) operation :withdrawals_list, - summary: "List withdrawals", - description: "Retrieves withdrawals with optional filtering and pagination.", + summary: "List validator withdrawal details on proof-of-stake networks", + description: + "Retrieves a paginated list of withdrawals, typically for proof-of-stake networks supporting validator withdrawals.", parameters: base_params() ++ define_paging_params(["index", "items_count"]), responses: [ ok: - {"Withdrawals", "application/json", + {"List of withdrawals with pagination.", "application/json", paginated_response( items: Schemas.Withdrawal, next_page_params_example: %{ @@ -61,7 +62,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do description: "Returns total withdrawals count and sum from cache.", parameters: base_params(), responses: [ - ok: {"Withdrawals counters", "application/json", Schemas.Withdrawal.Counter}, + ok: {"Withdrawals counters.", "application/json", Schemas.Withdrawal.Counter}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index ab3563711503..72cb1b524b47 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -287,9 +287,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: """ Filter by block type: - * reorg - Only show reorgs - * uncle - Only show uncle blocks - * block - Only show main blocks + * block - Standard blocks in the main chain + * uncle - Uncle/ommer blocks (valid but not in main chain) + * reorg - Blocks from chain reorganizations If omitted, default value "block" is used. """ } @@ -306,9 +306,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do schema: %Schema{type: :string, enum: ["validated", "pending"]}, required: false, description: """ - Filter by transaction type: - * validated - Only show validated transactions - * pending - Only show pending transactions + Filter transactions by status: + * pending - Transactions waiting to be mined/validated + * validated - Confirmed transactions included in blocks If omitted, default value "validated" is used. """ } @@ -390,12 +390,12 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do schema: %Schema{type: :string, enum: @allowed_transaction_types}, required: false, description: """ - Filter transactions by type: - * coin_transfer - Only show coin transfer transactions - * contract_call - Only show contract call transactions - * contract_creation - Only show contract creation transactions - * token_transfer - Only show token transfer transactions - * token_creation - Only show token creation transactions + Filter by transaction type. Comma-separated list of: + * token_transfer - Token transfer transactions + * contract_creation - Contract deployment transactions + * contract_call - Contract method call transactions + * coin_transfer - Native coin transfer transactions + * token_creation - Token creation transactions * blob_transaction - Only show blob transactions (Ethereum only) """ } diff --git a/cspell.json b/cspell.json index f4fd70c2b161..cd167afdada5 100644 --- a/cspell.json +++ b/cspell.json @@ -465,6 +465,7 @@ "nxdomain", "OFAC", "offchain", + "ommer", "omni", "onclick", "onconnect", From 9f6575748d32900b063073be1fb7ea99b631a6d6 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:30:50 +0300 Subject: [PATCH 0505/1006] feat: Add DIA market source (#12678) --- .dialyzer_ignore.exs | 5 +- .../channels/exchange_rate_channel_test.exs | 7 + .../v2/exchange_rate_channel_test.exs | 28 +- .../account/api/v2/user_controller_test.exs | 9 + .../api/v2/address_controller_test.exs | 7 + .../api/v2/search_controller_test.exs | 9 + .../api/v2/stats_controller_test.exs | 4 +- .../api/v2/token_controller_test.exs | 9 + .../benchmarks/explorer/chain/tokens.exs | 64 + apps/explorer/lib/explorer.ex | 18 + apps/explorer/lib/explorer/application.ex | 10 +- .../explorer/chain/import/runner/tokens.ex | 10 +- apps/explorer/lib/explorer/chain/token.ex | 7 +- .../lib/explorer/chain/token/fiat_value.ex | 60 + apps/explorer/lib/explorer/helper.ex | 2 +- .../lib/explorer/market/fetcher/history.ex | 12 +- apps/explorer/lib/explorer/market/market.ex | 105 +- .../lib/explorer/market/market_history.ex | 12 +- apps/explorer/lib/explorer/market/source.ex | 3 +- .../lib/explorer/market/source/dia.ex | 280 + .../lib/explorer/market/source/mobula.ex | 5 +- .../explorer/chain/address/token_test.exs | 9 + .../test/explorer/chain/token_test.exs | 39 + .../explorer/market/market_history_test.exs | 4 +- .../test/explorer/market/market_test.exs | 130 +- .../market/source/crypto_rank_test.exs | 4 +- .../test/explorer/market/source/dia_test.exs | 437 + apps/explorer/test/support/benchmark_case.ex | 54 + .../fixture/exchange_rates/coin_gecko.json | 1806 - .../market/dia/native_coin_history.json | 29490 ++++++++++++++++ .../market/dia/secondary_coin_history.json | 29490 ++++++++++++++++ apps/indexer/lib/indexer/memory/monitor.ex | 2 +- .../nft_media_handler/dispatcher_interface.ex | 28 +- config/config_helper.exs | 2 + config/runtime.exs | 10 +- docker-compose/envs/common-blockscout.env | 4 + 36 files changed, 60299 insertions(+), 1876 deletions(-) create mode 100644 apps/explorer/benchmarks/explorer/chain/tokens.exs create mode 100644 apps/explorer/lib/explorer/chain/token/fiat_value.ex create mode 100644 apps/explorer/lib/explorer/market/source/dia.ex create mode 100644 apps/explorer/test/explorer/market/source/dia_test.exs create mode 100644 apps/explorer/test/support/benchmark_case.ex delete mode 100644 apps/explorer/test/support/fixture/exchange_rates/coin_gecko.json create mode 100644 apps/explorer/test/support/fixture/market/dia/native_coin_history.json create mode 100644 apps/explorer/test/support/fixture/market/dia/secondary_coin_history.json diff --git a/.dialyzer_ignore.exs b/.dialyzer_ignore.exs index 02d124e136c7..a7bbcb4ee44d 100644 --- a/.dialyzer_ignore.exs +++ b/.dialyzer_ignore.exs @@ -9,8 +9,5 @@ {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 1}, {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :exact_eq, 14}, {"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 14}, - ~r/lib\/phoenix\/router.ex/, - {"lib/explorer/chain/search.ex", :pattern_match, 100}, - {"lib/explorer/chain/search.ex", :pattern_match, 282}, - {"lib/explorer/chain/search.ex", :pattern_match, 379} + ~r/lib\/phoenix\/router.ex/ ] diff --git a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs index 87bd37080bd0..035c39baff8b 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs @@ -65,6 +65,13 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do end test "subscribed user is notified with market history", %{token: token} do + initial_value = :persistent_term.get(:market_history_fetcher_enabled, false) + :persistent_term.put(:market_history_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_history_fetcher_enabled, initial_value) + end) + Coin.handle_info({nil, {{:ok, token}, false}}, %{}) Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) diff --git a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs index 6e080cad45b6..1264eea2d62b 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do alias BlockScoutWeb.Notifier alias Explorer.Market.Fetcher.{Coin, History} - alias Explorer.Market.{MarketHistory, Token} + alias Explorer.Market.{MarketHistory, MarketHistoryCache, Token} alias Explorer.Market.Source.OneCoinSource alias Explorer.Market @@ -64,7 +64,31 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do end test "subscribed user is notified with market history", %{token: token} do - Coin.handle_info({nil, {{:ok, token}, false}}, %{}) + initial_market_history_fetcher_enabled_value = :persistent_term.get(:market_history_fetcher_enabled, false) + :persistent_term.put(:market_history_fetcher_enabled, true) + Supervisor.terminate_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) + Supervisor.restart_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()}) + + source_configuration = Application.get_env(:explorer, Explorer.Market.Source) + fetcher_configuration = Application.get_env(:explorer, Coin) + + Application.put_env(:explorer, Explorer.Market.Source, + native_coin_source: OneCoinSource, + secondary_coin_source: OneCoinSource + ) + + Application.put_env(:explorer, Coin, Keyword.merge(fetcher_configuration, table_name: :rates, enabled: true)) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.Source, source_configuration) + Application.put_env(:explorer, Coin, fetcher_configuration) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) + :persistent_term.put(:market_history_fetcher_enabled, initial_market_history_fetcher_enabled_value) + end) + + {:ok, state} = Coin.init([]) + Coin.handle_info({nil, {{:ok, token}, false}}, state) Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs index 450bf1b73bde..4a333c873070 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -21,6 +21,15 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do end describe "Test account/api/account/v2/user" do + setup do + initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) + :persistent_term.put(:market_token_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_token_fetcher_enabled, initial_value) + end) + end + test "get user info", %{conn: conn, user: user} do result_conn = conn diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 69242b1d2c75..31a93eb1e651 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -3228,6 +3228,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end test "get tokens", %{conn: conn} do + initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) + :persistent_term.put(:market_token_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_token_fetcher_enabled, initial_value) + end) + address = insert(:address) ctbs_erc_20 = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index c33e9560f3ce..06fb2ca16151 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -7,6 +7,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do alias Plug.Conn.Query describe "/search" do + setup do + initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) + :persistent_term.put(:market_token_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_token_fetcher_enabled, initial_value) + end) + end + test "get token-transfers with ok reputation", %{conn: conn} do init_value = Application.get_env(:block_scout_web, :hide_scam_addresses) Application.put_env(:block_scout_web, :hide_scam_addresses, true) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs index 24d51e2b9f55..dfad76535357 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs @@ -242,7 +242,7 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do addresses = insert_transactions_in_last_seconds(55, 10800) addresses = - Enum.map(addresses, fn addr -> to_string(addr.hash) end) |> dbg(limit: :infinity, printable_limit: :infinity) + Enum.map(addresses, fn addr -> to_string(addr.hash) end) request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "3h"}) assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200) @@ -250,7 +250,7 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do assert is_map(next_page_params) request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "3h"})) - assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) |> dbg() + assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) assert length(items) == 5 end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index f0e8642e142a..4a491d055539 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -469,6 +469,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do end describe "/tokens" do + setup do + initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) + :persistent_term.put(:market_token_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_token_fetcher_enabled, initial_value) + end) + end + defp check_tokens_pagination(tokens, conn, additional_params \\ %{}) do request = get(conn, "/api/v2/tokens", additional_params) assert response = json_response(request, 200) diff --git a/apps/explorer/benchmarks/explorer/chain/tokens.exs b/apps/explorer/benchmarks/explorer/chain/tokens.exs new file mode 100644 index 000000000000..14244e17d7b4 --- /dev/null +++ b/apps/explorer/benchmarks/explorer/chain/tokens.exs @@ -0,0 +1,64 @@ +defmodule Explorer.Chain.FiatValueBenchmark do + @moduledoc """ + Benchmark for the performance of fetching fiat value type data from the database. + """ + + use Explorer.BenchmarkCase + + alias Explorer.Repo + alias Explorer.Chain.Token + alias Explorer.Market.Fetcher.Token, as: TokenFetcher + + def list_tokens do + Benchee.run(%{"Fiat value type performance" => fn _ -> Repo.all(Token) end}, + inputs: + for with_market_data <- [false, true], + enabled_token_fetcher? when with_market_data or not enabled_token_fetcher? <- [false, true], + token_count <- [50, 100, 10000], + into: %{} do + {"#{token_count} tokens#{if with_market_data, do: " with market data", else: ""}#{if enabled_token_fetcher?, do: " with enabled token fetcher", else: ""}", + %{ + token_count: token_count, + with_market_data: with_market_data, + enabled_token_fetcher: enabled_token_fetcher? + }} + end, + before_scenario: fn %{ + token_count: token_count, + with_market_data: with_market_data, + enabled_token_fetcher: enabled_token_fetcher? + } = input -> + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo, ownership_timeout: :infinity) + + Repo.delete_all(Token) + + market_data = fn i -> + if with_market_data do + [fiat_value: Decimal.new(i), circulating_market_cap: Decimal.new(i)] + else + [fiat_value: nil, circulating_market_cap: nil] + end + end + + 1..token_count + |> Enum.each(fn i -> + insert(:token, market_data.(i)) + end) + + if enabled_token_fetcher? do + Application.put_env(:explorer, Explorer.Market.Source, tokens_source: Explorer.Market.Source.OneCoinSource) + TokenFetcher.start_link([]) + end + end, + load: @path, + save: [ + path: @path, + tag: "fiat-value-no-check" + ], + time: 5, + formatters: [Benchee.Formatters.Console] + ) + end +end + +Explorer.Chain.FiatValueBenchmark.list_tokens() diff --git a/apps/explorer/lib/explorer.ex b/apps/explorer/lib/explorer.ex index 0becee7b1aa3..bf774e3c05e8 100644 --- a/apps/explorer/lib/explorer.ex +++ b/apps/explorer/lib/explorer.ex @@ -19,4 +19,22 @@ defmodule Explorer do def coin_name do Application.get_env(:explorer, :coin_name) end + + @doc """ + Retrieves the current operational mode of the Explorer application. + + The mode determines which components of the Explorer application are active: + - `:all` - Both API web server and blockchain indexer run together + - `:indexer` - Only blockchain indexer modules run without the web server + - `:api` - Only the web server runs without performing indexing operations + + ## Returns + - `:all` if both API and indexer components should be active + - `:indexer` if only the indexer component should be active + - `:api` if only the API web server component should be active + """ + @spec mode() :: :all | :indexer | :api + def mode do + Application.get_env(:explorer, :mode) + end end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 684cf2e6a65a..dd2b75d51234 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -116,10 +116,10 @@ defmodule Explorer.Application do defp configurable_children do configurable_children_set = [ - configure_libcluster(), configure_mode_dependent_process(Explorer.Market.Fetcher.Coin, :api), configure_mode_dependent_process(Explorer.Market.Fetcher.Token, :indexer), configure_mode_dependent_process(Explorer.Market.Fetcher.History, :indexer), + configure_mode_dependent_process(Explorer.Market, :api), configure(Explorer.ChainSpec.GenesisData), configure(Explorer.Chain.Cache.Counters.ContractsCount), configure(Explorer.Chain.Cache.Counters.NewContractsCount), @@ -328,7 +328,9 @@ defmodule Explorer.Application do configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, configure_mode_dependent_process(Explorer.Utility.RateLimiter, :api), - Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api) + Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api), + # keep at the end + configure_libcluster() ] |> List.flatten() @@ -431,7 +433,7 @@ defmodule Explorer.Application do end defp configure_mode_dependent_process(process, mode) do - if should_start?(process) and Application.get_env(:explorer, :mode) in [mode, :all] do + if should_start?(process) and Explorer.mode() in [mode, :all] do process else [] @@ -499,7 +501,7 @@ defmodule Explorer.Application do end defp configure_libcluster do - if Application.get_env(:explorer, :mode) in [:indexer, :api] do + if Explorer.mode() in [:indexer, :api] do libcluster() else [] diff --git a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex index 79676ff72d00..be57c209b545 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/tokens.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/tokens.ex @@ -267,20 +267,22 @@ defmodule Explorer.Chain.Import.Runner.Tokens do fragment("COALESCE(EXCLUDED.circulating_market_cap, ?)", token.circulating_market_cap), volume_24h: fragment("COALESCE(EXCLUDED.volume_24h, ?)", token.volume_24h), icon_url: fragment("COALESCE(?, EXCLUDED.icon_url)", token.icon_url), + decimals: fragment("COALESCE(?, EXCLUDED.decimals)", token.decimals), inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at), updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at) ] ], where: fragment( - "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.type, EXCLUDED.fiat_value, EXCLUDED.circulating_market_cap, EXCLUDED.volume_24h, EXCLUDED.icon_url) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.type, EXCLUDED.fiat_value, EXCLUDED.circulating_market_cap, EXCLUDED.volume_24h, EXCLUDED.icon_url, EXCLUDED.decimals) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)", token.name, token.symbol, token.type, token.fiat_value, token.circulating_market_cap, token.volume_24h, - token.icon_url + token.icon_url, + token.decimals ) ) end @@ -297,10 +299,10 @@ defmodule Explorer.Chain.Import.Runner.Tokens do `:volume_24h` """ @spec market_data_fields_to_update() :: [ - :name | :symbol | :type | :fiat_value | :circulating_market_cap | :volume_24h + :name | :symbol | :type | :fiat_value | :circulating_market_cap | :volume_24h | :decimals ] def market_data_fields_to_update do - [:name, :symbol, :type, :fiat_value, :circulating_market_cap, :volume_24h] + [:name, :symbol, :type, :fiat_value, :circulating_market_cap, :volume_24h, :decimals] end defp should_update?(_new_token, nil, _fields_to_replace), do: true diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 1eba3e00e03a..6f96932ffe7c 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -3,6 +3,7 @@ defmodule Explorer.Chain.Token.Schema do use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] alias Explorer.Chain.{Address, Address.Reputation, Hash} + alias Explorer.Chain.Token.FiatValue if @bridged_tokens_enabled do @bridged_field [ @@ -28,11 +29,11 @@ defmodule Explorer.Chain.Token.Schema do field(:skip_metadata, :boolean) field(:total_supply_updated_at_block, :integer) field(:metadata_updated_at, :utc_datetime_usec) - field(:fiat_value, :decimal) - field(:circulating_market_cap, :decimal) + field(:fiat_value, FiatValue) + field(:circulating_market_cap, FiatValue) field(:icon_url, :string) field(:is_verified_via_admin_panel, :boolean) - field(:volume_24h, :decimal) + field(:volume_24h, FiatValue) field(:transfer_count, :integer) belongs_to( diff --git a/apps/explorer/lib/explorer/chain/token/fiat_value.ex b/apps/explorer/lib/explorer/chain/token/fiat_value.ex new file mode 100644 index 000000000000..b19733063d8d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/token/fiat_value.ex @@ -0,0 +1,60 @@ +defmodule Explorer.Chain.Token.FiatValue do + @moduledoc """ + Represents money values, used to hide the value if there is a chance that the value is not relevant. + """ + + use Ecto.Type + + alias Explorer.Market + + @type t :: Decimal.t() | nil + + @impl Ecto.Type + def type, do: :decimal + + @impl Ecto.Type + def cast(value) when is_binary(value) do + case Decimal.parse(value) do + {decimal, ""} -> + {:ok, decimal} + + _ -> + :error + end + end + + @impl Ecto.Type + def cast(value) when is_integer(value) do + {:ok, Decimal.new(value)} + end + + @impl Ecto.Type + def cast(value) when is_float(value) do + {:ok, Decimal.from_float(value)} + end + + @impl Ecto.Type + def cast(%Decimal{} = decimal) do + {:ok, decimal} + end + + @impl Ecto.Type + def cast(_), do: :error + + @impl Ecto.Type + def dump(%Decimal{} = decimal) do + {:ok, decimal} + end + + @impl Ecto.Type + def dump(_), do: :error + + @impl Ecto.Type + def load(%Decimal{} = decimal) do + if Market.token_fetcher_enabled?() do + {:ok, decimal} + else + {:ok, nil} + end + end +end diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 0e4bf8b9fa39..9d8a06304a0e 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -660,7 +660,7 @@ defmodule Explorer.Helper do """ @spec indexer_node?(Node.t()) :: boolean() def indexer_node?(node) do - (node |> :rpc.call(Application, :get_env, [:explorer, :mode]) |> process_rpc_response(node, nil)) in [ + (node |> :rpc.call(Explorer, :mode, []) |> process_rpc_response(node, nil)) in [ :all, :indexer ] diff --git a/apps/explorer/lib/explorer/market/fetcher/history.ex b/apps/explorer/lib/explorer/market/fetcher/history.ex index baa66d65ba3b..8e897b3df325 100644 --- a/apps/explorer/lib/explorer/market/fetcher/history.ex +++ b/apps/explorer/lib/explorer/market/fetcher/history.ex @@ -64,11 +64,16 @@ defmodule Explorer.Market.Fetcher.History do }} end) - state = %{types_states: types_states} + if Enum.all?(types_states, fn {_type, type_state} -> is_nil(type_state.source) end) do + Logger.info("No market history sources are configured") + :ignore + else + state = %{types_states: types_states} - send(self(), {:fetch_all, config(:first_fetch_day_count)}) + send(self(), {:fetch_all, config(:first_fetch_day_count)}) - {:ok, state} + {:ok, state} + end end def handle_info({:fetch_all, day_count}, state) do @@ -121,6 +126,7 @@ defmodule Explorer.Market.Fetcher.History do new_types_states = put_in(state.types_states, [type, :failed_attempts], failed_attempts) new_state = %{state | types_states: new_types_states} + maybe_insert_and_schedule_refetch(new_state) {:noreply, new_state} end diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index e58a2788fc05..1934b5aee2dd 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -3,9 +3,68 @@ defmodule Explorer.Market do Context for data related to the cryptocurrency market. """ - alias Explorer.Market.Fetcher.Coin + use GenServer + + require Logger + alias Explorer.Helper + alias Explorer.Market.Fetcher.Coin, as: CoinFetcher + alias Explorer.Market.Fetcher.History, as: HistoryFetcher + alias Explorer.Market.Fetcher.Token, as: TokenFetcher + alias Explorer.Market.{MarketHistory, MarketHistoryCache, Token} + @history_key :market_history_fetcher_enabled + @tokens_key :market_token_fetcher_enabled + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl GenServer + def init(_opts) do + if Explorer.mode() == :all do + {history_pid, token_pid} = find_history_and_token_fetchers() + :persistent_term.put(@history_key, !is_nil(history_pid)) + :persistent_term.put(@tokens_key, !is_nil(token_pid)) + :ignore + else + {:ok, nil, {:continue, 1}} + end + end + + @impl GenServer + def handle_continue(attempt, _state) do + attempt |> Kernel.**(3) |> :timer.seconds() |> :timer.sleep() + + case Node.list() + |> Enum.filter(&Helper.indexer_node?/1) do + [] -> + if attempt < 5 do + {:noreply, nil, {:continue, attempt + 1}} + else + raise "No indexer nodes discovered after #{attempt} attempts" + end + + [indexer] -> + {history_pid, token_pid} = + indexer + |> :rpc.call(__MODULE__, :find_history_and_token_fetchers, []) + |> Helper.process_rpc_response(indexer, {nil, nil}) + + :persistent_term.put(@history_key, !is_nil(history_pid)) + :persistent_term.put(@tokens_key, !is_nil(token_pid)) + {:stop, :normal} + + multiple_indexers -> + if attempt < 5 do + {:noreply, nil, {:continue, attempt + 1}} + else + raise "Multiple indexer nodes discovered: #{inspect(multiple_indexers)}" + end + end + end + @doc """ Retrieves the history for the recent specified amount of days. @@ -13,18 +72,11 @@ defmodule Explorer.Market do """ @spec fetch_recent_history(boolean()) :: [MarketHistory.t()] def fetch_recent_history(secondary_coin? \\ false) do - MarketHistoryCache.fetch(secondary_coin?) - end - - @doc """ - Retrieves today's native coin exchange rate from the database. - """ - @spec get_native_coin_exchange_rate_from_db(boolean()) :: Token.t() - def get_native_coin_exchange_rate_from_db(secondary_coin? \\ false) do - secondary_coin? - |> fetch_recent_history() - |> List.first() - |> MarketHistory.to_token() + if history_fetcher_enabled?() do + MarketHistoryCache.fetch(secondary_coin?) + else + [] + end end @doc """ @@ -32,7 +84,7 @@ defmodule Explorer.Market do """ @spec get_coin_exchange_rate() :: Token.t() def get_coin_exchange_rate do - Coin.get_coin_exchange_rate() || get_native_coin_exchange_rate_from_db() + CoinFetcher.get_coin_exchange_rate() || Token.null() end @doc """ @@ -40,7 +92,7 @@ defmodule Explorer.Market do """ @spec get_secondary_coin_exchange_rate() :: Token.t() def get_secondary_coin_exchange_rate do - Coin.get_secondary_coin_exchange_rate() || get_native_coin_exchange_rate_from_db(true) + CoinFetcher.get_secondary_coin_exchange_rate() || Token.null() end @doc """ @@ -70,4 +122,27 @@ defmodule Explorer.Market do |> MarketHistory.price_at_date(false, options) |> MarketHistory.to_token() end + + @doc """ + Checks if the market token fetcher is enabled in the application. + + This function retrieves the enablement status from persistent term storage + using the `:market_token_fetcher_enabled` key. If the key is not set, it + defaults to `false`. + + ## Returns + - `true` if the market token fetcher is enabled + - `false` if the market token fetcher is disabled or not configured + """ + @spec token_fetcher_enabled?() :: boolean() + def token_fetcher_enabled? do + :persistent_term.get(@tokens_key, false) + end + + @spec history_fetcher_enabled?() :: boolean() + defp history_fetcher_enabled? do + :persistent_term.get(@history_key, false) + end + + defp find_history_and_token_fetchers, do: {GenServer.whereis(HistoryFetcher), GenServer.whereis(TokenFetcher)} end diff --git a/apps/explorer/lib/explorer/market/market_history.ex b/apps/explorer/lib/explorer/market/market_history.ex index 56f6e8cb4097..5a865496f4d5 100644 --- a/apps/explorer/lib/explorer/market/market_history.ex +++ b/apps/explorer/lib/explorer/market/market_history.ex @@ -41,7 +41,7 @@ defmodule Explorer.Market.MarketHistory do end @doc false - @spec bulk_insert([map()]) :: {non_neg_integer(), nil | [term()]} + @spec bulk_insert([map()]) :: {:ok, {non_neg_integer(), nil | [term()]}} | {:error, term()} def bulk_insert(records) do records_without_zeroes = records @@ -53,10 +53,12 @@ defmodule Explorer.Market.MarketHistory do # Enforce MarketHistory ShareLocks order (see docs: sharelocks.md) |> Enum.sort_by(& &1.date) - Repo.safe_insert_all(__MODULE__, records_without_zeroes, - on_conflict: market_history_on_conflict(), - conflict_target: [:date, :secondary_coin] - ) + Repo.transaction(fn -> + Repo.safe_insert_all(__MODULE__, records_without_zeroes, + on_conflict: market_history_on_conflict(), + conflict_target: [:date, :secondary_coin] + ) + end) end @spec to_token(t() | nil) :: Token.t() diff --git a/apps/explorer/lib/explorer/market/source.ex b/apps/explorer/lib/explorer/market/source.ex index 13b80d0d336a..2377b4f63b7d 100644 --- a/apps/explorer/lib/explorer/market/source.ex +++ b/apps/explorer/lib/explorer/market/source.ex @@ -42,6 +42,7 @@ defmodule Explorer.Market.Source do CryptoCompare, CryptoRank, DefiLlama, + DIA, Mobula } @@ -234,7 +235,7 @@ defmodule Explorer.Market.Source do Decimal.new(value) end - @sources [CoinGecko, CoinMarketCap, CryptoCompare, CryptoRank, DefiLlama, Mobula] + @sources [CoinGecko, CoinMarketCap, CryptoCompare, CryptoRank, DefiLlama, Mobula, DIA] @doc """ Returns a module for fetching native coin market data. diff --git a/apps/explorer/lib/explorer/market/source/dia.ex b/apps/explorer/lib/explorer/market/source/dia.ex new file mode 100644 index 000000000000..78720534a90d --- /dev/null +++ b/apps/explorer/lib/explorer/market/source/dia.ex @@ -0,0 +1,280 @@ +defmodule Explorer.Market.Source.DIA do + @moduledoc """ + Adapter for fetching exchange rates from https://www.diadata.org/ + """ + + require Logger + + alias Explorer.Chain.Hash + alias Explorer.Market.{Source, Token} + + @behaviour Source + + @impl Source + def native_coin_fetching_enabled?, do: not is_nil(config(:coin_address_hash)) + + @impl Source + def fetch_native_coin, do: do_fetch_coin(config(:coin_address_hash), "Coin address hash not specified") + + @impl Source + def secondary_coin_fetching_enabled?, do: not is_nil(config(:secondary_coin_address_hash)) + + @impl Source + def fetch_secondary_coin, + do: do_fetch_coin(config(:secondary_coin_address_hash), "Secondary coin address hash not specified") + + @impl Source + def tokens_fetching_enabled?, do: not is_nil(config(:blockchain)) + + @impl Source + def fetch_tokens(state, batch_size) when state in [[], nil] do + case init_tokens_fetching() do + {:error, _reason} = error -> + error + + tokens_to_fetch when is_list(tokens_to_fetch) and length(tokens_to_fetch) > 0 -> + fetch_tokens(tokens_to_fetch, batch_size) + + _ -> + {:error, "Tokens not found for configured blockchain: #{config(:blockchain)}"} + end + end + + @impl Source + def fetch_tokens(state, batch_size) do + # it safe to pattern match here because init_tokens_fetching/0 + # would have returned an error otherwise + blockchain = config(:blockchain) + {to_fetch, remaining} = Enum.split(state, batch_size) + + tasks_results = + to_fetch + |> Task.async_stream( + fn token -> + case Source.http_request( + base_url() + |> URI.append_path("/assetQuotation") + |> URI.append_path("/#{blockchain}") + |> URI.append_path("/#{token.contract_address_hash}") + |> URI.to_string(), + [] + ) do + {:ok, data} -> + token_to_import = + Map.merge(token, %{ + symbol: data["Symbol"], + name: data["Name"], + fiat_value: Source.to_decimal(data["Price"]), + volume_24h: Source.to_decimal(data["VolumeYesterdayUSD"]), + type: "ERC-20" + }) + + {:ok, token_to_import} + + {:error, reason} -> + {:error, {token, reason}} + end + end, + max_concurrency: 5, + timeout: :timer.seconds(60), + zip_input_on_exit: true + ) + |> Enum.group_by( + fn + {:ok, {:ok, _}} -> :ok + _ -> :error + end, + fn + {:ok, {:ok, token_to_import}} -> token_to_import + {:ok, {:error, {token, reason}}} -> {token, reason} + {:error, {token, reason}} -> {token, reason} + end + ) + + to_import = Map.get(tasks_results, :ok, []) + tokens_with_errors = Map.get(tasks_results, :error, []) + + remaining_with_errors = + if Enum.empty?(tokens_with_errors) do + remaining + else + Logger.error("Errors while fetching tokens from DIA: #{inspect(tokens_with_errors)}") + remaining ++ Enum.map(tokens_with_errors, fn {token, _reason} -> token end) + end + + if Enum.empty?(to_import) and !Enum.empty?(tokens_with_errors) do + {:error, tokens_with_errors} + else + {:ok, remaining_with_errors, Enum.empty?(remaining_with_errors), to_import} + end + end + + @impl Source + def native_coin_price_history_fetching_enabled?, do: not is_nil(config(:coin_address_hash)) + + @impl Source + def fetch_native_coin_price_history(previous_days), do: do_fetch_coin_price_history(previous_days, false) + + @impl Source + def secondary_coin_price_history_fetching_enabled?, do: not is_nil(config(:secondary_coin_address_hash)) + + @impl Source + def fetch_secondary_coin_price_history(previous_days), do: do_fetch_coin_price_history(previous_days, true) + + @impl Source + def market_cap_history_fetching_enabled?, do: :ignore + + @impl Source + def fetch_market_cap_history(_previous_days), do: :ignore + + @impl Source + def tvl_history_fetching_enabled?, do: :ignore + + @impl Source + def fetch_tvl_history(_previous_days), do: :ignore + + defp do_fetch_coin(coin_address_hash, coin_address_hash_not_specified_error) do + with {:coin, coin_address_hash} when not is_nil(coin_address_hash) <- {:coin, coin_address_hash}, + {:blockchain, blockchain} when not is_nil(blockchain) <- {:blockchain, config(:blockchain)}, + {:ok, %{"Price" => _price} = data} <- + Source.http_request( + base_url() + |> URI.append_path("/assetQuotation/#{blockchain}/#{coin_address_hash}") + |> URI.to_string(), + [] + ) do + {:ok, + %Token{ + available_supply: nil, + total_supply: nil, + btc_value: nil, + last_updated: Source.maybe_get_date(data["Time"]), + market_cap: nil, + tvl: nil, + name: data["Name"], + symbol: data["Symbol"], + fiat_value: Source.to_decimal(data["Price"]), + volume_24h: Source.to_decimal(data["VolumeYesterdayUSD"]), + image_url: nil + }} + else + {:coin, nil} -> {:error, coin_address_hash_not_specified_error} + {:blockchain, nil} -> {:error, "Blockchain not specified"} + {:ok, unexpected_response} -> {:error, Source.unexpected_response_error("DIA", unexpected_response)} + {:error, _reason} = error -> error + end + end + + defp init_tokens_fetching do + with blockchain when not is_nil(blockchain) <- config(:blockchain), + coin_address_hash = config(:coin_address_hash), + {:ok, tokens} <- + Source.http_request( + base_url() + |> URI.append_path("/quotedAssets") + |> URI.append_query("blockchain=#{blockchain}") + |> URI.to_string(), + [] + ) do + tokens + |> Enum.reduce([], fn + %{ + "Asset" => %{ + "Address" => token_contract_address_hash_string, + "Decimals" => decimals + } + }, + acc -> + case (is_nil(coin_address_hash) || + String.downcase(token_contract_address_hash_string) != String.downcase(coin_address_hash)) && + Hash.Address.cast(token_contract_address_hash_string) do + {:ok, token_contract_address_hash} -> + token = %{ + contract_address_hash: token_contract_address_hash, + decimals: decimals + } + + [token | acc] + + _ -> + acc + end + + _, acc -> + acc + end) + else + nil -> {:error, "Blockchain not specified"} + {:error, reason} -> {:error, reason} + end + end + + defp do_fetch_coin_price_history(previous_days, secondary_coin?) do + datetime_now = DateTime.utc_now() + unix_now = datetime_now |> DateTime.to_unix() + unix_from = unix_now - previous_days * 24 * 60 * 60 + + with {:coin, coin_address_hash} when not is_nil(coin_address_hash) <- + {:coin, if(secondary_coin?, do: config(:secondary_coin_address_hash), else: config(:coin_address_hash))}, + {:blockchain, blockchain} when not is_nil(blockchain) <- {:blockchain, config(:blockchain)}, + {:ok, %{"DataPoints" => [%{"Series" => [%{"values" => values}]}]}} <- + Source.http_request( + base_url() + |> URI.append_path("/assetChartPoints/MA120/#{blockchain}/#{coin_address_hash}") + |> URI.append_query("starttime=#{unix_from}") + |> URI.append_query("endtime=#{unix_now}") + |> URI.to_string(), + [] + ) do + values + |> Enum.reduce_while(%{}, fn value, acc -> + with time when not is_nil(time) <- List.first(value), + {:ok, datetime, _} <- DateTime.from_iso8601(time), + date = DateTime.to_date(datetime), + price when not is_nil(price) <- List.last(value) do + {:cont, + Map.update( + acc, + date, + %{ + closing_price: Source.to_decimal(price), + date: date, + opening_price: Source.to_decimal(price), + secondary_coin: secondary_coin? + }, + fn existing_entry -> + %{ + existing_entry + | opening_price: Source.to_decimal(price) + } + end + )} + else + _ -> + {:halt, {:error, "Wrong format of DIA coin price history response: #{inspect(value)}"}} + end + end) + |> case do + {:error, _reason} = error -> + error + + price_history_map -> + {:ok, Map.values(price_history_map)} + end + else + {:coin, nil} -> {:error, "#{Source.secondary_coin_string(secondary_coin?)} address hash not specified"} + {:blockchain, nil} -> {:error, "Blockchain not specified"} + {:ok, _} -> {:ok, []} + {:error, _reason} = error -> error + end + end + + defp base_url do + :base_url |> config() |> URI.parse() + end + + @spec config(atom()) :: term + defp config(key) do + Application.get_env(:explorer, __MODULE__)[key] + end +end diff --git a/apps/explorer/lib/explorer/market/source/mobula.ex b/apps/explorer/lib/explorer/market/source/mobula.ex index ed6756e04117..2d8da7399c96 100644 --- a/apps/explorer/lib/explorer/market/source/mobula.ex +++ b/apps/explorer/lib/explorer/market/source/mobula.ex @@ -66,7 +66,10 @@ defmodule Explorer.Market.Source.Mobula do end end) - {:ok, offset + batch_size, initial_tokens_len < batch_size, tokens_to_import} + fetch_finished? = initial_tokens_len < batch_size + new_state = if fetch_finished?, do: nil, else: offset + batch_size + + {:ok, new_state, fetch_finished?, tokens_to_import} else nil -> {:error, "Platform ID not specified"} {:ok, unexpected_response} -> {:error, Source.unexpected_response_error("Mobula", unexpected_response)} diff --git a/apps/explorer/test/explorer/chain/address/token_test.exs b/apps/explorer/test/explorer/chain/address/token_test.exs index 501afcc77eda..849ea4a18891 100644 --- a/apps/explorer/test/explorer/chain/address/token_test.exs +++ b/apps/explorer/test/explorer/chain/address/token_test.exs @@ -292,6 +292,15 @@ defmodule Explorer.Chain.Address.TokenTest do end describe "page_tokens/2" do + setup do + initial_value = :persistent_term.get(:market_token_fetcher_enabled, false) + :persistent_term.put(:market_token_fetcher_enabled, true) + + on_exit(fn -> + :persistent_term.put(:market_token_fetcher_enabled, initial_value) + end) + end + test "just bring the normal query when PagingOptions.key is nil" do options = %PagingOptions{key: nil} diff --git a/apps/explorer/test/explorer/chain/token_test.exs b/apps/explorer/test/explorer/chain/token_test.exs index 2d4e69587296..65b896033618 100644 --- a/apps/explorer/test/explorer/chain/token_test.exs +++ b/apps/explorer/test/explorer/chain/token_test.exs @@ -148,4 +148,43 @@ defmodule Explorer.Chain.TokenTest do assert {:ok, _updated_token} = Token.update(token, update_params) end end + + describe "list_top/2" do + test "returns market data with enabled token fetcher" do + old_source_env = Application.get_env(:explorer, Explorer.Market.Source) + old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Token) + + Application.put_env( + :explorer, + Explorer.Market.Source, + Keyword.merge(old_source_env, tokens_source: Explorer.Market.Source.OneCoinSource) + ) + + Application.put_env(:explorer, Explorer.Market.Fetcher.Token, Keyword.merge(old_fetcher_env, enabled: true)) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.Source, old_source_env) + Application.put_env(:explorer, Explorer.Market.Fetcher.Token, old_fetcher_env) + end) + + start_supervised!(Explorer.Market.Fetcher.Token) + start_supervised!(Explorer.Market) + + insert_list(10, :token) + + market_data = Token.list_top(nil) + + assert Enum.all?(market_data, fn token -> not is_nil(token.fiat_value) end) + end + + test "ignores market data with disabled token fetcher" do + insert_list(10, :token) + + start_supervised!(Explorer.Market) + + market_data = Token.list_top(nil) + + assert Enum.all?(market_data, fn token -> is_nil(token.fiat_value) end) + end + end end diff --git a/apps/explorer/test/explorer/market/market_history_test.exs b/apps/explorer/test/explorer/market/market_history_test.exs index ab7927ec3ee7..7bf700d93065 100644 --- a/apps/explorer/test/explorer/market/market_history_test.exs +++ b/apps/explorer/test/explorer/market/market_history_test.exs @@ -11,14 +11,14 @@ defmodule Explorer.Market.MarketHistoryTest do %{date: ~D[2023-01-02], opening_price: Decimal.new("7.50"), closing_price: Decimal.new("5.00")} ] - assert {2, _} = MarketHistory.bulk_insert(records) + assert {:ok, {2, _}} = MarketHistory.bulk_insert(records) records = [ %{date: ~D[2023-01-01], opening_price: Decimal.new("50.00"), closing_price: Decimal.new("55.00")}, %{date: ~D[2023-01-02], opening_price: Decimal.new("10.00"), closing_price: Decimal.new("8.00")} ] - assert {2, _} = MarketHistory.bulk_insert(records) + assert {:ok, {2, _}} = MarketHistory.bulk_insert(records) assert [date1, date2] = Repo.all(MarketHistory) assert date1.opening_price == Decimal.new("50.00") diff --git a/apps/explorer/test/explorer/market/market_test.exs b/apps/explorer/test/explorer/market/market_test.exs index b2651d4ec28a..e27d0d326545 100644 --- a/apps/explorer/test/explorer/market/market_test.exs +++ b/apps/explorer/test/explorer/market/market_test.exs @@ -3,7 +3,6 @@ defmodule Explorer.MarketTest do alias Explorer.Market alias Explorer.Market.MarketHistory - alias Explorer.Repo setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) @@ -17,24 +16,123 @@ defmodule Explorer.MarketTest do :ok end - test "fetch_recent_history/1" do - ConCache.delete(:market_history, :last_update) + describe "fetch_recent_history/1" do + test "with enabled history fetcher" do + ConCache.delete(:market_history, :last_update) - today = Date.utc_today() + start_supervised!(Explorer.Market.Fetcher.History) + start_supervised!(Explorer.Market) - records = - for i <- 0..29 do - %{ - date: Timex.shift(today, days: i * -1), - closing_price: Decimal.new(1), - opening_price: Decimal.new(1) - } - end + today = Date.utc_today() - MarketHistory.bulk_insert(records) + records = + for i <- 0..29 do + %{ + date: Timex.shift(today, days: i * -1), + closing_price: Decimal.new(1), + opening_price: Decimal.new(1) + } + end - history = Market.fetch_recent_history() - assert length(history) == 30 - assert Enum.at(history, 0).date == Enum.at(records, 0).date + MarketHistory.bulk_insert(records) + + history = Market.fetch_recent_history() + assert length(history) == 30 + assert Enum.at(history, 0).date == Enum.at(records, 0).date + end + + test "with disabled history fetcher" do + ConCache.delete(:market_history, :last_update) + start_supervised!(Explorer.Market) + + old_env = Application.get_env(:explorer, Explorer.Market.Fetcher.History) + Application.put_env(:explorer, Explorer.Market.Fetcher.History, Keyword.merge(old_env, enabled: false)) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.Fetcher.History, old_env) + end) + + today = Date.utc_today() + + records = + for i <- 0..29 do + %{ + date: Timex.shift(today, days: i * -1), + closing_price: Decimal.new(1), + opening_price: Decimal.new(1) + } + end + + MarketHistory.bulk_insert(records) + + history = Market.fetch_recent_history() + assert length(history) == 0 + end + end + + describe "get_coin_exchange_rate/0" do + test "with enabled coin fetcher" do + old_source_env = Application.get_env(:explorer, Explorer.Market.Source) + old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Coin) + + Application.put_env( + :explorer, + Explorer.Market.Source, + Keyword.merge(old_source_env, native_coin_source: Explorer.Market.Source.OneCoinSource) + ) + + Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, Keyword.merge(old_fetcher_env, enabled: true)) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.Source, old_source_env) + Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, old_fetcher_env) + end) + + start_supervised!(Explorer.Market.Fetcher.Coin) + + :timer.sleep(100) + + exchange_rate = Market.get_coin_exchange_rate() + + assert exchange_rate.fiat_value == Decimal.new(1) + end + + test "with disabled coin fetcher" do + exchange_rate = Market.get_coin_exchange_rate() + assert exchange_rate.fiat_value == nil + end + end + + describe "get_secondary_coin_exchange_rate/0" do + test "with enabled coin fetcher" do + old_source_env = Application.get_env(:explorer, Explorer.Market.Source) + old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Coin) + + Application.put_env( + :explorer, + Explorer.Market.Source, + Keyword.merge(old_source_env, secondary_coin_source: Explorer.Market.Source.OneCoinSource) + ) + + Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, Keyword.merge(old_fetcher_env, enabled: true)) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Market.Source, old_source_env) + Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, old_fetcher_env) + end) + + start_supervised!(Explorer.Market.Fetcher.Coin) + + :timer.sleep(100) + + exchange_rate = Market.get_secondary_coin_exchange_rate() + + assert exchange_rate.fiat_value == Decimal.new(1) + end + + test "with disabled coin fetcher" do + exchange_rate = Market.get_secondary_coin_exchange_rate() + assert exchange_rate.fiat_value == nil + end end end diff --git a/apps/explorer/test/explorer/market/source/crypto_rank_test.exs b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs index fefa411f0386..f0820796cf8d 100644 --- a/apps/explorer/test/explorer/market/source/crypto_rank_test.exs +++ b/apps/explorer/test/explorer/market/source/crypto_rank_test.exs @@ -106,11 +106,11 @@ defmodule Explorer.Market.Source.CryptoRankTest do end describe "tokens_fetching_enabled?" do - test "returns true if coin_id is configured" do + test "returns true if platform is configured" do assert CryptoRank.tokens_fetching_enabled?() end - test "returns false if coin_id is not configured", %{old_env: old_env} do + test "returns false if platform is not configured", %{old_env: old_env} do Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, platform: nil)) refute CryptoRank.tokens_fetching_enabled?() diff --git a/apps/explorer/test/explorer/market/source/dia_test.exs b/apps/explorer/test/explorer/market/source/dia_test.exs new file mode 100644 index 000000000000..26b9af444110 --- /dev/null +++ b/apps/explorer/test/explorer/market/source/dia_test.exs @@ -0,0 +1,437 @@ +defmodule Explorer.Market.Source.DIATest do + use ExUnit.Case + + alias Explorer.Market.Source.DIA + alias Plug.Conn + + @native_coin_history_json File.read!("./test/support/fixture/market/dia/native_coin_history.json") + @secondary_coin_history_json File.read!("./test/support/fixture/market/dia/secondary_coin_history.json") + + setup do + bypass = Bypass.open() + old_env = Application.get_env(:explorer, DIA, []) + + new_env = + Keyword.merge( + old_env, + blockchain: "Ethereum", + base_url: "http://localhost:#{bypass.port}", + coin_address_hash: "0x0000000000000000000000000000000000000000", + secondary_coin_address_hash: "0x0000000000000000000000000000000000000001" + ) + + Application.put_env( + :explorer, + DIA, + new_env + ) + + Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + + on_exit(fn -> + Application.put_env(:explorer, DIA, old_env) + Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter) + end) + + {:ok, env: new_env, bypass: bypass} + end + + describe "native_coin_fetching_enabled?" do + test "returns true if coin_address_hash is configured" do + assert DIA.native_coin_fetching_enabled?() + end + + test "returns false if coin_address_hash is not configured", %{env: env} do + Application.put_env(:explorer, DIA, Keyword.merge(env, coin_address_hash: nil)) + refute DIA.native_coin_fetching_enabled?() + end + end + + describe "fetch_native_coin" do + test "fetches native coin", %{bypass: bypass} do + Bypass.expect_once( + bypass, + "GET", + "/assetQuotation/Ethereum/0x0000000000000000000000000000000000000000", + fn conn -> + Conn.resp( + conn, + 200, + json_coin("0x0000000000000000000000000000000000000000", 3027.98578732332, "ETH", "Ethereum") + ) + end + ) + + assert {:ok, + %Explorer.Market.Token{ + available_supply: nil, + btc_value: nil, + fiat_value: Decimal.new("3027.98578732332"), + image_url: nil, + last_updated: ~U[2025-11-27 08:09:59Z], + market_cap: nil, + name: "Ethereum", + symbol: "ETH", + total_supply: nil, + tvl: nil, + volume_24h: Decimal.new("6577658642.868258") + }} == + DIA.fetch_native_coin() + end + end + + describe "secondary_coin_fetching_enabled?" do + test "returns true if secondary_coin_address_hash is configured" do + assert DIA.secondary_coin_fetching_enabled?() + end + + test "returns false if secondary_coin_address_hash is not configured", %{env: env} do + Application.put_env(:explorer, DIA, Keyword.merge(env, secondary_coin_address_hash: nil)) + + refute DIA.secondary_coin_fetching_enabled?() + end + end + + describe "fetch_secondary_coin" do + test "fetches secondary coin", %{bypass: bypass} do + Bypass.expect_once( + bypass, + "GET", + "/assetQuotation/Ethereum/0x0000000000000000000000000000000000000001", + fn conn -> + Conn.resp( + conn, + 200, + json_coin("0x0000000000000000000000000000000000000001", 91169.97153780921, "WBTC", "Wrapped BTC") + ) + end + ) + + assert {:ok, + %Explorer.Market.Token{ + available_supply: nil, + btc_value: nil, + fiat_value: Decimal.new("91169.97153780921"), + image_url: nil, + last_updated: ~U[2025-11-27 08:09:59Z], + market_cap: nil, + name: "Wrapped BTC", + symbol: "WBTC", + total_supply: nil, + tvl: nil, + volume_24h: Decimal.new("6577658642.868258") + }} == + DIA.fetch_secondary_coin() + end + end + + describe "tokens_fetching_enabled?" do + test "returns true if blockchain is configured" do + assert DIA.tokens_fetching_enabled?() + end + + test "returns false if blockchain is not configured", %{env: env} do + Application.put_env(:explorer, DIA, Keyword.merge(env, blockchain: nil)) + + refute DIA.tokens_fetching_enabled?() + end + end + + describe "fetch_tokens" do + test "fetches tokens", %{bypass: bypass} do + Bypass.expect(bypass, "GET", "/quotedAssets", fn conn -> + assert conn.query_string == "blockchain=Ethereum" + Conn.resp(conn, 200, json_tokens_list()) + end) + + Bypass.expect(bypass, "GET", "/assetQuotation/Ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", fn conn -> + Conn.resp( + conn, + 200, + json_coin("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 0.9997188333561603, "USDC", "USD Coin") + ) + end) + + Bypass.expect(bypass, "GET", "/assetQuotation/Ethereum/0xdac17f958d2ee523a2206206994597c13d831ec7", fn conn -> + Conn.resp( + conn, + 200, + json_coin("0xdAC17F958D2ee523a2206206994597C13D831ec7", 0.9999313493241913, "USDT", "Tether USD") + ) + end) + + usdt_and_usdc_to_fetch = [ + %{ + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: "\xDA\xC1\d\x95\x8D.\xE5#\xA2 b\x06\x99E\x97\xC1=\x83\x1E\xC7" + }, + decimals: 6 + }, + %{ + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: "\xA0\xB8i\x91\xC6!\x8B6\xC1ѝJ.\x9E\xB0\xCE6\x06\xEBH" + }, + decimals: 6 + } + ] + + usdt_and_usdc = [ + %{ + name: "Tether USD", + type: "ERC-20", + symbol: "USDT", + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<218, 193, 127, 149, 141, 46, 229, 35, 162, 32, 98, 6, 153, 69, 151, 193, 61, 131, 30, 199>> + }, + fiat_value: Decimal.new("0.9999313493241913"), + volume_24h: Decimal.new("6577658642.868258"), + decimals: 6 + }, + %{ + name: "USD Coin", + type: "ERC-20", + symbol: "USDC", + contract_address_hash: %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<160, 184, 105, 145, 198, 33, 139, 54, 193, 209, 157, 74, 46, 158, 176, 206, 54, 6, 235, 72>> + }, + fiat_value: Decimal.new("0.9997188333561603"), + volume_24h: Decimal.new("6577658642.868258"), + decimals: 6 + } + ] + + assert {:ok, [usdt_or_usdc_to_fetch] = state, false, [usdc_or_usdt_a]} = DIA.fetch_tokens(nil, 1) + + assert usdt_or_usdc_to_fetch in usdt_and_usdc_to_fetch + + assert {:ok, [], true, [usdc_or_usdt_b]} = DIA.fetch_tokens(state, 1) + + assert usdc_or_usdt_a in usdt_and_usdc + assert usdc_or_usdt_b in usdt_and_usdc + assert usdc_or_usdt_a != usdc_or_usdt_b + end + end + + describe "native_coin_price_history_fetching_enabled?" do + test "returns true if coin_address_hash is configured" do + assert DIA.native_coin_price_history_fetching_enabled?() + end + + test "returns false if coin_address_hash is not configured", %{env: env} do + Application.put_env(:explorer, DIA, Keyword.merge(env, coin_address_hash: nil)) + + refute DIA.native_coin_price_history_fetching_enabled?() + end + end + + describe "fetch_native_coin_price_history" do + test "fetches native coin price history", %{bypass: bypass} do + previous_days = 5 + + Bypass.expect_once( + bypass, + "GET", + "/assetChartPoints/MA120/Ethereum/0x0000000000000000000000000000000000000000", + fn conn -> + "starttime=" <> from_str = conn.query_string + {from_int, "&endtime=" <> to_str} = Integer.parse(from_str) + {to_int, ""} = Integer.parse(to_str) + assert to_int - from_int == previous_days * 24 * 60 * 60 + Conn.resp(conn, 200, @native_coin_history_json) + end + ) + + assert {:ok, + [ + %{ + closing_price: Decimal.new("2769.8064113406717"), + date: ~D[2025-11-22], + opening_price: Decimal.new("2765.03927910001"), + secondary_coin: false + }, + %{ + closing_price: Decimal.new("2801.5141364778983"), + date: ~D[2025-11-23], + opening_price: Decimal.new("2768.2062977060264"), + secondary_coin: false + }, + %{ + closing_price: Decimal.new("2952.88025966878"), + date: ~D[2025-11-24], + opening_price: Decimal.new("2798.783002464827"), + secondary_coin: false + }, + %{ + closing_price: Decimal.new("2959.1210339664754"), + date: ~D[2025-11-25], + opening_price: Decimal.new("2949.9206856724004"), + secondary_coin: false + }, + %{ + closing_price: Decimal.new("3027.151520522141"), + date: ~D[2025-11-26], + opening_price: Decimal.new("2959.062301250391"), + secondary_coin: false + } + ]} == + DIA.fetch_native_coin_price_history(previous_days) + end + end + + describe "secondary_coin_price_history_fetching_enabled?" do + test "returns true if secondary_coin_address_hash is configured" do + assert DIA.secondary_coin_price_history_fetching_enabled?() + end + + test "returns false if secondary_coin_address_hash is not configured", %{env: env} do + Application.put_env(:explorer, DIA, Keyword.merge(env, secondary_coin_address_hash: nil)) + refute DIA.secondary_coin_price_history_fetching_enabled?() + end + end + + describe "fetch_secondary_coin_price_history" do + test "fetches secondary coin price history", %{bypass: bypass} do + previous_days = 5 + + Bypass.expect_once( + bypass, + "GET", + "/assetChartPoints/MA120/Ethereum/0x0000000000000000000000000000000000000001", + fn conn -> + "starttime=" <> from_str = conn.query_string + {from_int, "&endtime=" <> to_str} = Integer.parse(from_str) + {to_int, ""} = Integer.parse(to_str) + assert to_int - from_int == previous_days * 24 * 60 * 60 + Conn.resp(conn, 200, @secondary_coin_history_json) + end + ) + + assert {:ok, + [ + %{ + closing_price: Decimal.new("1.0014961501515858"), + date: ~D[2025-11-22], + opening_price: Decimal.new("0.9998295379999973"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("1.0003179715114685"), + date: ~D[2025-11-23], + opening_price: Decimal.new("0.9998108086782626"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("0.9999003120252966"), + date: ~D[2025-11-24], + opening_price: Decimal.new("1.0000636244141254"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("0.9997550066438543"), + date: ~D[2025-11-25], + opening_price: Decimal.new("1.0006631013107907"), + secondary_coin: true + }, + %{ + closing_price: Decimal.new("0.9997050546597661"), + date: ~D[2025-11-26], + opening_price: Decimal.new("0.9998251538809131"), + secondary_coin: true + } + ]} == + DIA.fetch_secondary_coin_price_history(previous_days) + end + end + + describe "market_cap_history_fetching_enabled?" do + test "ignored" do + assert DIA.market_cap_history_fetching_enabled?() == :ignore + end + end + + describe "fetch_market_cap_history" do + test "ignored" do + assert DIA.fetch_market_cap_history(0) == :ignore + end + end + + describe "tvl_history_fetching_enabled?" do + test "ignored" do + assert DIA.tvl_history_fetching_enabled?() == :ignore + end + end + + describe "fetch_tvl_history" do + test "ignored" do + assert DIA.fetch_tvl_history(0) == :ignore + end + end + + # cspell:disable + + defp json_coin(coin_address_hash, fiat_value, symbol, name) do + """ + { + "Symbol": "#{symbol}", + "Name": "#{name}", + "Address": "#{coin_address_hash}", + "Blockchain": "Ethereum", + "Price": #{fiat_value}, + "PriceYesterday": 2935.2004802640054, + "VolumeYesterdayUSD": 6577658642.868258, + "Time": "2025-11-27T08:09:59Z", + "Source": "diadata.org", + "Signature": "0x5b080262b6ee4f5303251ef65dc81b61dc6e5ceb6e2ff0d9ca8a6c9175c0bef472bbadfe4a3a339134ebc094b4336fb6ad0e50bac3622968bb03819789b6dbea01" + } + """ + end + + defp json_tokens_list do + """ + [ + { + "Asset": { + "Symbol": "ETH", + "Name": "Ether", + "Address": "0x0000000000000000000000000000000000000000", + "Decimals": 18, + "Blockchain": "Ethereum" + }, + "Volume": 6577658642.868258, + "VolumeUSD": 0, + "Index": 0 + }, + { + "Asset": { + "Symbol": "USDC", + "Name": "USD Coin", + "Address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Decimals": 6, + "Blockchain": "Ethereum" + }, + "Volume": 2756384237.21397, + "VolumeUSD": 0, + "Index": 0 + }, + { + "Asset": { + "Symbol": "USDT", + "Name": "Tether USD", + "Address": "0xdAC17F958D2ee523a2206206994597C13D831ec7", + "Decimals": 6, + "Blockchain": "Ethereum" + }, + "Volume": 504424765.759824, + "VolumeUSD": 0, + "Index": 0 + } + ] + """ + end + + # cspell:enable +end diff --git a/apps/explorer/test/support/benchmark_case.ex b/apps/explorer/test/support/benchmark_case.ex new file mode 100644 index 000000000000..fa65f511164e --- /dev/null +++ b/apps/explorer/test/support/benchmark_case.ex @@ -0,0 +1,54 @@ +defmodule Explorer.BenchmarkCase do + @moduledoc """ + This module defines the benchmark case to be used by benchmarks. + + This module provides common setup and utilities for benchmarking, + similar to DataCase but optimized for benchmarking needs, including: + + - Database sandbox management + - Factory imports + - Common helpers + + ## Example + + ```elixir + defmodule Explorer.MyBenchmark do + use Explorer.BenchmarkCase + + def run do + Benchee.run(...) + end + end + + # Run the benchmark + Explorer.MyBenchmark.run() + ``` + """ + + defmacro __using__(_opts) do + caller_file = __CALLER__.file + path = caller_file |> String.replace(Path.extname(caller_file), ".benchee") + + quote do + import Explorer.Factory + + @path unquote(path) + + @doc """ + Resets the database for consistent benchmarks + """ + def reset_db do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo, ownership_timeout: :infinity) + end + + @doc """ + Setup common benchmark environment + """ + def benchmark_setup do + {:ok, _} = Application.ensure_all_started(:explorer) + + :ok + end + end + end +end diff --git a/apps/explorer/test/support/fixture/exchange_rates/coin_gecko.json b/apps/explorer/test/support/fixture/exchange_rates/coin_gecko.json deleted file mode 100644 index cc110eb402d9..000000000000 --- a/apps/explorer/test/support/fixture/exchange_rates/coin_gecko.json +++ /dev/null @@ -1,1806 +0,0 @@ -{ - "asset_platform_id": null, - "block_time_in_minutes": 0, - "categories": [], - "coingecko_rank": 376, - "coingecko_score": 33.7, - "community_data": { - "facebook_likes": 0, - "reddit_accounts_active_48h": 0, - "reddit_average_comments_48h": 0.0, - "reddit_average_posts_48h": 0.0, - "reddit_subscribers": 0, - "telegram_channel_user_count": 5125, - "twitter_followers": 17737 - }, - "community_score": 9.403, - "country_origin": "", - "description": { - "ar": "", - "de": "", - "en": "", - "es": "", - "fr": "", - "hu": "", - "id": "", - "it": "", - "ja": "", - "ko": "", - "nl": "", - "pl": "", - "pt": "", - "ro": "", - "ru": "", - "sv": "", - "th": "", - "tr": "", - "vi": "", - "zh": "", - "zh-tw": "" - }, - "developer_data": { - "closed_issues": 0, - "code_additions_deletions_4_weeks": { - "additions": 0, - "deletions": 0 - }, - "commit_count_4_weeks": 0, - "forks": 44, - "last_4_weeks_commit_activity_series": [ - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0 - ], - "pull_request_contributors": 6, - "pull_requests_merged": 133, - "stars": 58, - "subscribers": 8, - "total_issues": 0 - }, - "developer_score": 52.776, - "genesis_date": null, - "id": "poa-network", - "image": { - "large": "https://assets.coingecko.com/coins/images/3157/large/poa-network.png?1548331565", - "small": "https://assets.coingecko.com/coins/images/3157/small/poa-network.png?1548331565", - "thumb": "https://assets.coingecko.com/coins/images/3157/thumb/poa-network.png?1548331565" - }, - "last_updated": "2019-08-21T08:36:49.371Z", - "links": { - "announcement_url": [ - "", - "" - ], - "bitcointalk_thread_identifier": null, - "blockchain_site": [ - "https://blockscout.com/poa/core/", - "", - "", - "", - "" - ], - "chat_url": [ - "", - "", - "" - ], - "facebook_username": "", - "homepage": [ - "https://poa.network/", - "", - "" - ], - "official_forum_url": [ - "", - "", - "" - ], - "repos_url": { - "bitbucket": [], - "github": [ - "https://github.com/poanetwork/poa-network-consensus-contracts" - ] - }, - "subreddit_url": null, - "telegram_channel_identifier": "oraclesnetwork", - "twitter_screen_name": "poanetwork" - }, - "liquidity_score": 19.334, - "localization": { - "ar": "POA Network", - "de": "POA Network", - "en": "POA Network", - "es": "POA Network", - "fr": "POA Network", - "hu": "POA Network", - "id": "POA Network", - "it": "POA Network", - "ja": "ポアネットワーク", - "ko": "POA 네트워크", - "nl": "POA Network", - "pl": "POA Network", - "pt": "POA Network", - "ro": "POA Network", - "ru": "POA Network", - "sv": "POA Network", - "th": "POA Network", - "tr": "POA Network", - "vi": "POA Network", - "zh": "POA Network", - "zh-tw": "POA Network" - }, - "market_cap_rank": 499, - "market_data": { - "ath": { - "aed": 3.06, - "ars": 18.93, - "aud": 1.25, - "bch": 0.00076772, - "bdt": 70.74, - "bhd": 0.314495, - "bmd": 0.833914, - "bnb": 0.01322268, - "brl": 3.36, - "btc": 0.00010137, - "cad": 1.2, - "chf": 0.938803, - "clp": 519.58, - "cny": 5.94, - "czk": 17.83, - "dkk": 5.87, - "eos": 0.02444974, - "eth": 0.0011725, - "eur": 0.787803, - "gbp": 0.689151, - "hkd": 7.35, - "huf": 219.96, - "idr": 13179.33, - "ils": 2.97, - "inr": 62.94, - "jpy": 102.6, - "krw": 1000.83, - "kwd": 0.251571, - "lkr": 131.79, - "ltc": 0.00537407, - "mmk": 1125.41, - "mxn": 18.15, - "myr": 3.76, - "nok": 6.67, - "nzd": 1.35, - "php": 48.59, - "pkr": 96.55, - "pln": 3.34, - "rub": 58.27, - "sar": 3.13, - "sek": 8.13, - "sgd": 1.25, - "thb": 26.65, - "try": 3.54, - "twd": 27.89, - "uah": 1.44, - "usd": 0.935923, - "vef": 58307, - "vnd": 1281.67, - "xag": 0.056398, - "xau": 0.00071129, - "xdr": 0.656022, - "xlm": 0.58965135, - "xrp": 0.2920468, - "zar": 11.63 - }, - "ath_change_percentage": { - "aed": -98.3819, - "ars": -96.09901, - "aud": -98.40941, - "bch": -94.11046, - "bdt": -98.39058, - "bhd": -98.38261, - "bmd": -98.38194, - "bnb": -96.24179, - "brl": -98.37442, - "btc": -98.68937, - "cad": -98.49893, - "chf": -98.59269, - "clp": -98.15517, - "cny": -98.39701, - "czk": -98.2406, - "dkk": -98.45473, - "eos": -84.56025, - "eth": -93.8673, - "eur": -98.45627, - "gbp": -98.38711, - "hkd": -98.55948, - "huf": -98.18974, - "idr": -98.54127, - "ils": -98.39903, - "inr": -98.46908, - "jpy": -98.59899, - "krw": -98.37868, - "kwd": -98.3684, - "lkr": -98.16735, - "ltc": -96.51103, - "mmk": -98.17585, - "mxn": -98.53298, - "myr": -98.49907, - "nok": -98.18519, - "nzd": -98.43938, - "php": -98.54716, - "pkr": -97.76349, - "pln": -98.41583, - "rub": -98.4658, - "sar": -98.3821, - "sek": -98.39542, - "sgd": -98.51152, - "thb": -98.44172, - "try": -97.81784, - "twd": -98.48322, - "uah": -76.52065, - "usd": -98.5583, - "vef": -94.2496, - "vnd": -75.59837, - "xag": -98.59644, - "xau": -98.73411, - "xdr": -98.49988, - "xlm": -66.24383, - "xrp": -82.56607, - "zar": -98.23009 - }, - "ath_date": { - "aed": "2018-05-10T00:00:00.000Z", - "ars": "2018-05-10T00:00:00.000Z", - "aud": "2018-05-10T09:45:31.809Z", - "bch": "2018-04-14T00:00:00.000Z", - "bdt": "2018-05-10T00:00:00.000Z", - "bhd": "2018-05-10T00:00:00.000Z", - "bmd": "2018-05-10T00:00:00.000Z", - "bnb": "2018-10-16T12:40:17.749Z", - "brl": "2018-05-10T09:45:31.809Z", - "btc": "2018-05-10T09:45:31.809Z", - "cad": "2018-05-10T09:45:31.809Z", - "chf": "2018-05-10T09:45:31.809Z", - "clp": "2018-05-10T00:00:00.000Z", - "cny": "2018-05-10T09:45:31.809Z", - "czk": "2018-05-10T00:00:00.000Z", - "dkk": "2018-05-10T09:45:31.809Z", - "eos": "2018-10-16T12:40:17.749Z", - "eth": "2018-04-09T00:00:00.000Z", - "eur": "2018-05-10T09:45:31.809Z", - "gbp": "2018-05-10T09:45:31.809Z", - "hkd": "2018-05-10T09:45:31.809Z", - "huf": "2018-05-10T00:00:00.000Z", - "idr": "2018-05-10T09:45:31.809Z", - "ils": "2018-05-10T00:00:00.000Z", - "inr": "2018-05-10T09:45:31.809Z", - "jpy": "2018-05-10T09:45:31.809Z", - "krw": "2018-05-10T09:45:31.809Z", - "kwd": "2018-05-10T00:00:00.000Z", - "lkr": "2018-05-10T00:00:00.000Z", - "ltc": "2018-05-10T00:00:00.000Z", - "mmk": "2018-05-10T00:00:00.000Z", - "mxn": "2018-05-10T09:45:31.809Z", - "myr": "2018-05-10T09:45:31.809Z", - "nok": "2018-05-10T00:00:00.000Z", - "nzd": "2018-05-10T09:45:31.809Z", - "php": "2018-05-10T09:45:31.809Z", - "pkr": "2018-05-10T00:00:00.000Z", - "pln": "2018-05-10T09:45:31.809Z", - "rub": "2018-05-10T09:45:31.809Z", - "sar": "2018-05-10T00:00:00.000Z", - "sek": "2018-05-10T09:45:31.809Z", - "sgd": "2018-05-10T09:45:31.809Z", - "thb": "2018-05-10T00:00:00.000Z", - "try": "2018-05-10T00:00:00.000Z", - "twd": "2018-05-10T09:45:31.809Z", - "uah": "2019-05-27T12:49:29.532Z", - "usd": "2018-05-10T09:45:31.809Z", - "vef": "2018-05-10T00:00:00.000Z", - "vnd": "2019-05-27T12:49:29.532Z", - "xag": "2018-05-10T09:45:31.809Z", - "xau": "2018-05-10T09:45:31.809Z", - "xdr": "2018-05-10T09:45:31.809Z", - "xlm": "2018-10-16T12:49:26.379Z", - "xrp": "2018-10-16T12:40:17.749Z", - "zar": "2018-05-10T09:45:31.809Z" - }, - "circulating_supply": 220167621.0, - "current_price": { - "aed": 0.04942712, - "ars": 0.73641, - "aud": 0.0198328, - "bch": 4.486e-05, - "bdt": 1.14, - "bhd": 0.00507448, - "bmd": 0.01345698, - "bnb": 0.00049505, - "brl": 0.054544, - "btc": 1.32e-06, - "cad": 0.0178991, - "chf": 0.01317061, - "clp": 9.55, - "cny": 0.095047, - "czk": 0.312629, - "dkk": 0.090366, - "eos": 0.0037559, - "eth": 7.147e-05, - "eur": 0.01211961, - "gbp": 0.01107566, - "hkd": 0.105541, - "huf": 3.97, - "idr": 191.75, - "ils": 0.04745883, - "inr": 0.960893, - "jpy": 1.43, - "krw": 16.19, - "kwd": 0.00409361, - "lkr": 2.4, - "ltc": 0.00018598, - "mmk": 20.47, - "mxn": 0.26536, - "myr": 0.056211, - "nok": 0.120606, - "nzd": 0.02100918, - "php": 0.704042, - "pkr": 2.15, - "pln": 0.052749, - "rub": 0.891153, - "sar": 0.050476, - "sek": 0.129909, - "sgd": 0.01861909, - "thb": 0.414071, - "try": 0.076819, - "twd": 0.422042, - "uah": 0.338308, - "usd": 0.01345698, - "vef": 3343.89, - "vnd": 311.66, - "xag": 0.00078867, - "xau": 8.98e-06, - "xdr": 0.00981471, - "xlm": 0.1975895, - "xrp": 0.05067322, - "zar": 0.204922 - }, - "high_24h": { - "aed": 0.053611, - "ars": 0.810805, - "aud": 0.02152674, - "bch": 4.628e-05, - "bdt": 1.23, - "bhd": 0.00550347, - "bmd": 0.0145949, - "bnb": 0.00052215, - "brl": 0.058875, - "btc": 1.37e-06, - "cad": 0.01943935, - "chf": 0.01428979, - "clp": 10.36, - "cny": 0.103044, - "czk": 0.339287, - "dkk": 0.098076, - "eos": 0.00402026, - "eth": 7.423e-05, - "eur": 0.01315448, - "gbp": 0.01201087, - "hkd": 0.114474, - "huf": 4.31, - "idr": 208.11, - "ils": 0.051491, - "inr": 1.04, - "jpy": 1.55, - "krw": 17.63, - "kwd": 0.00443977, - "lkr": 2.6, - "ltc": 0.00019631, - "mmk": 22.24, - "mxn": 0.288202, - "myr": 0.061014, - "nok": 0.131376, - "nzd": 0.02274396, - "php": 0.76332, - "pkr": 2.34, - "pln": 0.057291, - "rub": 0.972261, - "sar": 0.054726, - "sek": 0.14165, - "sgd": 0.02021141, - "thb": 0.449117, - "try": 0.083602, - "twd": 0.457778, - "uah": 0.367456, - "usd": 0.0145949, - "vef": 3626.65, - "vnd": 338.27, - "xag": 0.00085369, - "xau": 9.72e-06, - "xdr": 0.01064365, - "xlm": 0.20827298, - "xrp": 0.05318558, - "zar": 0.223696 - }, - "last_updated": "2019-08-21T08:36:49.371Z", - "low_24h": { - "aed": 0.0491677, - "ars": 0.732555, - "aud": 0.0197477, - "bch": 4.364e-05, - "bdt": 1.13, - "bhd": 0.00504693, - "bmd": 0.01338635, - "bnb": 0.00049273, - "brl": 0.054256, - "btc": 1.3e-06, - "cad": 0.01782004, - "chf": 0.01311131, - "clp": 9.51, - "cny": 0.094517, - "czk": 0.311235, - "dkk": 0.089982, - "eos": 0.00373147, - "eth": 7.098e-05, - "eur": 0.01206786, - "gbp": 0.01103078, - "hkd": 0.104993, - "huf": 3.95, - "idr": 190.79, - "ils": 0.04723279, - "inr": 0.958004, - "jpy": 1.43, - "krw": 16.1, - "kwd": 0.00406945, - "lkr": 2.39, - "ltc": 0.00018228, - "mmk": 20.37, - "mxn": 0.264134, - "myr": 0.055903, - "nok": 0.120189, - "nzd": 0.02091282, - "php": 0.700581, - "pkr": 2.14, - "pln": 0.052569, - "rub": 0.886879, - "sar": 0.050214, - "sek": 0.129578, - "sgd": 0.01853166, - "thb": 0.412199, - "try": 0.076627, - "twd": 0.4198, - "uah": 0.336532, - "usd": 0.01338635, - "vef": 3326.34, - "vnd": 310.32, - "xag": 0.00078625, - "xau": 8.93e-06, - "xdr": 0.0097632, - "xlm": 0.19635382, - "xrp": 0.05037993, - "zar": 0.204544 - }, - "market_cap": { - "aed": 10882251, - "ars": 162133701, - "aud": 4366541, - "bch": 9877, - "bdt": 249997342, - "bhd": 1117236, - "bmd": 2962791, - "bnb": 108994, - "brl": 12008719, - "btc": 290.732, - "cad": 3940802, - "chf": 2899743, - "clp": 2101619696, - "cny": 20926193, - "czk": 68830802, - "dkk": 19895675, - "eos": 826927, - "eth": 15736, - "eur": 2668346, - "gbp": 2438501, - "hkd": 23236814, - "huf": 873552757, - "idr": 42217105275, - "ils": 10448899, - "inr": 211557455, - "jpy": 315583737, - "krw": 3564593111, - "kwd": 901281, - "lkr": 529480380, - "ltc": 40948, - "mmk": 4507718929, - "mxn": 58423768, - "myr": 12375904, - "nok": 26553448, - "nzd": 4625542, - "php": 155007300, - "pkr": 474133000, - "pln": 11613646, - "rub": 196202962, - "sar": 11113192, - "sek": 28601664, - "sgd": 4099321, - "thb": 91165079, - "try": 16912998, - "twd": 92920077, - "uah": 74484427, - "usd": 2962791, - "vef": 736216950688, - "vnd": 68617204931, - "xag": 173639, - "xau": 1976.89, - "xdr": 2160882, - "xlm": 43502811, - "xrp": 11156602, - "zar": 45117091 - }, - "market_cap_change_24h": -160615.84039035, - "market_cap_change_24h_in_currency": { - "aed": -590899.65443375, - "ars": -9124373.5980551, - "aud": -239353.78003874, - "bch": 102.549, - "bdt": -13867874.4362154, - "bhd": -60251.00254582, - "bmd": -160615.84039035, - "bnb": -1507.543569741, - "brl": -718070.38245432, - "btc": 0.03769596, - "cad": -221121.61427417, - "chf": -161095.94704691, - "clp": -127118490.79824165, - "cny": -1134429.68067712, - "czk": -3925858.7665586, - "dkk": -1136450.3780936, - "eos": -24548.1071608166, - "eth": -56.533938946, - "eur": -152193.5816755, - "gbp": -144996.42715597, - "hkd": -1263688.72303977, - "huf": -48860628.81047357, - "idr": -2346101857.217293, - "ils": -576162.08791993, - "inr": -12451722.53525829, - "jpy": -16857632.06248754, - "krw": -209731718.0214291, - "kwd": -49171.67933105, - "lkr": -26048761.01962376, - "ltc": -309.0831858893, - "mmk": -251710124.48363495, - "mxn": -3503488.89323037, - "myr": -687829.52792143, - "nok": -1513569.75537506, - "nzd": -244980.03753899, - "php": -8388263.35503852, - "pkr": -26642819.2931419, - "pln": -696325.10549893, - "rub": -12221976.62125358, - "sar": -601457.67784725, - "sek": -1668070.90346983, - "sgd": -230174.18438848, - "thb": -5107689.97148955, - "try": -927927.24538566, - "twd": -5107834.82635887, - "uah": -4241989.31565817, - "usd": -160615.84039035, - "vef": -39911051485.156494, - "vnd": -4088394297.5983276, - "xag": -9606.58322042, - "xau": -102.32803274, - "xdr": -115659.93877989, - "xlm": -1276957.0373063833, - "xrp": -122973.5124849025, - "zar": -2969034.78716005 - }, - "market_cap_change_percentage_24h": -5.14233, - "market_cap_change_percentage_24h_in_currency": { - "aed": -5.15028, - "ars": -5.32785, - "aud": -5.19668, - "bch": 1.0491, - "bdt": -5.25567, - "bhd": -5.11691, - "bmd": -5.14233, - "bnb": -1.36428, - "brl": -5.6422, - "btc": 0.01297, - "cad": -5.31297, - "chf": -5.26313, - "clp": -5.70361, - "cny": -5.14233, - "czk": -5.39588, - "dkk": -5.4034, - "eos": -2.88301, - "eth": -0.35797, - "eur": -5.3959, - "gbp": -5.61241, - "hkd": -5.15781, - "huf": -5.29704, - "idr": -5.26466, - "ils": -5.22593, - "inr": -5.55858, - "jpy": -5.07086, - "krw": -5.5568, - "kwd": -5.1735, - "lkr": -4.689, - "ltc": -0.74917, - "mmk": -5.28866, - "mxn": -5.65743, - "myr": -5.26518, - "nok": -5.3927, - "nzd": -5.02985, - "php": -5.13372, - "pkr": -5.32031, - "pln": -5.65659, - "rub": -5.86397, - "sar": -5.13424, - "sek": -5.51069, - "sgd": -5.31642, - "thb": -5.30544, - "try": -5.20112, - "twd": -5.21059, - "uah": -5.38827, - "usd": -5.14233, - "vef": -5.14233, - "vnd": -5.62322, - "xag": -5.24248, - "xau": -4.92146, - "xdr": -5.08051, - "xlm": -2.85164, - "xrp": -1.09023, - "zar": -6.17441 - }, - "market_cap_rank": 499, - "price_change_24h": -0.00070501, - "price_change_24h_in_currency": { - "aed": -0.00259384, - "ars": -0.0401764, - "aud": -0.00106049, - "bch": 4.401e-07, - "bdt": -0.06091731, - "bhd": -0.00026442, - "bmd": -0.00070501, - "bnb": -7.9537e-06, - "brl": -0.00316161, - "btc": -7e-10, - "cad": -0.000975, - "chf": -0.00070391, - "clp": -0.55975334, - "cny": -0.00498938, - "czk": -0.01720638, - "dkk": -0.00497873, - "eos": -0.0001041039, - "eth": -1.756e-07, - "eur": -0.00066664, - "gbp": -0.00063744, - "hkd": -0.00554812, - "huf": -0.21652693, - "idr": -10.32117361, - "ils": -0.00254123, - "inr": -0.05433204, - "jpy": -0.07396812, - "krw": -0.92440016, - "kwd": -0.00021588, - "lkr": -0.11324611, - "ltc": -1.1432e-06, - "mmk": -1.10591975, - "mxn": -0.01564173, - "myr": -0.00303114, - "nok": -0.0066618, - "nzd": -0.00108422, - "php": -0.03666507, - "pkr": -0.11708205, - "pln": -0.0030605, - "rub": -0.05396307, - "sar": -0.00263569, - "sek": -0.00737328, - "sgd": -0.00101489, - "thb": -0.02240118, - "try": -0.00415931, - "twd": -0.02243055, - "uah": -0.01864934, - "usd": -0.00070501, - "vef": -175.18566197, - "vnd": -17.91454955, - "xag": -4.268e-05, - "xau": -4.5e-07, - "xdr": -0.00050746, - "xlm": -0.0043814329, - "xrp": -0.0005808077, - "zar": -0.01325238 - }, - "price_change_percentage_14d": -20.09212, - "price_change_percentage_14d_in_currency": { - "aed": -20.09664, - "ars": -3.40252, - "aud": -20.3743, - "bch": -10.35771, - "bdt": -20.21051, - "bhd": -20.07198, - "bmd": -20.09212, - "bnb": -18.90438, - "brl": -18.21564, - "btc": -10.20835, - "cad": -19.99931, - "chf": -19.86474, - "clp": -20.70274, - "cny": -19.67703, - "czk": -19.20224, - "dkk": -19.44946, - "eos": -6.27667, - "eth": -3.85819, - "eur": -19.3615, - "gbp": -19.99287, - "hkd": -20.04155, - "huf": -18.72941, - "idr": -19.92585, - "ils": -19.11955, - "inr": -19.53455, - "jpy": -19.93789, - "krw": -20.72061, - "kwd": -20.08213, - "lkr": -19.39749, - "ltc": 3.01755, - "mmk": -19.52559, - "mxn": -19.64657, - "myr": -20.39088, - "nok": -19.55283, - "nzd": -18.54, - "php": -19.79625, - "pkr": -19.69932, - "pln": -18.70635, - "rub": -18.9447, - "sar": -20.11299, - "sek": -19.43426, - "sgd": -19.93866, - "thb": -19.96922, - "try": -17.50796, - "twd": -20.26871, - "uah": -21.43567, - "usd": -20.09212, - "vef": -20.09212, - "vnd": -20.39781, - "xag": -22.82706, - "xau": -21.25807, - "xdr": -19.80447, - "xlm": -6.90503, - "xrp": -5.82792, - "zar": -18.53383 - }, - "price_change_percentage_1h_in_currency": { - "aed": -0.65082, - "ars": -0.65814, - "aud": -0.81241, - "bch": 1.34916, - "bdt": -0.64945, - "bhd": -0.62736, - "bmd": -0.65082, - "bnb": -0.66128, - "brl": -0.65109, - "btc": -0.64949, - "cad": -0.75327, - "chf": -0.6566, - "clp": -0.81121, - "cny": -0.61001, - "czk": -0.71198, - "dkk": -0.72865, - "eos": -0.27623, - "eth": -0.72711, - "eur": -0.73062, - "gbp": -0.79594, - "hkd": -0.65424, - "huf": -0.65304, - "idr": -0.69877, - "ils": -0.68895, - "inr": -0.85681, - "jpy": -0.70261, - "krw": -0.64504, - "kwd": -0.68347, - "lkr": -0.66193, - "ltc": 0.20669, - "mmk": -0.65082, - "mxn": -0.79588, - "myr": -0.58632, - "nok": -0.94166, - "nzd": -0.77361, - "php": -0.65651, - "pkr": -0.65082, - "pln": -0.82133, - "rub": -0.80523, - "sar": -0.62904, - "sek": -0.92466, - "sgd": -0.72845, - "thb": -0.71559, - "try": -0.93842, - "twd": -0.63818, - "uah": -0.65082, - "usd": -0.65082, - "vef": -0.65082, - "vnd": -0.72098, - "xag": -0.86625, - "xau": -0.66422, - "xdr": -0.65082, - "xlm": -0.6397, - "xrp": -0.05073, - "zar": -1.25864 - }, - "price_change_percentage_1y": -80.39683, - "price_change_percentage_1y_in_currency": { - "aed": -80.39794, - "ars": -61.05676, - "aud": -78.80429, - "bch": -66.67604, - "bdt": -80.26581, - "bhd": -80.39891, - "bmd": -80.39683, - "brl": -79.97401, - "btc": -87.93415, - "cad": -80.00734, - "chf": -80.63119, - "clp": -79.21186, - "cny": -79.80673, - "czk": -79.67769, - "dkk": -79.72199, - "eth": -71.77736, - "eur": -79.71573, - "gbp": -79.34655, - "hkd": -80.41399, - "huf": -79.49619, - "idr": -80.5156, - "ils": -81.08059, - "inr": -79.93604, - "jpy": -81.01454, - "krw": -78.90452, - "kwd": -80.34132, - "lkr": -78.15787, - "ltc": -85.4361, - "mmk": -80.27721, - "mxn": -79.65762, - "myr": -80.02222, - "nok": -79.18424, - "nzd": -79.67365, - "php": -80.76747, - "pkr": -74.51734, - "pln": -79.50758, - "rub": -80.62327, - "sar": -80.39647, - "sek": -79.31728, - "sgd": -80.18923, - "thb": -81.67711, - "try": -81.65737, - "twd": -79.98887, - "uah": -82.20648, - "usd": -80.39683, - "vef": -80.39558, - "vnd": -80.34916, - "xag": -83.03235, - "xau": -84.4215, - "xdr": -80.03907, - "zar": -79.43625 - }, - "price_change_percentage_200d": -50.1114, - "price_change_percentage_200d_in_currency": { - "aed": -50.11196, - "ars": -26.51722, - "aud": -46.67198, - "bch": -80.79243, - "bdt": -49.71149, - "bhd": -50.09578, - "bmd": -50.1114, - "bnb": -87.93302, - "brl": -44.75887, - "btc": -83.02991, - "cad": -49.34427, - "chf": -50.94689, - "clp": -45.88183, - "cny": -47.75857, - "czk": -48.37087, - "dkk": -48.59098, - "eos": -67.57399, - "eth": -71.6239, - "eur": -48.52075, - "gbp": -46.27358, - "hkd": -50.136, - "huf": -46.91923, - "idr": -49.00487, - "ils": -51.63145, - "inr": -50.15368, - "jpy": -51.46878, - "krw": -46.36111, - "kwd": -49.91845, - "lkr": -49.53248, - "ltc": -77.44522, - "mmk": -49.82943, - "mxn": -48.49618, - "myr": -49.12989, - "nok": -47.01887, - "nzd": -46.2816, - "php": -50.10377, - "pkr": -42.33563, - "pln": -47.7461, - "rub": -49.52654, - "sar": -50.10515, - "sek": -46.7992, - "sgd": -48.88484, - "thb": -50.95616, - "try": -45.32063, - "twd": -49.1634, - "uah": -54.76622, - "usd": -50.1114, - "vef": -50.1114, - "vnd": -50.2089, - "xag": -53.50147, - "xau": -56.13406, - "xdr": -49.07152, - "xlm": -39.53366, - "xrp": -42.37148, - "zar": -42.98063 - }, - "price_change_percentage_24h": -4.97817, - "price_change_percentage_24h_in_currency": { - "aed": -4.98614, - "ars": -5.17346, - "aud": -5.07575, - "bch": 0.99066, - "bdt": -5.0917, - "bhd": -4.95271, - "bmd": -4.97817, - "bnb": -1.58124, - "brl": -5.4789, - "btc": -0.05257, - "cad": -5.16583, - "chf": -5.07341, - "clp": -5.53921, - "cny": -4.98758, - "czk": -5.21666, - "dkk": -5.22182, - "eos": -2.69699, - "eth": -0.24506, - "eur": -5.21371, - "gbp": -5.44209, - "hkd": -4.99428, - "huf": -5.17487, - "idr": -5.1077, - "ils": -5.08246, - "inr": -5.35173, - "jpy": -4.90717, - "krw": -5.40119, - "kwd": -5.00939, - "lkr": -4.49721, - "ltc": -0.61091, - "mmk": -5.12476, - "mxn": -5.56641, - "myr": -5.1165, - "nok": -5.23449, - "nzd": -4.90745, - "php": -4.95001, - "pkr": -5.15646, - "pln": -5.48383, - "rub": -5.70968, - "sar": -4.96254, - "sek": -5.3709, - "sgd": -5.16905, - "thb": -5.13232, - "try": -5.13634, - "twd": -5.04655, - "uah": -5.22453, - "usd": -4.97817, - "vef": -4.97817, - "vnd": -5.43568, - "xag": -5.1341, - "xau": -4.8084, - "xdr": -4.91624, - "xlm": -2.16934, - "xrp": -1.13319, - "zar": -6.07423 - }, - "price_change_percentage_30d": -34.83569, - "price_change_percentage_30d_in_currency": { - "aed": -34.83544, - "ars": -15.98426, - "aud": -32.35353, - "bch": -30.11906, - "bdt": -34.93771, - "bhd": -34.7457, - "bmd": -34.83569, - "bnb": -27.08407, - "brl": -29.51868, - "btc": -32.08124, - "cad": -33.60701, - "chf": -35.09987, - "clp": -32.84418, - "cny": -33.12269, - "czk": -33.51308, - "dkk": -34.24389, - "eos": -21.08346, - "eth": -21.69788, - "eur": -34.16271, - "gbp": -32.89529, - "hkd": -34.53494, - "huf": -33.76133, - "idr": -33.48833, - "ils": -35.07099, - "inr": -32.4077, - "jpy": -35.60577, - "krw": -33.33925, - "kwd": -34.83162, - "lkr": -33.78522, - "ltc": -10.07379, - "mmk": -34.67478, - "mxn": -32.46726, - "myr": -33.79734, - "nok": -31.87864, - "nzd": -31.23716, - "php": -33.28065, - "pkr": -34.80085, - "pln": -32.59928, - "rub": -31.5309, - "sar": -34.83447, - "sek": -32.96828, - "sgd": -33.73135, - "thb": -35.04678, - "try": -34.23351, - "twd": -34.17354, - "uah": -36.37221, - "usd": -34.83569, - "vef": -34.83569, - "vnd": -35.19581, - "xag": -38.03606, - "xau": -38.0235, - "xdr": -34.25025, - "xlm": -12.74201, - "xrp": -18.8697, - "zar": -28.75132 - }, - "price_change_percentage_60d": -61.24989, - "price_change_percentage_60d_in_currency": { - "aed": -61.25202, - "ars": -50.32249, - "aud": -60.412, - "bch": -43.0945, - "bdt": -61.20391, - "bhd": -61.23344, - "bmd": -61.24989, - "bnb": -44.49169, - "brl": -58.90604, - "btc": -61.42639, - "cad": -61.04496, - "chf": -61.10703, - "clp": -59.79652, - "cny": -60.16104, - "czk": -60.03065, - "dkk": -60.38034, - "eos": -23.51938, - "eth": -39.23291, - "eur": -60.26215, - "gbp": -59.36192, - "hkd": -61.10542, - "huf": -59.91464, - "idr": -60.90777, - "ils": -62.26651, - "inr": -60.23354, - "jpy": -61.53855, - "krw": -59.78067, - "kwd": -61.16844, - "lkr": -60.71768, - "ltc": -25.65935, - "mmk": -61.20413, - "mxn": -60.04539, - "myr": -60.87356, - "nok": -59.13075, - "nzd": -60.12056, - "php": -60.49041, - "pkr": -60.53743, - "pln": -59.42569, - "rub": -59.28727, - "sar": -61.24451, - "sek": -59.99926, - "sgd": -60.43237, - "thb": -61.1805, - "try": -62.00162, - "twd": -60.74424, - "uah": -62.83383, - "usd": -61.24989, - "vef": -61.24989, - "vnd": -61.16958, - "xag": -65.19693, - "xau": -63.82007, - "xdr": -60.62729, - "xlm": -29.85582, - "xrp": -35.11899, - "zar": -58.83133 - }, - "price_change_percentage_7d": -12.91815, - "price_change_percentage_7d_in_currency": { - "aed": -12.92308, - "ars": -14.25115, - "aud": -12.76443, - "bch": 0.88592, - "bdt": -13.147, - "bhd": -12.88673, - "bmd": -12.91815, - "bnb": -5.78627, - "brl": -10.95714, - "btc": -6.92663, - "cad": -12.35787, - "chf": -12.68629, - "clp": -12.25865, - "cny": -12.67706, - "czk": -12.47009, - "dkk": -12.42719, - "eos": -0.91188, - "eth": -3.2526, - "eur": -12.3442, - "gbp": -13.56715, - "hkd": -12.94473, - "huf": -11.29007, - "idr": -13.63998, - "ils": -11.80257, - "inr": -12.53093, - "jpy": -12.98836, - "krw": -13.1693, - "kwd": -12.90068, - "lkr": -11.99684, - "ltc": 1.8824, - "mmk": -12.2934, - "mxn": -11.39186, - "myr": -13.13461, - "nok": -11.99025, - "nzd": -12.14546, - "php": -12.50757, - "pkr": -13.04362, - "pln": -11.96443, - "rub": -11.16958, - "sar": -12.91768, - "sek": -11.88369, - "sgd": -12.90808, - "thb": -12.98884, - "try": -10.77039, - "twd": -12.26523, - "uah": -12.97908, - "usd": -12.91815, - "vef": -12.91815, - "vnd": -13.40154, - "xag": -13.34686, - "xau": -12.77828, - "xdr": -12.6042, - "xlm": -3.71389, - "xrp": -2.40223, - "zar": -12.40714 - }, - "roi": null, - "total_supply": 252193195.0, - "total_volume": { - "aed": 440559, - "ars": 6563849, - "aud": 176776, - "bch": 399.879, - "bdt": 10120936, - "bhd": 45230, - "bmd": 119946, - "bnb": 4413, - "brl": 486163, - "btc": 11.770053, - "cad": 159540, - "chf": 117394, - "clp": 85082341, - "cny": 847180, - "czk": 2786558, - "dkk": 805460, - "eos": 33477, - "eth": 637.075, - "eur": 108026, - "gbp": 98721, - "hkd": 940723, - "huf": 35365063, - "idr": 1709124708, - "ils": 423015, - "inr": 8564729, - "jpy": 12776147, - "krw": 144309614, - "kwd": 36488, - "lkr": 21435577, - "ltc": 1658, - "mmk": 182491285, - "mxn": 2365238, - "myr": 501028, - "nok": 1074994, - "nzd": 187261, - "php": 6275343, - "pkr": 19194884, - "pln": 470169, - "rub": 7943115, - "sar": 449908, - "sek": 1157915, - "sgd": 165958, - "thb": 3690743, - "try": 684709, - "twd": 3761793, - "uah": 3015441, - "usd": 119946, - "vef": 29805136391, - "vnd": 2777910981, - "xag": 7029.61, - "xau": 80.03, - "xdr": 87482, - "xlm": 1761175, - "xrp": 451666, - "zar": 1826528 - } - }, - "name": "POA Network", - "public_interest_score": 27.778, - "public_interest_stats": { - "alexa_rank": 702581, - "bing_matches": 107000 - }, - "sentiment_votes_down_percentage": null, - "sentiment_votes_up_percentage": null, - "status_updates": [], - "symbol": "poa", - "tickers": [ - { - "base": "POA", - "bid_ask_spread_percentage": 1.503759, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.32e-06, - "eth": 7.139e-05, - "usd": 0.01344331 - }, - "converted_volume": { - "btc": 10.856044, - "eth": 587.121, - "usd": 110562 - }, - "is_anomaly": false, - "is_stale": false, - "last": 1.32e-06, - "last_fetch_at": "2019-08-21T08:34:21+00:00", - "last_traded_at": "2019-08-21T08:34:21+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "binance", - "name": "Binance" - }, - "target": "BTC", - "timestamp": "2019-08-21T08:34:21+00:00", - "trade_url": "https://www.binance.com/trade.html?symbol=POA_BTC", - "trust_score": "green", - "volume": 8224275.5227272725 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 1.729386, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.34e-06, - "eth": 7.226e-05, - "usd": 0.01360764 - }, - "converted_volume": { - "btc": 0.35219856, - "eth": 19.04774, - "usd": 3586.91 - }, - "is_anomaly": false, - "is_stale": false, - "last": 7.238e-05, - "last_fetch_at": "2019-08-21T08:34:26+00:00", - "last_traded_at": "2019-08-21T08:34:26+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "binance", - "name": "Binance" - }, - "target": "ETH", - "timestamp": "2019-08-21T08:34:26+00:00", - "trade_url": "https://www.binance.com/trade.html?symbol=POA_ETH", - "trust_score": "green", - "volume": 263595.07819839736 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 1.39165, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.34e-06, - "eth": 7.259e-05, - "usd": 0.01363846 - }, - "converted_volume": { - "btc": 0.24038472, - "eth": 13.008637, - "usd": 2444.17 - }, - "is_anomaly": false, - "is_stale": false, - "last": 0.000502, - "last_fetch_at": "2019-08-21T08:34:26+00:00", - "last_traded_at": "2019-08-21T08:13:08+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "binance", - "name": "Binance" - }, - "target": "BNB", - "timestamp": "2019-08-21T08:13:08+00:00", - "trade_url": "https://www.binance.com/trade.html?symbol=POA_BNB", - "trust_score": "green", - "volume": 179211.83864541832 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 2.985075, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.3e-06, - "eth": 7.036e-05, - "usd": 0.01324803 - }, - "converted_volume": { - "btc": 0.1595777, - "eth": 8.637423, - "usd": 1626.22 - }, - "is_anomaly": false, - "is_stale": false, - "last": 1.3e-06, - "last_fetch_at": "2019-08-21T08:36:07+00:00", - "last_traded_at": "2019-08-21T08:36:07+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "bibox", - "name": "Bibox" - }, - "target": "BTC", - "timestamp": "2019-08-21T08:36:07+00:00", - "trade_url": "https://www.bibox.com/exchange?coinPair=POA_BTC", - "trust_score": "yellow", - "volume": 122752.0737 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 3.247202, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.31e-06, - "eth": 7.077e-05, - "usd": 0.01332498 - }, - "converted_volume": { - "btc": 0.1127777, - "eth": 6.104291, - "usd": 1149.29 - }, - "is_anomaly": false, - "is_stale": false, - "last": 7.087e-05, - "last_fetch_at": "2019-08-21T08:35:50+00:00", - "last_traded_at": "2019-08-21T08:35:50+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "bibox", - "name": "Bibox" - }, - "target": "ETH", - "timestamp": "2019-08-21T08:35:50+00:00", - "trade_url": "https://www.bibox.com/exchange?coinPair=POA_ETH", - "trust_score": "yellow", - "volume": 86251.0552 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": 13.032676, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.16e-06, - "eth": 6.257e-05, - "usd": 0.01177973 - }, - "converted_volume": { - "btc": 0.00087672, - "eth": 0.04748752, - "usd": 8.94 - }, - "is_anomaly": false, - "is_stale": false, - "last": 1.1551e-06, - "last_fetch_at": "2019-08-21T08:37:38+00:00", - "last_traded_at": "2019-08-21T07:08:25+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "BTC", - "timestamp": "2019-08-21T07:08:25+00:00", - "trade_url": "https://hitbtc.com/POA20-to-BTC", - "trust_score": "red", - "volume": 759.0 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": 33.072445, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.57e-06, - "eth": 8.48e-05, - "usd": 0.01595278 - }, - "converted_volume": { - "btc": 0.00021915, - "eth": 0.0118714, - "usd": 2.23 - }, - "is_anomaly": false, - "is_stale": false, - "last": 8.4891e-05, - "last_fetch_at": "2019-08-21T08:37:21+00:00", - "last_traded_at": "2019-08-21T07:37:25+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "ETH", - "timestamp": "2019-08-21T07:37:25+00:00", - "trade_url": "https://hitbtc.com/POA20-to-ETH", - "trust_score": "red", - "volume": 140.0 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": 53.1004, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.15e-06, - "eth": 6.232e-05, - "usd": 0.0117249 - }, - "converted_volume": { - "btc": 7.478e-05, - "eth": 0.00405098, - "usd": 0.762119 - }, - "is_anomaly": false, - "is_stale": false, - "last": 0.0117249, - "last_fetch_at": "2019-08-21T08:37:46+00:00", - "last_traded_at": "2019-08-21T07:37:33+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "USD", - "timestamp": "2019-08-21T07:37:33+00:00", - "trade_url": "https://hitbtc.com/POA20-to-USD", - "trust_score": null, - "volume": 65.0 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": null, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.32e-06, - "eth": 7.134e-05, - "usd": 0.01339322 - }, - "converted_volume": { - "btc": 0.03831341, - "eth": 2.073192, - "usd": 389.22 - }, - "is_anomaly": false, - "is_stale": false, - "last": 0.034733328618915156, - "last_fetch_at": "2019-08-21T08:21:59+00:00", - "last_traded_at": "2019-08-21T08:21:59+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "bancor", - "name": "Bancor Network" - }, - "target": "BNT", - "timestamp": "2019-08-21T08:21:59+00:00", - "trade_url": null, - "trust_score": null, - "volume": 29061.03634563883 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": null, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.31e-06, - "eth": 7.074e-05, - "usd": 0.0132809 - }, - "converted_volume": { - "btc": 0.009587, - "eth": 0.51876605, - "usd": 97.39 - }, - "is_anomaly": false, - "is_stale": false, - "last": 7.0704231362995e-05, - "last_fetch_at": "2019-08-21T08:26:12+00:00", - "last_traded_at": "2019-08-21T08:21:13+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "uniswap", - "name": "Uniswap" - }, - "target": "ETH", - "timestamp": "2019-08-21T08:21:13+00:00", - "trade_url": null, - "trust_score": null, - "volume": 7333.316809051611 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 40.083051, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.33e-06, - "eth": 7.005e-05, - "usd": 0.01377146 - }, - "converted_volume": { - "btc": 0.0022752, - "eth": 0.12002061, - "usd": 23.59 - }, - "is_anomaly": false, - "is_stale": true, - "last": 7.004e-05, - "last_fetch_at": "2019-08-21T08:32:14+00:00", - "last_traded_at": "2019-08-18T18:43:13+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "ethfinex", - "name": "Ethfinex" - }, - "target": "ETH", - "timestamp": "2019-08-18T18:43:13+00:00", - "trade_url": "https://ethfinex.com", - "trust_score": "red", - "volume": 1713.3067 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 44.440399, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.33e-06, - "eth": 7.311e-05, - "usd": 0.013499 - }, - "converted_volume": { - "btc": 0.00442255, - "eth": 0.24305992, - "usd": 44.88 - }, - "is_anomaly": false, - "is_stale": true, - "last": 0.013499, - "last_fetch_at": "2019-08-21T08:32:13+00:00", - "last_traded_at": "2019-08-14T17:19:27+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "ethfinex", - "name": "Ethfinex" - }, - "target": "USD", - "timestamp": "2019-08-14T17:19:27+00:00", - "trade_url": "https://ethfinex.com", - "trust_score": "red", - "volume": 3324.44883078 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 14.012015, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.36e-06, - "eth": 7.49e-05, - "usd": 0.01446844 - }, - "converted_volume": { - "btc": 5.45e-06, - "eth": 0.00029958, - "usd": 0.057874 - }, - "is_anomaly": false, - "is_stale": true, - "last": 7.4743e-05, - "last_fetch_at": "2019-08-21T08:37:09+00:00", - "last_traded_at": "2019-08-21T02:52:09+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "ETH", - "timestamp": "2019-08-21T02:52:09+00:00", - "trade_url": "https://hitbtc.com/POA-to-ETH", - "trust_score": null, - "volume": 4.0 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 64.645979, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.36e-06, - "eth": 7.473e-05, - "usd": 0.01443604 - }, - "converted_volume": { - "btc": 2.72e-06, - "eth": 0.00014946, - "usd": 0.02887207 - }, - "is_anomaly": false, - "is_stale": true, - "last": 0.0143016, - "last_fetch_at": "2019-08-21T08:37:44+00:00", - "last_traded_at": "2019-08-21T02:52:25+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "DAI", - "timestamp": "2019-08-21T02:52:25+00:00", - "trade_url": "https://hitbtc.com/POA-to-DAI", - "trust_score": null, - "volume": 2.0 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 64.353312, - "coin_id": "poa-network", - "converted_last": { - "btc": 2.23e-06, - "eth": 0.00012505, - "usd": 0.02310309 - }, - "converted_volume": { - "btc": 0.00275182, - "eth": 0.15430951, - "usd": 28.51 - }, - "is_anomaly": true, - "is_stale": true, - "last": 2.23e-06, - "last_fetch_at": "2019-08-21T08:32:12+00:00", - "last_traded_at": "2019-08-17T12:50:06+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "ethfinex", - "name": "Ethfinex" - }, - "target": "BTC", - "timestamp": "2019-08-17T12:50:06+00:00", - "trade_url": "https://ethfinex.com", - "trust_score": "red", - "volume": 1234.0 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 21.080977, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.23e-06, - "eth": 6.72e-05, - "usd": 0.01322824 - }, - "converted_volume": { - "btc": 0.0, - "eth": 0.0, - "usd": 0.0 - }, - "is_anomaly": true, - "is_stale": true, - "last": 1.2331e-06, - "last_fetch_at": "2019-08-21T08:37:17+00:00", - "last_traded_at": "2019-08-20T18:50:31+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "BTC", - "timestamp": "2019-08-20T18:50:31+00:00", - "trade_url": "https://hitbtc.com/POA-to-BTC", - "trust_score": null, - "volume": 0.0 - }, - { - "base": "POA", - "bid_ask_spread_percentage": 36.441715, - "coin_id": "poa-network", - "converted_last": { - "btc": 1.22e-06, - "eth": 6.66e-05, - "usd": 0.0131106 - }, - "converted_volume": { - "btc": 0.0, - "eth": 0.0, - "usd": 0.0 - }, - "is_anomaly": true, - "is_stale": true, - "last": 0.0131106, - "last_fetch_at": "2019-08-21T08:37:14+00:00", - "last_traded_at": "2019-08-20T18:50:15+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "USD", - "timestamp": "2019-08-20T18:50:15+00:00", - "trade_url": "https://hitbtc.com/POA-to-USD", - "trust_score": null, - "volume": 0.0 - }, - { - "base": "POA20", - "bid_ask_spread_percentage": 70.656026, - "coin_id": "poa-network", - "converted_last": { - "btc": 7.522e-07, - "eth": 4.006e-05, - "usd": 0.0080416 - }, - "converted_volume": { - "btc": 0.0, - "eth": 0.0, - "usd": 0.0 - }, - "is_anomaly": true, - "is_stale": true, - "last": 0.008, - "last_fetch_at": "2019-08-21T08:37:29+00:00", - "last_traded_at": "2019-08-19T09:09:06+00:00", - "market": { - "has_trading_incentive": false, - "identifier": "hitbtc", - "name": "HitBTC" - }, - "target": "DAI", - "timestamp": "2019-08-19T09:09:06+00:00", - "trade_url": "https://hitbtc.com/POA20-to-DAI", - "trust_score": null, - "volume": 0.0 - } - ] -} diff --git a/apps/explorer/test/support/fixture/market/dia/native_coin_history.json b/apps/explorer/test/support/fixture/market/dia/native_coin_history.json new file mode 100644 index 000000000000..1dba2391a797 --- /dev/null +++ b/apps/explorer/test/support/fixture/market/dia/native_coin_history.json @@ -0,0 +1,29490 @@ +{ + "DataPoints": [ + { + "statement_id": 0, + "Series": [ + { + "name": "filters", + "columns": [ + "time", + "address", + "blockchain", + "exchange", + "filter", + "symbol", + "value" + ], + "values": [ + [ + "2025-11-26T23:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.151520522141 + ], + [ + "2025-11-26T23:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.9952309956097 + ], + [ + "2025-11-26T23:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.7906787878583 + ], + [ + "2025-11-26T23:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.016492941718 + ], + [ + "2025-11-26T23:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.348125747006 + ], + [ + "2025-11-26T23:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.155385791533 + ], + [ + "2025-11-26T23:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.405882509573 + ], + [ + "2025-11-26T23:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.354572989277 + ], + [ + "2025-11-26T23:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.819066265902 + ], + [ + "2025-11-26T23:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.676800539638 + ], + [ + "2025-11-26T23:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.317946538746 + ], + [ + "2025-11-26T23:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.465210342624 + ], + [ + "2025-11-26T23:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.7064363256623 + ], + [ + "2025-11-26T23:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.4536572902407 + ], + [ + "2025-11-26T23:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.4573464586233 + ], + [ + "2025-11-26T23:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.7946899822246 + ], + [ + "2025-11-26T23:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.813687093641 + ], + [ + "2025-11-26T23:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.629349725893 + ], + [ + "2025-11-26T23:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.738119120008 + ], + [ + "2025-11-26T23:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.32716019548 + ], + [ + "2025-11-26T23:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.803311656306 + ], + [ + "2025-11-26T23:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.7996816298323 + ], + [ + "2025-11-26T23:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.2930118929366 + ], + [ + "2025-11-26T23:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.9135444207636 + ], + [ + "2025-11-26T23:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.9010124855263 + ], + [ + "2025-11-26T23:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.161406928666 + ], + [ + "2025-11-26T23:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.077602092493 + ], + [ + "2025-11-26T23:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.397443868711 + ], + [ + "2025-11-26T23:03:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.79667545943 + ], + [ + "2025-11-26T23:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.2401761320916 + ], + [ + "2025-11-26T22:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.0524342154113 + ], + [ + "2025-11-26T22:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.9170363250464 + ], + [ + "2025-11-26T22:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.5615556587113 + ], + [ + "2025-11-26T22:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.287008682643 + ], + [ + "2025-11-26T22:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.8148994578582 + ], + [ + "2025-11-26T22:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.260801742472 + ], + [ + "2025-11-26T22:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.2842749893493 + ], + [ + "2025-11-26T22:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.684110569924 + ], + [ + "2025-11-26T22:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.6690954254645 + ], + [ + "2025-11-26T22:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.1150399483636 + ], + [ + "2025-11-26T22:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.0667491255904 + ], + [ + "2025-11-26T22:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.012855349582 + ], + [ + "2025-11-26T22:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.210697286231 + ], + [ + "2025-11-26T22:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.128943239289 + ], + [ + "2025-11-26T22:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.7342678537702 + ], + [ + "2025-11-26T22:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.601243443932 + ], + [ + "2025-11-26T22:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.052469621102 + ], + [ + "2025-11-26T22:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.45200910658 + ], + [ + "2025-11-26T22:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.7005241522597 + ], + [ + "2025-11-26T22:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.7265772544047 + ], + [ + "2025-11-26T22:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.392016430371 + ], + [ + "2025-11-26T22:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.1805430193212 + ], + [ + "2025-11-26T22:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.3997598695014 + ], + [ + "2025-11-26T22:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.9226602725307 + ], + [ + "2025-11-26T22:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.264822958744 + ], + [ + "2025-11-26T22:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.7746139284013 + ], + [ + "2025-11-26T22:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.894148642655 + ], + [ + "2025-11-26T22:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.576694655565 + ], + [ + "2025-11-26T22:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.3015064767524 + ], + [ + "2025-11-26T22:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.770671881976 + ], + [ + "2025-11-26T21:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.8434786213347 + ], + [ + "2025-11-26T21:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.1922815859207 + ], + [ + "2025-11-26T21:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.654629118373 + ], + [ + "2025-11-26T21:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.0755305745884 + ], + [ + "2025-11-26T21:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.3879050909513 + ], + [ + "2025-11-26T21:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.9124781486066 + ], + [ + "2025-11-26T21:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.1643263488086 + ], + [ + "2025-11-26T21:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.185672247033 + ], + [ + "2025-11-26T21:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.255434855479 + ], + [ + "2025-11-26T21:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.5275185481796 + ], + [ + "2025-11-26T21:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.0723822072646 + ], + [ + "2025-11-26T21:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.415828591947 + ], + [ + "2025-11-26T21:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.400605228132 + ], + [ + "2025-11-26T21:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3032.247633930668 + ], + [ + "2025-11-26T21:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.339188175033 + ], + [ + "2025-11-26T21:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.1376741926374 + ], + [ + "2025-11-26T21:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.9126560295676 + ], + [ + "2025-11-26T21:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.033134827457 + ], + [ + "2025-11-26T21:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.559101779141 + ], + [ + "2025-11-26T21:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3017.3953971036576 + ], + [ + "2025-11-26T21:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.7745116883684 + ], + [ + "2025-11-26T21:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.1705015189277 + ], + [ + "2025-11-26T21:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.118301963756 + ], + [ + "2025-11-26T21:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.7732787399927 + ], + [ + "2025-11-26T21:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.287382141945 + ], + [ + "2025-11-26T21:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.671110711504 + ], + [ + "2025-11-26T21:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.9497378818064 + ], + [ + "2025-11-26T21:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.0134677292485 + ], + [ + "2025-11-26T21:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.2256571852154 + ], + [ + "2025-11-26T21:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.4116632955706 + ], + [ + "2025-11-26T20:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.5223550569067 + ], + [ + "2025-11-26T20:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.3761884761716 + ], + [ + "2025-11-26T20:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.034673073346 + ], + [ + "2025-11-26T20:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.537003079573 + ], + [ + "2025-11-26T20:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3016.896092503228 + ], + [ + "2025-11-26T20:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3011.068848239424 + ], + [ + "2025-11-26T20:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3012.096681943245 + ], + [ + "2025-11-26T20:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3011.167286510448 + ], + [ + "2025-11-26T20:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.021829899216 + ], + [ + "2025-11-26T20:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3017.871870622501 + ], + [ + "2025-11-26T20:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.133754430514 + ], + [ + "2025-11-26T20:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.6283892970437 + ], + [ + "2025-11-26T20:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3017.13797554675 + ], + [ + "2025-11-26T20:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.3641586468293 + ], + [ + "2025-11-26T20:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.890682654623 + ], + [ + "2025-11-26T20:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.775070918795 + ], + [ + "2025-11-26T20:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.070512465065 + ], + [ + "2025-11-26T20:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.5116064856097 + ], + [ + "2025-11-26T20:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.0205580764505 + ], + [ + "2025-11-26T20:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3016.532189306943 + ], + [ + "2025-11-26T20:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.7433828326098 + ], + [ + "2025-11-26T20:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.0717579321126 + ], + [ + "2025-11-26T20:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.4067342147405 + ], + [ + "2025-11-26T20:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3022.9121810736424 + ], + [ + "2025-11-26T20:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.388270553996 + ], + [ + "2025-11-26T20:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3013.1177959667116 + ], + [ + "2025-11-26T20:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3013.4346831895605 + ], + [ + "2025-11-26T20:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.8658203730974 + ], + [ + "2025-11-26T20:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.475509064918 + ], + [ + "2025-11-26T20:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.300445979269 + ], + [ + "2025-11-26T19:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3035.2078397385976 + ], + [ + "2025-11-26T19:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.8589888899864 + ], + [ + "2025-11-26T19:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.319939956977 + ], + [ + "2025-11-26T19:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3035.27045458342 + ], + [ + "2025-11-26T19:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3035.297152806807 + ], + [ + "2025-11-26T19:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.466162766857 + ], + [ + "2025-11-26T19:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3033.9634036132547 + ], + [ + "2025-11-26T19:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3032.586488841507 + ], + [ + "2025-11-26T19:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3033.771793256169 + ], + [ + "2025-11-26T19:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.2746915843404 + ], + [ + "2025-11-26T19:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.176474279526 + ], + [ + "2025-11-26T19:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.8179613752545 + ], + [ + "2025-11-26T19:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.7458395268773 + ], + [ + "2025-11-26T19:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.7624773888506 + ], + [ + "2025-11-26T19:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3032.156844702454 + ], + [ + "2025-11-26T19:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.99129346763 + ], + [ + "2025-11-26T19:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.432744061586 + ], + [ + "2025-11-26T19:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.618729767293 + ], + [ + "2025-11-26T19:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.308867781954 + ], + [ + "2025-11-26T19:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.89430595998 + ], + [ + "2025-11-26T19:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.0947723010295 + ], + [ + "2025-11-26T19:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.7149833267435 + ], + [ + "2025-11-26T19:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.9729681606345 + ], + [ + "2025-11-26T19:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3019.8541867855147 + ], + [ + "2025-11-26T19:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.1975551672153 + ], + [ + "2025-11-26T19:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.1813037360967 + ], + [ + "2025-11-26T19:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.113166620033 + ], + [ + "2025-11-26T19:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.469033539616 + ], + [ + "2025-11-26T19:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.7787558256537 + ], + [ + "2025-11-26T19:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.130234929596 + ], + [ + "2025-11-26T18:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3032.8897067242237 + ], + [ + "2025-11-26T18:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.548632506069 + ], + [ + "2025-11-26T18:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.7953021683716 + ], + [ + "2025-11-26T18:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3026.6357149286873 + ], + [ + "2025-11-26T18:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3027.8934212174263 + ], + [ + "2025-11-26T18:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.5476659673045 + ], + [ + "2025-11-26T18:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3020.97834202484 + ], + [ + "2025-11-26T18:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3017.148257754839 + ], + [ + "2025-11-26T18:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.3758637454152 + ], + [ + "2025-11-26T18:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.5340710499595 + ], + [ + "2025-11-26T18:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3021.416968113133 + ], + [ + "2025-11-26T18:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.9369183863214 + ], + [ + "2025-11-26T18:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.915298796048 + ], + [ + "2025-11-26T18:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.4463738093873 + ], + [ + "2025-11-26T18:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3037.056458887182 + ], + [ + "2025-11-26T18:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3036.7047543300077 + ], + [ + "2025-11-26T18:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3038.9542468319737 + ], + [ + "2025-11-26T18:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.601555504045 + ], + [ + "2025-11-26T18:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3036.2512604162016 + ], + [ + "2025-11-26T18:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.575831783197 + ], + [ + "2025-11-26T18:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3038.858851452559 + ], + [ + "2025-11-26T18:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3042.8549111004386 + ], + [ + "2025-11-26T18:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3040.1981534911088 + ], + [ + "2025-11-26T18:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.778022634944 + ], + [ + "2025-11-26T18:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.9615724234436 + ], + [ + "2025-11-26T18:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3034.9219468612077 + ], + [ + "2025-11-26T18:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3033.2923933201073 + ], + [ + "2025-11-26T18:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3033.098636209787 + ], + [ + "2025-11-26T18:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3031.7210079629162 + ], + [ + "2025-11-26T18:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3030.436417973437 + ], + [ + "2025-11-26T17:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3029.583713897495 + ], + [ + "2025-11-26T17:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3025.081792785059 + ], + [ + "2025-11-26T17:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3018.604415897914 + ], + [ + "2025-11-26T17:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3015.4195432331385 + ], + [ + "2025-11-26T17:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3023.680525946276 + ], + [ + "2025-11-26T17:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3028.695471338884 + ], + [ + "2025-11-26T17:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3024.9210623992867 + ], + [ + "2025-11-26T17:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3017.0070804179295 + ], + [ + "2025-11-26T17:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3011.2665244947057 + ], + [ + "2025-11-26T17:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3006.416644568037 + ], + [ + "2025-11-26T17:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3003.8410317174767 + ], + [ + "2025-11-26T17:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3004.968057980944 + ], + [ + "2025-11-26T17:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 3001.3745097741526 + ], + [ + "2025-11-26T17:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2984.28629607061 + ], + [ + "2025-11-26T17:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.849823604123 + ], + [ + "2025-11-26T17:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.532012946143 + ], + [ + "2025-11-26T17:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.4515140834587 + ], + [ + "2025-11-26T17:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.682091995241 + ], + [ + "2025-11-26T17:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.904302541376 + ], + [ + "2025-11-26T17:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.6694179811057 + ], + [ + "2025-11-26T17:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.26623617124 + ], + [ + "2025-11-26T17:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.4925434129564 + ], + [ + "2025-11-26T17:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.681391520425 + ], + [ + "2025-11-26T17:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.1873979115157 + ], + [ + "2025-11-26T17:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.8125357751082 + ], + [ + "2025-11-26T17:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.4131543138524 + ], + [ + "2025-11-26T17:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.6159697915386 + ], + [ + "2025-11-26T17:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.1492563809475 + ], + [ + "2025-11-26T17:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.085897777473 + ], + [ + "2025-11-26T17:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.754822642638 + ], + [ + "2025-11-26T16:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.496468200753 + ], + [ + "2025-11-26T16:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.2258330271575 + ], + [ + "2025-11-26T16:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.364311875727 + ], + [ + "2025-11-26T16:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.3947300353807 + ], + [ + "2025-11-26T16:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.1459397403164 + ], + [ + "2025-11-26T16:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.331213221082 + ], + [ + "2025-11-26T16:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.962740252946 + ], + [ + "2025-11-26T16:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.258636449231 + ], + [ + "2025-11-26T16:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.327651506909 + ], + [ + "2025-11-26T16:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.787646220086 + ], + [ + "2025-11-26T16:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.8428510752797 + ], + [ + "2025-11-26T16:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.3963885392086 + ], + [ + "2025-11-26T16:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.3636279937064 + ], + [ + "2025-11-26T16:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.364268747501 + ], + [ + "2025-11-26T16:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.7023778783755 + ], + [ + "2025-11-26T16:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.7986459292288 + ], + [ + "2025-11-26T16:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.5601893309704 + ], + [ + "2025-11-26T16:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.3534702153224 + ], + [ + "2025-11-26T16:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.8060888554273 + ], + [ + "2025-11-26T16:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.4600149270177 + ], + [ + "2025-11-26T16:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.214435687855 + ], + [ + "2025-11-26T16:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.055519145734 + ], + [ + "2025-11-26T16:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.607541613766 + ], + [ + "2025-11-26T16:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.698941381484 + ], + [ + "2025-11-26T16:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.681358479241 + ], + [ + "2025-11-26T16:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.054499753902 + ], + [ + "2025-11-26T16:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.5629246358453 + ], + [ + "2025-11-26T16:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.8500270187155 + ], + [ + "2025-11-26T16:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.4468661959922 + ], + [ + "2025-11-26T16:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.8386823427986 + ], + [ + "2025-11-26T15:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.8708064065904 + ], + [ + "2025-11-26T15:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.92752455132 + ], + [ + "2025-11-26T15:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.19176345128 + ], + [ + "2025-11-26T15:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.838447799192 + ], + [ + "2025-11-26T15:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.5625706893256 + ], + [ + "2025-11-26T15:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.217045655755 + ], + [ + "2025-11-26T15:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.5322973266134 + ], + [ + "2025-11-26T15:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.661452139863 + ], + [ + "2025-11-26T15:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.2169799839644 + ], + [ + "2025-11-26T15:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.1195376194714 + ], + [ + "2025-11-26T15:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.118071120733 + ], + [ + "2025-11-26T15:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.462881602796 + ], + [ + "2025-11-26T15:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.5074227088667 + ], + [ + "2025-11-26T15:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.422399524901 + ], + [ + "2025-11-26T15:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.435922340358 + ], + [ + "2025-11-26T15:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.8889300276 + ], + [ + "2025-11-26T15:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.80216875357 + ], + [ + "2025-11-26T15:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.675669262228 + ], + [ + "2025-11-26T15:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.539016046155 + ], + [ + "2025-11-26T15:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.8779482456384 + ], + [ + "2025-11-26T15:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.2202188412944 + ], + [ + "2025-11-26T15:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.055282453235 + ], + [ + "2025-11-26T15:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.594559550768 + ], + [ + "2025-11-26T15:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.9824546670857 + ], + [ + "2025-11-26T15:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.2116134440116 + ], + [ + "2025-11-26T15:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.5014824179802 + ], + [ + "2025-11-26T15:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.203013089112 + ], + [ + "2025-11-26T15:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.5988355384693 + ], + [ + "2025-11-26T15:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.1574554646345 + ], + [ + "2025-11-26T15:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.6308512124283 + ], + [ + "2025-11-26T14:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.2646687793635 + ], + [ + "2025-11-26T14:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.9646652157967 + ], + [ + "2025-11-26T14:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.9882934138336 + ], + [ + "2025-11-26T14:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.8050018709578 + ], + [ + "2025-11-26T14:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.6764723291462 + ], + [ + "2025-11-26T14:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.6026259898686 + ], + [ + "2025-11-26T14:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.6434559633394 + ], + [ + "2025-11-26T14:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.679927571382 + ], + [ + "2025-11-26T14:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.017549622513 + ], + [ + "2025-11-26T14:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.68950659883 + ], + [ + "2025-11-26T14:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.5506567681873 + ], + [ + "2025-11-26T14:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.0327696119166 + ], + [ + "2025-11-26T14:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.463684523494 + ], + [ + "2025-11-26T14:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.1720085277816 + ], + [ + "2025-11-26T14:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.0705358995 + ], + [ + "2025-11-26T14:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.582781360747 + ], + [ + "2025-11-26T14:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.365662808429 + ], + [ + "2025-11-26T14:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.6616009889367 + ], + [ + "2025-11-26T14:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.504298030269 + ], + [ + "2025-11-26T14:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.2747626745495 + ], + [ + "2025-11-26T14:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.2008497693146 + ], + [ + "2025-11-26T14:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.189386208329 + ], + [ + "2025-11-26T14:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.3667295627984 + ], + [ + "2025-11-26T14:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.6532911067807 + ], + [ + "2025-11-26T14:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.560023244092 + ], + [ + "2025-11-26T14:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.6697045554965 + ], + [ + "2025-11-26T14:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.1962678075383 + ], + [ + "2025-11-26T14:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.4110204066947 + ], + [ + "2025-11-26T14:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.660784351018 + ], + [ + "2025-11-26T14:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.025372019662 + ], + [ + "2025-11-26T13:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.749130043076 + ], + [ + "2025-11-26T13:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.53139005059 + ], + [ + "2025-11-26T13:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.321398116442 + ], + [ + "2025-11-26T13:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.010708745985 + ], + [ + "2025-11-26T13:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.631234852231 + ], + [ + "2025-11-26T13:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.5396178164438 + ], + [ + "2025-11-26T13:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.908322841552 + ], + [ + "2025-11-26T13:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.6499531237096 + ], + [ + "2025-11-26T13:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.3089405657056 + ], + [ + "2025-11-26T13:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.0615722956504 + ], + [ + "2025-11-26T13:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.391691250555 + ], + [ + "2025-11-26T13:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.180713876524 + ], + [ + "2025-11-26T13:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.183150440062 + ], + [ + "2025-11-26T13:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.593049557681 + ], + [ + "2025-11-26T13:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.718521398907 + ], + [ + "2025-11-26T13:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.199497364437 + ], + [ + "2025-11-26T13:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.4319867440477 + ], + [ + "2025-11-26T13:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.522525245532 + ], + [ + "2025-11-26T13:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.230677281002 + ], + [ + "2025-11-26T13:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.6806335274227 + ], + [ + "2025-11-26T13:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.250730262095 + ], + [ + "2025-11-26T13:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.6026577593884 + ], + [ + "2025-11-26T13:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.942136425657 + ], + [ + "2025-11-26T13:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.8690005278468 + ], + [ + "2025-11-26T13:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.112735005857 + ], + [ + "2025-11-26T13:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.273137705173 + ], + [ + "2025-11-26T13:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.691743062504 + ], + [ + "2025-11-26T13:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.8582820274787 + ], + [ + "2025-11-26T13:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.221277493662 + ], + [ + "2025-11-26T13:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.009137313316 + ], + [ + "2025-11-26T12:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.734506484024 + ], + [ + "2025-11-26T12:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.8403702262367 + ], + [ + "2025-11-26T12:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.3142516928415 + ], + [ + "2025-11-26T12:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.502419841903 + ], + [ + "2025-11-26T12:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.8024884973665 + ], + [ + "2025-11-26T12:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.019194327666 + ], + [ + "2025-11-26T12:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.4868039422795 + ], + [ + "2025-11-26T12:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.7416929607393 + ], + [ + "2025-11-26T12:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.1861709850614 + ], + [ + "2025-11-26T12:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.7834531439935 + ], + [ + "2025-11-26T12:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.960836166363 + ], + [ + "2025-11-26T12:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.0491424595903 + ], + [ + "2025-11-26T12:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.5386959316543 + ], + [ + "2025-11-26T12:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.0958487004355 + ], + [ + "2025-11-26T12:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.9690510031583 + ], + [ + "2025-11-26T12:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.892072113158 + ], + [ + "2025-11-26T12:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.140615152582 + ], + [ + "2025-11-26T12:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.611253563305 + ], + [ + "2025-11-26T12:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.313358432984 + ], + [ + "2025-11-26T12:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.802403424873 + ], + [ + "2025-11-26T12:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.203371793062 + ], + [ + "2025-11-26T12:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.8053506291967 + ], + [ + "2025-11-26T12:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.7123421470155 + ], + [ + "2025-11-26T12:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.300095784219 + ], + [ + "2025-11-26T12:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.241342167253 + ], + [ + "2025-11-26T12:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.9789080996925 + ], + [ + "2025-11-26T12:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.965864274086 + ], + [ + "2025-11-26T12:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.695742700139 + ], + [ + "2025-11-26T12:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.9405103143886 + ], + [ + "2025-11-26T12:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.4300098636895 + ], + [ + "2025-11-26T11:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.320481214955 + ], + [ + "2025-11-26T11:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.513562304243 + ], + [ + "2025-11-26T11:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.7880772690073 + ], + [ + "2025-11-26T11:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.332580964658 + ], + [ + "2025-11-26T11:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.181194951116 + ], + [ + "2025-11-26T11:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.007248180217 + ], + [ + "2025-11-26T11:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.8515218467837 + ], + [ + "2025-11-26T11:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.6673300156394 + ], + [ + "2025-11-26T11:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.4055542946803 + ], + [ + "2025-11-26T11:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.990645158666 + ], + [ + "2025-11-26T11:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.0890679003655 + ], + [ + "2025-11-26T11:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.126312424463 + ], + [ + "2025-11-26T11:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.3931226520526 + ], + [ + "2025-11-26T11:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.743360862661 + ], + [ + "2025-11-26T11:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.5332342293477 + ], + [ + "2025-11-26T11:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.110691022866 + ], + [ + "2025-11-26T11:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.4502848274096 + ], + [ + "2025-11-26T11:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.7580797133555 + ], + [ + "2025-11-26T11:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.511925936891 + ], + [ + "2025-11-26T11:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.68638353527 + ], + [ + "2025-11-26T11:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.625490461781 + ], + [ + "2025-11-26T11:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.7959081068134 + ], + [ + "2025-11-26T11:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.480229958646 + ], + [ + "2025-11-26T11:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.809563915068 + ], + [ + "2025-11-26T11:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.3586366230884 + ], + [ + "2025-11-26T11:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.5959338135212 + ], + [ + "2025-11-26T11:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.242779831083 + ], + [ + "2025-11-26T11:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.9564501588534 + ], + [ + "2025-11-26T11:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.841527851626 + ], + [ + "2025-11-26T11:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.338224870277 + ], + [ + "2025-11-26T10:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.2722258353797 + ], + [ + "2025-11-26T10:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.050146157738 + ], + [ + "2025-11-26T10:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.6773747595594 + ], + [ + "2025-11-26T10:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.381052257795 + ], + [ + "2025-11-26T10:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.9749510126644 + ], + [ + "2025-11-26T10:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.106916851508 + ], + [ + "2025-11-26T10:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.453100479823 + ], + [ + "2025-11-26T10:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.791449241497 + ], + [ + "2025-11-26T10:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.9429565184896 + ], + [ + "2025-11-26T10:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.2084965354147 + ], + [ + "2025-11-26T10:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.9460048110077 + ], + [ + "2025-11-26T10:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.5761596488996 + ], + [ + "2025-11-26T10:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.5310783516943 + ], + [ + "2025-11-26T10:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.241471867122 + ], + [ + "2025-11-26T10:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.386021036281 + ], + [ + "2025-11-26T10:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.002297905133 + ], + [ + "2025-11-26T10:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.5768837074834 + ], + [ + "2025-11-26T10:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.8403671868377 + ], + [ + "2025-11-26T10:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.7705005402413 + ], + [ + "2025-11-26T10:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.3393125336597 + ], + [ + "2025-11-26T10:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.158111800588 + ], + [ + "2025-11-26T10:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.1234810700053 + ], + [ + "2025-11-26T10:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.203177905144 + ], + [ + "2025-11-26T10:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.5524213156646 + ], + [ + "2025-11-26T10:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.045470371478 + ], + [ + "2025-11-26T10:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.8639918494705 + ], + [ + "2025-11-26T10:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.0172036085664 + ], + [ + "2025-11-26T10:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.7686584335397 + ], + [ + "2025-11-26T10:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.525331620353 + ], + [ + "2025-11-26T10:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.153560006629 + ], + [ + "2025-11-26T09:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.3161955384962 + ], + [ + "2025-11-26T09:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.714736538994 + ], + [ + "2025-11-26T09:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.0313534933334 + ], + [ + "2025-11-26T09:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.5962242510745 + ], + [ + "2025-11-26T09:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.740653261345 + ], + [ + "2025-11-26T09:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.516693509086 + ], + [ + "2025-11-26T09:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.221822063625 + ], + [ + "2025-11-26T09:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.291769152591 + ], + [ + "2025-11-26T09:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.966592496411 + ], + [ + "2025-11-26T09:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.5433145654574 + ], + [ + "2025-11-26T09:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.423518739349 + ], + [ + "2025-11-26T09:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.847304186992 + ], + [ + "2025-11-26T09:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.3752368736978 + ], + [ + "2025-11-26T09:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.912679997563 + ], + [ + "2025-11-26T09:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.6163500396324 + ], + [ + "2025-11-26T09:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.23123587222 + ], + [ + "2025-11-26T09:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.6137075017245 + ], + [ + "2025-11-26T09:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.7525810211546 + ], + [ + "2025-11-26T09:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.541366622421 + ], + [ + "2025-11-26T09:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.72777218742 + ], + [ + "2025-11-26T09:19:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.9968584264057 + ], + [ + "2025-11-26T09:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.8032364374326 + ], + [ + "2025-11-26T09:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.7959746293673 + ], + [ + "2025-11-26T09:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.8678268041285 + ], + [ + "2025-11-26T09:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.3512333440685 + ], + [ + "2025-11-26T09:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.844531886493 + ], + [ + "2025-11-26T09:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.5045745677926 + ], + [ + "2025-11-26T09:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.4626857679914 + ], + [ + "2025-11-26T09:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.27586243742 + ], + [ + "2025-11-26T09:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.679607506038 + ], + [ + "2025-11-26T08:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.476029357599 + ], + [ + "2025-11-26T08:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.089300557565 + ], + [ + "2025-11-26T08:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.9847423611077 + ], + [ + "2025-11-26T08:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.307881260014 + ], + [ + "2025-11-26T08:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.5804000105654 + ], + [ + "2025-11-26T08:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.8805718402655 + ], + [ + "2025-11-26T08:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.966971877987 + ], + [ + "2025-11-26T08:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.806785115112 + ], + [ + "2025-11-26T08:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.337538714483 + ], + [ + "2025-11-26T08:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.4026843070274 + ], + [ + "2025-11-26T08:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.046910561961 + ], + [ + "2025-11-26T08:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.0239733745684 + ], + [ + "2025-11-26T08:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.8574719304993 + ], + [ + "2025-11-26T08:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.4094433541313 + ], + [ + "2025-11-26T08:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.778861015195 + ], + [ + "2025-11-26T08:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.496550856435 + ], + [ + "2025-11-26T08:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.6243691196632 + ], + [ + "2025-11-26T08:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.9108837723848 + ], + [ + "2025-11-26T08:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.214127607114 + ], + [ + "2025-11-26T08:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.2055620883293 + ], + [ + "2025-11-26T08:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.8827996423015 + ], + [ + "2025-11-26T08:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.8829935402273 + ], + [ + "2025-11-26T08:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.3606837533634 + ], + [ + "2025-11-26T08:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.366115506712 + ], + [ + "2025-11-26T08:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.2055075987464 + ], + [ + "2025-11-26T08:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.2004802640054 + ], + [ + "2025-11-26T08:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.334679821757 + ], + [ + "2025-11-26T08:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.320269158006 + ], + [ + "2025-11-26T08:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.6108919909366 + ], + [ + "2025-11-26T08:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.828884927482 + ], + [ + "2025-11-26T07:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.2196921096747 + ], + [ + "2025-11-26T07:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.5771338329705 + ], + [ + "2025-11-26T07:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.859303874028 + ], + [ + "2025-11-26T07:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.836003853413 + ], + [ + "2025-11-26T07:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.631859532357 + ], + [ + "2025-11-26T07:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.048648618009 + ], + [ + "2025-11-26T07:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.131471925049 + ], + [ + "2025-11-26T07:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.197054104234 + ], + [ + "2025-11-26T07:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.080272178511 + ], + [ + "2025-11-26T07:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.471399907659 + ], + [ + "2025-11-26T07:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.491460895034 + ], + [ + "2025-11-26T07:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.2647985935755 + ], + [ + "2025-11-26T07:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.274943740413 + ], + [ + "2025-11-26T07:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.052695225883 + ], + [ + "2025-11-26T07:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.2574124840753 + ], + [ + "2025-11-26T07:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.523902249845 + ], + [ + "2025-11-26T07:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.1840236976896 + ], + [ + "2025-11-26T07:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.2988880045546 + ], + [ + "2025-11-26T07:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.6337595486107 + ], + [ + "2025-11-26T07:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.616942605512 + ], + [ + "2025-11-26T07:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.1445229020605 + ], + [ + "2025-11-26T07:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.9102116574836 + ], + [ + "2025-11-26T07:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.294099403695 + ], + [ + "2025-11-26T07:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.0908957357765 + ], + [ + "2025-11-26T07:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.51711633162 + ], + [ + "2025-11-26T07:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.6780375890476 + ], + [ + "2025-11-26T07:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.48713198736 + ], + [ + "2025-11-26T07:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.9810255336884 + ], + [ + "2025-11-26T07:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.379177993403 + ], + [ + "2025-11-26T07:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.252889863863 + ], + [ + "2025-11-26T06:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.0350325295954 + ], + [ + "2025-11-26T06:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.9352408461277 + ], + [ + "2025-11-26T06:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.494841736899 + ], + [ + "2025-11-26T06:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.7001173742156 + ], + [ + "2025-11-26T06:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.194693707555 + ], + [ + "2025-11-26T06:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.3848517240453 + ], + [ + "2025-11-26T06:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.1751894529166 + ], + [ + "2025-11-26T06:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.3129524478836 + ], + [ + "2025-11-26T06:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.6970935897566 + ], + [ + "2025-11-26T06:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.227040742291 + ], + [ + "2025-11-26T06:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.873495053849 + ], + [ + "2025-11-26T06:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.0169954150224 + ], + [ + "2025-11-26T06:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.35671039409 + ], + [ + "2025-11-26T06:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.2848004627635 + ], + [ + "2025-11-26T06:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.666665724114 + ], + [ + "2025-11-26T06:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.7426486252166 + ], + [ + "2025-11-26T06:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.8797014800653 + ], + [ + "2025-11-26T06:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.169300004837 + ], + [ + "2025-11-26T06:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.944203063805 + ], + [ + "2025-11-26T06:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.4525555048463 + ], + [ + "2025-11-26T06:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.2770306403945 + ], + [ + "2025-11-26T06:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.066284263337 + ], + [ + "2025-11-26T06:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.370020497128 + ], + [ + "2025-11-26T06:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.3712315753833 + ], + [ + "2025-11-26T06:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.127779679332 + ], + [ + "2025-11-26T06:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.6390229490867 + ], + [ + "2025-11-26T06:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.867646978111 + ], + [ + "2025-11-26T06:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.317176805528 + ], + [ + "2025-11-26T06:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.5646782951217 + ], + [ + "2025-11-26T06:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.171622988449 + ], + [ + "2025-11-26T05:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.305557493166 + ], + [ + "2025-11-26T05:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.9684176315536 + ], + [ + "2025-11-26T05:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.9079897108686 + ], + [ + "2025-11-26T05:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.017199652863 + ], + [ + "2025-11-26T05:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.9731932581417 + ], + [ + "2025-11-26T05:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.5580145192052 + ], + [ + "2025-11-26T05:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.2813987214936 + ], + [ + "2025-11-26T05:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.7764051534537 + ], + [ + "2025-11-26T05:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.7334514861955 + ], + [ + "2025-11-26T05:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.471216558321 + ], + [ + "2025-11-26T05:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.2113414324863 + ], + [ + "2025-11-26T05:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.3258983260503 + ], + [ + "2025-11-26T05:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.4310854093824 + ], + [ + "2025-11-26T05:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.410890510638 + ], + [ + "2025-11-26T05:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.459014505879 + ], + [ + "2025-11-26T05:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.12289046199 + ], + [ + "2025-11-26T05:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.9498236322106 + ], + [ + "2025-11-26T05:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.7356321702773 + ], + [ + "2025-11-26T05:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.5665720799925 + ], + [ + "2025-11-26T05:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.3689600886873 + ], + [ + "2025-11-26T05:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.4818324647026 + ], + [ + "2025-11-26T05:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.521587931057 + ], + [ + "2025-11-26T05:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.1820284208557 + ], + [ + "2025-11-26T05:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.214566808 + ], + [ + "2025-11-26T05:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.965680234375 + ], + [ + "2025-11-26T05:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.6403867882564 + ], + [ + "2025-11-26T05:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.6567848622203 + ], + [ + "2025-11-26T05:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.9533260756693 + ], + [ + "2025-11-26T05:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.5558934828064 + ], + [ + "2025-11-26T05:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.2950584147025 + ], + [ + "2025-11-26T04:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.463356016824 + ], + [ + "2025-11-26T04:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.8640933350844 + ], + [ + "2025-11-26T04:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.028100251433 + ], + [ + "2025-11-26T04:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.201632795194 + ], + [ + "2025-11-26T04:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.792217400524 + ], + [ + "2025-11-26T04:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.132314193 + ], + [ + "2025-11-26T04:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.6385737320998 + ], + [ + "2025-11-26T04:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.369752641011 + ], + [ + "2025-11-26T04:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.6704926593043 + ], + [ + "2025-11-26T04:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.731180149152 + ], + [ + "2025-11-26T04:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.605574475818 + ], + [ + "2025-11-26T04:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.536696794494 + ], + [ + "2025-11-26T04:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.3556729233655 + ], + [ + "2025-11-26T04:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.9966539095353 + ], + [ + "2025-11-26T04:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.530986995635 + ], + [ + "2025-11-26T04:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.0266856994367 + ], + [ + "2025-11-26T04:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.8908919146857 + ], + [ + "2025-11-26T04:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.5414376545377 + ], + [ + "2025-11-26T04:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.773803759667 + ], + [ + "2025-11-26T04:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.5434633626933 + ], + [ + "2025-11-26T04:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.9832362711663 + ], + [ + "2025-11-26T04:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.960830912848 + ], + [ + "2025-11-26T04:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.109681292959 + ], + [ + "2025-11-26T04:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.423317468068 + ], + [ + "2025-11-26T04:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.081706938979 + ], + [ + "2025-11-26T04:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.793345810545 + ], + [ + "2025-11-26T04:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.0052286052787 + ], + [ + "2025-11-26T04:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.38157601157 + ], + [ + "2025-11-26T04:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.6530569203783 + ], + [ + "2025-11-26T04:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.782660364043 + ], + [ + "2025-11-26T03:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.4468286218353 + ], + [ + "2025-11-26T03:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.700376908767 + ], + [ + "2025-11-26T03:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.0068563608547 + ], + [ + "2025-11-26T03:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.555692150941 + ], + [ + "2025-11-26T03:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.2813259684544 + ], + [ + "2025-11-26T03:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.7516675553115 + ], + [ + "2025-11-26T03:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.991922390069 + ], + [ + "2025-11-26T03:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.7233924867055 + ], + [ + "2025-11-26T03:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.146255642844 + ], + [ + "2025-11-26T03:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.8856562772007 + ], + [ + "2025-11-26T03:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.637196675603 + ], + [ + "2025-11-26T03:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.3242319976803 + ], + [ + "2025-11-26T03:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.0903962974953 + ], + [ + "2025-11-26T03:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.5738932748627 + ], + [ + "2025-11-26T03:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.502608878407 + ], + [ + "2025-11-26T03:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.562789302658 + ], + [ + "2025-11-26T03:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.7518230831356 + ], + [ + "2025-11-26T03:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.2493195878246 + ], + [ + "2025-11-26T03:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.6100185120263 + ], + [ + "2025-11-26T03:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.4403613767054 + ], + [ + "2025-11-26T03:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.330427984946 + ], + [ + "2025-11-26T03:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.5354769981104 + ], + [ + "2025-11-26T03:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.0823980959635 + ], + [ + "2025-11-26T03:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.0584139147963 + ], + [ + "2025-11-26T03:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.9353500988705 + ], + [ + "2025-11-26T03:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.798252811467 + ], + [ + "2025-11-26T03:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.924130290219 + ], + [ + "2025-11-26T03:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.0890730159754 + ], + [ + "2025-11-26T03:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.353260007459 + ], + [ + "2025-11-26T03:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.728413930324 + ], + [ + "2025-11-26T02:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.3955235848985 + ], + [ + "2025-11-26T02:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.2190183839175 + ], + [ + "2025-11-26T02:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.6040177364875 + ], + [ + "2025-11-26T02:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.8235078564758 + ], + [ + "2025-11-26T02:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.6034502980224 + ], + [ + "2025-11-26T02:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.4147793707793 + ], + [ + "2025-11-26T02:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.0660996743172 + ], + [ + "2025-11-26T02:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.798731178741 + ], + [ + "2025-11-26T02:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.546927768173 + ], + [ + "2025-11-26T02:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.0265132405393 + ], + [ + "2025-11-26T02:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.6446128748876 + ], + [ + "2025-11-26T02:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.18416130437 + ], + [ + "2025-11-26T02:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.3307124431785 + ], + [ + "2025-11-26T02:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.402380257789 + ], + [ + "2025-11-26T02:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.591672822258 + ], + [ + "2025-11-26T02:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.547915868642 + ], + [ + "2025-11-26T02:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.468455137117 + ], + [ + "2025-11-26T02:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.6029553873877 + ], + [ + "2025-11-26T02:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.8357731550022 + ], + [ + "2025-11-26T02:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.0255726065784 + ], + [ + "2025-11-26T02:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.1333480333187 + ], + [ + "2025-11-26T02:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.8270118191335 + ], + [ + "2025-11-26T02:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.8535864958153 + ], + [ + "2025-11-26T02:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.169304748153 + ], + [ + "2025-11-26T02:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.0966844356635 + ], + [ + "2025-11-26T02:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.4046988519804 + ], + [ + "2025-11-26T02:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.9791592824026 + ], + [ + "2025-11-26T02:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.513116160631 + ], + [ + "2025-11-26T02:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.575840929596 + ], + [ + "2025-11-26T02:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.490347907269 + ], + [ + "2025-11-26T01:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.609496104451 + ], + [ + "2025-11-26T01:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.8845236571665 + ], + [ + "2025-11-26T01:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.2093380586834 + ], + [ + "2025-11-26T01:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.273586343689 + ], + [ + "2025-11-26T01:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.4094003620953 + ], + [ + "2025-11-26T01:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.2224259072213 + ], + [ + "2025-11-26T01:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.8820743359383 + ], + [ + "2025-11-26T01:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.75811246079 + ], + [ + "2025-11-26T01:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2979.019752917284 + ], + [ + "2025-11-26T01:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.925507941516 + ], + [ + "2025-11-26T01:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.0725256976793 + ], + [ + "2025-11-26T01:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.3239500578948 + ], + [ + "2025-11-26T01:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.230009743699 + ], + [ + "2025-11-26T01:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.4728133567287 + ], + [ + "2025-11-26T01:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.9377011346883 + ], + [ + "2025-11-26T01:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.1265649390575 + ], + [ + "2025-11-26T01:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.5241255340356 + ], + [ + "2025-11-26T01:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.7857368677464 + ], + [ + "2025-11-26T01:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.6821783688074 + ], + [ + "2025-11-26T01:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.9757829252303 + ], + [ + "2025-11-26T01:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.202548460901 + ], + [ + "2025-11-26T01:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.5681182898625 + ], + [ + "2025-11-26T01:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.738872460329 + ], + [ + "2025-11-26T01:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.834530467997 + ], + [ + "2025-11-26T01:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.640114195599 + ], + [ + "2025-11-26T01:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.7748966201284 + ], + [ + "2025-11-26T01:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.0356793474543 + ], + [ + "2025-11-26T01:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.009776368899 + ], + [ + "2025-11-26T01:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.600615332331 + ], + [ + "2025-11-26T01:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.643954295729 + ], + [ + "2025-11-26T00:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.731168679213 + ], + [ + "2025-11-26T00:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.02037581628 + ], + [ + "2025-11-26T00:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.2323723003624 + ], + [ + "2025-11-26T00:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.280870766218 + ], + [ + "2025-11-26T00:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.8484969623482 + ], + [ + "2025-11-26T00:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.8627513812453 + ], + [ + "2025-11-26T00:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.048143020825 + ], + [ + "2025-11-26T00:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.054074346517 + ], + [ + "2025-11-26T00:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.6141463081094 + ], + [ + "2025-11-26T00:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.7114480252703 + ], + [ + "2025-11-26T00:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.908324248734 + ], + [ + "2025-11-26T00:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.7311896378787 + ], + [ + "2025-11-26T00:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.986193649611 + ], + [ + "2025-11-26T00:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.8380858479077 + ], + [ + "2025-11-26T00:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.4574326086217 + ], + [ + "2025-11-26T00:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.9109029558617 + ], + [ + "2025-11-26T00:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.7682384473583 + ], + [ + "2025-11-26T00:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.427100525848 + ], + [ + "2025-11-26T00:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.864174466647 + ], + [ + "2025-11-26T00:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.7849027764 + ], + [ + "2025-11-26T00:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.4466330528476 + ], + [ + "2025-11-26T00:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.51407711134 + ], + [ + "2025-11-26T00:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.992141794122 + ], + [ + "2025-11-26T00:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.2289274540467 + ], + [ + "2025-11-26T00:11:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.7848911714045 + ], + [ + "2025-11-26T00:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.5322939669554 + ], + [ + "2025-11-26T00:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.601378522957 + ], + [ + "2025-11-26T00:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.4488461963847 + ], + [ + "2025-11-26T00:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.862482459695 + ], + [ + "2025-11-26T00:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.062301250391 + ], + [ + "2025-11-25T23:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.1210339664754 + ], + [ + "2025-11-25T23:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.383302838337 + ], + [ + "2025-11-25T23:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.4493739636077 + ], + [ + "2025-11-25T23:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.666750758642 + ], + [ + "2025-11-25T23:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.476894774917 + ], + [ + "2025-11-25T23:50:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.272576400263 + ], + [ + "2025-11-25T23:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.7245930196073 + ], + [ + "2025-11-25T23:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.63094606737 + ], + [ + "2025-11-25T23:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.565339851819 + ], + [ + "2025-11-25T23:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.286296885321 + ], + [ + "2025-11-25T23:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.333860355092 + ], + [ + "2025-11-25T23:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.126753318705 + ], + [ + "2025-11-25T23:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.191251949839 + ], + [ + "2025-11-25T23:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.62433335115 + ], + [ + "2025-11-25T23:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.129322028203 + ], + [ + "2025-11-25T23:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.6875426595093 + ], + [ + "2025-11-25T23:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.314356250502 + ], + [ + "2025-11-25T23:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.1469064747625 + ], + [ + "2025-11-25T23:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.117377930124 + ], + [ + "2025-11-25T23:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.0702194719634 + ], + [ + "2025-11-25T23:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.7439889452385 + ], + [ + "2025-11-25T23:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.456722505905 + ], + [ + "2025-11-25T23:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.8765975936913 + ], + [ + "2025-11-25T23:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.9037482689373 + ], + [ + "2025-11-25T23:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.249429393934 + ], + [ + "2025-11-25T23:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.2484963572842 + ], + [ + "2025-11-25T23:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.6959546059925 + ], + [ + "2025-11-25T23:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.1364873019 + ], + [ + "2025-11-25T23:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.8898613115 + ], + [ + "2025-11-25T23:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.3725996519915 + ], + [ + "2025-11-25T22:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.290560055061 + ], + [ + "2025-11-25T22:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.2104027532837 + ], + [ + "2025-11-25T22:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.9398012681277 + ], + [ + "2025-11-25T22:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.997112938222 + ], + [ + "2025-11-25T22:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.7867481604685 + ], + [ + "2025-11-25T22:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.6183974109417 + ], + [ + "2025-11-25T22:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.630923480878 + ], + [ + "2025-11-25T22:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.6026535940705 + ], + [ + "2025-11-25T22:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.610471417995 + ], + [ + "2025-11-25T22:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.4927976074578 + ], + [ + "2025-11-25T22:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.241342249739 + ], + [ + "2025-11-25T22:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.5217864312 + ], + [ + "2025-11-25T22:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.5709423929848 + ], + [ + "2025-11-25T22:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.677206040501 + ], + [ + "2025-11-25T22:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.5492744925514 + ], + [ + "2025-11-25T22:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.748162645214 + ], + [ + "2025-11-25T22:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.6585973507645 + ], + [ + "2025-11-25T22:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.4880679841326 + ], + [ + "2025-11-25T22:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.598921012492 + ], + [ + "2025-11-25T22:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.8027877085415 + ], + [ + "2025-11-25T22:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.0058534179793 + ], + [ + "2025-11-25T22:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.6706148674725 + ], + [ + "2025-11-25T22:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.8610599706176 + ], + [ + "2025-11-25T22:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.8240629759753 + ], + [ + "2025-11-25T22:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.8588558739702 + ], + [ + "2025-11-25T22:10:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.8809353944257 + ], + [ + "2025-11-25T22:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.214317855613 + ], + [ + "2025-11-25T22:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.043630825805 + ], + [ + "2025-11-25T22:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.2065733601767 + ], + [ + "2025-11-25T22:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.1740690963834 + ], + [ + "2025-11-25T21:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.685438310566 + ], + [ + "2025-11-25T21:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.395383946968 + ], + [ + "2025-11-25T21:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.2785053629264 + ], + [ + "2025-11-25T21:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.1871335199976 + ], + [ + "2025-11-25T21:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.585379337488 + ], + [ + "2025-11-25T21:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.542785143297 + ], + [ + "2025-11-25T21:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.5852635053025 + ], + [ + "2025-11-25T21:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.5754927849675 + ], + [ + "2025-11-25T21:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.0869374494578 + ], + [ + "2025-11-25T21:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.0835393593684 + ], + [ + "2025-11-25T21:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.8685256709205 + ], + [ + "2025-11-25T21:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.3472987139467 + ], + [ + "2025-11-25T21:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.2563397478043 + ], + [ + "2025-11-25T21:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.511874694518 + ], + [ + "2025-11-25T21:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.431709847041 + ], + [ + "2025-11-25T21:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.6095920414764 + ], + [ + "2025-11-25T21:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.773997684772 + ], + [ + "2025-11-25T21:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.384097870118 + ], + [ + "2025-11-25T21:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.065571861881 + ], + [ + "2025-11-25T21:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.3118072591124 + ], + [ + "2025-11-25T21:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.5431639829176 + ], + [ + "2025-11-25T21:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.703378423347 + ], + [ + "2025-11-25T21:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.6672868324654 + ], + [ + "2025-11-25T21:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.0995434378506 + ], + [ + "2025-11-25T21:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.1408397567234 + ], + [ + "2025-11-25T21:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.367520785747 + ], + [ + "2025-11-25T21:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.200380755664 + ], + [ + "2025-11-25T21:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.3513944267725 + ], + [ + "2025-11-25T21:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.845770544833 + ], + [ + "2025-11-25T21:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.8661415072115 + ], + [ + "2025-11-25T21:00:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.239830663175 + ], + [ + "2025-11-25T20:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.6882822777493 + ], + [ + "2025-11-25T20:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.8636805785754 + ], + [ + "2025-11-25T20:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.3897922045107 + ], + [ + "2025-11-25T20:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.609155167689 + ], + [ + "2025-11-25T20:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.9232105950277 + ], + [ + "2025-11-25T20:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.0084800880472 + ], + [ + "2025-11-25T20:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.7251896462794 + ], + [ + "2025-11-25T20:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.047762256397 + ], + [ + "2025-11-25T20:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.207421494343 + ], + [ + "2025-11-25T20:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.3909585818433 + ], + [ + "2025-11-25T20:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.554415835041 + ], + [ + "2025-11-25T20:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.1633751179234 + ], + [ + "2025-11-25T20:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.9326250930594 + ], + [ + "2025-11-25T20:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.400038560357 + ], + [ + "2025-11-25T20:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.2460972804847 + ], + [ + "2025-11-25T20:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.7808467879718 + ], + [ + "2025-11-25T20:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.0277561729954 + ], + [ + "2025-11-25T20:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.9554560593588 + ], + [ + "2025-11-25T20:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.645931648351 + ], + [ + "2025-11-25T20:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.457054356633 + ], + [ + "2025-11-25T20:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.359800812322 + ], + [ + "2025-11-25T20:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.3981072560173 + ], + [ + "2025-11-25T20:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.143210596286 + ], + [ + "2025-11-25T20:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.5626257230724 + ], + [ + "2025-11-25T20:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.1013404831065 + ], + [ + "2025-11-25T20:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.2622258471533 + ], + [ + "2025-11-25T20:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.013668278518 + ], + [ + "2025-11-25T20:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.3670226307845 + ], + [ + "2025-11-25T20:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.0031600778034 + ], + [ + "2025-11-25T19:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.7057397548283 + ], + [ + "2025-11-25T19:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.9861735010927 + ], + [ + "2025-11-25T19:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.065605819314 + ], + [ + "2025-11-25T19:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.0949294062702 + ], + [ + "2025-11-25T19:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.2575814223646 + ], + [ + "2025-11-25T19:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.7952778679046 + ], + [ + "2025-11-25T19:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.778599448861 + ], + [ + "2025-11-25T19:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.740317102181 + ], + [ + "2025-11-25T19:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.3487893856172 + ], + [ + "2025-11-25T19:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.789093747701 + ], + [ + "2025-11-25T19:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.08305553579 + ], + [ + "2025-11-25T19:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2887.984879466269 + ], + [ + "2025-11-25T19:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.3064036326105 + ], + [ + "2025-11-25T19:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2885.180185304743 + ], + [ + "2025-11-25T19:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2883.8329971766007 + ], + [ + "2025-11-25T19:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.9802547364134 + ], + [ + "2025-11-25T19:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2876.41921745579 + ], + [ + "2025-11-25T19:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.6361388123546 + ], + [ + "2025-11-25T19:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2875.755557407133 + ], + [ + "2025-11-25T19:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.1951587067597 + ], + [ + "2025-11-25T19:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.692617197032 + ], + [ + "2025-11-25T19:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.1637341857254 + ], + [ + "2025-11-25T19:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.1803513650075 + ], + [ + "2025-11-25T19:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.4957890054 + ], + [ + "2025-11-25T19:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.8384623884735 + ], + [ + "2025-11-25T19:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.913908239898 + ], + [ + "2025-11-25T19:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.3435129404197 + ], + [ + "2025-11-25T19:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.480206555332 + ], + [ + "2025-11-25T19:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.0121705985075 + ], + [ + "2025-11-25T19:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.106750804255 + ], + [ + "2025-11-25T18:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.4072466452258 + ], + [ + "2025-11-25T18:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.4157449606355 + ], + [ + "2025-11-25T18:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.4169623636726 + ], + [ + "2025-11-25T18:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.6215674394684 + ], + [ + "2025-11-25T18:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.8529172106796 + ], + [ + "2025-11-25T18:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.2747482029686 + ], + [ + "2025-11-25T18:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.739153890164 + ], + [ + "2025-11-25T18:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.326210872826 + ], + [ + "2025-11-25T18:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.527045431205 + ], + [ + "2025-11-25T18:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.26371543641 + ], + [ + "2025-11-25T18:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.3581014487636 + ], + [ + "2025-11-25T18:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.5865870630923 + ], + [ + "2025-11-25T18:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.3897341892134 + ], + [ + "2025-11-25T18:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.347558571257 + ], + [ + "2025-11-25T18:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2951.004855718467 + ], + [ + "2025-11-25T18:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.991764136277 + ], + [ + "2025-11-25T18:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.4420398308953 + ], + [ + "2025-11-25T18:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.3525618045687 + ], + [ + "2025-11-25T18:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.059957772071 + ], + [ + "2025-11-25T18:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.6420841235104 + ], + [ + "2025-11-25T18:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.4946044351104 + ], + [ + "2025-11-25T18:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.770441200522 + ], + [ + "2025-11-25T18:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.054684378161 + ], + [ + "2025-11-25T18:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.186624282435 + ], + [ + "2025-11-25T18:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.888230421978 + ], + [ + "2025-11-25T18:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.9312905494735 + ], + [ + "2025-11-25T18:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.913564322787 + ], + [ + "2025-11-25T18:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.9666525400594 + ], + [ + "2025-11-25T18:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.0052522356827 + ], + [ + "2025-11-25T18:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.2083160567136 + ], + [ + "2025-11-25T17:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.3054197350198 + ], + [ + "2025-11-25T17:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.3273353856243 + ], + [ + "2025-11-25T17:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.5072268951467 + ], + [ + "2025-11-25T17:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.744016936501 + ], + [ + "2025-11-25T17:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.536137409632 + ], + [ + "2025-11-25T17:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.4966937805148 + ], + [ + "2025-11-25T17:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.038874539562 + ], + [ + "2025-11-25T17:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.1679567237943 + ], + [ + "2025-11-25T17:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.6555381872904 + ], + [ + "2025-11-25T17:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.1636737245067 + ], + [ + "2025-11-25T17:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.0073877353952 + ], + [ + "2025-11-25T17:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.4326580698425 + ], + [ + "2025-11-25T17:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.4687339718466 + ], + [ + "2025-11-25T17:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.705922679686 + ], + [ + "2025-11-25T17:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.267240655678 + ], + [ + "2025-11-25T17:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.5814526948316 + ], + [ + "2025-11-25T17:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.0853099598826 + ], + [ + "2025-11-25T17:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.7715421658436 + ], + [ + "2025-11-25T17:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.4480990034 + ], + [ + "2025-11-25T17:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.439489072851 + ], + [ + "2025-11-25T17:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.8484873716848 + ], + [ + "2025-11-25T17:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.0509042107196 + ], + [ + "2025-11-25T17:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.5246645322936 + ], + [ + "2025-11-25T17:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.2482429526617 + ], + [ + "2025-11-25T17:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.0987540725914 + ], + [ + "2025-11-25T17:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.7764836742554 + ], + [ + "2025-11-25T17:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.0630056631185 + ], + [ + "2025-11-25T17:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.457660639885 + ], + [ + "2025-11-25T17:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.407531204903 + ], + [ + "2025-11-25T17:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.4050043699553 + ], + [ + "2025-11-25T16:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.769914242029 + ], + [ + "2025-11-25T16:57:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.2794534942523 + ], + [ + "2025-11-25T16:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.8227619344884 + ], + [ + "2025-11-25T16:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.6382605769672 + ], + [ + "2025-11-25T16:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.8933095809807 + ], + [ + "2025-11-25T16:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.5282225301976 + ], + [ + "2025-11-25T16:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.7143934925043 + ], + [ + "2025-11-25T16:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.586738126733 + ], + [ + "2025-11-25T16:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.5051581028956 + ], + [ + "2025-11-25T16:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.5266414285315 + ], + [ + "2025-11-25T16:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.2176970401215 + ], + [ + "2025-11-25T16:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.0664956622213 + ], + [ + "2025-11-25T16:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.1062374573153 + ], + [ + "2025-11-25T16:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.304441828586 + ], + [ + "2025-11-25T16:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.3734521591614 + ], + [ + "2025-11-25T16:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.244323798677 + ], + [ + "2025-11-25T16:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.420171386891 + ], + [ + "2025-11-25T16:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.9551942499547 + ], + [ + "2025-11-25T16:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.7577201866566 + ], + [ + "2025-11-25T16:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.754004369561 + ], + [ + "2025-11-25T16:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.151625539209 + ], + [ + "2025-11-25T16:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.288619925643 + ], + [ + "2025-11-25T16:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.369443011761 + ], + [ + "2025-11-25T16:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.4767745925215 + ], + [ + "2025-11-25T16:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.2501142368037 + ], + [ + "2025-11-25T16:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.9701208100473 + ], + [ + "2025-11-25T16:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.771418134724 + ], + [ + "2025-11-25T16:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.326862449308 + ], + [ + "2025-11-25T16:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.430313039108 + ], + [ + "2025-11-25T16:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.19431913164 + ], + [ + "2025-11-25T15:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.1527894816873 + ], + [ + "2025-11-25T15:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.322466256628 + ], + [ + "2025-11-25T15:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.5824512848094 + ], + [ + "2025-11-25T15:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.0033205710906 + ], + [ + "2025-11-25T15:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.9800317886725 + ], + [ + "2025-11-25T15:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.85250754541 + ], + [ + "2025-11-25T15:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.394316666003 + ], + [ + "2025-11-25T15:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.1386826494295 + ], + [ + "2025-11-25T15:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.9346440596987 + ], + [ + "2025-11-25T15:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.3987439058487 + ], + [ + "2025-11-25T15:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.340842178321 + ], + [ + "2025-11-25T15:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.5492739698957 + ], + [ + "2025-11-25T15:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.0833149815785 + ], + [ + "2025-11-25T15:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.1570891344327 + ], + [ + "2025-11-25T15:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.8070165669433 + ], + [ + "2025-11-25T15:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.7368532902633 + ], + [ + "2025-11-25T15:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2875.872759804861 + ], + [ + "2025-11-25T15:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2872.3772413527176 + ], + [ + "2025-11-25T15:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.3151736727873 + ], + [ + "2025-11-25T15:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.757949233907 + ], + [ + "2025-11-25T15:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.9771051296693 + ], + [ + "2025-11-25T15:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.1472418457474 + ], + [ + "2025-11-25T15:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.459196619543 + ], + [ + "2025-11-25T15:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2879.979753727136 + ], + [ + "2025-11-25T15:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2888.014292432281 + ], + [ + "2025-11-25T15:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2872.822539554824 + ], + [ + "2025-11-25T15:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2867.1666019366544 + ], + [ + "2025-11-25T15:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2860.573287300417 + ], + [ + "2025-11-25T15:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.9915308966374 + ], + [ + "2025-11-25T15:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.533372900368 + ], + [ + "2025-11-25T14:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2878.74422945269 + ], + [ + "2025-11-25T14:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.2948127228306 + ], + [ + "2025-11-25T14:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2871.185001764947 + ], + [ + "2025-11-25T14:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2871.4248775813926 + ], + [ + "2025-11-25T14:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2862.3187252755415 + ], + [ + "2025-11-25T14:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.9636426501434 + ], + [ + "2025-11-25T14:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.595837210763 + ], + [ + "2025-11-25T14:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.1125850670546 + ], + [ + "2025-11-25T14:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.242572952728 + ], + [ + "2025-11-25T14:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.358570938569 + ], + [ + "2025-11-25T14:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.2555505180526 + ], + [ + "2025-11-25T14:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.5975283004086 + ], + [ + "2025-11-25T14:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.0734357244655 + ], + [ + "2025-11-25T14:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.1415197650613 + ], + [ + "2025-11-25T14:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.8026543890937 + ], + [ + "2025-11-25T14:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.1919437002093 + ], + [ + "2025-11-25T14:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.035631410525 + ], + [ + "2025-11-25T14:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.0320984539803 + ], + [ + "2025-11-25T14:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.9555031375303 + ], + [ + "2025-11-25T14:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.1746956890556 + ], + [ + "2025-11-25T14:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.780703797096 + ], + [ + "2025-11-25T14:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.4699589902552 + ], + [ + "2025-11-25T14:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.3128955610177 + ], + [ + "2025-11-25T14:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.978022442177 + ], + [ + "2025-11-25T14:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.949615614862 + ], + [ + "2025-11-25T14:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.3696546300257 + ], + [ + "2025-11-25T14:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.8238664918495 + ], + [ + "2025-11-25T14:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.834392735277 + ], + [ + "2025-11-25T14:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.791005914536 + ], + [ + "2025-11-25T13:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.725349626592 + ], + [ + "2025-11-25T13:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.0778560413346 + ], + [ + "2025-11-25T13:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.845936324498 + ], + [ + "2025-11-25T13:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.094288293443 + ], + [ + "2025-11-25T13:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.1706813171754 + ], + [ + "2025-11-25T13:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.408625780537 + ], + [ + "2025-11-25T13:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.5431196462723 + ], + [ + "2025-11-25T13:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.226720114856 + ], + [ + "2025-11-25T13:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.6783576621224 + ], + [ + "2025-11-25T13:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.3843275616177 + ], + [ + "2025-11-25T13:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.3539003331553 + ], + [ + "2025-11-25T13:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.10752715448 + ], + [ + "2025-11-25T13:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.7827710604147 + ], + [ + "2025-11-25T13:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.8569052691682 + ], + [ + "2025-11-25T13:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.489471686846 + ], + [ + "2025-11-25T13:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.083698830142 + ], + [ + "2025-11-25T13:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.2712731387055 + ], + [ + "2025-11-25T13:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.1975680305386 + ], + [ + "2025-11-25T13:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.370610932095 + ], + [ + "2025-11-25T13:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.4028503224454 + ], + [ + "2025-11-25T13:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.75114754462 + ], + [ + "2025-11-25T13:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.5690365708983 + ], + [ + "2025-11-25T13:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.9858944469515 + ], + [ + "2025-11-25T13:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.0249416040438 + ], + [ + "2025-11-25T13:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.9646914003424 + ], + [ + "2025-11-25T13:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.791580312053 + ], + [ + "2025-11-25T13:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.9257994427508 + ], + [ + "2025-11-25T13:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.2116701495784 + ], + [ + "2025-11-25T13:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.6521766947835 + ], + [ + "2025-11-25T13:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.2317309669543 + ], + [ + "2025-11-25T12:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.1950571353063 + ], + [ + "2025-11-25T12:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.3281659885097 + ], + [ + "2025-11-25T12:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.147106852427 + ], + [ + "2025-11-25T12:53:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.261615028771 + ], + [ + "2025-11-25T12:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.7039156755145 + ], + [ + "2025-11-25T12:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.6561089470483 + ], + [ + "2025-11-25T12:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.921147541774 + ], + [ + "2025-11-25T12:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.067849689041 + ], + [ + "2025-11-25T12:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.3202566273303 + ], + [ + "2025-11-25T12:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.8067430944716 + ], + [ + "2025-11-25T12:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.1694812738915 + ], + [ + "2025-11-25T12:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.9623277889 + ], + [ + "2025-11-25T12:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.8390447282386 + ], + [ + "2025-11-25T12:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.9414155384625 + ], + [ + "2025-11-25T12:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.3538105174066 + ], + [ + "2025-11-25T12:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.10406524373 + ], + [ + "2025-11-25T12:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.087883471956 + ], + [ + "2025-11-25T12:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.218225004926 + ], + [ + "2025-11-25T12:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.364861003702 + ], + [ + "2025-11-25T12:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.5971017638585 + ], + [ + "2025-11-25T12:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.364986719194 + ], + [ + "2025-11-25T12:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.5916319359703 + ], + [ + "2025-11-25T12:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.3128970196385 + ], + [ + "2025-11-25T12:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.242940018195 + ], + [ + "2025-11-25T12:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.996733145544 + ], + [ + "2025-11-25T12:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.569044948713 + ], + [ + "2025-11-25T12:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.4887799293165 + ], + [ + "2025-11-25T12:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.5071010870975 + ], + [ + "2025-11-25T12:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.793677175374 + ], + [ + "2025-11-25T12:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.56816399269 + ], + [ + "2025-11-25T11:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.3232405660733 + ], + [ + "2025-11-25T11:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.9662243887205 + ], + [ + "2025-11-25T11:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.7246353312353 + ], + [ + "2025-11-25T11:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.4583311882034 + ], + [ + "2025-11-25T11:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.9742820230354 + ], + [ + "2025-11-25T11:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.5231769620364 + ], + [ + "2025-11-25T11:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.9007921250222 + ], + [ + "2025-11-25T11:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.124102731036 + ], + [ + "2025-11-25T11:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.3556501402577 + ], + [ + "2025-11-25T11:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.326084619482 + ], + [ + "2025-11-25T11:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.055527276837 + ], + [ + "2025-11-25T11:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.259596966005 + ], + [ + "2025-11-25T11:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.1995035735654 + ], + [ + "2025-11-25T11:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.533626650877 + ], + [ + "2025-11-25T11:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.4002765573528 + ], + [ + "2025-11-25T11:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.3220893069897 + ], + [ + "2025-11-25T11:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.1122345221274 + ], + [ + "2025-11-25T11:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.6752580576135 + ], + [ + "2025-11-25T11:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.4416966498366 + ], + [ + "2025-11-25T11:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.562460948457 + ], + [ + "2025-11-25T11:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.896819186055 + ], + [ + "2025-11-25T11:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.2869692681184 + ], + [ + "2025-11-25T11:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.144286365895 + ], + [ + "2025-11-25T11:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.1991756287034 + ], + [ + "2025-11-25T11:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.736645041163 + ], + [ + "2025-11-25T11:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.2720650285505 + ], + [ + "2025-11-25T11:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.5010633608617 + ], + [ + "2025-11-25T11:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.0591428860553 + ], + [ + "2025-11-25T11:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.973300744731 + ], + [ + "2025-11-25T11:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.8846311641682 + ], + [ + "2025-11-25T10:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.7223173788207 + ], + [ + "2025-11-25T10:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.1985546754936 + ], + [ + "2025-11-25T10:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2896.106696526224 + ], + [ + "2025-11-25T10:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.929789625524 + ], + [ + "2025-11-25T10:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.2074870817783 + ], + [ + "2025-11-25T10:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.88845202099 + ], + [ + "2025-11-25T10:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.4659283469155 + ], + [ + "2025-11-25T10:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.5366836213716 + ], + [ + "2025-11-25T10:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2894.8987459937657 + ], + [ + "2025-11-25T10:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2893.8759937888653 + ], + [ + "2025-11-25T10:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.6200194970506 + ], + [ + "2025-11-25T10:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2895.560776187952 + ], + [ + "2025-11-25T10:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.070273226301 + ], + [ + "2025-11-25T10:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.1497575495487 + ], + [ + "2025-11-25T10:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.7727188450312 + ], + [ + "2025-11-25T10:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.7841779475925 + ], + [ + "2025-11-25T10:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.9685666058626 + ], + [ + "2025-11-25T10:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2890.3593647496295 + ], + [ + "2025-11-25T10:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2885.7049973805138 + ], + [ + "2025-11-25T10:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.9798878530323 + ], + [ + "2025-11-25T10:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2881.0272901173944 + ], + [ + "2025-11-25T10:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2884.7361314207337 + ], + [ + "2025-11-25T10:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.529860251159 + ], + [ + "2025-11-25T10:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2887.843843044949 + ], + [ + "2025-11-25T10:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.0299461507984 + ], + [ + "2025-11-25T10:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.4956317202577 + ], + [ + "2025-11-25T10:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.1662278250897 + ], + [ + "2025-11-25T10:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.8678059997223 + ], + [ + "2025-11-25T10:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2885.7525943140668 + ], + [ + "2025-11-25T10:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2888.7064069092635 + ], + [ + "2025-11-25T09:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2891.6109583009243 + ], + [ + "2025-11-25T09:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.672879189027 + ], + [ + "2025-11-25T09:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2886.0834315094235 + ], + [ + "2025-11-25T09:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2888.048808474307 + ], + [ + "2025-11-25T09:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2888.711862570423 + ], + [ + "2025-11-25T09:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.6781922494156 + ], + [ + "2025-11-25T09:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2885.3111658949642 + ], + [ + "2025-11-25T09:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2888.118128295657 + ], + [ + "2025-11-25T09:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2882.661443716595 + ], + [ + "2025-11-25T09:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.0691724910416 + ], + [ + "2025-11-25T09:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.7754480119065 + ], + [ + "2025-11-25T09:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2878.2163673234795 + ], + [ + "2025-11-25T09:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2875.643296981712 + ], + [ + "2025-11-25T09:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.622712199072 + ], + [ + "2025-11-25T09:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.9487008282667 + ], + [ + "2025-11-25T09:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2871.0349445070196 + ], + [ + "2025-11-25T09:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.645641805467 + ], + [ + "2025-11-25T09:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.5363593027846 + ], + [ + "2025-11-25T09:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.424136999083 + ], + [ + "2025-11-25T09:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2870.238368707053 + ], + [ + "2025-11-25T09:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2870.0769189555836 + ], + [ + "2025-11-25T09:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.0118625020573 + ], + [ + "2025-11-25T09:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2870.730301603727 + ], + [ + "2025-11-25T09:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.062398231564 + ], + [ + "2025-11-25T09:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2870.275248330321 + ], + [ + "2025-11-25T09:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.138138088865 + ], + [ + "2025-11-25T09:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2875.3258871628714 + ], + [ + "2025-11-25T09:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.218785358106 + ], + [ + "2025-11-25T09:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.85217734841 + ], + [ + "2025-11-25T09:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2871.645594686188 + ], + [ + "2025-11-25T08:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2871.4023773264125 + ], + [ + "2025-11-25T08:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2874.4685490463735 + ], + [ + "2025-11-25T08:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.7846747491208 + ], + [ + "2025-11-25T08:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2887.174077808871 + ], + [ + "2025-11-25T08:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2889.969793762413 + ], + [ + "2025-11-25T08:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.1042230112007 + ], + [ + "2025-11-25T08:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.4083103015128 + ], + [ + "2025-11-25T08:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.5794293898102 + ], + [ + "2025-11-25T08:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.2439910496732 + ], + [ + "2025-11-25T08:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.1435571085417 + ], + [ + "2025-11-25T08:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.1643696627243 + ], + [ + "2025-11-25T08:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2899.019556273523 + ], + [ + "2025-11-25T08:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.3168806124913 + ], + [ + "2025-11-25T08:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2897.4368786439936 + ], + [ + "2025-11-25T08:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.918905146053 + ], + [ + "2025-11-25T08:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.4325236681975 + ], + [ + "2025-11-25T08:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.4628902557693 + ], + [ + "2025-11-25T08:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.6580082644327 + ], + [ + "2025-11-25T08:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.1066583017914 + ], + [ + "2025-11-25T08:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.3710368787742 + ], + [ + "2025-11-25T08:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.017868952166 + ], + [ + "2025-11-25T08:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.0896357448546 + ], + [ + "2025-11-25T08:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2898.755176224638 + ], + [ + "2025-11-25T08:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.3704100836626 + ], + [ + "2025-11-25T08:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2902.366015033 + ], + [ + "2025-11-25T08:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.3909311181133 + ], + [ + "2025-11-25T08:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.3316999245176 + ], + [ + "2025-11-25T08:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2901.0455293233363 + ], + [ + "2025-11-25T08:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2900.5322918211414 + ], + [ + "2025-11-25T08:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2904.542942737753 + ], + [ + "2025-11-25T07:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2903.008865083118 + ], + [ + "2025-11-25T07:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.249629780636 + ], + [ + "2025-11-25T07:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2905.071332276281 + ], + [ + "2025-11-25T07:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.8281790707056 + ], + [ + "2025-11-25T07:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2906.176106483525 + ], + [ + "2025-11-25T07:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.540572576359 + ], + [ + "2025-11-25T07:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.5642618559227 + ], + [ + "2025-11-25T07:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.7774280554836 + ], + [ + "2025-11-25T07:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.6087673912984 + ], + [ + "2025-11-25T07:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.012845127453 + ], + [ + "2025-11-25T07:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.626995806325 + ], + [ + "2025-11-25T07:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.129275543179 + ], + [ + "2025-11-25T07:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.288958932832 + ], + [ + "2025-11-25T07:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.1071443736605 + ], + [ + "2025-11-25T07:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.6994049637588 + ], + [ + "2025-11-25T07:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.730712545259 + ], + [ + "2025-11-25T07:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.3720261412723 + ], + [ + "2025-11-25T07:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.563683898954 + ], + [ + "2025-11-25T07:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.500755179599 + ], + [ + "2025-11-25T07:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.3068942210602 + ], + [ + "2025-11-25T07:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.018277498745 + ], + [ + "2025-11-25T07:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.0042205929067 + ], + [ + "2025-11-25T07:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.552525178048 + ], + [ + "2025-11-25T07:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.5571275038374 + ], + [ + "2025-11-25T07:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.7188735331342 + ], + [ + "2025-11-25T07:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.6154760979857 + ], + [ + "2025-11-25T07:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.245693436618 + ], + [ + "2025-11-25T07:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.175852957793 + ], + [ + "2025-11-25T07:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.988671714983 + ], + [ + "2025-11-25T07:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.430143111362 + ], + [ + "2025-11-25T06:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.8845210657287 + ], + [ + "2025-11-25T06:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.5003602026422 + ], + [ + "2025-11-25T06:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.8568965661816 + ], + [ + "2025-11-25T06:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.45723657674 + ], + [ + "2025-11-25T06:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.817699052494 + ], + [ + "2025-11-25T06:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.161725381848 + ], + [ + "2025-11-25T06:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.4791751252305 + ], + [ + "2025-11-25T06:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.6967422217863 + ], + [ + "2025-11-25T06:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.8042149939206 + ], + [ + "2025-11-25T06:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.183515673781 + ], + [ + "2025-11-25T06:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.0989412308263 + ], + [ + "2025-11-25T06:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.0492968397375 + ], + [ + "2025-11-25T06:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.077535828968 + ], + [ + "2025-11-25T06:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.082464299336 + ], + [ + "2025-11-25T06:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.764744310789 + ], + [ + "2025-11-25T06:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.7616578984034 + ], + [ + "2025-11-25T06:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.361445685142 + ], + [ + "2025-11-25T06:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.9718267930225 + ], + [ + "2025-11-25T06:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.895483970998 + ], + [ + "2025-11-25T06:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.1706475778783 + ], + [ + "2025-11-25T06:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.797008222425 + ], + [ + "2025-11-25T06:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.0946017610295 + ], + [ + "2025-11-25T06:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.9089092293066 + ], + [ + "2025-11-25T06:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.742811373671 + ], + [ + "2025-11-25T06:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.3614946258526 + ], + [ + "2025-11-25T06:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.4963203551165 + ], + [ + "2025-11-25T06:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.63578565365 + ], + [ + "2025-11-25T06:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.163383526646 + ], + [ + "2025-11-25T06:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.906052679942 + ], + [ + "2025-11-25T06:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.8885811716323 + ], + [ + "2025-11-25T05:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.150099001158 + ], + [ + "2025-11-25T05:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.2843735621523 + ], + [ + "2025-11-25T05:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.755910079109 + ], + [ + "2025-11-25T05:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.053725259364 + ], + [ + "2025-11-25T05:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.6531051397374 + ], + [ + "2025-11-25T05:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.1911402830237 + ], + [ + "2025-11-25T05:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.702093188023 + ], + [ + "2025-11-25T05:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.6278340556364 + ], + [ + "2025-11-25T05:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.3192169923886 + ], + [ + "2025-11-25T05:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.912019686736 + ], + [ + "2025-11-25T05:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.6966390138155 + ], + [ + "2025-11-25T05:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.278422045186 + ], + [ + "2025-11-25T05:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.926237474774 + ], + [ + "2025-11-25T05:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.058449991874 + ], + [ + "2025-11-25T05:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.4838870608783 + ], + [ + "2025-11-25T05:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.106851551591 + ], + [ + "2025-11-25T05:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.0861850091123 + ], + [ + "2025-11-25T05:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.6284013253926 + ], + [ + "2025-11-25T05:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.6674149074483 + ], + [ + "2025-11-25T05:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.608388357344 + ], + [ + "2025-11-25T05:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.7147975102052 + ], + [ + "2025-11-25T05:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.682149899748 + ], + [ + "2025-11-25T05:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.350514628574 + ], + [ + "2025-11-25T05:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.240842474512 + ], + [ + "2025-11-25T05:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.197015827705 + ], + [ + "2025-11-25T05:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.192299630862 + ], + [ + "2025-11-25T05:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.59216507638 + ], + [ + "2025-11-25T05:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.2892434206333 + ], + [ + "2025-11-25T05:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.1346932449246 + ], + [ + "2025-11-25T05:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.7483433081934 + ], + [ + "2025-11-25T04:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.0235608779085 + ], + [ + "2025-11-25T04:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.9652973983975 + ], + [ + "2025-11-25T04:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.1915186658553 + ], + [ + "2025-11-25T04:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.770562039147 + ], + [ + "2025-11-25T04:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.931852082166 + ], + [ + "2025-11-25T04:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.594184355404 + ], + [ + "2025-11-25T04:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.154495666739 + ], + [ + "2025-11-25T04:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.262196222043 + ], + [ + "2025-11-25T04:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.201651724356 + ], + [ + "2025-11-25T04:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2933.0877276006636 + ], + [ + "2025-11-25T04:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.8890489798746 + ], + [ + "2025-11-25T04:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.76894325132 + ], + [ + "2025-11-25T04:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.7043971219005 + ], + [ + "2025-11-25T04:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.882686405999 + ], + [ + "2025-11-25T04:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.233745599834 + ], + [ + "2025-11-25T04:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.196311111129 + ], + [ + "2025-11-25T04:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.8273974311664 + ], + [ + "2025-11-25T04:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.973485684578 + ], + [ + "2025-11-25T04:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.6716143010403 + ], + [ + "2025-11-25T04:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.9990892867736 + ], + [ + "2025-11-25T04:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.8881967030165 + ], + [ + "2025-11-25T04:17:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.482724603553 + ], + [ + "2025-11-25T04:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.414599521039 + ], + [ + "2025-11-25T04:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.005910304715 + ], + [ + "2025-11-25T04:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.1946251699833 + ], + [ + "2025-11-25T04:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.130583980309 + ], + [ + "2025-11-25T04:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.4640599042373 + ], + [ + "2025-11-25T04:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.3386159674797 + ], + [ + "2025-11-25T04:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.041867383155 + ], + [ + "2025-11-25T04:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.1214433601167 + ], + [ + "2025-11-25T03:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.948457924176 + ], + [ + "2025-11-25T03:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.185287809479 + ], + [ + "2025-11-25T03:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.6169602834702 + ], + [ + "2025-11-25T03:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.637093048854 + ], + [ + "2025-11-25T03:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2907.916673648405 + ], + [ + "2025-11-25T03:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.323394556368 + ], + [ + "2025-11-25T03:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2910.999198765501 + ], + [ + "2025-11-25T03:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.282517699142 + ], + [ + "2025-11-25T03:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.294131629538 + ], + [ + "2025-11-25T03:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2908.60388618842 + ], + [ + "2025-11-25T03:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.633987141561 + ], + [ + "2025-11-25T03:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.7674875867447 + ], + [ + "2025-11-25T03:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.291644863047 + ], + [ + "2025-11-25T03:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.936022793239 + ], + [ + "2025-11-25T03:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.136174463249 + ], + [ + "2025-11-25T03:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.030211267642 + ], + [ + "2025-11-25T03:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.274712635732 + ], + [ + "2025-11-25T03:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.4949392929507 + ], + [ + "2025-11-25T03:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.580327978865 + ], + [ + "2025-11-25T03:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.578584772894 + ], + [ + "2025-11-25T03:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.6082798644024 + ], + [ + "2025-11-25T03:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.1421587059444 + ], + [ + "2025-11-25T03:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.973417803585 + ], + [ + "2025-11-25T03:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.5734983259244 + ], + [ + "2025-11-25T03:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.4770215706894 + ], + [ + "2025-11-25T03:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.9561646857 + ], + [ + "2025-11-25T03:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.7650579835636 + ], + [ + "2025-11-25T03:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.0583799232186 + ], + [ + "2025-11-25T03:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.0525621605407 + ], + [ + "2025-11-25T03:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.8974087268443 + ], + [ + "2025-11-25T02:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2929.586954374568 + ], + [ + "2025-11-25T02:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.9886156565663 + ], + [ + "2025-11-25T02:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.4514959011954 + ], + [ + "2025-11-25T02:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2928.3458578840555 + ], + [ + "2025-11-25T02:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.3152619968287 + ], + [ + "2025-11-25T02:50:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.9017195989036 + ], + [ + "2025-11-25T02:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.320075888693 + ], + [ + "2025-11-25T02:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.5263534569085 + ], + [ + "2025-11-25T02:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.4344436643955 + ], + [ + "2025-11-25T02:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.646686773465 + ], + [ + "2025-11-25T02:39:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.9978841131465 + ], + [ + "2025-11-25T02:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.962061546253 + ], + [ + "2025-11-25T02:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2912.8679147439966 + ], + [ + "2025-11-25T02:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.3335557165055 + ], + [ + "2025-11-25T02:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.0972908887366 + ], + [ + "2025-11-25T02:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2915.4597988252603 + ], + [ + "2025-11-25T02:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.997870961074 + ], + [ + "2025-11-25T02:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.0188814459984 + ], + [ + "2025-11-25T02:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.128779762502 + ], + [ + "2025-11-25T02:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.2609261764296 + ], + [ + "2025-11-25T02:19:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.719554278818 + ], + [ + "2025-11-25T02:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.274971629893 + ], + [ + "2025-11-25T02:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.042542404901 + ], + [ + "2025-11-25T02:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.975712693814 + ], + [ + "2025-11-25T02:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2919.7697496080264 + ], + [ + "2025-11-25T02:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.5300840900177 + ], + [ + "2025-11-25T02:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.986819953469 + ], + [ + "2025-11-25T02:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.3277706354847 + ], + [ + "2025-11-25T02:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.785030366394 + ], + [ + "2025-11-25T02:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2916.7766526656724 + ], + [ + "2025-11-25T01:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2920.2021619610887 + ], + [ + "2025-11-25T01:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2924.035950736119 + ], + [ + "2025-11-25T01:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.9800713075206 + ], + [ + "2025-11-25T01:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2926.242505019737 + ], + [ + "2025-11-25T01:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.3635905082706 + ], + [ + "2025-11-25T01:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.5877345440113 + ], + [ + "2025-11-25T01:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.751265845768 + ], + [ + "2025-11-25T01:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.8790009839113 + ], + [ + "2025-11-25T01:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.7097491054637 + ], + [ + "2025-11-25T01:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.9228760930264 + ], + [ + "2025-11-25T01:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.7421362226746 + ], + [ + "2025-11-25T01:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.0327523383335 + ], + [ + "2025-11-25T01:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.087678153927 + ], + [ + "2025-11-25T01:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.5160564090183 + ], + [ + "2025-11-25T01:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.883273792466 + ], + [ + "2025-11-25T01:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.024770335191 + ], + [ + "2025-11-25T01:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.4711933723265 + ], + [ + "2025-11-25T01:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.123707424354 + ], + [ + "2025-11-25T01:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.005583431902 + ], + [ + "2025-11-25T01:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.956289204093 + ], + [ + "2025-11-25T01:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.6350905958257 + ], + [ + "2025-11-25T01:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.332834615023 + ], + [ + "2025-11-25T01:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.4642229045467 + ], + [ + "2025-11-25T01:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.708948664206 + ], + [ + "2025-11-25T01:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.514784928628 + ], + [ + "2025-11-25T01:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.5598298708746 + ], + [ + "2025-11-25T01:07:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.6894702425325 + ], + [ + "2025-11-25T01:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.0586625159904 + ], + [ + "2025-11-25T01:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.509299283835 + ], + [ + "2025-11-25T01:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.1709491678266 + ], + [ + "2025-11-25T00:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.1289466681405 + ], + [ + "2025-11-25T00:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.40843488616 + ], + [ + "2025-11-25T00:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.085452543199 + ], + [ + "2025-11-25T00:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.0076787456323 + ], + [ + "2025-11-25T00:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.669727012378 + ], + [ + "2025-11-25T00:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.747613290039 + ], + [ + "2025-11-25T00:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.3796311879896 + ], + [ + "2025-11-25T00:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.9413546968285 + ], + [ + "2025-11-25T00:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.7412428886823 + ], + [ + "2025-11-25T00:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.0445627463514 + ], + [ + "2025-11-25T00:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.6189654396326 + ], + [ + "2025-11-25T00:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.9623061782795 + ], + [ + "2025-11-25T00:35:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.3128125524054 + ], + [ + "2025-11-25T00:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.0655103971217 + ], + [ + "2025-11-25T00:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.0718647005247 + ], + [ + "2025-11-25T00:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.722753149805 + ], + [ + "2025-11-25T00:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.657178723783 + ], + [ + "2025-11-25T00:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2934.7275241005477 + ], + [ + "2025-11-25T00:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2935.153570301041 + ], + [ + "2025-11-25T00:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.6156476168767 + ], + [ + "2025-11-25T00:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2937.20921442894 + ], + [ + "2025-11-25T00:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2936.795532831221 + ], + [ + "2025-11-25T00:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.0044512604986 + ], + [ + "2025-11-25T00:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.550678681602 + ], + [ + "2025-11-25T00:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.23910254053 + ], + [ + "2025-11-25T00:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.0986412112866 + ], + [ + "2025-11-25T00:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.625413773054 + ], + [ + "2025-11-25T00:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.987131680593 + ], + [ + "2025-11-25T00:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2942.9594773691715 + ], + [ + "2025-11-25T00:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2949.9206856724004 + ], + [ + "2025-11-24T23:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.88025966878 + ], + [ + "2025-11-24T23:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.6880661095797 + ], + [ + "2025-11-24T23:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2952.4699992133155 + ], + [ + "2025-11-24T23:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.8576134609543 + ], + [ + "2025-11-24T23:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.8925844816845 + ], + [ + "2025-11-24T23:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.203167159954 + ], + [ + "2025-11-24T23:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.3151028377965 + ], + [ + "2025-11-24T23:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.839888683509 + ], + [ + "2025-11-24T23:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.171736888373 + ], + [ + "2025-11-24T23:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.66680670781 + ], + [ + "2025-11-24T23:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.510042252904 + ], + [ + "2025-11-24T23:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.047333276213 + ], + [ + "2025-11-24T23:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.9948753763106 + ], + [ + "2025-11-24T23:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.544616529462 + ], + [ + "2025-11-24T23:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.2535038585315 + ], + [ + "2025-11-24T23:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.3330293854183 + ], + [ + "2025-11-24T23:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.267192044647 + ], + [ + "2025-11-24T23:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.931856268202 + ], + [ + "2025-11-24T23:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.028585272856 + ], + [ + "2025-11-24T23:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.7340961975656 + ], + [ + "2025-11-24T23:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.652429505102 + ], + [ + "2025-11-24T23:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.7407146172336 + ], + [ + "2025-11-24T23:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.862661988686 + ], + [ + "2025-11-24T23:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.504360766954 + ], + [ + "2025-11-24T23:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.4039808257985 + ], + [ + "2025-11-24T23:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.2436673451357 + ], + [ + "2025-11-24T23:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.2288236920717 + ], + [ + "2025-11-24T23:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.805146664146 + ], + [ + "2025-11-24T23:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.0303653972305 + ], + [ + "2025-11-24T23:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.651996223779 + ], + [ + "2025-11-24T22:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.8423383175027 + ], + [ + "2025-11-24T22:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.291296069925 + ], + [ + "2025-11-24T22:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.1240585645432 + ], + [ + "2025-11-24T22:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.912708123889 + ], + [ + "2025-11-24T22:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.5573791108886 + ], + [ + "2025-11-24T22:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.5600440956814 + ], + [ + "2025-11-24T22:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.6126549753485 + ], + [ + "2025-11-24T22:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.8998685903016 + ], + [ + "2025-11-24T22:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.2846756929644 + ], + [ + "2025-11-24T22:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.732290988764 + ], + [ + "2025-11-24T22:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.596262323827 + ], + [ + "2025-11-24T22:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.803413120235 + ], + [ + "2025-11-24T22:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.390347952095 + ], + [ + "2025-11-24T22:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.756322074248 + ], + [ + "2025-11-24T22:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.168193229311 + ], + [ + "2025-11-24T22:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.3156723154307 + ], + [ + "2025-11-24T22:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.4406485180943 + ], + [ + "2025-11-24T22:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.254967941473 + ], + [ + "2025-11-24T22:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.755051854393 + ], + [ + "2025-11-24T22:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.7776323743906 + ], + [ + "2025-11-24T22:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.8041189661617 + ], + [ + "2025-11-24T22:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.4093629540557 + ], + [ + "2025-11-24T22:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.4069757397815 + ], + [ + "2025-11-24T22:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.231097457165 + ], + [ + "2025-11-24T22:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.3514234549602 + ], + [ + "2025-11-24T22:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.034632327852 + ], + [ + "2025-11-24T22:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.9020739261664 + ], + [ + "2025-11-24T22:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.1622810391627 + ], + [ + "2025-11-24T22:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.931754961189 + ], + [ + "2025-11-24T22:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.7575472806407 + ], + [ + "2025-11-24T21:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.8920865259497 + ], + [ + "2025-11-24T21:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.7534922796917 + ], + [ + "2025-11-24T21:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.2155241460673 + ], + [ + "2025-11-24T21:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.9759038849966 + ], + [ + "2025-11-24T21:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.893510109461 + ], + [ + "2025-11-24T21:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.7776964143286 + ], + [ + "2025-11-24T21:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2950.7109810987768 + ], + [ + "2025-11-24T21:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.2366115030068 + ], + [ + "2025-11-24T21:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.793214161909 + ], + [ + "2025-11-24T21:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.5005169590686 + ], + [ + "2025-11-24T21:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.4639971976176 + ], + [ + "2025-11-24T21:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.09068735751 + ], + [ + "2025-11-24T21:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.5634737986234 + ], + [ + "2025-11-24T21:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.195189584833 + ], + [ + "2025-11-24T21:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.9743188241578 + ], + [ + "2025-11-24T21:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.95884286779 + ], + [ + "2025-11-24T21:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.406704879239 + ], + [ + "2025-11-24T21:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2965.988056765487 + ], + [ + "2025-11-24T21:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.5530031334683 + ], + [ + "2025-11-24T21:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.6905242235543 + ], + [ + "2025-11-24T21:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2963.832359483736 + ], + [ + "2025-11-24T21:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.1039206793935 + ], + [ + "2025-11-24T21:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.1468484813245 + ], + [ + "2025-11-24T21:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.2467558825365 + ], + [ + "2025-11-24T21:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2964.919485868995 + ], + [ + "2025-11-24T21:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.769763987707 + ], + [ + "2025-11-24T21:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.5078947307447 + ], + [ + "2025-11-24T21:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.938856269956 + ], + [ + "2025-11-24T21:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.3997992904688 + ], + [ + "2025-11-24T21:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.4739056252897 + ], + [ + "2025-11-24T20:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.219403320872 + ], + [ + "2025-11-24T20:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.2229283072143 + ], + [ + "2025-11-24T20:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2980.0089116287127 + ], + [ + "2025-11-24T20:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.4348423241463 + ], + [ + "2025-11-24T20:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.6603113471565 + ], + [ + "2025-11-24T20:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2980.058683053835 + ], + [ + "2025-11-24T20:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2981.375961192756 + ], + [ + "2025-11-24T20:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2978.0135524551697 + ], + [ + "2025-11-24T20:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2977.391994078317 + ], + [ + "2025-11-24T20:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2979.578964781072 + ], + [ + "2025-11-24T20:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.6500973886027 + ], + [ + "2025-11-24T20:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.8212039093196 + ], + [ + "2025-11-24T20:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.1852240878375 + ], + [ + "2025-11-24T20:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.9218613653384 + ], + [ + "2025-11-24T20:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.386568347793 + ], + [ + "2025-11-24T20:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.876799295731 + ], + [ + "2025-11-24T20:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.7500600129715 + ], + [ + "2025-11-24T20:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.6668180789256 + ], + [ + "2025-11-24T20:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2978.4568024393393 + ], + [ + "2025-11-24T20:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2976.8896574339365 + ], + [ + "2025-11-24T20:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2979.978167594805 + ], + [ + "2025-11-24T20:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.804682731531 + ], + [ + "2025-11-24T20:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.6469826264047 + ], + [ + "2025-11-24T20:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2975.0585110576994 + ], + [ + "2025-11-24T20:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.5109261794223 + ], + [ + "2025-11-24T20:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.743063392159 + ], + [ + "2025-11-24T20:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.627251358698 + ], + [ + "2025-11-24T20:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.192799515609 + ], + [ + "2025-11-24T20:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.5418365418072 + ], + [ + "2025-11-24T20:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2962.4026690615337 + ], + [ + "2025-11-24T19:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.6111606119935 + ], + [ + "2025-11-24T19:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.1959749292578 + ], + [ + "2025-11-24T19:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.6912613496675 + ], + [ + "2025-11-24T19:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.9548642217887 + ], + [ + "2025-11-24T19:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2961.8078905788207 + ], + [ + "2025-11-24T19:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2957.033406360468 + ], + [ + "2025-11-24T19:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.987756561174 + ], + [ + "2025-11-24T19:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2958.3365538583807 + ], + [ + "2025-11-24T19:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2959.1483566967627 + ], + [ + "2025-11-24T19:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.563041578135 + ], + [ + "2025-11-24T19:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.527290609369 + ], + [ + "2025-11-24T19:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.6422517442643 + ], + [ + "2025-11-24T19:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.886365206947 + ], + [ + "2025-11-24T19:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.3494422766585 + ], + [ + "2025-11-24T19:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2973.5044654158746 + ], + [ + "2025-11-24T19:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2968.2748815092073 + ], + [ + "2025-11-24T19:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2969.1729832918004 + ], + [ + "2025-11-24T19:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.682016240104 + ], + [ + "2025-11-24T19:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.9066805715906 + ], + [ + "2025-11-24T19:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2971.037823294929 + ], + [ + "2025-11-24T19:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.709351908446 + ], + [ + "2025-11-24T19:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2972.6264497942207 + ], + [ + "2025-11-24T19:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2970.675134717522 + ], + [ + "2025-11-24T19:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2966.8471223087167 + ], + [ + "2025-11-24T19:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.7558774291924 + ], + [ + "2025-11-24T19:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.5898818493097 + ], + [ + "2025-11-24T19:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2974.7082206693076 + ], + [ + "2025-11-24T19:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2967.7496048389826 + ], + [ + "2025-11-24T19:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2960.704076356668 + ], + [ + "2025-11-24T19:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2955.1977251886733 + ], + [ + "2025-11-24T18:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2956.321181417345 + ], + [ + "2025-11-24T18:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.0599894525985 + ], + [ + "2025-11-24T18:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.2793995535553 + ], + [ + "2025-11-24T18:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.579505900239 + ], + [ + "2025-11-24T18:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2954.971019900047 + ], + [ + "2025-11-24T18:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2953.249713393271 + ], + [ + "2025-11-24T18:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.7101018535136 + ], + [ + "2025-11-24T18:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.62622984051 + ], + [ + "2025-11-24T18:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.6508987522957 + ], + [ + "2025-11-24T18:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.2207901365337 + ], + [ + "2025-11-24T18:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.5463125883466 + ], + [ + "2025-11-24T18:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.519757651191 + ], + [ + "2025-11-24T18:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.79456029257 + ], + [ + "2025-11-24T18:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.348777300339 + ], + [ + "2025-11-24T18:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2932.7593685570055 + ], + [ + "2025-11-24T18:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.848851859948 + ], + [ + "2025-11-24T18:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2938.8313314595835 + ], + [ + "2025-11-24T18:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.1247825318883 + ], + [ + "2025-11-24T18:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2940.9036839422747 + ], + [ + "2025-11-24T18:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.302883848422 + ], + [ + "2025-11-24T18:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.3110447768604 + ], + [ + "2025-11-24T18:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2945.635960483933 + ], + [ + "2025-11-24T18:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2947.8568535406066 + ], + [ + "2025-11-24T18:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.794492222735 + ], + [ + "2025-11-24T18:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.160126912523 + ], + [ + "2025-11-24T18:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2948.7559532160876 + ], + [ + "2025-11-24T18:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2944.752282263503 + ], + [ + "2025-11-24T18:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2941.6386475500826 + ], + [ + "2025-11-24T18:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2946.9410975904834 + ], + [ + "2025-11-24T18:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2943.235182433932 + ], + [ + "2025-11-24T17:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2939.8615474651074 + ], + [ + "2025-11-24T17:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2931.956341795544 + ], + [ + "2025-11-24T17:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2930.6336525199 + ], + [ + "2025-11-24T17:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.9661798717875 + ], + [ + "2025-11-24T17:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2925.446549950532 + ], + [ + "2025-11-24T17:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.718149630943 + ], + [ + "2025-11-24T17:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.9661304775295 + ], + [ + "2025-11-24T17:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2922.0613255496814 + ], + [ + "2025-11-24T17:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2921.9551106366703 + ], + [ + "2025-11-24T17:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2927.4056999351233 + ], + [ + "2025-11-24T17:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2923.526751193933 + ], + [ + "2025-11-24T17:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2917.8035502979187 + ], + [ + "2025-11-24T17:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2918.3494306831667 + ], + [ + "2025-11-24T17:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2914.723645129074 + ], + [ + "2025-11-24T17:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2911.758689606823 + ], + [ + "2025-11-24T17:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2913.2928360137876 + ], + [ + "2025-11-24T17:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2909.324700469768 + ], + [ + "2025-11-24T17:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2892.2453244228614 + ], + [ + "2025-11-24T17:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.0362904013955 + ], + [ + "2025-11-24T17:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.8940743903736 + ], + [ + "2025-11-24T17:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2866.261121749773 + ], + [ + "2025-11-24T17:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2864.3040125771763 + ], + [ + "2025-11-24T17:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.201344630871 + ], + [ + "2025-11-24T17:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.785537615933 + ], + [ + "2025-11-24T17:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.422749880912 + ], + [ + "2025-11-24T17:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.4908758736524 + ], + [ + "2025-11-24T17:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.599578858636 + ], + [ + "2025-11-24T17:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.081069198393 + ], + [ + "2025-11-24T17:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2862.595012318289 + ], + [ + "2025-11-24T17:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.7446569424505 + ], + [ + "2025-11-24T16:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.7972570023776 + ], + [ + "2025-11-24T16:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2859.2469685086853 + ], + [ + "2025-11-24T16:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2859.822421727083 + ], + [ + "2025-11-24T16:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2860.3328936724447 + ], + [ + "2025-11-24T16:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2865.340772047467 + ], + [ + "2025-11-24T16:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.210082073674 + ], + [ + "2025-11-24T16:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2866.7478074020223 + ], + [ + "2025-11-24T16:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.183612248234 + ], + [ + "2025-11-24T16:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.4296347051427 + ], + [ + "2025-11-24T16:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.769277795648 + ], + [ + "2025-11-24T16:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2851.261157579745 + ], + [ + "2025-11-24T16:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.265351802046 + ], + [ + "2025-11-24T16:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.0470145357376 + ], + [ + "2025-11-24T16:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2847.720088672618 + ], + [ + "2025-11-24T16:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2850.88350213025 + ], + [ + "2025-11-24T16:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2849.874931776676 + ], + [ + "2025-11-24T16:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.450181826349 + ], + [ + "2025-11-24T16:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2849.1060461191937 + ], + [ + "2025-11-24T16:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2848.9062974399353 + ], + [ + "2025-11-24T16:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2849.6932877046215 + ], + [ + "2025-11-24T16:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2847.8421599964254 + ], + [ + "2025-11-24T16:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.9994254816024 + ], + [ + "2025-11-24T16:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.3246534123837 + ], + [ + "2025-11-24T16:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.738777034296 + ], + [ + "2025-11-24T16:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.9158817391176 + ], + [ + "2025-11-24T16:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2847.774315133134 + ], + [ + "2025-11-24T16:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2849.694982343296 + ], + [ + "2025-11-24T16:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.143223586264 + ], + [ + "2025-11-24T16:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.707799439013 + ], + [ + "2025-11-24T16:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.724449269404 + ], + [ + "2025-11-24T15:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.109595450076 + ], + [ + "2025-11-24T15:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.5971026283514 + ], + [ + "2025-11-24T15:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.677795173792 + ], + [ + "2025-11-24T15:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.0356132894603 + ], + [ + "2025-11-24T15:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2850.3940067497415 + ], + [ + "2025-11-24T15:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.697563708565 + ], + [ + "2025-11-24T15:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2850.4442082881433 + ], + [ + "2025-11-24T15:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2851.910773614806 + ], + [ + "2025-11-24T15:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.922968356743 + ], + [ + "2025-11-24T15:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.2126828566065 + ], + [ + "2025-11-24T15:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.000629224491 + ], + [ + "2025-11-24T15:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.5860555632603 + ], + [ + "2025-11-24T15:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.2490061756826 + ], + [ + "2025-11-24T15:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.1884448581095 + ], + [ + "2025-11-24T15:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.4455541719403 + ], + [ + "2025-11-24T15:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.0883120759804 + ], + [ + "2025-11-24T15:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.7342201359 + ], + [ + "2025-11-24T15:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.7215301526066 + ], + [ + "2025-11-24T15:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.8751968757306 + ], + [ + "2025-11-24T15:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.9349012045914 + ], + [ + "2025-11-24T15:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.062909418763 + ], + [ + "2025-11-24T15:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.7186861832492 + ], + [ + "2025-11-24T15:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.0777805488706 + ], + [ + "2025-11-24T15:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.348578215394 + ], + [ + "2025-11-24T15:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.9244668947586 + ], + [ + "2025-11-24T15:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.685748557931 + ], + [ + "2025-11-24T15:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.9875097227764 + ], + [ + "2025-11-24T15:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.774821701471 + ], + [ + "2025-11-24T15:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.2619101187433 + ], + [ + "2025-11-24T15:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.7030548872985 + ], + [ + "2025-11-24T14:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.0244834076225 + ], + [ + "2025-11-24T14:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.4464648044836 + ], + [ + "2025-11-24T14:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.6484478759944 + ], + [ + "2025-11-24T14:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.941748267748 + ], + [ + "2025-11-24T14:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.3396464397183 + ], + [ + "2025-11-24T14:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.9817176873767 + ], + [ + "2025-11-24T14:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.7796983292774 + ], + [ + "2025-11-24T14:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.1504712274113 + ], + [ + "2025-11-24T14:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.2520092764116 + ], + [ + "2025-11-24T14:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.352167870544 + ], + [ + "2025-11-24T14:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.726726145581 + ], + [ + "2025-11-24T14:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.2718494323476 + ], + [ + "2025-11-24T14:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.481152219592 + ], + [ + "2025-11-24T14:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.7201075475773 + ], + [ + "2025-11-24T14:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.977512314869 + ], + [ + "2025-11-24T14:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.2435886416324 + ], + [ + "2025-11-24T14:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.7944269939135 + ], + [ + "2025-11-24T14:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.92160566448 + ], + [ + "2025-11-24T14:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.5436288476653 + ], + [ + "2025-11-24T14:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.692272996374 + ], + [ + "2025-11-24T14:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.611608679754 + ], + [ + "2025-11-24T14:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.276974236058 + ], + [ + "2025-11-24T14:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.9883508691687 + ], + [ + "2025-11-24T14:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.6531849652283 + ], + [ + "2025-11-24T14:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.1961250366285 + ], + [ + "2025-11-24T14:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.1951476467602 + ], + [ + "2025-11-24T14:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.5120184621965 + ], + [ + "2025-11-24T14:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.8353565038 + ], + [ + "2025-11-24T14:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.413247564516 + ], + [ + "2025-11-24T13:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.3965608288404 + ], + [ + "2025-11-24T13:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.678399834287 + ], + [ + "2025-11-24T13:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.9238273513956 + ], + [ + "2025-11-24T13:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.4898959737134 + ], + [ + "2025-11-24T13:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.0201568032503 + ], + [ + "2025-11-24T13:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.4359797305174 + ], + [ + "2025-11-24T13:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.889635356269 + ], + [ + "2025-11-24T13:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.9424184919176 + ], + [ + "2025-11-24T13:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.6348046773232 + ], + [ + "2025-11-24T13:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.882041067516 + ], + [ + "2025-11-24T13:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.1290856648397 + ], + [ + "2025-11-24T13:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.5771116112182 + ], + [ + "2025-11-24T13:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.949416943276 + ], + [ + "2025-11-24T13:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.1460057435656 + ], + [ + "2025-11-24T13:32:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.083559052442 + ], + [ + "2025-11-24T13:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.2827625980044 + ], + [ + "2025-11-24T13:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.484247446253 + ], + [ + "2025-11-24T13:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.9423683071595 + ], + [ + "2025-11-24T13:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.2668455005855 + ], + [ + "2025-11-24T13:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.835913202656 + ], + [ + "2025-11-24T13:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.1180244395573 + ], + [ + "2025-11-24T13:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.3005169684557 + ], + [ + "2025-11-24T13:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.3016730884788 + ], + [ + "2025-11-24T13:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.587727454975 + ], + [ + "2025-11-24T13:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.4011717345643 + ], + [ + "2025-11-24T13:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.4369464544175 + ], + [ + "2025-11-24T13:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.16064994642 + ], + [ + "2025-11-24T13:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.179628718178 + ], + [ + "2025-11-24T13:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.065329212668 + ], + [ + "2025-11-24T13:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.2634676568573 + ], + [ + "2025-11-24T12:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.1495801224773 + ], + [ + "2025-11-24T12:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.9489489282496 + ], + [ + "2025-11-24T12:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.9171094338394 + ], + [ + "2025-11-24T12:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.329531956624 + ], + [ + "2025-11-24T12:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.378938503706 + ], + [ + "2025-11-24T12:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.951952719188 + ], + [ + "2025-11-24T12:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.4237387771796 + ], + [ + "2025-11-24T12:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.0161501729 + ], + [ + "2025-11-24T12:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.5484269538574 + ], + [ + "2025-11-24T12:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.0180423707675 + ], + [ + "2025-11-24T12:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.178546855289 + ], + [ + "2025-11-24T12:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.7217325624356 + ], + [ + "2025-11-24T12:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.152026425622 + ], + [ + "2025-11-24T12:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.7857156700975 + ], + [ + "2025-11-24T12:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.6480084033083 + ], + [ + "2025-11-24T12:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.0453579752007 + ], + [ + "2025-11-24T12:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.724732960827 + ], + [ + "2025-11-24T12:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.759723847959 + ], + [ + "2025-11-24T12:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.4315393115508 + ], + [ + "2025-11-24T12:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.73902638824 + ], + [ + "2025-11-24T12:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.0148342823136 + ], + [ + "2025-11-24T12:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.5348112779348 + ], + [ + "2025-11-24T12:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.2164739477616 + ], + [ + "2025-11-24T12:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.202552673174 + ], + [ + "2025-11-24T12:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.603710946968 + ], + [ + "2025-11-24T12:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.6506617703003 + ], + [ + "2025-11-24T12:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.433294987948 + ], + [ + "2025-11-24T12:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.0672431642442 + ], + [ + "2025-11-24T12:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.1777607646773 + ], + [ + "2025-11-24T12:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.7528986466496 + ], + [ + "2025-11-24T11:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.992908198734 + ], + [ + "2025-11-24T11:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.576423820639 + ], + [ + "2025-11-24T11:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.160333955923 + ], + [ + "2025-11-24T11:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.910175230218 + ], + [ + "2025-11-24T11:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.013912642675 + ], + [ + "2025-11-24T11:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.141161348198 + ], + [ + "2025-11-24T11:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.32172532634 + ], + [ + "2025-11-24T11:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.3169683164942 + ], + [ + "2025-11-24T11:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.127535869936 + ], + [ + "2025-11-24T11:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.3761792992664 + ], + [ + "2025-11-24T11:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.371061654692 + ], + [ + "2025-11-24T11:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.191121256055 + ], + [ + "2025-11-24T11:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.5183803038153 + ], + [ + "2025-11-24T11:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.2286028491926 + ], + [ + "2025-11-24T11:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.112421945256 + ], + [ + "2025-11-24T11:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.827125337616 + ], + [ + "2025-11-24T11:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.140487292319 + ], + [ + "2025-11-24T11:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.4079723668256 + ], + [ + "2025-11-24T11:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.553327905876 + ], + [ + "2025-11-24T11:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.5427250677026 + ], + [ + "2025-11-24T11:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.0149304071283 + ], + [ + "2025-11-24T11:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.2498482877254 + ], + [ + "2025-11-24T11:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.1515012770615 + ], + [ + "2025-11-24T11:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.6025559326004 + ], + [ + "2025-11-24T11:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.794869109491 + ], + [ + "2025-11-24T11:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.9412709886105 + ], + [ + "2025-11-24T11:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.761734203763 + ], + [ + "2025-11-24T11:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.4689136879942 + ], + [ + "2025-11-24T11:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.725871218954 + ], + [ + "2025-11-24T11:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.4593599494538 + ], + [ + "2025-11-24T10:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.504842360324 + ], + [ + "2025-11-24T10:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.99439479524 + ], + [ + "2025-11-24T10:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.55767265255 + ], + [ + "2025-11-24T10:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.5764680973707 + ], + [ + "2025-11-24T10:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.504020522216 + ], + [ + "2025-11-24T10:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.41409391772 + ], + [ + "2025-11-24T10:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.3071398959205 + ], + [ + "2025-11-24T10:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.482886086873 + ], + [ + "2025-11-24T10:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.7623456718243 + ], + [ + "2025-11-24T10:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.7451578159585 + ], + [ + "2025-11-24T10:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.3830775472256 + ], + [ + "2025-11-24T10:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.225760042655 + ], + [ + "2025-11-24T10:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.097139510031 + ], + [ + "2025-11-24T10:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.0828538417645 + ], + [ + "2025-11-24T10:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.012793086537 + ], + [ + "2025-11-24T10:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.1149237832974 + ], + [ + "2025-11-24T10:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.412276753662 + ], + [ + "2025-11-24T10:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.1174025806654 + ], + [ + "2025-11-24T10:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.1275891092905 + ], + [ + "2025-11-24T10:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.248325517741 + ], + [ + "2025-11-24T10:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.5963958602156 + ], + [ + "2025-11-24T10:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.5812041437366 + ], + [ + "2025-11-24T10:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.8564661422406 + ], + [ + "2025-11-24T10:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.0120324786453 + ], + [ + "2025-11-24T10:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.633247275313 + ], + [ + "2025-11-24T10:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.7635556804917 + ], + [ + "2025-11-24T10:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.8260323258473 + ], + [ + "2025-11-24T10:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.810219827445 + ], + [ + "2025-11-24T10:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.808383420192 + ], + [ + "2025-11-24T10:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.169339196292 + ], + [ + "2025-11-24T09:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.1858187525377 + ], + [ + "2025-11-24T09:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.7206975470126 + ], + [ + "2025-11-24T09:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.979166089721 + ], + [ + "2025-11-24T09:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.306765195641 + ], + [ + "2025-11-24T09:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.77853865985 + ], + [ + "2025-11-24T09:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.261005041267 + ], + [ + "2025-11-24T09:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.0752889128617 + ], + [ + "2025-11-24T09:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.2913073134628 + ], + [ + "2025-11-24T09:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.205222130654 + ], + [ + "2025-11-24T09:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.6887872901907 + ], + [ + "2025-11-24T09:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.3435393902837 + ], + [ + "2025-11-24T09:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.9887778634366 + ], + [ + "2025-11-24T09:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.919134928984 + ], + [ + "2025-11-24T09:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.013986437111 + ], + [ + "2025-11-24T09:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.27255779142 + ], + [ + "2025-11-24T09:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.880714093113 + ], + [ + "2025-11-24T09:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.8830873138854 + ], + [ + "2025-11-24T09:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.2747099760095 + ], + [ + "2025-11-24T09:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.525126609474 + ], + [ + "2025-11-24T09:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.169338801228 + ], + [ + "2025-11-24T09:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.715854158826 + ], + [ + "2025-11-24T09:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.1039028540513 + ], + [ + "2025-11-24T09:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.665765891389 + ], + [ + "2025-11-24T09:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.75886027284 + ], + [ + "2025-11-24T09:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.3170262723866 + ], + [ + "2025-11-24T09:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.1049660367426 + ], + [ + "2025-11-24T09:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.8927314237762 + ], + [ + "2025-11-24T09:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.4170331680793 + ], + [ + "2025-11-24T09:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.632799004028 + ], + [ + "2025-11-24T09:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.6079916422805 + ], + [ + "2025-11-24T08:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.6289809698724 + ], + [ + "2025-11-24T08:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.9846084045225 + ], + [ + "2025-11-24T08:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.9444042535806 + ], + [ + "2025-11-24T08:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.979455440244 + ], + [ + "2025-11-24T08:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.6472312840965 + ], + [ + "2025-11-24T08:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.157663992501 + ], + [ + "2025-11-24T08:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.275583202913 + ], + [ + "2025-11-24T08:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.2932099859026 + ], + [ + "2025-11-24T08:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.273299226159 + ], + [ + "2025-11-24T08:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.5523399525187 + ], + [ + "2025-11-24T08:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.0078294348646 + ], + [ + "2025-11-24T08:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.9332792133537 + ], + [ + "2025-11-24T08:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.7119015008025 + ], + [ + "2025-11-24T08:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.1761116039997 + ], + [ + "2025-11-24T08:32:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.811969867441 + ], + [ + "2025-11-24T08:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.876338827851 + ], + [ + "2025-11-24T08:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.1384785806363 + ], + [ + "2025-11-24T08:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.485562010504 + ], + [ + "2025-11-24T08:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.763041504133 + ], + [ + "2025-11-24T08:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.839252519545 + ], + [ + "2025-11-24T08:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.14531817655 + ], + [ + "2025-11-24T08:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.902172084506 + ], + [ + "2025-11-24T08:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.001364583275 + ], + [ + "2025-11-24T08:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.6903445872526 + ], + [ + "2025-11-24T08:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.0917406116428 + ], + [ + "2025-11-24T08:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.709714602877 + ], + [ + "2025-11-24T08:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.110838570415 + ], + [ + "2025-11-24T08:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.6046940021547 + ], + [ + "2025-11-24T08:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.286520471068 + ], + [ + "2025-11-24T08:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.486784884906 + ], + [ + "2025-11-24T07:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.0860246512225 + ], + [ + "2025-11-24T07:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.9949747223386 + ], + [ + "2025-11-24T07:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.1720527779075 + ], + [ + "2025-11-24T07:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.001493019861 + ], + [ + "2025-11-24T07:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.45980079259 + ], + [ + "2025-11-24T07:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.586381324959 + ], + [ + "2025-11-24T07:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.075452354635 + ], + [ + "2025-11-24T07:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.3741446454696 + ], + [ + "2025-11-24T07:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.410106275801 + ], + [ + "2025-11-24T07:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.5212648770125 + ], + [ + "2025-11-24T07:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.21190057056 + ], + [ + "2025-11-24T07:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.4968113273517 + ], + [ + "2025-11-24T07:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.3392540296886 + ], + [ + "2025-11-24T07:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.438547616935 + ], + [ + "2025-11-24T07:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.339549812489 + ], + [ + "2025-11-24T07:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.021620409026 + ], + [ + "2025-11-24T07:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.3559365979972 + ], + [ + "2025-11-24T07:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.6139895295823 + ], + [ + "2025-11-24T07:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.387297411296 + ], + [ + "2025-11-24T07:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.987003092427 + ], + [ + "2025-11-24T07:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.971457162536 + ], + [ + "2025-11-24T07:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.019708921531 + ], + [ + "2025-11-24T07:15:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.8772163905587 + ], + [ + "2025-11-24T07:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.243234013873 + ], + [ + "2025-11-24T07:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.7280963441667 + ], + [ + "2025-11-24T07:10:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.986122623993 + ], + [ + "2025-11-24T07:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.286023765252 + ], + [ + "2025-11-24T07:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.9499021980614 + ], + [ + "2025-11-24T07:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.161523514845 + ], + [ + "2025-11-24T07:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.4164452711584 + ], + [ + "2025-11-24T06:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.265876524305 + ], + [ + "2025-11-24T06:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.1961061885186 + ], + [ + "2025-11-24T06:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.676202291254 + ], + [ + "2025-11-24T06:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.3516317232884 + ], + [ + "2025-11-24T06:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.6733950386483 + ], + [ + "2025-11-24T06:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.0785024625948 + ], + [ + "2025-11-24T06:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.0609575513513 + ], + [ + "2025-11-24T06:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.095734291567 + ], + [ + "2025-11-24T06:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.8210853184 + ], + [ + "2025-11-24T06:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.345118880038 + ], + [ + "2025-11-24T06:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.245183312753 + ], + [ + "2025-11-24T06:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.0443839065697 + ], + [ + "2025-11-24T06:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.4100276409413 + ], + [ + "2025-11-24T06:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.075343785424 + ], + [ + "2025-11-24T06:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.669921472923 + ], + [ + "2025-11-24T06:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.7193182057154 + ], + [ + "2025-11-24T06:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.015869759275 + ], + [ + "2025-11-24T06:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.0408729484902 + ], + [ + "2025-11-24T06:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.6106536647885 + ], + [ + "2025-11-24T06:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.025680939299 + ], + [ + "2025-11-24T06:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2847.1655930480856 + ], + [ + "2025-11-24T06:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.433619373823 + ], + [ + "2025-11-24T06:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.172262029406 + ], + [ + "2025-11-24T06:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.022509199449 + ], + [ + "2025-11-24T06:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.364435619939 + ], + [ + "2025-11-24T06:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.0078165342875 + ], + [ + "2025-11-24T06:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.7309472686065 + ], + [ + "2025-11-24T06:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.2372943839337 + ], + [ + "2025-11-24T06:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2862.9788342317206 + ], + [ + "2025-11-24T06:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2864.9482809890123 + ], + [ + "2025-11-24T05:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2866.001162358664 + ], + [ + "2025-11-24T05:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2865.915757903727 + ], + [ + "2025-11-24T05:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2864.9927329765956 + ], + [ + "2025-11-24T05:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2866.436531717957 + ], + [ + "2025-11-24T05:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.4608655200987 + ], + [ + "2025-11-24T05:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2864.566579501286 + ], + [ + "2025-11-24T05:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2867.082006566318 + ], + [ + "2025-11-24T05:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.9070249940046 + ], + [ + "2025-11-24T05:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.8541091323136 + ], + [ + "2025-11-24T05:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2873.822901595713 + ], + [ + "2025-11-24T05:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2869.9535476653173 + ], + [ + "2025-11-24T05:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2867.3805339406904 + ], + [ + "2025-11-24T05:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2864.3554044136727 + ], + [ + "2025-11-24T05:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.6242286886904 + ], + [ + "2025-11-24T05:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2870.93154656184 + ], + [ + "2025-11-24T05:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2878.7798663547724 + ], + [ + "2025-11-24T05:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2882.1960708765387 + ], + [ + "2025-11-24T05:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.083064719835 + ], + [ + "2025-11-24T05:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.5620492312255 + ], + [ + "2025-11-24T05:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2882.0521735382804 + ], + [ + "2025-11-24T05:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2877.393243885741 + ], + [ + "2025-11-24T05:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2878.9335515793828 + ], + [ + "2025-11-24T05:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2880.708094891304 + ], + [ + "2025-11-24T05:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2881.1342939375972 + ], + [ + "2025-11-24T05:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2875.787178971778 + ], + [ + "2025-11-24T05:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.919779299185 + ], + [ + "2025-11-24T05:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.6407887475825 + ], + [ + "2025-11-24T05:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.6951493356837 + ], + [ + "2025-11-24T05:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.8978519912407 + ], + [ + "2025-11-24T05:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.3213786123383 + ], + [ + "2025-11-24T04:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.1731057947045 + ], + [ + "2025-11-24T04:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.3259710339044 + ], + [ + "2025-11-24T04:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.9930095290647 + ], + [ + "2025-11-24T04:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.8837550208145 + ], + [ + "2025-11-24T04:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.8500853204464 + ], + [ + "2025-11-24T04:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.682659340777 + ], + [ + "2025-11-24T04:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.402599464074 + ], + [ + "2025-11-24T04:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.9109333570595 + ], + [ + "2025-11-24T04:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.382162572806 + ], + [ + "2025-11-24T04:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.044029707909 + ], + [ + "2025-11-24T04:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.789341658479 + ], + [ + "2025-11-24T04:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.6403683190238 + ], + [ + "2025-11-24T04:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.606258352088 + ], + [ + "2025-11-24T04:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.6553522878917 + ], + [ + "2025-11-24T04:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.7097692431553 + ], + [ + "2025-11-24T04:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.4961153734903 + ], + [ + "2025-11-24T04:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.1477698089457 + ], + [ + "2025-11-24T04:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.523431615333 + ], + [ + "2025-11-24T04:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.6888678774035 + ], + [ + "2025-11-24T04:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.870501379858 + ], + [ + "2025-11-24T04:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.3883356614706 + ], + [ + "2025-11-24T04:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.6748682410494 + ], + [ + "2025-11-24T04:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.258752714035 + ], + [ + "2025-11-24T04:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2868.1071811262887 + ], + [ + "2025-11-24T04:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2863.7954599664827 + ], + [ + "2025-11-24T04:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2860.017670645689 + ], + [ + "2025-11-24T04:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.519687720347 + ], + [ + "2025-11-24T04:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.60844670727 + ], + [ + "2025-11-24T04:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2854.998507619734 + ], + [ + "2025-11-24T04:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.6203480259915 + ], + [ + "2025-11-24T03:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.7085628006553 + ], + [ + "2025-11-24T03:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.4617463003256 + ], + [ + "2025-11-24T03:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.72965034127 + ], + [ + "2025-11-24T03:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.397361887044 + ], + [ + "2025-11-24T03:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.5218388606027 + ], + [ + "2025-11-24T03:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.3393995704164 + ], + [ + "2025-11-24T03:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.7478424037827 + ], + [ + "2025-11-24T03:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.2423028642606 + ], + [ + "2025-11-24T03:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.3441876583825 + ], + [ + "2025-11-24T03:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.2646574248533 + ], + [ + "2025-11-24T03:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2854.7261683165657 + ], + [ + "2025-11-24T03:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.1824760625395 + ], + [ + "2025-11-24T03:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2862.567428670513 + ], + [ + "2025-11-24T03:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2862.586791090011 + ], + [ + "2025-11-24T03:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.8889190963637 + ], + [ + "2025-11-24T03:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2857.536433755234 + ], + [ + "2025-11-24T03:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.9108101057327 + ], + [ + "2025-11-24T03:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.7109670066698 + ], + [ + "2025-11-24T03:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2860.1109484547087 + ], + [ + "2025-11-24T03:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2861.030075128435 + ], + [ + "2025-11-24T03:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.684787142152 + ], + [ + "2025-11-24T03:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.375647766022 + ], + [ + "2025-11-24T03:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.6050353807354 + ], + [ + "2025-11-24T03:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.132781253603 + ], + [ + "2025-11-24T03:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.97761140344 + ], + [ + "2025-11-24T03:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.6937910048714 + ], + [ + "2025-11-24T03:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.232233655028 + ], + [ + "2025-11-24T03:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.498555219249 + ], + [ + "2025-11-24T03:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.043073101256 + ], + [ + "2025-11-24T03:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.950427430574 + ], + [ + "2025-11-24T02:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.5812128646257 + ], + [ + "2025-11-24T02:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.6581796302166 + ], + [ + "2025-11-24T02:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.859465598572 + ], + [ + "2025-11-24T02:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.2524471072124 + ], + [ + "2025-11-24T02:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.404473639135 + ], + [ + "2025-11-24T02:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.5016823752726 + ], + [ + "2025-11-24T02:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.5472847236765 + ], + [ + "2025-11-24T02:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.6062841159187 + ], + [ + "2025-11-24T02:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.4006458735716 + ], + [ + "2025-11-24T02:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.578617701187 + ], + [ + "2025-11-24T02:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.758861449977 + ], + [ + "2025-11-24T02:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.2999462166367 + ], + [ + "2025-11-24T02:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.6704272329575 + ], + [ + "2025-11-24T02:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2852.793682294398 + ], + [ + "2025-11-24T02:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.1037484830795 + ], + [ + "2025-11-24T02:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.609572521522 + ], + [ + "2025-11-24T02:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2856.7663088062804 + ], + [ + "2025-11-24T02:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2861.687832148089 + ], + [ + "2025-11-24T02:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2859.008074961798 + ], + [ + "2025-11-24T02:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2861.4616237360688 + ], + [ + "2025-11-24T02:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2861.761994833643 + ], + [ + "2025-11-24T02:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2860.2267543221083 + ], + [ + "2025-11-24T02:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2858.4714958893496 + ], + [ + "2025-11-24T02:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2853.750441670302 + ], + [ + "2025-11-24T02:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2851.890252124752 + ], + [ + "2025-11-24T02:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2846.558694013556 + ], + [ + "2025-11-24T02:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.2693906385184 + ], + [ + "2025-11-24T02:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.2898841113984 + ], + [ + "2025-11-24T02:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.599567347711 + ], + [ + "2025-11-24T02:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.255041924752 + ], + [ + "2025-11-24T01:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.583032065395 + ], + [ + "2025-11-24T01:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.198216410052 + ], + [ + "2025-11-24T01:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.768245521999 + ], + [ + "2025-11-24T01:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.7865905503872 + ], + [ + "2025-11-24T01:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.581448846857 + ], + [ + "2025-11-24T01:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.645655862826 + ], + [ + "2025-11-24T01:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.0942622167395 + ], + [ + "2025-11-24T01:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.04903442261 + ], + [ + "2025-11-24T01:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.0537007441562 + ], + [ + "2025-11-24T01:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.5349090167106 + ], + [ + "2025-11-24T01:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.4415191125013 + ], + [ + "2025-11-24T01:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.9883161051184 + ], + [ + "2025-11-24T01:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.5638380871114 + ], + [ + "2025-11-24T01:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.0791271335133 + ], + [ + "2025-11-24T01:32:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.332656523004 + ], + [ + "2025-11-24T01:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.7989337461154 + ], + [ + "2025-11-24T01:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.391490987516 + ], + [ + "2025-11-24T01:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.560735666073 + ], + [ + "2025-11-24T01:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.641401217804 + ], + [ + "2025-11-24T01:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.1305753396873 + ], + [ + "2025-11-24T01:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.4516090123584 + ], + [ + "2025-11-24T01:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.7853990208096 + ], + [ + "2025-11-24T01:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.288292661701 + ], + [ + "2025-11-24T01:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.87612783259 + ], + [ + "2025-11-24T01:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.244657981129 + ], + [ + "2025-11-24T01:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.7140116343076 + ], + [ + "2025-11-24T01:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.50074728733 + ], + [ + "2025-11-24T01:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.4435149819965 + ], + [ + "2025-11-24T01:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.5351912919455 + ], + [ + "2025-11-24T01:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.6697964227574 + ], + [ + "2025-11-24T00:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.3991276925162 + ], + [ + "2025-11-24T00:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.184496039506 + ], + [ + "2025-11-24T00:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.1255520542004 + ], + [ + "2025-11-24T00:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.656943228843 + ], + [ + "2025-11-24T00:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.109320837387 + ], + [ + "2025-11-24T00:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.105795295864 + ], + [ + "2025-11-24T00:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.3394745271107 + ], + [ + "2025-11-24T00:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.653202107151 + ], + [ + "2025-11-24T00:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.9939697769605 + ], + [ + "2025-11-24T00:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.3220120392234 + ], + [ + "2025-11-24T00:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.0449314674156 + ], + [ + "2025-11-24T00:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.6508336624966 + ], + [ + "2025-11-24T00:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.6768893357976 + ], + [ + "2025-11-24T00:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.7606050502436 + ], + [ + "2025-11-24T00:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.9002438316606 + ], + [ + "2025-11-24T00:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.390639028626 + ], + [ + "2025-11-24T00:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.1182397694033 + ], + [ + "2025-11-24T00:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2777.0014866003817 + ], + [ + "2025-11-24T00:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2771.5094628475435 + ], + [ + "2025-11-24T00:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2766.35104893 + ], + [ + "2025-11-24T00:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.3834264574925 + ], + [ + "2025-11-24T00:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2777.9588496300235 + ], + [ + "2025-11-24T00:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2778.2054337263053 + ], + [ + "2025-11-24T00:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.4650408575276 + ], + [ + "2025-11-24T00:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.32362783889 + ], + [ + "2025-11-24T00:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.617159360575 + ], + [ + "2025-11-24T00:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.3047119430557 + ], + [ + "2025-11-24T00:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.5298729855426 + ], + [ + "2025-11-24T00:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.70280741913 + ], + [ + "2025-11-24T00:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.783002464827 + ], + [ + "2025-11-23T23:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.5141364778983 + ], + [ + "2025-11-23T23:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.540900696033 + ], + [ + "2025-11-23T23:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.5182139054973 + ], + [ + "2025-11-23T23:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.2282713147597 + ], + [ + "2025-11-23T23:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.0256191855947 + ], + [ + "2025-11-23T23:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.2225551553643 + ], + [ + "2025-11-23T23:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.007065875865 + ], + [ + "2025-11-23T23:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.6670532441185 + ], + [ + "2025-11-23T23:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.0810007042774 + ], + [ + "2025-11-23T23:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.5541592247573 + ], + [ + "2025-11-23T23:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.303596971007 + ], + [ + "2025-11-23T23:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.4040123875966 + ], + [ + "2025-11-23T23:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.5715466526135 + ], + [ + "2025-11-23T23:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.6246779045773 + ], + [ + "2025-11-23T23:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.295891578169 + ], + [ + "2025-11-23T23:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.1388028481065 + ], + [ + "2025-11-23T23:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.931104971849 + ], + [ + "2025-11-23T23:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.4279378931897 + ], + [ + "2025-11-23T23:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.028950859129 + ], + [ + "2025-11-23T23:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.383355153614 + ], + [ + "2025-11-23T23:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.3675810319164 + ], + [ + "2025-11-23T23:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.163355572044 + ], + [ + "2025-11-23T23:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.500215699899 + ], + [ + "2025-11-23T23:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.682885934961 + ], + [ + "2025-11-23T23:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.8279066509695 + ], + [ + "2025-11-23T23:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.534448004924 + ], + [ + "2025-11-23T23:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.1009015680465 + ], + [ + "2025-11-23T23:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.300526508474 + ], + [ + "2025-11-23T23:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.2555731561642 + ], + [ + "2025-11-23T23:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.4114635740584 + ], + [ + "2025-11-23T22:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.4865873399153 + ], + [ + "2025-11-23T22:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.6213913406805 + ], + [ + "2025-11-23T22:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.582778428403 + ], + [ + "2025-11-23T22:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.1071686942064 + ], + [ + "2025-11-23T22:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.585142349418 + ], + [ + "2025-11-23T22:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.686571082753 + ], + [ + "2025-11-23T22:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.885853411046 + ], + [ + "2025-11-23T22:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.85939900461 + ], + [ + "2025-11-23T22:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.595055884048 + ], + [ + "2025-11-23T22:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.167927392373 + ], + [ + "2025-11-23T22:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.8633831197985 + ], + [ + "2025-11-23T22:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.756552092606 + ], + [ + "2025-11-23T22:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.9382850664724 + ], + [ + "2025-11-23T22:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.711892357617 + ], + [ + "2025-11-23T22:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.998307215859 + ], + [ + "2025-11-23T22:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.683912066417 + ], + [ + "2025-11-23T22:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.646504672991 + ], + [ + "2025-11-23T22:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.7886024523423 + ], + [ + "2025-11-23T22:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.6674454253066 + ], + [ + "2025-11-23T22:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.3490618040296 + ], + [ + "2025-11-23T22:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.8454817430325 + ], + [ + "2025-11-23T22:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.9393269559678 + ], + [ + "2025-11-23T22:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.995078963878 + ], + [ + "2025-11-23T22:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.2968230471256 + ], + [ + "2025-11-23T22:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.1562045075075 + ], + [ + "2025-11-23T22:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.740670098427 + ], + [ + "2025-11-23T22:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.1885941982982 + ], + [ + "2025-11-23T22:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.1240565846915 + ], + [ + "2025-11-23T22:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.584264007418 + ], + [ + "2025-11-23T22:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.4054507520787 + ], + [ + "2025-11-23T21:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.2959906946635 + ], + [ + "2025-11-23T21:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.752077356975 + ], + [ + "2025-11-23T21:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.7874141928805 + ], + [ + "2025-11-23T21:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.3911698043953 + ], + [ + "2025-11-23T21:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.9765650391732 + ], + [ + "2025-11-23T21:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.7223408430805 + ], + [ + "2025-11-23T21:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.840538577572 + ], + [ + "2025-11-23T21:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.2742559840462 + ], + [ + "2025-11-23T21:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.899543701218 + ], + [ + "2025-11-23T21:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.1009521948436 + ], + [ + "2025-11-23T21:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.49539575158 + ], + [ + "2025-11-23T21:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.735658737843 + ], + [ + "2025-11-23T21:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.578780861324 + ], + [ + "2025-11-23T21:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.737662191786 + ], + [ + "2025-11-23T21:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.817237595275 + ], + [ + "2025-11-23T21:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.3515976488857 + ], + [ + "2025-11-23T21:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.2621349374604 + ], + [ + "2025-11-23T21:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.4523628159936 + ], + [ + "2025-11-23T21:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.055780299179 + ], + [ + "2025-11-23T21:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.347602015809 + ], + [ + "2025-11-23T21:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.991157794401 + ], + [ + "2025-11-23T21:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.39038546379 + ], + [ + "2025-11-23T21:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.3835062990406 + ], + [ + "2025-11-23T21:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.4863304654305 + ], + [ + "2025-11-23T21:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.448909588657 + ], + [ + "2025-11-23T21:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.3920322575746 + ], + [ + "2025-11-23T21:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.9892890009883 + ], + [ + "2025-11-23T21:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.1620455097463 + ], + [ + "2025-11-23T21:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.334889698061 + ], + [ + "2025-11-23T21:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.564780811827 + ], + [ + "2025-11-23T20:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.4688061503207 + ], + [ + "2025-11-23T20:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.0203736906205 + ], + [ + "2025-11-23T20:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.9485462282246 + ], + [ + "2025-11-23T20:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.9083771028254 + ], + [ + "2025-11-23T20:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.5962216183725 + ], + [ + "2025-11-23T20:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.840334458068 + ], + [ + "2025-11-23T20:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.711742027871 + ], + [ + "2025-11-23T20:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.794346035183 + ], + [ + "2025-11-23T20:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.0026204305386 + ], + [ + "2025-11-23T20:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.235498036752 + ], + [ + "2025-11-23T20:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.395493800654 + ], + [ + "2025-11-23T20:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.361746821338 + ], + [ + "2025-11-23T20:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.1301169671906 + ], + [ + "2025-11-23T20:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.7120824250787 + ], + [ + "2025-11-23T20:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.076137603509 + ], + [ + "2025-11-23T20:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.6109171905246 + ], + [ + "2025-11-23T20:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.325504609311 + ], + [ + "2025-11-23T20:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.361988799767 + ], + [ + "2025-11-23T20:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.1435703932 + ], + [ + "2025-11-23T20:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.2168629884313 + ], + [ + "2025-11-23T20:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.6871180853027 + ], + [ + "2025-11-23T20:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.1232121486214 + ], + [ + "2025-11-23T20:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.9752617707527 + ], + [ + "2025-11-23T20:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.2565403278118 + ], + [ + "2025-11-23T20:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.8773306685157 + ], + [ + "2025-11-23T20:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.7880852708554 + ], + [ + "2025-11-23T20:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.060612922841 + ], + [ + "2025-11-23T20:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.201149669178 + ], + [ + "2025-11-23T20:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.4170771699187 + ], + [ + "2025-11-23T20:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.8594554149404 + ], + [ + "2025-11-23T19:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.561777103808 + ], + [ + "2025-11-23T19:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.369161842805 + ], + [ + "2025-11-23T19:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.4480908005585 + ], + [ + "2025-11-23T19:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.9004287496523 + ], + [ + "2025-11-23T19:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.433612105265 + ], + [ + "2025-11-23T19:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.1420977656794 + ], + [ + "2025-11-23T19:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.4563979621225 + ], + [ + "2025-11-23T19:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.3333139565466 + ], + [ + "2025-11-23T19:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.864652398292 + ], + [ + "2025-11-23T19:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.7219080490127 + ], + [ + "2025-11-23T19:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.0513085364337 + ], + [ + "2025-11-23T19:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.0127932992527 + ], + [ + "2025-11-23T19:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.865442976775 + ], + [ + "2025-11-23T19:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.909546202483 + ], + [ + "2025-11-23T19:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.1838958937387 + ], + [ + "2025-11-23T19:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.59333743371 + ], + [ + "2025-11-23T19:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.921239561801 + ], + [ + "2025-11-23T19:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.4785272430786 + ], + [ + "2025-11-23T19:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.1123583756844 + ], + [ + "2025-11-23T19:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.677858168582 + ], + [ + "2025-11-23T19:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.173742752725 + ], + [ + "2025-11-23T19:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.973559595566 + ], + [ + "2025-11-23T19:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.2801608433824 + ], + [ + "2025-11-23T19:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.4889074854304 + ], + [ + "2025-11-23T19:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.647788654286 + ], + [ + "2025-11-23T19:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.6902360224694 + ], + [ + "2025-11-23T19:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.641665059359 + ], + [ + "2025-11-23T19:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.9005562733923 + ], + [ + "2025-11-23T19:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.7153114269668 + ], + [ + "2025-11-23T19:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.1245754056627 + ], + [ + "2025-11-23T18:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.7230159104142 + ], + [ + "2025-11-23T18:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.0793882782905 + ], + [ + "2025-11-23T18:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.9954249852362 + ], + [ + "2025-11-23T18:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.138441825618 + ], + [ + "2025-11-23T18:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.7982912334687 + ], + [ + "2025-11-23T18:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.028546655885 + ], + [ + "2025-11-23T18:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.4555552534925 + ], + [ + "2025-11-23T18:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.342558151854 + ], + [ + "2025-11-23T18:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.6267327182973 + ], + [ + "2025-11-23T18:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.13926921044 + ], + [ + "2025-11-23T18:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.1559906501857 + ], + [ + "2025-11-23T18:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.093494953076 + ], + [ + "2025-11-23T18:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.1890017261 + ], + [ + "2025-11-23T18:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.4869312423707 + ], + [ + "2025-11-23T18:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.1444978384757 + ], + [ + "2025-11-23T18:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.229779021817 + ], + [ + "2025-11-23T18:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.8470167088776 + ], + [ + "2025-11-23T18:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.9837937221746 + ], + [ + "2025-11-23T18:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.8977695216513 + ], + [ + "2025-11-23T18:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.5500061097314 + ], + [ + "2025-11-23T18:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.9607305798213 + ], + [ + "2025-11-23T18:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.225631954117 + ], + [ + "2025-11-23T18:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.4687192075125 + ], + [ + "2025-11-23T18:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.3206896312777 + ], + [ + "2025-11-23T18:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.882528766238 + ], + [ + "2025-11-23T18:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.5143056543798 + ], + [ + "2025-11-23T18:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.4689979516475 + ], + [ + "2025-11-23T18:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.6550600454116 + ], + [ + "2025-11-23T18:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.323526435477 + ], + [ + "2025-11-23T18:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.2822267502356 + ], + [ + "2025-11-23T18:00:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.477706778234 + ], + [ + "2025-11-23T17:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.3197585209145 + ], + [ + "2025-11-23T17:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.7248900756476 + ], + [ + "2025-11-23T17:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.037627357415 + ], + [ + "2025-11-23T17:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.440078728751 + ], + [ + "2025-11-23T17:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.0667328009517 + ], + [ + "2025-11-23T17:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.197212070392 + ], + [ + "2025-11-23T17:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.6418955866347 + ], + [ + "2025-11-23T17:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.8432618620823 + ], + [ + "2025-11-23T17:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.4447909581227 + ], + [ + "2025-11-23T17:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.2738433105606 + ], + [ + "2025-11-23T17:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.9079680050154 + ], + [ + "2025-11-23T17:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.9883131365586 + ], + [ + "2025-11-23T17:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.4867504494814 + ], + [ + "2025-11-23T17:31:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.169523085152 + ], + [ + "2025-11-23T17:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.777701552578 + ], + [ + "2025-11-23T17:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.950034688787 + ], + [ + "2025-11-23T17:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.4831777751633 + ], + [ + "2025-11-23T17:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.9985570775984 + ], + [ + "2025-11-23T17:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.6585565167293 + ], + [ + "2025-11-23T17:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.597746808651 + ], + [ + "2025-11-23T17:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.417297625978 + ], + [ + "2025-11-23T17:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.0006789443223 + ], + [ + "2025-11-23T17:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2798.9793993220837 + ], + [ + "2025-11-23T17:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.297474918653 + ], + [ + "2025-11-23T17:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.2251839634896 + ], + [ + "2025-11-23T17:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.960356847677 + ], + [ + "2025-11-23T17:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.097415258769 + ], + [ + "2025-11-23T17:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.646123305424 + ], + [ + "2025-11-23T17:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.9819408804838 + ], + [ + "2025-11-23T16:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.876400948122 + ], + [ + "2025-11-23T16:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.7183727031706 + ], + [ + "2025-11-23T16:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.652309320529 + ], + [ + "2025-11-23T16:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.0810619620347 + ], + [ + "2025-11-23T16:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.231444873005 + ], + [ + "2025-11-23T16:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.5626658444353 + ], + [ + "2025-11-23T16:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.7742743880754 + ], + [ + "2025-11-23T16:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.9493915505454 + ], + [ + "2025-11-23T16:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.86715929865 + ], + [ + "2025-11-23T16:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.402968452854 + ], + [ + "2025-11-23T16:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.5343062893126 + ], + [ + "2025-11-23T16:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.6377170903024 + ], + [ + "2025-11-23T16:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.396351152994 + ], + [ + "2025-11-23T16:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.1038374034074 + ], + [ + "2025-11-23T16:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.9219256628276 + ], + [ + "2025-11-23T16:30:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.954104894974 + ], + [ + "2025-11-23T16:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.9273360680004 + ], + [ + "2025-11-23T16:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.0922340724987 + ], + [ + "2025-11-23T16:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.1040685649837 + ], + [ + "2025-11-23T16:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.938193750037 + ], + [ + "2025-11-23T16:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.8009003149355 + ], + [ + "2025-11-23T16:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.0441442733377 + ], + [ + "2025-11-23T16:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.440074154102 + ], + [ + "2025-11-23T16:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.927677277802 + ], + [ + "2025-11-23T16:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.3508406829387 + ], + [ + "2025-11-23T16:10:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2843.336346252057 + ], + [ + "2025-11-23T16:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.5333576744433 + ], + [ + "2025-11-23T16:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.6829081280257 + ], + [ + "2025-11-23T16:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.9250145582128 + ], + [ + "2025-11-23T16:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.9154918936842 + ], + [ + "2025-11-23T15:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.9777153140076 + ], + [ + "2025-11-23T15:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.685281609964 + ], + [ + "2025-11-23T15:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.868402166831 + ], + [ + "2025-11-23T15:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.9053769536354 + ], + [ + "2025-11-23T15:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.2052675556492 + ], + [ + "2025-11-23T15:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.2504958480995 + ], + [ + "2025-11-23T15:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.8844943947115 + ], + [ + "2025-11-23T15:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.0615707390275 + ], + [ + "2025-11-23T15:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.3723929168013 + ], + [ + "2025-11-23T15:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.351325697657 + ], + [ + "2025-11-23T15:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.4155484147605 + ], + [ + "2025-11-23T15:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.227616926826 + ], + [ + "2025-11-23T15:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.48842667025 + ], + [ + "2025-11-23T15:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.3582548066056 + ], + [ + "2025-11-23T15:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.5411714778143 + ], + [ + "2025-11-23T15:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.3768169316095 + ], + [ + "2025-11-23T15:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.6462919377514 + ], + [ + "2025-11-23T15:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.4333723299696 + ], + [ + "2025-11-23T15:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.078052765253 + ], + [ + "2025-11-23T15:22:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.9175527753055 + ], + [ + "2025-11-23T15:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.6490267519944 + ], + [ + "2025-11-23T15:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.557175298362 + ], + [ + "2025-11-23T15:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.478365215955 + ], + [ + "2025-11-23T15:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.546459780404 + ], + [ + "2025-11-23T15:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.396540989756 + ], + [ + "2025-11-23T15:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.786533520108 + ], + [ + "2025-11-23T15:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.2579199602324 + ], + [ + "2025-11-23T15:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.4974958982807 + ], + [ + "2025-11-23T15:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.6261521385145 + ], + [ + "2025-11-23T15:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.146388337869 + ], + [ + "2025-11-23T14:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.3438514010036 + ], + [ + "2025-11-23T14:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.8643690493523 + ], + [ + "2025-11-23T14:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.2970613270663 + ], + [ + "2025-11-23T14:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.208875188246 + ], + [ + "2025-11-23T14:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.685926092215 + ], + [ + "2025-11-23T14:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.51313382392 + ], + [ + "2025-11-23T14:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.731841641821 + ], + [ + "2025-11-23T14:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.7475478755487 + ], + [ + "2025-11-23T14:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.356532939525 + ], + [ + "2025-11-23T14:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.0110767755514 + ], + [ + "2025-11-23T14:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.989297563397 + ], + [ + "2025-11-23T14:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.839384212069 + ], + [ + "2025-11-23T14:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.320907317497 + ], + [ + "2025-11-23T14:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.995229793308 + ], + [ + "2025-11-23T14:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.355878245435 + ], + [ + "2025-11-23T14:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.5654895880166 + ], + [ + "2025-11-23T14:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.011447380909 + ], + [ + "2025-11-23T14:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.9974647814233 + ], + [ + "2025-11-23T14:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.6208960510166 + ], + [ + "2025-11-23T14:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.409594770946 + ], + [ + "2025-11-23T14:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.3139894501946 + ], + [ + "2025-11-23T14:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.9815473633817 + ], + [ + "2025-11-23T14:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.8407946354587 + ], + [ + "2025-11-23T14:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.430284568809 + ], + [ + "2025-11-23T14:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.1062121717973 + ], + [ + "2025-11-23T14:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.5676011823425 + ], + [ + "2025-11-23T14:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.5231889695865 + ], + [ + "2025-11-23T14:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2855.5404183314863 + ], + [ + "2025-11-23T14:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2850.660479857965 + ], + [ + "2025-11-23T14:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2846.7865362514563 + ], + [ + "2025-11-23T13:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.1843140735427 + ], + [ + "2025-11-23T13:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.7132228090845 + ], + [ + "2025-11-23T13:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.9289121158986 + ], + [ + "2025-11-23T13:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.014138923983 + ], + [ + "2025-11-23T13:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.8334136930325 + ], + [ + "2025-11-23T13:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.371302549211 + ], + [ + "2025-11-23T13:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.1448281723347 + ], + [ + "2025-11-23T13:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2842.4011659151283 + ], + [ + "2025-11-23T13:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2840.0263600888843 + ], + [ + "2025-11-23T13:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.9843815543504 + ], + [ + "2025-11-23T13:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.2169958236223 + ], + [ + "2025-11-23T13:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.161174558012 + ], + [ + "2025-11-23T13:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.6169672473297 + ], + [ + "2025-11-23T13:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.347381010256 + ], + [ + "2025-11-23T13:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.5170199249615 + ], + [ + "2025-11-23T13:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.691894259068 + ], + [ + "2025-11-23T13:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.2094198130026 + ], + [ + "2025-11-23T13:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.53091969049 + ], + [ + "2025-11-23T13:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.1937063886016 + ], + [ + "2025-11-23T13:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.2446570073057 + ], + [ + "2025-11-23T13:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.148366424448 + ], + [ + "2025-11-23T13:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.060540936147 + ], + [ + "2025-11-23T13:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.496981894655 + ], + [ + "2025-11-23T13:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.3106074434486 + ], + [ + "2025-11-23T13:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.918424482467 + ], + [ + "2025-11-23T13:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.312067487946 + ], + [ + "2025-11-23T13:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.078682443924 + ], + [ + "2025-11-23T13:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.713036046841 + ], + [ + "2025-11-23T13:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.8882104465056 + ], + [ + "2025-11-23T13:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.5640204642564 + ], + [ + "2025-11-23T12:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.18399615629 + ], + [ + "2025-11-23T12:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.457137222904 + ], + [ + "2025-11-23T12:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.3756375745666 + ], + [ + "2025-11-23T12:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.904614102238 + ], + [ + "2025-11-23T12:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.635615639586 + ], + [ + "2025-11-23T12:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.6594289370687 + ], + [ + "2025-11-23T12:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.774953680891 + ], + [ + "2025-11-23T12:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.202061672447 + ], + [ + "2025-11-23T12:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.0998070096425 + ], + [ + "2025-11-23T12:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.1363060457406 + ], + [ + "2025-11-23T12:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.48139953286 + ], + [ + "2025-11-23T12:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2837.4409262830527 + ], + [ + "2025-11-23T12:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.9261586724415 + ], + [ + "2025-11-23T12:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.6994866951836 + ], + [ + "2025-11-23T12:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.3519121150293 + ], + [ + "2025-11-23T12:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.075830021268 + ], + [ + "2025-11-23T12:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2833.894165982704 + ], + [ + "2025-11-23T12:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.796850406598 + ], + [ + "2025-11-23T12:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2836.5783667621145 + ], + [ + "2025-11-23T12:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2841.2426198500334 + ], + [ + "2025-11-23T12:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2839.2639846433126 + ], + [ + "2025-11-23T12:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.4704115981003 + ], + [ + "2025-11-23T12:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.483690441902 + ], + [ + "2025-11-23T12:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.38028107478 + ], + [ + "2025-11-23T12:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.033337415251 + ], + [ + "2025-11-23T12:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.8633818334556 + ], + [ + "2025-11-23T12:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.5606419966516 + ], + [ + "2025-11-23T12:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.4208458496364 + ], + [ + "2025-11-23T12:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.6081346220267 + ], + [ + "2025-11-23T12:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.347754488546 + ], + [ + "2025-11-23T11:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.94209484836 + ], + [ + "2025-11-23T11:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.9381301071735 + ], + [ + "2025-11-23T11:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.7602872478715 + ], + [ + "2025-11-23T11:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.868420072211 + ], + [ + "2025-11-23T11:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.534577679244 + ], + [ + "2025-11-23T11:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.876211420119 + ], + [ + "2025-11-23T11:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.6280866818793 + ], + [ + "2025-11-23T11:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.2977046202855 + ], + [ + "2025-11-23T11:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.7105425447694 + ], + [ + "2025-11-23T11:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.5075740774823 + ], + [ + "2025-11-23T11:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.1769990957005 + ], + [ + "2025-11-23T11:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.316026208821 + ], + [ + "2025-11-23T11:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.2285194537317 + ], + [ + "2025-11-23T11:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.376262204351 + ], + [ + "2025-11-23T11:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.429624253037 + ], + [ + "2025-11-23T11:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.947529573773 + ], + [ + "2025-11-23T11:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.973872762775 + ], + [ + "2025-11-23T11:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.8184382391505 + ], + [ + "2025-11-23T11:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.8602410568683 + ], + [ + "2025-11-23T11:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.784577489343 + ], + [ + "2025-11-23T11:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.138574206889 + ], + [ + "2025-11-23T11:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.1658144794683 + ], + [ + "2025-11-23T11:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.134945751235 + ], + [ + "2025-11-23T11:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.8969595773005 + ], + [ + "2025-11-23T11:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.262207881878 + ], + [ + "2025-11-23T11:10:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.765473852405 + ], + [ + "2025-11-23T11:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.7506592796713 + ], + [ + "2025-11-23T11:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.3201602627228 + ], + [ + "2025-11-23T11:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.2665740997304 + ], + [ + "2025-11-23T11:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.388853456922 + ], + [ + "2025-11-23T10:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.360244025447 + ], + [ + "2025-11-23T10:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.253815935061 + ], + [ + "2025-11-23T10:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.7400668981636 + ], + [ + "2025-11-23T10:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.5345334248564 + ], + [ + "2025-11-23T10:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.5798713886425 + ], + [ + "2025-11-23T10:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.1800667147386 + ], + [ + "2025-11-23T10:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.344597420347 + ], + [ + "2025-11-23T10:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.994862704672 + ], + [ + "2025-11-23T10:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.1259277027198 + ], + [ + "2025-11-23T10:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.1860065228802 + ], + [ + "2025-11-23T10:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.0996141938954 + ], + [ + "2025-11-23T10:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.9899846735416 + ], + [ + "2025-11-23T10:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.5707024655358 + ], + [ + "2025-11-23T10:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.0016772161393 + ], + [ + "2025-11-23T10:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.344033836901 + ], + [ + "2025-11-23T10:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.135816440902 + ], + [ + "2025-11-23T10:28:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.347604948192 + ], + [ + "2025-11-23T10:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.5936022266646 + ], + [ + "2025-11-23T10:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.8723262094773 + ], + [ + "2025-11-23T10:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.4549395481486 + ], + [ + "2025-11-23T10:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.4119147983015 + ], + [ + "2025-11-23T10:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.7968875235474 + ], + [ + "2025-11-23T10:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.9647582890684 + ], + [ + "2025-11-23T10:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.248993359288 + ], + [ + "2025-11-23T10:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.3474378048786 + ], + [ + "2025-11-23T10:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.304661459685 + ], + [ + "2025-11-23T10:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.8884430186718 + ], + [ + "2025-11-23T10:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.9366950333474 + ], + [ + "2025-11-23T10:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.1229148243456 + ], + [ + "2025-11-23T10:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.9062692310335 + ], + [ + "2025-11-23T09:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.666762238106 + ], + [ + "2025-11-23T09:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.193142501168 + ], + [ + "2025-11-23T09:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.1998820461745 + ], + [ + "2025-11-23T09:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.5073109777836 + ], + [ + "2025-11-23T09:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.0083096560243 + ], + [ + "2025-11-23T09:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.108001621663 + ], + [ + "2025-11-23T09:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.292902113228 + ], + [ + "2025-11-23T09:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.48488860357 + ], + [ + "2025-11-23T09:44:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.1082184419647 + ], + [ + "2025-11-23T09:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.1828251604425 + ], + [ + "2025-11-23T09:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.620907035229 + ], + [ + "2025-11-23T09:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.392533472191 + ], + [ + "2025-11-23T09:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.2926026528826 + ], + [ + "2025-11-23T09:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.2087403682335 + ], + [ + "2025-11-23T09:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.4013505370253 + ], + [ + "2025-11-23T09:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.543539031165 + ], + [ + "2025-11-23T09:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.9496026960114 + ], + [ + "2025-11-23T09:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.7727710942095 + ], + [ + "2025-11-23T09:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.190831765157 + ], + [ + "2025-11-23T09:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.0465432423057 + ], + [ + "2025-11-23T09:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.348333712838 + ], + [ + "2025-11-23T09:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.207581717759 + ], + [ + "2025-11-23T09:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.695931761751 + ], + [ + "2025-11-23T09:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.119424589934 + ], + [ + "2025-11-23T09:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.6327390536503 + ], + [ + "2025-11-23T09:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.218484186016 + ], + [ + "2025-11-23T09:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.2619777937043 + ], + [ + "2025-11-23T09:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.784309651276 + ], + [ + "2025-11-23T09:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.6106825764164 + ], + [ + "2025-11-23T09:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.24019516809 + ], + [ + "2025-11-23T08:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.9637197387733 + ], + [ + "2025-11-23T08:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.088886602591 + ], + [ + "2025-11-23T08:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.454688114963 + ], + [ + "2025-11-23T08:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.717028801804 + ], + [ + "2025-11-23T08:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.173732896085 + ], + [ + "2025-11-23T08:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.7178009512004 + ], + [ + "2025-11-23T08:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.078576862168 + ], + [ + "2025-11-23T08:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.3133804219096 + ], + [ + "2025-11-23T08:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.8790261200456 + ], + [ + "2025-11-23T08:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.7828358654574 + ], + [ + "2025-11-23T08:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.1264803975914 + ], + [ + "2025-11-23T08:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.6117677639036 + ], + [ + "2025-11-23T08:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.503590838672 + ], + [ + "2025-11-23T08:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.968249757023 + ], + [ + "2025-11-23T08:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.4894372804947 + ], + [ + "2025-11-23T08:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.7582554660003 + ], + [ + "2025-11-23T08:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.6811227019875 + ], + [ + "2025-11-23T08:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.8922329532834 + ], + [ + "2025-11-23T08:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.339053522605 + ], + [ + "2025-11-23T08:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.2095454724804 + ], + [ + "2025-11-23T08:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.8739775145664 + ], + [ + "2025-11-23T08:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.6680744948694 + ], + [ + "2025-11-23T08:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.74685797194 + ], + [ + "2025-11-23T08:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.026368383354 + ], + [ + "2025-11-23T08:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.3632841226063 + ], + [ + "2025-11-23T08:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.2967220978053 + ], + [ + "2025-11-23T08:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.1260488150883 + ], + [ + "2025-11-23T08:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.626667249108 + ], + [ + "2025-11-23T08:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.661244566746 + ], + [ + "2025-11-23T08:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2794.4147193166123 + ], + [ + "2025-11-23T07:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.9138766502774 + ], + [ + "2025-11-23T07:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.4340928546917 + ], + [ + "2025-11-23T07:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.5744153203796 + ], + [ + "2025-11-23T07:54:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2792.599451716229 + ], + [ + "2025-11-23T07:52:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.0967757020167 + ], + [ + "2025-11-23T07:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.7506785908176 + ], + [ + "2025-11-23T07:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.254187313426 + ], + [ + "2025-11-23T07:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.2075888072936 + ], + [ + "2025-11-23T07:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.5285588510487 + ], + [ + "2025-11-23T07:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.066142839337 + ], + [ + "2025-11-23T07:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.1263356835425 + ], + [ + "2025-11-23T07:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.146340041448 + ], + [ + "2025-11-23T07:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.6570234387264 + ], + [ + "2025-11-23T07:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2782.7536499139915 + ], + [ + "2025-11-23T07:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.270740899186 + ], + [ + "2025-11-23T07:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.665994995939 + ], + [ + "2025-11-23T07:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.3224256647914 + ], + [ + "2025-11-23T07:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.056266376221 + ], + [ + "2025-11-23T07:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2791.4478431438524 + ], + [ + "2025-11-23T07:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.0822029213227 + ], + [ + "2025-11-23T07:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.0957318567134 + ], + [ + "2025-11-23T07:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2795.983901353263 + ], + [ + "2025-11-23T07:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.902062486985 + ], + [ + "2025-11-23T07:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.7823785152195 + ], + [ + "2025-11-23T07:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.1362397295534 + ], + [ + "2025-11-23T07:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.0830147773854 + ], + [ + "2025-11-23T07:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2796.41100262963 + ], + [ + "2025-11-23T07:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2797.839191078092 + ], + [ + "2025-11-23T07:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2803.231662508694 + ], + [ + "2025-11-23T07:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.3219925944154 + ], + [ + "2025-11-23T06:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.1880379991308 + ], + [ + "2025-11-23T06:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.8970382819366 + ], + [ + "2025-11-23T06:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.4444761427067 + ], + [ + "2025-11-23T06:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.934704172379 + ], + [ + "2025-11-23T06:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.5523210551883 + ], + [ + "2025-11-23T06:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.7367205135192 + ], + [ + "2025-11-23T06:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.1706935078937 + ], + [ + "2025-11-23T06:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.1557649234564 + ], + [ + "2025-11-23T06:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.521045410974 + ], + [ + "2025-11-23T06:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.7693687999526 + ], + [ + "2025-11-23T06:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.59368135648 + ], + [ + "2025-11-23T06:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.8075005799496 + ], + [ + "2025-11-23T06:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.7774708985153 + ], + [ + "2025-11-23T06:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.769610251876 + ], + [ + "2025-11-23T06:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.7093414404776 + ], + [ + "2025-11-23T06:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.804154348502 + ], + [ + "2025-11-23T06:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.134424019504 + ], + [ + "2025-11-23T06:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.3184429359767 + ], + [ + "2025-11-23T06:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.02670594915 + ], + [ + "2025-11-23T06:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.730638837939 + ], + [ + "2025-11-23T06:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.6387735678863 + ], + [ + "2025-11-23T06:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.6191615182197 + ], + [ + "2025-11-23T06:16:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.1805307879454 + ], + [ + "2025-11-23T06:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.183244910847 + ], + [ + "2025-11-23T06:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.667308493941 + ], + [ + "2025-11-23T06:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.274179470899 + ], + [ + "2025-11-23T06:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.483130086973 + ], + [ + "2025-11-23T06:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.1085033464005 + ], + [ + "2025-11-23T06:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.9829938618623 + ], + [ + "2025-11-23T06:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.5461236394867 + ], + [ + "2025-11-23T06:00:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.2605352751493 + ], + [ + "2025-11-23T05:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.13509876763 + ], + [ + "2025-11-23T05:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.6411813885156 + ], + [ + "2025-11-23T05:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.894058137425 + ], + [ + "2025-11-23T05:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.594106780679 + ], + [ + "2025-11-23T05:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.9578998731836 + ], + [ + "2025-11-23T05:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.5509408085177 + ], + [ + "2025-11-23T05:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.4429473018513 + ], + [ + "2025-11-23T05:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.4536005346213 + ], + [ + "2025-11-23T05:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.3976428376673 + ], + [ + "2025-11-23T05:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2815.39717980399 + ], + [ + "2025-11-23T05:38:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.4575284133866 + ], + [ + "2025-11-23T05:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.9712213837565 + ], + [ + "2025-11-23T05:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.486780282538 + ], + [ + "2025-11-23T05:32:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.150627351334 + ], + [ + "2025-11-23T05:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.096368465042 + ], + [ + "2025-11-23T05:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.021925281687 + ], + [ + "2025-11-23T05:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.5191815885187 + ], + [ + "2025-11-23T05:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.2749132120134 + ], + [ + "2025-11-23T05:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.0358433325086 + ], + [ + "2025-11-23T05:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.5198824762465 + ], + [ + "2025-11-23T05:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.2458007381715 + ], + [ + "2025-11-23T05:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.5255034695224 + ], + [ + "2025-11-23T05:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.4236374269763 + ], + [ + "2025-11-23T05:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.259041632418 + ], + [ + "2025-11-23T05:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.8845393184565 + ], + [ + "2025-11-23T05:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.705906387421 + ], + [ + "2025-11-23T05:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.399520518252 + ], + [ + "2025-11-23T05:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.0552477110896 + ], + [ + "2025-11-23T05:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.773222984343 + ], + [ + "2025-11-23T04:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.2315618533967 + ], + [ + "2025-11-23T04:58:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.8670967554726 + ], + [ + "2025-11-23T04:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.9754743162857 + ], + [ + "2025-11-23T04:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.918343151204 + ], + [ + "2025-11-23T04:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.6288570310553 + ], + [ + "2025-11-23T04:50:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.685465896728 + ], + [ + "2025-11-23T04:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.8004040049514 + ], + [ + "2025-11-23T04:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.1292126004146 + ], + [ + "2025-11-23T04:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.6448077846244 + ], + [ + "2025-11-23T04:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.7741825173507 + ], + [ + "2025-11-23T04:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.7627209309094 + ], + [ + "2025-11-23T04:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.5260695528873 + ], + [ + "2025-11-23T04:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.377443269895 + ], + [ + "2025-11-23T04:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.908428564858 + ], + [ + "2025-11-23T04:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.035469396137 + ], + [ + "2025-11-23T04:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2835.104699289568 + ], + [ + "2025-11-23T04:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.857722938137 + ], + [ + "2025-11-23T04:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.144837644351 + ], + [ + "2025-11-23T04:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2844.5643870191393 + ], + [ + "2025-11-23T04:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2845.9421785657455 + ], + [ + "2025-11-23T04:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2838.2808155145294 + ], + [ + "2025-11-23T04:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2834.553523930966 + ], + [ + "2025-11-23T04:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2831.3671422734246 + ], + [ + "2025-11-23T04:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2832.601397397395 + ], + [ + "2025-11-23T04:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2829.38561327716 + ], + [ + "2025-11-23T04:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.021381256922 + ], + [ + "2025-11-23T04:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.411814942492 + ], + [ + "2025-11-23T04:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.2031979109097 + ], + [ + "2025-11-23T04:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2823.489195334712 + ], + [ + "2025-11-23T04:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.639268927638 + ], + [ + "2025-11-23T03:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.4197294282867 + ], + [ + "2025-11-23T03:57:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.901718651421 + ], + [ + "2025-11-23T03:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.1838705071405 + ], + [ + "2025-11-23T03:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.7919820299635 + ], + [ + "2025-11-23T03:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.4337159122015 + ], + [ + "2025-11-23T03:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.578689109877 + ], + [ + "2025-11-23T03:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.810424683847 + ], + [ + "2025-11-23T03:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.84897021905 + ], + [ + "2025-11-23T03:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.7676992407405 + ], + [ + "2025-11-23T03:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2817.376979970314 + ], + [ + "2025-11-23T03:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.0721545798306 + ], + [ + "2025-11-23T03:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.7281086786693 + ], + [ + "2025-11-23T03:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2818.472352122482 + ], + [ + "2025-11-23T03:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.9515981620566 + ], + [ + "2025-11-23T03:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.694339003867 + ], + [ + "2025-11-23T03:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.1264007651735 + ], + [ + "2025-11-23T03:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.73903344585 + ], + [ + "2025-11-23T03:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.386400039563 + ], + [ + "2025-11-23T03:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.926757638837 + ], + [ + "2025-11-23T03:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.6303199043355 + ], + [ + "2025-11-23T03:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2812.662138641915 + ], + [ + "2025-11-23T03:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.7858587273167 + ], + [ + "2025-11-23T02:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.7293678144306 + ], + [ + "2025-11-23T02:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.9605559318793 + ], + [ + "2025-11-23T02:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2822.4962067597526 + ], + [ + "2025-11-23T02:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2825.567836454693 + ], + [ + "2025-11-23T02:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.0171984725835 + ], + [ + "2025-11-23T02:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2830.252231863054 + ], + [ + "2025-11-23T02:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2826.1242149814166 + ], + [ + "2025-11-23T02:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2828.164251901681 + ], + [ + "2025-11-23T02:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.5450793499576 + ], + [ + "2025-11-23T02:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2827.4790721131067 + ], + [ + "2025-11-23T02:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2824.9970072407145 + ], + [ + "2025-11-23T02:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.3210116902237 + ], + [ + "2025-11-23T02:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.3838413524913 + ], + [ + "2025-11-23T02:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2821.8998492136975 + ], + [ + "2025-11-23T02:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.650630341539 + ], + [ + "2025-11-23T02:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2819.7301035906703 + ], + [ + "2025-11-23T02:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2820.4514744610856 + ], + [ + "2025-11-23T02:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.2084487899806 + ], + [ + "2025-11-23T02:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.5732502417172 + ], + [ + "2025-11-23T02:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.75396210773 + ], + [ + "2025-11-23T02:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2811.556677946743 + ], + [ + "2025-11-23T02:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2810.7682001556436 + ], + [ + "2025-11-23T02:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.9276253093412 + ], + [ + "2025-11-23T02:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.978665739997 + ], + [ + "2025-11-23T02:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2806.478625839174 + ], + [ + "2025-11-23T02:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2807.443245055614 + ], + [ + "2025-11-23T02:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2816.5594987326967 + ], + [ + "2025-11-23T02:06:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2814.934597289183 + ], + [ + "2025-11-23T02:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2813.4659868359095 + ], + [ + "2025-11-23T02:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.506044260758 + ], + [ + "2025-11-23T01:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2804.702642206047 + ], + [ + "2025-11-23T01:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2809.330988595139 + ], + [ + "2025-11-23T01:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2808.0548935859742 + ], + [ + "2025-11-23T01:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2805.9618291725874 + ], + [ + "2025-11-23T01:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2801.0974272865437 + ], + [ + "2025-11-23T01:50:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.6824614492775 + ], + [ + "2025-11-23T01:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2800.4724406721516 + ], + [ + "2025-11-23T01:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.0445204624107 + ], + [ + "2025-11-23T01:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2802.931696898953 + ], + [ + "2025-11-23T01:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.9049050292588 + ], + [ + "2025-11-23T01:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2799.5574206826645 + ], + [ + "2025-11-23T01:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.848668965036 + ], + [ + "2025-11-23T01:36:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2788.422897356836 + ], + [ + "2025-11-23T01:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2789.456204875813 + ], + [ + "2025-11-23T01:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.2450647434566 + ], + [ + "2025-11-23T01:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.702316362292 + ], + [ + "2025-11-23T01:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.151711742746 + ], + [ + "2025-11-23T01:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2783.4636353995556 + ], + [ + "2025-11-23T01:24:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.8457986890458 + ], + [ + "2025-11-23T01:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.6819967847855 + ], + [ + "2025-11-23T01:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.936569422953 + ], + [ + "2025-11-23T01:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2784.4932022697935 + ], + [ + "2025-11-23T01:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.797983220695 + ], + [ + "2025-11-23T01:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.6124273660184 + ], + [ + "2025-11-23T01:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.445187320547 + ], + [ + "2025-11-23T01:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.6399241946056 + ], + [ + "2025-11-23T01:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2778.648319807401 + ], + [ + "2025-11-23T01:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2778.069901650798 + ], + [ + "2025-11-23T01:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2774.771154715162 + ], + [ + "2025-11-23T01:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2772.640283545438 + ], + [ + "2025-11-23T00:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2771.407896555969 + ], + [ + "2025-11-23T00:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.4573096158138 + ], + [ + "2025-11-23T00:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2771.003671075701 + ], + [ + "2025-11-23T00:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2773.9796070801162 + ], + [ + "2025-11-23T00:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2775.803412693695 + ], + [ + "2025-11-23T00:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2774.7415996384852 + ], + [ + "2025-11-23T00:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.228354871945 + ], + [ + "2025-11-23T00:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2782.335998133393 + ], + [ + "2025-11-23T00:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.1984089727594 + ], + [ + "2025-11-23T00:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.1767169180325 + ], + [ + "2025-11-23T00:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2785.931987529415 + ], + [ + "2025-11-23T00:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.604352987399 + ], + [ + "2025-11-23T00:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2782.70839296709 + ], + [ + "2025-11-23T00:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.294584399474 + ], + [ + "2025-11-23T00:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2778.331180401475 + ], + [ + "2025-11-23T00:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2776.3558294880104 + ], + [ + "2025-11-23T00:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.7608264468467 + ], + [ + "2025-11-23T00:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2774.697424697642 + ], + [ + "2025-11-23T00:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2775.131769956238 + ], + [ + "2025-11-23T00:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2772.0635410969417 + ], + [ + "2025-11-23T00:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2775.4925082062564 + ], + [ + "2025-11-23T00:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2768.2062977060264 + ], + [ + "2025-11-22T23:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.8064113406717 + ], + [ + "2025-11-22T23:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.542770556066 + ], + [ + "2025-11-22T23:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.2770735314693 + ], + [ + "2025-11-22T23:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2770.755565396828 + ], + [ + "2025-11-22T23:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2770.9042689556363 + ], + [ + "2025-11-22T23:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2772.7124161059896 + ], + [ + "2025-11-22T23:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.3192408139485 + ], + [ + "2025-11-22T23:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2772.154556498309 + ], + [ + "2025-11-22T23:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2771.5168598283344 + ], + [ + "2025-11-22T23:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2777.779739647873 + ], + [ + "2025-11-22T23:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2778.5971950827943 + ], + [ + "2025-11-22T23:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2776.311078709733 + ], + [ + "2025-11-22T23:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.1307972489403 + ], + [ + "2025-11-22T23:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2774.6771677485526 + ], + [ + "2025-11-22T23:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2777.30968302654 + ], + [ + "2025-11-22T23:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2776.2828774743216 + ], + [ + "2025-11-22T23:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2775.94560522718 + ], + [ + "2025-11-22T23:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2774.4111072487703 + ], + [ + "2025-11-22T23:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2772.3410453345673 + ], + [ + "2025-11-22T23:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2769.3337126427073 + ], + [ + "2025-11-22T23:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2771.178617024482 + ], + [ + "2025-11-22T23:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2773.53399689043 + ], + [ + "2025-11-22T23:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2780.9238606957197 + ], + [ + "2025-11-22T23:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2779.350972715226 + ], + [ + "2025-11-22T23:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2777.8848079267937 + ], + [ + "2025-11-22T23:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2781.253298561646 + ], + [ + "2025-11-22T23:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2782.3221848966773 + ], + [ + "2025-11-22T23:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.782106537095 + ], + [ + "2025-11-22T23:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2786.5087756973066 + ], + [ + "2025-11-22T23:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2787.003671286566 + ], + [ + "2025-11-22T22:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2790.862634112999 + ], + [ + "2025-11-22T22:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.691223436594 + ], + [ + "2025-11-22T22:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2793.714019018721 + ], + [ + "2025-11-22T22:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2776.345245144196 + ], + [ + "2025-11-22T22:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2765.476800666934 + ], + [ + "2025-11-22T22:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2766.7124448654654 + ], + [ + "2025-11-22T22:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2765.4514085460046 + ], + [ + "2025-11-22T22:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2766.8896937489903 + ], + [ + "2025-11-22T22:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2765.958732578387 + ], + [ + "2025-11-22T22:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2764.5420589554938 + ], + [ + "2025-11-22T22:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2765.3936684826967 + ], + [ + "2025-11-22T22:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2762.8699084886753 + ], + [ + "2025-11-22T22:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2760.45149075781 + ], + [ + "2025-11-22T22:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2760.475706680004 + ], + [ + "2025-11-22T22:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2759.422883243859 + ], + [ + "2025-11-22T22:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2758.030829556638 + ], + [ + "2025-11-22T22:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2759.5015928709668 + ], + [ + "2025-11-22T22:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2763.720032508668 + ], + [ + "2025-11-22T22:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2764.9636205994507 + ], + [ + "2025-11-22T22:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2761.524970747996 + ], + [ + "2025-11-22T22:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2762.2106019425564 + ], + [ + "2025-11-22T22:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2762.591323823876 + ], + [ + "2025-11-22T22:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2755.3789199536154 + ], + [ + "2025-11-22T22:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.1760964246037 + ], + [ + "2025-11-22T22:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.920226448035 + ], + [ + "2025-11-22T22:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.983937026727 + ], + [ + "2025-11-22T22:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.918739039599 + ], + [ + "2025-11-22T22:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.64199806091 + ], + [ + "2025-11-22T22:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.1227963948254 + ], + [ + "2025-11-22T22:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.142832194521 + ], + [ + "2025-11-22T21:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.2656205332437 + ], + [ + "2025-11-22T21:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.992314425164 + ], + [ + "2025-11-22T21:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.6594764253036 + ], + [ + "2025-11-22T21:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.8695307575767 + ], + [ + "2025-11-22T21:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.2290529878555 + ], + [ + "2025-11-22T21:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.979595938965 + ], + [ + "2025-11-22T21:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.481628361015 + ], + [ + "2025-11-22T21:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.8537275590807 + ], + [ + "2025-11-22T21:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.2036856576383 + ], + [ + "2025-11-22T21:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.5787109324615 + ], + [ + "2025-11-22T21:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.0710725356 + ], + [ + "2025-11-22T21:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.3438860201213 + ], + [ + "2025-11-22T21:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.905877842125 + ], + [ + "2025-11-22T21:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.5454332135814 + ], + [ + "2025-11-22T21:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.832946234099 + ], + [ + "2025-11-22T21:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.6178502701323 + ], + [ + "2025-11-22T21:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.5785083906776 + ], + [ + "2025-11-22T21:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.794903313834 + ], + [ + "2025-11-22T21:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.816145575658 + ], + [ + "2025-11-22T21:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.959907861941 + ], + [ + "2025-11-22T21:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.8693119552604 + ], + [ + "2025-11-22T21:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.88171788127 + ], + [ + "2025-11-22T21:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.585655140549 + ], + [ + "2025-11-22T21:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2737.025670244498 + ], + [ + "2025-11-22T21:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.138060516647 + ], + [ + "2025-11-22T21:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.4196200651913 + ], + [ + "2025-11-22T21:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.403065526106 + ], + [ + "2025-11-22T21:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2741.3325150578353 + ], + [ + "2025-11-22T21:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.607790686036 + ], + [ + "2025-11-22T21:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.9463307066108 + ], + [ + "2025-11-22T20:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.8757053014465 + ], + [ + "2025-11-22T20:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2735.1847011000523 + ], + [ + "2025-11-22T20:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2733.681391295416 + ], + [ + "2025-11-22T20:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2737.023487269208 + ], + [ + "2025-11-22T20:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2737.0775988184555 + ], + [ + "2025-11-22T20:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.662866036831 + ], + [ + "2025-11-22T20:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2741.918074774279 + ], + [ + "2025-11-22T20:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.7999896336655 + ], + [ + "2025-11-22T20:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.0144280007453 + ], + [ + "2025-11-22T20:42:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.19990995441 + ], + [ + "2025-11-22T20:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.884611085993 + ], + [ + "2025-11-22T20:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.6404202144636 + ], + [ + "2025-11-22T20:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.1577969252526 + ], + [ + "2025-11-22T20:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.723271028897 + ], + [ + "2025-11-22T20:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.9907277625866 + ], + [ + "2025-11-22T20:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.4672483398435 + ], + [ + "2025-11-22T20:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.0456596675012 + ], + [ + "2025-11-22T20:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.1098033444423 + ], + [ + "2025-11-22T20:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.028716242399 + ], + [ + "2025-11-22T20:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.5758952322994 + ], + [ + "2025-11-22T20:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.7093039965757 + ], + [ + "2025-11-22T20:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.72617057098 + ], + [ + "2025-11-22T20:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.480486119231 + ], + [ + "2025-11-22T20:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.3030220964715 + ], + [ + "2025-11-22T20:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.4581370734354 + ], + [ + "2025-11-22T20:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.4313618166143 + ], + [ + "2025-11-22T20:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.1977018188095 + ], + [ + "2025-11-22T20:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.9946981394364 + ], + [ + "2025-11-22T20:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.4693110316666 + ], + [ + "2025-11-22T20:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.332399677763 + ], + [ + "2025-11-22T19:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.7177893271255 + ], + [ + "2025-11-22T19:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.4906106270932 + ], + [ + "2025-11-22T19:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.707369815802 + ], + [ + "2025-11-22T19:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.089042291305 + ], + [ + "2025-11-22T19:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.4713176817413 + ], + [ + "2025-11-22T19:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.034271667036 + ], + [ + "2025-11-22T19:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.465364128157 + ], + [ + "2025-11-22T19:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.884712120497 + ], + [ + "2025-11-22T19:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.7122893974165 + ], + [ + "2025-11-22T19:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.7135774819276 + ], + [ + "2025-11-22T19:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.761534555166 + ], + [ + "2025-11-22T19:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.2944793507754 + ], + [ + "2025-11-22T19:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.004769599667 + ], + [ + "2025-11-22T19:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.032871540823 + ], + [ + "2025-11-22T19:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.2146162454305 + ], + [ + "2025-11-22T19:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.288258003633 + ], + [ + "2025-11-22T19:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.5476619440533 + ], + [ + "2025-11-22T19:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.7929916919898 + ], + [ + "2025-11-22T19:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.509543921128 + ], + [ + "2025-11-22T19:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.424818152948 + ], + [ + "2025-11-22T19:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.714768366837 + ], + [ + "2025-11-22T19:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.0479407143953 + ], + [ + "2025-11-22T19:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.1658675496637 + ], + [ + "2025-11-22T19:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.9225199792763 + ], + [ + "2025-11-22T19:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.4380082704706 + ], + [ + "2025-11-22T19:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.45263636897 + ], + [ + "2025-11-22T19:08:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.7029036105414 + ], + [ + "2025-11-22T19:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.540579691065 + ], + [ + "2025-11-22T18:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.093399867236 + ], + [ + "2025-11-22T18:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.245937904153 + ], + [ + "2025-11-22T18:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.6495664293243 + ], + [ + "2025-11-22T18:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.759337011483 + ], + [ + "2025-11-22T18:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.1022831390137 + ], + [ + "2025-11-22T18:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.3445539400354 + ], + [ + "2025-11-22T18:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.162123165552 + ], + [ + "2025-11-22T18:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.4150145701533 + ], + [ + "2025-11-22T18:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.2267450464183 + ], + [ + "2025-11-22T18:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.0343111993498 + ], + [ + "2025-11-22T18:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.7356666445526 + ], + [ + "2025-11-22T18:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.9492855361964 + ], + [ + "2025-11-22T18:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.315067271702 + ], + [ + "2025-11-22T18:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.2181580643896 + ], + [ + "2025-11-22T18:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.836255809655 + ], + [ + "2025-11-22T18:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.3210839877615 + ], + [ + "2025-11-22T18:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.5771551528346 + ], + [ + "2025-11-22T18:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.1627188357106 + ], + [ + "2025-11-22T18:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.434325922027 + ], + [ + "2025-11-22T18:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.6797169964575 + ], + [ + "2025-11-22T18:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.825874779915 + ], + [ + "2025-11-22T18:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.362044713597 + ], + [ + "2025-11-22T17:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.086742730443 + ], + [ + "2025-11-22T17:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.18898972511 + ], + [ + "2025-11-22T17:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.8051512632223 + ], + [ + "2025-11-22T17:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.3523413741564 + ], + [ + "2025-11-22T17:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.1096137634754 + ], + [ + "2025-11-22T17:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.2774962323956 + ], + [ + "2025-11-22T17:48:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.7838899408584 + ], + [ + "2025-11-22T17:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.4619707517923 + ], + [ + "2025-11-22T17:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.7092978427386 + ], + [ + "2025-11-22T17:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.1869481206218 + ], + [ + "2025-11-22T17:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.827942987769 + ], + [ + "2025-11-22T17:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.803097094085 + ], + [ + "2025-11-22T17:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.486730330575 + ], + [ + "2025-11-22T17:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.88597046639 + ], + [ + "2025-11-22T17:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.314967639841 + ], + [ + "2025-11-22T17:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.7146762729462 + ], + [ + "2025-11-22T17:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2755.0204307053064 + ], + [ + "2025-11-22T17:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.6357834334517 + ], + [ + "2025-11-22T17:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.7619474642893 + ], + [ + "2025-11-22T17:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.9679804702178 + ], + [ + "2025-11-22T17:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.3166890452203 + ], + [ + "2025-11-22T17:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.8934774911704 + ], + [ + "2025-11-22T17:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.356662557206 + ], + [ + "2025-11-22T17:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.249986806769 + ], + [ + "2025-11-22T17:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.5384130187267 + ], + [ + "2025-11-22T17:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.746428897278 + ], + [ + "2025-11-22T17:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.0528739130195 + ], + [ + "2025-11-22T17:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.797163429539 + ], + [ + "2025-11-22T17:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.257909012841 + ], + [ + "2025-11-22T17:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.4416345124887 + ], + [ + "2025-11-22T17:00:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.812601266146 + ], + [ + "2025-11-22T16:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2755.571679714315 + ], + [ + "2025-11-22T16:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.726548206302 + ], + [ + "2025-11-22T16:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2751.649081312899 + ], + [ + "2025-11-22T16:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2756.506651641448 + ], + [ + "2025-11-22T16:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2755.797180252845 + ], + [ + "2025-11-22T16:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2753.0015599508597 + ], + [ + "2025-11-22T16:46:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2754.137919095537 + ], + [ + "2025-11-22T16:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.5115183674047 + ], + [ + "2025-11-22T16:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.3463893603844 + ], + [ + "2025-11-22T16:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.655361677206 + ], + [ + "2025-11-22T16:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.4773412353993 + ], + [ + "2025-11-22T16:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.974043569746 + ], + [ + "2025-11-22T16:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.4974784582973 + ], + [ + "2025-11-22T16:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.9989925439463 + ], + [ + "2025-11-22T16:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.8274574813045 + ], + [ + "2025-11-22T16:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.6357209203916 + ], + [ + "2025-11-22T16:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2749.6687687465906 + ], + [ + "2025-11-22T16:23:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.110160994188 + ], + [ + "2025-11-22T16:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.424455919776 + ], + [ + "2025-11-22T16:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.5184484933607 + ], + [ + "2025-11-22T16:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.6088183626 + ], + [ + "2025-11-22T16:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.9938857673756 + ], + [ + "2025-11-22T16:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.580979669457 + ], + [ + "2025-11-22T16:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.3667067344822 + ], + [ + "2025-11-22T16:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.83548441073 + ], + [ + "2025-11-22T16:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.0140084814725 + ], + [ + "2025-11-22T16:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2735.2171284319193 + ], + [ + "2025-11-22T16:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2735.964717171755 + ], + [ + "2025-11-22T16:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2737.873322210815 + ], + [ + "2025-11-22T15:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.210834785115 + ], + [ + "2025-11-22T15:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2729.90424071205 + ], + [ + "2025-11-22T15:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.0844753070296 + ], + [ + "2025-11-22T15:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.515860133666 + ], + [ + "2025-11-22T15:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.9683056708595 + ], + [ + "2025-11-22T15:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2730.0793449692624 + ], + [ + "2025-11-22T15:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2729.7954833758595 + ], + [ + "2025-11-22T15:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2728.8750354396925 + ], + [ + "2025-11-22T15:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2728.6704550145564 + ], + [ + "2025-11-22T15:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.018721731993 + ], + [ + "2025-11-22T15:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.41763739807 + ], + [ + "2025-11-22T15:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.4183207290607 + ], + [ + "2025-11-22T15:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.4650324511535 + ], + [ + "2025-11-22T15:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.638537750042 + ], + [ + "2025-11-22T15:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.2346609450074 + ], + [ + "2025-11-22T15:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.9745589028475 + ], + [ + "2025-11-22T15:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.1548537055924 + ], + [ + "2025-11-22T15:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.0828520715354 + ], + [ + "2025-11-22T15:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.3630593003336 + ], + [ + "2025-11-22T15:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2741.517838203738 + ], + [ + "2025-11-22T15:20:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.154339293096 + ], + [ + "2025-11-22T15:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2741.24260742532 + ], + [ + "2025-11-22T15:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.4154367210567 + ], + [ + "2025-11-22T15:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.756966889055 + ], + [ + "2025-11-22T15:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.9471948011324 + ], + [ + "2025-11-22T15:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.810694979252 + ], + [ + "2025-11-22T15:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.474904540455 + ], + [ + "2025-11-22T15:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.31680284459 + ], + [ + "2025-11-22T15:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.614215951918 + ], + [ + "2025-11-22T15:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.379948251116 + ], + [ + "2025-11-22T14:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.270367565472 + ], + [ + "2025-11-22T14:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.5944060727575 + ], + [ + "2025-11-22T14:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.8545148269955 + ], + [ + "2025-11-22T14:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.1990434657087 + ], + [ + "2025-11-22T14:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.561122661312 + ], + [ + "2025-11-22T14:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.747390504847 + ], + [ + "2025-11-22T14:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.055185277894 + ], + [ + "2025-11-22T14:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.3949067828735 + ], + [ + "2025-11-22T14:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.777671333745 + ], + [ + "2025-11-22T14:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.01462219749 + ], + [ + "2025-11-22T14:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2748.4916243627877 + ], + [ + "2025-11-22T14:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.814139790271 + ], + [ + "2025-11-22T14:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.235277767103 + ], + [ + "2025-11-22T14:33:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.8549519079074 + ], + [ + "2025-11-22T14:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2742.512944928356 + ], + [ + "2025-11-22T14:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2745.00698051284 + ], + [ + "2025-11-22T14:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2747.379429815662 + ], + [ + "2025-11-22T14:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2743.987991964305 + ], + [ + "2025-11-22T14:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2744.45120135803 + ], + [ + "2025-11-22T14:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.937818469865 + ], + [ + "2025-11-22T14:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2740.4282936283453 + ], + [ + "2025-11-22T14:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2746.2962929564346 + ], + [ + "2025-11-22T14:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.179913380196 + ], + [ + "2025-11-22T14:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.8634566820688 + ], + [ + "2025-11-22T14:11:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2730.1137043542467 + ], + [ + "2025-11-22T14:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2735.869508585558 + ], + [ + "2025-11-22T14:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2732.7608584760546 + ], + [ + "2025-11-22T14:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2733.1593122628624 + ], + [ + "2025-11-22T14:04:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2729.224260691309 + ], + [ + "2025-11-22T14:02:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2724.495095846988 + ], + [ + "2025-11-22T13:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.444014678376 + ], + [ + "2025-11-22T13:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.8161187976903 + ], + [ + "2025-11-22T13:56:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.0670217753295 + ], + [ + "2025-11-22T13:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2718.0311205818443 + ], + [ + "2025-11-22T13:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.602123611244 + ], + [ + "2025-11-22T13:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2722.747155791708 + ], + [ + "2025-11-22T13:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2721.6490955559007 + ], + [ + "2025-11-22T13:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.620361984509 + ], + [ + "2025-11-22T13:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.734873026668 + ], + [ + "2025-11-22T13:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.15591574362 + ], + [ + "2025-11-22T13:39:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2712.8933299954715 + ], + [ + "2025-11-22T13:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.2987537865924 + ], + [ + "2025-11-22T13:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.8232587536645 + ], + [ + "2025-11-22T13:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.9765409507067 + ], + [ + "2025-11-22T13:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.881037461352 + ], + [ + "2025-11-22T13:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.521752424903 + ], + [ + "2025-11-22T13:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.0735436312584 + ], + [ + "2025-11-22T13:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.0840268020233 + ], + [ + "2025-11-22T13:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2722.3297055726903 + ], + [ + "2025-11-22T13:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.885584446532 + ], + [ + "2025-11-22T13:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2724.865581203137 + ], + [ + "2025-11-22T13:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2726.4692940195023 + ], + [ + "2025-11-22T13:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2727.9299240020537 + ], + [ + "2025-11-22T13:13:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.4350308925605 + ], + [ + "2025-11-22T13:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2730.628794898496 + ], + [ + "2025-11-22T13:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2722.1666576572134 + ], + [ + "2025-11-22T13:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.3757118541707 + ], + [ + "2025-11-22T13:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.5427839025365 + ], + [ + "2025-11-22T13:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.830496431821 + ], + [ + "2025-11-22T13:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.9977210569773 + ], + [ + "2025-11-22T12:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.41887203012 + ], + [ + "2025-11-22T12:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2718.823140045308 + ], + [ + "2025-11-22T12:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.554816813255 + ], + [ + "2025-11-22T12:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.263577645946 + ], + [ + "2025-11-22T12:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.737684677073 + ], + [ + "2025-11-22T12:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.984203361669 + ], + [ + "2025-11-22T12:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.7867948902876 + ], + [ + "2025-11-22T12:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.6304265600065 + ], + [ + "2025-11-22T12:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.9269068796407 + ], + [ + "2025-11-22T12:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.1524184643263 + ], + [ + "2025-11-22T12:40:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.7662426351644 + ], + [ + "2025-11-22T12:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.2678964814027 + ], + [ + "2025-11-22T12:35:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.551815512656 + ], + [ + "2025-11-22T12:34:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.997846240723 + ], + [ + "2025-11-22T12:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2718.325267005115 + ], + [ + "2025-11-22T12:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.8665325840966 + ], + [ + "2025-11-22T12:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.9074230462365 + ], + [ + "2025-11-22T12:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.3558027003564 + ], + [ + "2025-11-22T12:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.4981950074325 + ], + [ + "2025-11-22T12:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2718.244274763687 + ], + [ + "2025-11-22T12:19:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2719.4629127517064 + ], + [ + "2025-11-22T12:18:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2717.80272966981 + ], + [ + "2025-11-22T12:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.4193663046603 + ], + [ + "2025-11-22T12:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.7766833200003 + ], + [ + "2025-11-22T12:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2716.0802082362643 + ], + [ + "2025-11-22T12:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2720.7682682196623 + ], + [ + "2025-11-22T12:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2726.4806271125653 + ], + [ + "2025-11-22T12:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2729.3664982482524 + ], + [ + "2025-11-22T12:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2732.576057314775 + ], + [ + "2025-11-22T12:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2733.517743757391 + ], + [ + "2025-11-22T11:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.6258418840284 + ], + [ + "2025-11-22T11:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.0650572655813 + ], + [ + "2025-11-22T11:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.197468416382 + ], + [ + "2025-11-22T11:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.186984814236 + ], + [ + "2025-11-22T11:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.9499732799086 + ], + [ + "2025-11-22T11:50:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.2775766998266 + ], + [ + "2025-11-22T11:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.7867651442502 + ], + [ + "2025-11-22T11:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.360156358462 + ], + [ + "2025-11-22T11:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.8742919350866 + ], + [ + "2025-11-22T11:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.8023928197917 + ], + [ + "2025-11-22T11:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.1846114492573 + ], + [ + "2025-11-22T11:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.0969790274944 + ], + [ + "2025-11-22T11:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.2620615385276 + ], + [ + "2025-11-22T11:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.762875907144 + ], + [ + "2025-11-22T11:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.2314085903254 + ], + [ + "2025-11-22T11:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2736.147697045631 + ], + [ + "2025-11-22T11:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.0050110734724 + ], + [ + "2025-11-22T11:26:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.2029346671684 + ], + [ + "2025-11-22T11:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.0464860460306 + ], + [ + "2025-11-22T11:21:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.7983606024327 + ], + [ + "2025-11-22T11:19:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.5760088911884 + ], + [ + "2025-11-22T11:17:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2725.597516760434 + ], + [ + "2025-11-22T11:15:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2728.0151333250606 + ], + [ + "2025-11-22T11:14:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2724.3004810241578 + ], + [ + "2025-11-22T11:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2723.3863463127627 + ], + [ + "2025-11-22T11:09:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2718.862365611161 + ], + [ + "2025-11-22T11:07:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.1674066327337 + ], + [ + "2025-11-22T11:05:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2709.065712084767 + ], + [ + "2025-11-22T11:03:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2711.7988212166574 + ], + [ + "2025-11-22T11:01:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2711.882952580508 + ], + [ + "2025-11-22T10:59:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2711.6460553135594 + ], + [ + "2025-11-22T10:57:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2709.36691957821 + ], + [ + "2025-11-22T10:55:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2706.8909349934484 + ], + [ + "2025-11-22T10:53:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2706.815509377634 + ], + [ + "2025-11-22T10:51:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2707.002269745001 + ], + [ + "2025-11-22T10:49:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.374014890025 + ], + [ + "2025-11-22T10:47:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.0279615739732 + ], + [ + "2025-11-22T10:45:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.340179519083 + ], + [ + "2025-11-22T10:43:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2709.2287304995016 + ], + [ + "2025-11-22T10:41:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.5460536091045 + ], + [ + "2025-11-22T10:39:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.6532514133423 + ], + [ + "2025-11-22T10:37:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.651916270245 + ], + [ + "2025-11-22T10:35:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2713.9786972580437 + ], + [ + "2025-11-22T10:33:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2715.076604007814 + ], + [ + "2025-11-22T10:31:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2711.793671330055 + ], + [ + "2025-11-22T10:29:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2712.2133860579584 + ], + [ + "2025-11-22T10:27:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2710.7438879160836 + ], + [ + "2025-11-22T10:25:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2714.292835786476 + ], + [ + "2025-11-22T10:23:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2724.569063999994 + ], + [ + "2025-11-22T08:29:49Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2729.1099999999924 + ], + [ + "2025-11-22T07:41:12Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2756.435219691037 + ], + [ + "2025-11-22T06:51:29Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2757.714923200011 + ], + [ + "2025-11-22T06:11:27Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.5527718000076 + ], + [ + "2025-11-22T05:30:28Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2734.253002800003 + ], + [ + "2025-11-22T04:47:29Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2731.964766100014 + ], + [ + "2025-11-22T04:12:00Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2739.5491411396697 + ], + [ + "2025-11-22T04:11:59Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2738.7151974100802 + ], + [ + "2025-11-22T03:21:25Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2750.57049019772 + ], + [ + "2025-11-22T02:32:21Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2752.169283735319 + ], + [ + "2025-11-22T01:39:58Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2775.0927024000052 + ], + [ + "2025-11-22T00:58:16Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2763.5804032999968 + ], + [ + "2025-11-22T00:19:05Z", + "0x0000000000000000000000000000000000000000", + "Ethereum", + null, + "MAIR120", + "ETH", + 2765.03927910001 + ] + ] + } + ], + "Messages": null + } + ] +} diff --git a/apps/explorer/test/support/fixture/market/dia/secondary_coin_history.json b/apps/explorer/test/support/fixture/market/dia/secondary_coin_history.json new file mode 100644 index 000000000000..47967c177fd2 --- /dev/null +++ b/apps/explorer/test/support/fixture/market/dia/secondary_coin_history.json @@ -0,0 +1,29490 @@ +{ + "DataPoints": [ + { + "statement_id": 0, + "Series": [ + { + "name": "filters", + "columns": [ + "time", + "address", + "blockchain", + "exchange", + "filter", + "symbol", + "value" + ], + "values": [ + [ + "2025-11-26T23:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997050546597661 + ], + [ + "2025-11-26T23:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997795461438774 + ], + [ + "2025-11-26T23:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997622624626337 + ], + [ + "2025-11-26T23:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997684844415092 + ], + [ + "2025-11-26T23:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997886378014167 + ], + [ + "2025-11-26T23:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997478447346754 + ], + [ + "2025-11-26T23:47:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999051970136559 + ], + [ + "2025-11-26T23:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998091933564365 + ], + [ + "2025-11-26T23:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999245107659688 + ], + [ + "2025-11-26T23:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998473334666932 + ], + [ + "2025-11-26T23:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999388072813172 + ], + [ + "2025-11-26T23:38:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999079003604031 + ], + [ + "2025-11-26T23:35:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999231276345876 + ], + [ + "2025-11-26T23:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999524333137157 + ], + [ + "2025-11-26T23:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998024996089331 + ], + [ + "2025-11-26T23:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999829616399664 + ], + [ + "2025-11-26T23:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998979810466406 + ], + [ + "2025-11-26T23:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999951038305009 + ], + [ + "2025-11-26T23:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998375793017942 + ], + [ + "2025-11-26T23:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999631309082428 + ], + [ + "2025-11-26T23:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999822591515583 + ], + [ + "2025-11-26T23:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999820967926312 + ], + [ + "2025-11-26T23:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997553972136282 + ], + [ + "2025-11-26T23:13:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997789319763989 + ], + [ + "2025-11-26T23:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999546534853955 + ], + [ + "2025-11-26T23:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999807222615671 + ], + [ + "2025-11-26T23:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997972719090844 + ], + [ + "2025-11-26T23:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000246048143222 + ], + [ + "2025-11-26T23:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000028329232342 + ], + [ + "2025-11-26T23:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999088747695573 + ], + [ + "2025-11-26T22:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998731025725487 + ], + [ + "2025-11-26T22:58:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000523971165438 + ], + [ + "2025-11-26T22:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999802270011808 + ], + [ + "2025-11-26T22:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000013842038813 + ], + [ + "2025-11-26T22:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999873653992938 + ], + [ + "2025-11-26T22:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000140504008697 + ], + [ + "2025-11-26T22:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997196218140781 + ], + [ + "2025-11-26T22:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000506190404534 + ], + [ + "2025-11-26T22:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999676594459156 + ], + [ + "2025-11-26T22:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998497675673141 + ], + [ + "2025-11-26T22:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997904114198473 + ], + [ + "2025-11-26T22:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999900635659701 + ], + [ + "2025-11-26T22:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999138852816493 + ], + [ + "2025-11-26T22:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999811263167344 + ], + [ + "2025-11-26T22:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998109831208168 + ], + [ + "2025-11-26T22:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998374686654539 + ], + [ + "2025-11-26T22:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999069491678029 + ], + [ + "2025-11-26T22:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998327235202427 + ], + [ + "2025-11-26T22:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999132137224496 + ], + [ + "2025-11-26T22:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999808250205675 + ], + [ + "2025-11-26T22:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997652729585584 + ], + [ + "2025-11-26T22:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998497046886966 + ], + [ + "2025-11-26T22:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997155872129425 + ], + [ + "2025-11-26T22:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998561997814405 + ], + [ + "2025-11-26T22:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999847620923936 + ], + [ + "2025-11-26T22:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998049516622451 + ], + [ + "2025-11-26T22:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997242120657608 + ], + [ + "2025-11-26T22:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997621660567204 + ], + [ + "2025-11-26T22:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999787874632257 + ], + [ + "2025-11-26T22:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997438232168326 + ], + [ + "2025-11-26T21:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998249859396864 + ], + [ + "2025-11-26T21:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997590827976958 + ], + [ + "2025-11-26T21:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998399469285009 + ], + [ + "2025-11-26T21:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997844338607101 + ], + [ + "2025-11-26T21:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999139483507542 + ], + [ + "2025-11-26T21:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997802537313051 + ], + [ + "2025-11-26T21:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999101709452008 + ], + [ + "2025-11-26T21:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999718103474216 + ], + [ + "2025-11-26T21:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998089246689195 + ], + [ + "2025-11-26T21:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999169994767035 + ], + [ + "2025-11-26T21:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999013584775756 + ], + [ + "2025-11-26T21:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998206992728874 + ], + [ + "2025-11-26T21:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998947983544066 + ], + [ + "2025-11-26T21:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999873658193712 + ], + [ + "2025-11-26T21:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99985412066119 + ], + [ + "2025-11-26T21:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998535326861892 + ], + [ + "2025-11-26T21:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998538108236031 + ], + [ + "2025-11-26T21:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996620029845299 + ], + [ + "2025-11-26T21:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998059294230369 + ], + [ + "2025-11-26T21:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999739292369874 + ], + [ + "2025-11-26T21:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999790085893341 + ], + [ + "2025-11-26T21:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999472366271309 + ], + [ + "2025-11-26T21:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999292898205866 + ], + [ + "2025-11-26T21:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998396426463748 + ], + [ + "2025-11-26T21:12:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997833807068368 + ], + [ + "2025-11-26T21:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001706223090219 + ], + [ + "2025-11-26T21:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001334461731919 + ], + [ + "2025-11-26T21:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999824169135552 + ], + [ + "2025-11-26T21:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998736556174154 + ], + [ + "2025-11-26T21:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997689316814466 + ], + [ + "2025-11-26T20:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997022653675538 + ], + [ + "2025-11-26T20:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997205670471739 + ], + [ + "2025-11-26T20:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998086459722371 + ], + [ + "2025-11-26T20:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997107720899308 + ], + [ + "2025-11-26T20:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996601518238335 + ], + [ + "2025-11-26T20:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996617890419368 + ], + [ + "2025-11-26T20:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997326419738951 + ], + [ + "2025-11-26T20:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996753937479669 + ], + [ + "2025-11-26T20:44:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996969265861991 + ], + [ + "2025-11-26T20:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999119450899464 + ], + [ + "2025-11-26T20:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999181295384537 + ], + [ + "2025-11-26T20:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999741493645329 + ], + [ + "2025-11-26T20:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996664585699486 + ], + [ + "2025-11-26T20:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999876462003662 + ], + [ + "2025-11-26T20:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999400706363507 + ], + [ + "2025-11-26T20:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998708300750315 + ], + [ + "2025-11-26T20:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998631462532491 + ], + [ + "2025-11-26T20:26:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998508607699774 + ], + [ + "2025-11-26T20:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000306001882797 + ], + [ + "2025-11-26T20:21:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999943718789138 + ], + [ + "2025-11-26T20:20:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997407612481735 + ], + [ + "2025-11-26T20:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999637354265408 + ], + [ + "2025-11-26T20:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002712078281848 + ], + [ + "2025-11-26T20:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001153250464254 + ], + [ + "2025-11-26T20:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000670567174224 + ], + [ + "2025-11-26T20:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999565297959054 + ], + [ + "2025-11-26T20:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003531215893302 + ], + [ + "2025-11-26T20:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998452411737069 + ], + [ + "2025-11-26T20:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997747230228671 + ], + [ + "2025-11-26T20:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998216798456928 + ], + [ + "2025-11-26T19:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998634296204367 + ], + [ + "2025-11-26T19:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997814692498667 + ], + [ + "2025-11-26T19:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999764066976513 + ], + [ + "2025-11-26T19:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998917370400144 + ], + [ + "2025-11-26T19:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998867240032958 + ], + [ + "2025-11-26T19:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998336874873804 + ], + [ + "2025-11-26T19:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998386312352499 + ], + [ + "2025-11-26T19:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998848608588113 + ], + [ + "2025-11-26T19:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999490331954825 + ], + [ + "2025-11-26T19:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998205899050473 + ], + [ + "2025-11-26T19:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997561218206235 + ], + [ + "2025-11-26T19:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997097420549027 + ], + [ + "2025-11-26T19:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997947277264851 + ], + [ + "2025-11-26T19:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998888653590094 + ], + [ + "2025-11-26T19:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997662729808814 + ], + [ + "2025-11-26T19:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997717309935543 + ], + [ + "2025-11-26T19:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998656429914854 + ], + [ + "2025-11-26T19:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999828113690766 + ], + [ + "2025-11-26T19:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997327599156698 + ], + [ + "2025-11-26T19:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997257994451819 + ], + [ + "2025-11-26T19:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996755389781946 + ], + [ + "2025-11-26T19:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996019750313625 + ], + [ + "2025-11-26T19:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997397839531686 + ], + [ + "2025-11-26T19:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995240273250311 + ], + [ + "2025-11-26T19:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999735806692482 + ], + [ + "2025-11-26T19:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999889303903008 + ], + [ + "2025-11-26T19:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997741682358227 + ], + [ + "2025-11-26T19:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996685634246064 + ], + [ + "2025-11-26T19:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998443787107734 + ], + [ + "2025-11-26T19:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000641301583235 + ], + [ + "2025-11-26T18:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996294607433589 + ], + [ + "2025-11-26T18:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998870809226978 + ], + [ + "2025-11-26T18:56:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001394109255597 + ], + [ + "2025-11-26T18:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997566571312322 + ], + [ + "2025-11-26T18:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997243167134897 + ], + [ + "2025-11-26T18:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997015569751374 + ], + [ + "2025-11-26T18:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998105989011111 + ], + [ + "2025-11-26T18:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998631443414778 + ], + [ + "2025-11-26T18:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998851921986562 + ], + [ + "2025-11-26T18:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997500326981517 + ], + [ + "2025-11-26T18:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997888849621616 + ], + [ + "2025-11-26T18:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000101942419972 + ], + [ + "2025-11-26T18:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999328031331489 + ], + [ + "2025-11-26T18:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997683415583205 + ], + [ + "2025-11-26T18:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998636003714575 + ], + [ + "2025-11-26T18:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999846865636755 + ], + [ + "2025-11-26T18:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997240588555163 + ], + [ + "2025-11-26T18:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997617284005578 + ], + [ + "2025-11-26T18:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996934370343453 + ], + [ + "2025-11-26T18:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998520973387829 + ], + [ + "2025-11-26T18:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997356009987507 + ], + [ + "2025-11-26T18:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997726159655821 + ], + [ + "2025-11-26T18:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997932927604325 + ], + [ + "2025-11-26T18:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997793007812747 + ], + [ + "2025-11-26T18:12:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999210634299216 + ], + [ + "2025-11-26T18:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998133859152907 + ], + [ + "2025-11-26T18:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998297223113496 + ], + [ + "2025-11-26T18:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998062902722398 + ], + [ + "2025-11-26T18:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999902539792241 + ], + [ + "2025-11-26T18:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998265950399238 + ], + [ + "2025-11-26T17:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998487654462093 + ], + [ + "2025-11-26T17:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996601869840217 + ], + [ + "2025-11-26T17:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997332931403637 + ], + [ + "2025-11-26T17:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997629107491472 + ], + [ + "2025-11-26T17:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998151904947202 + ], + [ + "2025-11-26T17:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998645306635691 + ], + [ + "2025-11-26T17:48:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999779786072583 + ], + [ + "2025-11-26T17:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999136008533324 + ], + [ + "2025-11-26T17:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997545695772351 + ], + [ + "2025-11-26T17:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999781316498219 + ], + [ + "2025-11-26T17:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999392336502844 + ], + [ + "2025-11-26T17:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998225119145083 + ], + [ + "2025-11-26T17:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999482459534397 + ], + [ + "2025-11-26T17:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999933555979289 + ], + [ + "2025-11-26T17:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999751405514377 + ], + [ + "2025-11-26T17:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000926723128327 + ], + [ + "2025-11-26T17:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999540623880309 + ], + [ + "2025-11-26T17:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000245762924402 + ], + [ + "2025-11-26T17:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000041414056701 + ], + [ + "2025-11-26T17:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998635971704719 + ], + [ + "2025-11-26T17:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999504758431653 + ], + [ + "2025-11-26T17:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999889382521444 + ], + [ + "2025-11-26T17:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999734396515993 + ], + [ + "2025-11-26T17:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998874247972065 + ], + [ + "2025-11-26T17:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997746791815078 + ], + [ + "2025-11-26T17:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997293543333358 + ], + [ + "2025-11-26T17:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997364091144402 + ], + [ + "2025-11-26T17:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999710120021226 + ], + [ + "2025-11-26T17:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997460493607886 + ], + [ + "2025-11-26T17:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997682661693696 + ], + [ + "2025-11-26T16:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002610765313573 + ], + [ + "2025-11-26T16:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000164279746285 + ], + [ + "2025-11-26T16:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997236340340758 + ], + [ + "2025-11-26T16:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996816050039016 + ], + [ + "2025-11-26T16:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997574122391331 + ], + [ + "2025-11-26T16:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997377811756192 + ], + [ + "2025-11-26T16:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997099633485602 + ], + [ + "2025-11-26T16:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997978261427499 + ], + [ + "2025-11-26T16:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000270096359578 + ], + [ + "2025-11-26T16:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000139001518643 + ], + [ + "2025-11-26T16:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999680006053887 + ], + [ + "2025-11-26T16:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999799231849043 + ], + [ + "2025-11-26T16:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999505580550245 + ], + [ + "2025-11-26T16:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998043986669755 + ], + [ + "2025-11-26T16:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999137567602109 + ], + [ + "2025-11-26T16:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999117011086135 + ], + [ + "2025-11-26T16:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997145896768646 + ], + [ + "2025-11-26T16:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000228418616584 + ], + [ + "2025-11-26T16:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999996521625361 + ], + [ + "2025-11-26T16:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998221497783564 + ], + [ + "2025-11-26T16:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997808350543667 + ], + [ + "2025-11-26T16:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999946719725185 + ], + [ + "2025-11-26T16:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999933841666305 + ], + [ + "2025-11-26T16:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996539965629246 + ], + [ + "2025-11-26T16:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999388373678275 + ], + [ + "2025-11-26T16:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999717993513633 + ], + [ + "2025-11-26T16:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999554457649874 + ], + [ + "2025-11-26T16:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996212078512571 + ], + [ + "2025-11-26T16:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998481709088812 + ], + [ + "2025-11-26T16:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996617178448969 + ], + [ + "2025-11-26T15:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998599977256668 + ], + [ + "2025-11-26T15:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998247468998889 + ], + [ + "2025-11-26T15:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997838349271123 + ], + [ + "2025-11-26T15:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996680762499556 + ], + [ + "2025-11-26T15:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001301501484987 + ], + [ + "2025-11-26T15:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003321719377836 + ], + [ + "2025-11-26T15:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997969681891393 + ], + [ + "2025-11-26T15:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997361269601972 + ], + [ + "2025-11-26T15:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012116787950047 + ], + [ + "2025-11-26T15:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006448272050328 + ], + [ + "2025-11-26T15:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997625736835359 + ], + [ + "2025-11-26T15:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996885337862177 + ], + [ + "2025-11-26T15:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996667464360156 + ], + [ + "2025-11-26T15:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996969821075177 + ], + [ + "2025-11-26T15:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996875972955518 + ], + [ + "2025-11-26T15:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997854517412105 + ], + [ + "2025-11-26T15:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997741051894488 + ], + [ + "2025-11-26T15:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996277161515187 + ], + [ + "2025-11-26T15:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998303683517451 + ], + [ + "2025-11-26T15:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999128592572264 + ], + [ + "2025-11-26T15:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998067808967316 + ], + [ + "2025-11-26T15:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999576045612363 + ], + [ + "2025-11-26T15:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999172882784734 + ], + [ + "2025-11-26T15:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999786926195497 + ], + [ + "2025-11-26T15:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998910526059578 + ], + [ + "2025-11-26T15:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998593847911524 + ], + [ + "2025-11-26T15:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997463985845988 + ], + [ + "2025-11-26T15:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997335831322945 + ], + [ + "2025-11-26T15:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997350005966174 + ], + [ + "2025-11-26T15:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996499573975608 + ], + [ + "2025-11-26T14:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997880951669488 + ], + [ + "2025-11-26T14:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998467100585365 + ], + [ + "2025-11-26T14:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997665955126598 + ], + [ + "2025-11-26T14:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9993733051790405 + ], + [ + "2025-11-26T14:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997675656915228 + ], + [ + "2025-11-26T14:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000355593908898 + ], + [ + "2025-11-26T14:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99957366410737 + ], + [ + "2025-11-26T14:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997092695040444 + ], + [ + "2025-11-26T14:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997041261350035 + ], + [ + "2025-11-26T14:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007325951959465 + ], + [ + "2025-11-26T14:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997683969300291 + ], + [ + "2025-11-26T14:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997994203398345 + ], + [ + "2025-11-26T14:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996455888482407 + ], + [ + "2025-11-26T14:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998111365413809 + ], + [ + "2025-11-26T14:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999186513417809 + ], + [ + "2025-11-26T14:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998349306120303 + ], + [ + "2025-11-26T14:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997753356165611 + ], + [ + "2025-11-26T14:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998454004896326 + ], + [ + "2025-11-26T14:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005717227414035 + ], + [ + "2025-11-26T14:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999192590693992 + ], + [ + "2025-11-26T14:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996550928412652 + ], + [ + "2025-11-26T14:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997929781589904 + ], + [ + "2025-11-26T14:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000069839705712 + ], + [ + "2025-11-26T14:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999722953832713 + ], + [ + "2025-11-26T14:12:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997055871445467 + ], + [ + "2025-11-26T14:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997762319082045 + ], + [ + "2025-11-26T14:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997230988084514 + ], + [ + "2025-11-26T14:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996814367429007 + ], + [ + "2025-11-26T14:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997094910107032 + ], + [ + "2025-11-26T14:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999735892229098 + ], + [ + "2025-11-26T13:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997171438701996 + ], + [ + "2025-11-26T13:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997452683765626 + ], + [ + "2025-11-26T13:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995011042892579 + ], + [ + "2025-11-26T13:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999819911218695 + ], + [ + "2025-11-26T13:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997386750138164 + ], + [ + "2025-11-26T13:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998690098092164 + ], + [ + "2025-11-26T13:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998813017377433 + ], + [ + "2025-11-26T13:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998262656491248 + ], + [ + "2025-11-26T13:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998985253855557 + ], + [ + "2025-11-26T13:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000290982977089 + ], + [ + "2025-11-26T13:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999968310708264 + ], + [ + "2025-11-26T13:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998644933340559 + ], + [ + "2025-11-26T13:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999326608674087 + ], + [ + "2025-11-26T13:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999454151382755 + ], + [ + "2025-11-26T13:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999480228507963 + ], + [ + "2025-11-26T13:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998839113887685 + ], + [ + "2025-11-26T13:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000751101203245 + ], + [ + "2025-11-26T13:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999749708832555 + ], + [ + "2025-11-26T13:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998677795768426 + ], + [ + "2025-11-26T13:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001797407224928 + ], + [ + "2025-11-26T13:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998584028755215 + ], + [ + "2025-11-26T13:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998364122456607 + ], + [ + "2025-11-26T13:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002083304641731 + ], + [ + "2025-11-26T13:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000567247691872 + ], + [ + "2025-11-26T13:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998135901409203 + ], + [ + "2025-11-26T13:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998688041802336 + ], + [ + "2025-11-26T13:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999700068569337 + ], + [ + "2025-11-26T13:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001911307874038 + ], + [ + "2025-11-26T13:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999065173620074 + ], + [ + "2025-11-26T13:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001358104425864 + ], + [ + "2025-11-26T12:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010881401184455 + ], + [ + "2025-11-26T12:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000522407727283 + ], + [ + "2025-11-26T12:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999456363383102 + ], + [ + "2025-11-26T12:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999941990534663 + ], + [ + "2025-11-26T12:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011616211067749 + ], + [ + "2025-11-26T12:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005001628383334 + ], + [ + "2025-11-26T12:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998870593605257 + ], + [ + "2025-11-26T12:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998063989016451 + ], + [ + "2025-11-26T12:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014870231504558 + ], + [ + "2025-11-26T12:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006566921115039 + ], + [ + "2025-11-26T12:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999378449434309 + ], + [ + "2025-11-26T12:38:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001523177051863 + ], + [ + "2025-11-26T12:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999075651351566 + ], + [ + "2025-11-26T12:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998879711465879 + ], + [ + "2025-11-26T12:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003299331802684 + ], + [ + "2025-11-26T12:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001973495290437 + ], + [ + "2025-11-26T12:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999777396932872 + ], + [ + "2025-11-26T12:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998806951891298 + ], + [ + "2025-11-26T12:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004617949295418 + ], + [ + "2025-11-26T12:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000779023336917 + ], + [ + "2025-11-26T12:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998366723309333 + ], + [ + "2025-11-26T12:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000826172317978 + ], + [ + "2025-11-26T12:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000409555625729 + ], + [ + "2025-11-26T12:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000709600051556 + ], + [ + "2025-11-26T12:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999592995851283 + ], + [ + "2025-11-26T12:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003221852584652 + ], + [ + "2025-11-26T12:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998136112017 + ], + [ + "2025-11-26T12:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000769920728871 + ], + [ + "2025-11-26T12:03:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000777288491647 + ], + [ + "2025-11-26T12:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998804967484063 + ], + [ + "2025-11-26T11:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999849576061272 + ], + [ + "2025-11-26T11:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00047612316213 + ], + [ + "2025-11-26T11:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999070806462985 + ], + [ + "2025-11-26T11:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999060264257905 + ], + [ + "2025-11-26T11:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000457220665788 + ], + [ + "2025-11-26T11:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000774720228562 + ], + [ + "2025-11-26T11:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998850743933388 + ], + [ + "2025-11-26T11:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999899643502157 + ], + [ + "2025-11-26T11:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006108042101518 + ], + [ + "2025-11-26T11:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009611932740086 + ], + [ + "2025-11-26T11:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998618121432056 + ], + [ + "2025-11-26T11:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999637870640251 + ], + [ + "2025-11-26T11:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998603810315907 + ], + [ + "2025-11-26T11:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998103162543649 + ], + [ + "2025-11-26T11:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998666014345974 + ], + [ + "2025-11-26T11:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999324578988507 + ], + [ + "2025-11-26T11:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999486328514133 + ], + [ + "2025-11-26T11:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998124748056777 + ], + [ + "2025-11-26T11:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998343725273814 + ], + [ + "2025-11-26T11:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999882749409426 + ], + [ + "2025-11-26T11:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998007626200522 + ], + [ + "2025-11-26T11:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002662016151371 + ], + [ + "2025-11-26T11:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999822439134867 + ], + [ + "2025-11-26T11:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003794133258153 + ], + [ + "2025-11-26T11:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998650666248726 + ], + [ + "2025-11-26T11:10:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999888665298494 + ], + [ + "2025-11-26T11:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998870049163512 + ], + [ + "2025-11-26T11:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001369748142113 + ], + [ + "2025-11-26T11:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002682417177429 + ], + [ + "2025-11-26T11:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000187198666304 + ], + [ + "2025-11-26T10:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999826512375682 + ], + [ + "2025-11-26T10:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000650525148273 + ], + [ + "2025-11-26T10:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998374769196834 + ], + [ + "2025-11-26T10:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008390148244821 + ], + [ + "2025-11-26T10:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997357187101388 + ], + [ + "2025-11-26T10:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000234381324837 + ], + [ + "2025-11-26T10:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999010756714347 + ], + [ + "2025-11-26T10:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998153798235446 + ], + [ + "2025-11-26T10:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002144414542458 + ], + [ + "2025-11-26T10:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001679236650538 + ], + [ + "2025-11-26T10:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005077780595162 + ], + [ + "2025-11-26T10:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000852265580893 + ], + [ + "2025-11-26T10:36:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010309277031708 + ], + [ + "2025-11-26T10:34:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00030168562641 + ], + [ + "2025-11-26T10:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999939333014742 + ], + [ + "2025-11-26T10:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000792564779008 + ], + [ + "2025-11-26T10:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009571367808856 + ], + [ + "2025-11-26T10:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012907726384097 + ], + [ + "2025-11-26T10:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003364545278 + ], + [ + "2025-11-26T10:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008890386864417 + ], + [ + "2025-11-26T10:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002238754353294 + ], + [ + "2025-11-26T10:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000717954129659 + ], + [ + "2025-11-26T10:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004293467654466 + ], + [ + "2025-11-26T10:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998719793195504 + ], + [ + "2025-11-26T10:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00028812911569 + ], + [ + "2025-11-26T10:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000323620754221 + ], + [ + "2025-11-26T10:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000040850699472 + ], + [ + "2025-11-26T10:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999884657911759 + ], + [ + "2025-11-26T10:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003971781453662 + ], + [ + "2025-11-26T10:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012451983212836 + ], + [ + "2025-11-26T09:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010612267585455 + ], + [ + "2025-11-26T09:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999295317321326 + ], + [ + "2025-11-26T09:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000298832818842 + ], + [ + "2025-11-26T09:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997796145204159 + ], + [ + "2025-11-26T09:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997545053376904 + ], + [ + "2025-11-26T09:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000123210305551 + ], + [ + "2025-11-26T09:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999973677148806 + ], + [ + "2025-11-26T09:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998203026163514 + ], + [ + "2025-11-26T09:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998188338487439 + ], + [ + "2025-11-26T09:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009521702798987 + ], + [ + "2025-11-26T09:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008210725784565 + ], + [ + "2025-11-26T09:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999211660053963 + ], + [ + "2025-11-26T09:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000012854704733 + ], + [ + "2025-11-26T09:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00110247472653 + ], + [ + "2025-11-26T09:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011074597669078 + ], + [ + "2025-11-26T09:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004391236972314 + ], + [ + "2025-11-26T09:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997422703739989 + ], + [ + "2025-11-26T09:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004679784524486 + ], + [ + "2025-11-26T09:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996855257125505 + ], + [ + "2025-11-26T09:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000103969947395 + ], + [ + "2025-11-26T09:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997636192888967 + ], + [ + "2025-11-26T09:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998191296791699 + ], + [ + "2025-11-26T09:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997028510023256 + ], + [ + "2025-11-26T09:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997526949664814 + ], + [ + "2025-11-26T09:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998469361391479 + ], + [ + "2025-11-26T09:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007032489597059 + ], + [ + "2025-11-26T09:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997380804805045 + ], + [ + "2025-11-26T09:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997309056122958 + ], + [ + "2025-11-26T09:03:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999713522071605 + ], + [ + "2025-11-26T09:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008052526591957 + ], + [ + "2025-11-26T08:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997341300107307 + ], + [ + "2025-11-26T08:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997839939344135 + ], + [ + "2025-11-26T08:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997615793512757 + ], + [ + "2025-11-26T08:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996508228770206 + ], + [ + "2025-11-26T08:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999752994385011 + ], + [ + "2025-11-26T08:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997522994016826 + ], + [ + "2025-11-26T08:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999761671350318 + ], + [ + "2025-11-26T08:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997408184859947 + ], + [ + "2025-11-26T08:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998140014891417 + ], + [ + "2025-11-26T08:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000348032240225 + ], + [ + "2025-11-26T08:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998634238020868 + ], + [ + "2025-11-26T08:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998413319211761 + ], + [ + "2025-11-26T08:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003165011247794 + ], + [ + "2025-11-26T08:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997800952051825 + ], + [ + "2025-11-26T08:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000760336666836 + ], + [ + "2025-11-26T08:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004220773050547 + ], + [ + "2025-11-26T08:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999744534052076 + ], + [ + "2025-11-26T08:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001474309680427 + ], + [ + "2025-11-26T08:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997674198165288 + ], + [ + "2025-11-26T08:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997494343177404 + ], + [ + "2025-11-26T08:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997620752378631 + ], + [ + "2025-11-26T08:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997883895378386 + ], + [ + "2025-11-26T08:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996893928388778 + ], + [ + "2025-11-26T08:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997849279830453 + ], + [ + "2025-11-26T08:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997267436071954 + ], + [ + "2025-11-26T08:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996435941080886 + ], + [ + "2025-11-26T08:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997554061819988 + ], + [ + "2025-11-26T08:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996804146662798 + ], + [ + "2025-11-26T08:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997335521003662 + ], + [ + "2025-11-26T08:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996924974697736 + ], + [ + "2025-11-26T07:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997046563251035 + ], + [ + "2025-11-26T07:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997200750108948 + ], + [ + "2025-11-26T07:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998234488819675 + ], + [ + "2025-11-26T07:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997630090030514 + ], + [ + "2025-11-26T07:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997880704132861 + ], + [ + "2025-11-26T07:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999786773984595 + ], + [ + "2025-11-26T07:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997552713163189 + ], + [ + "2025-11-26T07:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996816435801261 + ], + [ + "2025-11-26T07:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005236651722178 + ], + [ + "2025-11-26T07:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997013495670791 + ], + [ + "2025-11-26T07:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998390966242283 + ], + [ + "2025-11-26T07:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997090642020382 + ], + [ + "2025-11-26T07:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005119501868964 + ], + [ + "2025-11-26T07:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997662933727344 + ], + [ + "2025-11-26T07:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998006826401035 + ], + [ + "2025-11-26T07:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997439469216635 + ], + [ + "2025-11-26T07:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004962152925474 + ], + [ + "2025-11-26T07:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997856667541938 + ], + [ + "2025-11-26T07:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999793292610901 + ], + [ + "2025-11-26T07:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997210143328584 + ], + [ + "2025-11-26T07:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997538161459449 + ], + [ + "2025-11-26T07:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997664394930211 + ], + [ + "2025-11-26T07:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000495652776042 + ], + [ + "2025-11-26T07:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997846441698366 + ], + [ + "2025-11-26T07:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997315111726648 + ], + [ + "2025-11-26T07:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999749251742152 + ], + [ + "2025-11-26T07:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002500519376358 + ], + [ + "2025-11-26T07:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996375828930766 + ], + [ + "2025-11-26T07:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002011123253491 + ], + [ + "2025-11-26T07:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997597509798141 + ], + [ + "2025-11-26T06:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997643777598351 + ], + [ + "2025-11-26T06:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996929960820701 + ], + [ + "2025-11-26T06:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999799288684716 + ], + [ + "2025-11-26T06:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997246609331445 + ], + [ + "2025-11-26T06:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997228939002616 + ], + [ + "2025-11-26T06:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997754687624205 + ], + [ + "2025-11-26T06:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996964889086409 + ], + [ + "2025-11-26T06:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997619945917894 + ], + [ + "2025-11-26T06:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997858051111927 + ], + [ + "2025-11-26T06:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999784842085863 + ], + [ + "2025-11-26T06:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997860165414512 + ], + [ + "2025-11-26T06:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998029675607183 + ], + [ + "2025-11-26T06:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002245236116953 + ], + [ + "2025-11-26T06:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998243952674934 + ], + [ + "2025-11-26T06:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997917099608962 + ], + [ + "2025-11-26T06:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999771143980994 + ], + [ + "2025-11-26T06:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002720297736865 + ], + [ + "2025-11-26T06:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999586391694893 + ], + [ + "2025-11-26T06:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996119698624301 + ], + [ + "2025-11-26T06:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000239895251632 + ], + [ + "2025-11-26T06:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000503472465654 + ], + [ + "2025-11-26T06:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004204535472563 + ], + [ + "2025-11-26T06:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999778125915925 + ], + [ + "2025-11-26T06:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003852751482694 + ], + [ + "2025-11-26T06:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008513301257147 + ], + [ + "2025-11-26T06:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003795196546732 + ], + [ + "2025-11-26T06:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999867516333719 + ], + [ + "2025-11-26T06:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008064400353387 + ], + [ + "2025-11-26T06:03:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000938938386978 + ], + [ + "2025-11-26T06:01:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999006027384443 + ], + [ + "2025-11-26T05:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999517846362147 + ], + [ + "2025-11-26T05:58:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996815344605331 + ], + [ + "2025-11-26T05:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002643608501083 + ], + [ + "2025-11-26T05:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997879603960761 + ], + [ + "2025-11-26T05:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002061778950033 + ], + [ + "2025-11-26T05:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996836066332527 + ], + [ + "2025-11-26T05:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000426056014424 + ], + [ + "2025-11-26T05:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001754661455324 + ], + [ + "2025-11-26T05:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997879054879624 + ], + [ + "2025-11-26T05:42:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997733109967785 + ], + [ + "2025-11-26T05:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997501592176846 + ], + [ + "2025-11-26T05:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998437274398727 + ], + [ + "2025-11-26T05:35:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997796935919436 + ], + [ + "2025-11-26T05:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998811081730907 + ], + [ + "2025-11-26T05:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997561480942823 + ], + [ + "2025-11-26T05:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996812277256962 + ], + [ + "2025-11-26T05:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997550807460167 + ], + [ + "2025-11-26T05:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997245701241113 + ], + [ + "2025-11-26T05:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997921044455745 + ], + [ + "2025-11-26T05:21:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997485353475897 + ], + [ + "2025-11-26T05:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997823429747327 + ], + [ + "2025-11-26T05:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998825607962248 + ], + [ + "2025-11-26T05:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997792236819835 + ], + [ + "2025-11-26T05:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997780731353004 + ], + [ + "2025-11-26T05:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004284499186262 + ], + [ + "2025-11-26T05:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000065164903996 + ], + [ + "2025-11-26T05:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999726908003204 + ], + [ + "2025-11-26T05:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998037040762707 + ], + [ + "2025-11-26T05:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997757977013993 + ], + [ + "2025-11-26T05:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998408468579235 + ], + [ + "2025-11-26T04:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997376781387377 + ], + [ + "2025-11-26T04:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998576541599482 + ], + [ + "2025-11-26T04:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997875713967023 + ], + [ + "2025-11-26T04:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997876131312446 + ], + [ + "2025-11-26T04:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997830503040498 + ], + [ + "2025-11-26T04:50:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997412799290307 + ], + [ + "2025-11-26T04:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997671709658129 + ], + [ + "2025-11-26T04:46:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997982989149199 + ], + [ + "2025-11-26T04:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999764694388367 + ], + [ + "2025-11-26T04:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998156728319927 + ], + [ + "2025-11-26T04:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996824539818425 + ], + [ + "2025-11-26T04:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998107629805179 + ], + [ + "2025-11-26T04:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999787966232131 + ], + [ + "2025-11-26T04:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999802760125009 + ], + [ + "2025-11-26T04:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997842165726245 + ], + [ + "2025-11-26T04:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999784986429668 + ], + [ + "2025-11-26T04:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997492970626436 + ], + [ + "2025-11-26T04:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998425759912646 + ], + [ + "2025-11-26T04:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998067329423997 + ], + [ + "2025-11-26T04:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997841150861893 + ], + [ + "2025-11-26T04:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997286498556923 + ], + [ + "2025-11-26T04:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997695795551069 + ], + [ + "2025-11-26T04:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998438780687271 + ], + [ + "2025-11-26T04:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998012408398593 + ], + [ + "2025-11-26T04:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996622306076249 + ], + [ + "2025-11-26T04:10:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999601621306343 + ], + [ + "2025-11-26T04:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998144138206004 + ], + [ + "2025-11-26T04:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996865252326239 + ], + [ + "2025-11-26T04:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997587034396404 + ], + [ + "2025-11-26T04:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997967457153738 + ], + [ + "2025-11-26T03:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997516193922715 + ], + [ + "2025-11-26T03:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997618897440241 + ], + [ + "2025-11-26T03:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999745185698625 + ], + [ + "2025-11-26T03:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997380105275097 + ], + [ + "2025-11-26T03:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996930733922931 + ], + [ + "2025-11-26T03:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998449690673736 + ], + [ + "2025-11-26T03:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998134073098326 + ], + [ + "2025-11-26T03:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997677255998942 + ], + [ + "2025-11-26T03:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996570138108481 + ], + [ + "2025-11-26T03:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005456345553203 + ], + [ + "2025-11-26T03:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997663125124817 + ], + [ + "2025-11-26T03:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998008890591024 + ], + [ + "2025-11-26T03:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997616661663569 + ], + [ + "2025-11-26T03:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997812309355795 + ], + [ + "2025-11-26T03:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998683832126245 + ], + [ + "2025-11-26T03:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998504058441008 + ], + [ + "2025-11-26T03:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997428148446021 + ], + [ + "2025-11-26T03:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996978879869041 + ], + [ + "2025-11-26T03:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997406836175855 + ], + [ + "2025-11-26T03:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998450057599841 + ], + [ + "2025-11-26T03:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998612448117129 + ], + [ + "2025-11-26T03:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997344190850849 + ], + [ + "2025-11-26T03:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997269691896263 + ], + [ + "2025-11-26T03:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997026247658888 + ], + [ + "2025-11-26T03:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996956891903067 + ], + [ + "2025-11-26T03:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999688708648247 + ], + [ + "2025-11-26T03:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997965661264764 + ], + [ + "2025-11-26T03:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995709601122795 + ], + [ + "2025-11-26T03:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998251601191542 + ], + [ + "2025-11-26T03:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997760955634131 + ], + [ + "2025-11-26T02:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999849138920695 + ], + [ + "2025-11-26T02:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996820665133551 + ], + [ + "2025-11-26T02:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000765533712503 + ], + [ + "2025-11-26T02:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998295000081536 + ], + [ + "2025-11-26T02:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997837488726656 + ], + [ + "2025-11-26T02:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999780786113811 + ], + [ + "2025-11-26T02:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001922881191077 + ], + [ + "2025-11-26T02:46:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999149912448297 + ], + [ + "2025-11-26T02:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997736263674968 + ], + [ + "2025-11-26T02:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997395598595967 + ], + [ + "2025-11-26T02:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000661368695343 + ], + [ + "2025-11-26T02:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996479610545492 + ], + [ + "2025-11-26T02:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999604177009162 + ], + [ + "2025-11-26T02:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998676709826383 + ], + [ + "2025-11-26T02:32:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999796293757662 + ], + [ + "2025-11-26T02:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997477627469733 + ], + [ + "2025-11-26T02:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996593795156006 + ], + [ + "2025-11-26T02:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001836890667466 + ], + [ + "2025-11-26T02:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004377025366296 + ], + [ + "2025-11-26T02:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004248081968192 + ], + [ + "2025-11-26T02:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997849994654961 + ], + [ + "2025-11-26T02:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997437271199381 + ], + [ + "2025-11-26T02:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998306217872966 + ], + [ + "2025-11-26T02:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997842805000094 + ], + [ + "2025-11-26T02:11:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997480042720017 + ], + [ + "2025-11-26T02:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998113267930587 + ], + [ + "2025-11-26T02:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997550396553911 + ], + [ + "2025-11-26T02:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999666794880583 + ], + [ + "2025-11-26T02:03:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998622806096527 + ], + [ + "2025-11-26T02:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998469589691392 + ], + [ + "2025-11-26T02:00:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998733126913399 + ], + [ + "2025-11-26T01:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996911099605534 + ], + [ + "2025-11-26T01:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997847662073179 + ], + [ + "2025-11-26T01:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998912838606265 + ], + [ + "2025-11-26T01:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996760508379052 + ], + [ + "2025-11-26T01:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995727170090175 + ], + [ + "2025-11-26T01:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998828168504814 + ], + [ + "2025-11-26T01:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003752748819692 + ], + [ + "2025-11-26T01:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997790475952594 + ], + [ + "2025-11-26T01:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996867429113795 + ], + [ + "2025-11-26T01:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999437648685986 + ], + [ + "2025-11-26T01:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996462884838182 + ], + [ + "2025-11-26T01:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996734854954478 + ], + [ + "2025-11-26T01:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996210704185837 + ], + [ + "2025-11-26T01:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997780451708415 + ], + [ + "2025-11-26T01:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996605042579109 + ], + [ + "2025-11-26T01:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997060624648059 + ], + [ + "2025-11-26T01:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996391781807744 + ], + [ + "2025-11-26T01:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998085666912127 + ], + [ + "2025-11-26T01:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996712421660956 + ], + [ + "2025-11-26T01:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997367768842403 + ], + [ + "2025-11-26T01:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997459011588228 + ], + [ + "2025-11-26T01:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997204284617924 + ], + [ + "2025-11-26T01:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997122531838406 + ], + [ + "2025-11-26T01:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99979291493484 + ], + [ + "2025-11-26T01:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996890475625196 + ], + [ + "2025-11-26T01:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996479731966019 + ], + [ + "2025-11-26T01:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997320300101347 + ], + [ + "2025-11-26T01:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997786959801203 + ], + [ + "2025-11-26T01:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997450308788195 + ], + [ + "2025-11-26T00:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996339770544184 + ], + [ + "2025-11-26T00:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996896803981311 + ], + [ + "2025-11-26T00:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996998458339358 + ], + [ + "2025-11-26T00:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997370906591995 + ], + [ + "2025-11-26T00:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997200660116158 + ], + [ + "2025-11-26T00:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997382397680332 + ], + [ + "2025-11-26T00:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997046513341755 + ], + [ + "2025-11-26T00:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997860921698508 + ], + [ + "2025-11-26T00:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999817571443867 + ], + [ + "2025-11-26T00:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998076942552411 + ], + [ + "2025-11-26T00:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997520194461893 + ], + [ + "2025-11-26T00:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997091624293425 + ], + [ + "2025-11-26T00:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998195058251192 + ], + [ + "2025-11-26T00:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995077364844934 + ], + [ + "2025-11-26T00:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997305951133287 + ], + [ + "2025-11-26T00:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999791873143188 + ], + [ + "2025-11-26T00:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999837874353635 + ], + [ + "2025-11-26T00:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995781356317536 + ], + [ + "2025-11-26T00:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998128299699816 + ], + [ + "2025-11-26T00:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997397895555438 + ], + [ + "2025-11-26T00:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997787321702079 + ], + [ + "2025-11-26T00:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998103627772589 + ], + [ + "2025-11-26T00:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997876061978161 + ], + [ + "2025-11-26T00:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997863821999154 + ], + [ + "2025-11-26T00:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997517671165176 + ], + [ + "2025-11-26T00:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997299149662209 + ], + [ + "2025-11-26T00:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997949185361393 + ], + [ + "2025-11-26T00:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997904453270198 + ], + [ + "2025-11-26T00:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997449561708841 + ], + [ + "2025-11-26T00:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998251538809131 + ], + [ + "2025-11-25T23:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997550066438543 + ], + [ + "2025-11-25T23:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996791734897135 + ], + [ + "2025-11-25T23:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997937585492749 + ], + [ + "2025-11-25T23:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001966764615313 + ], + [ + "2025-11-25T23:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997532092177781 + ], + [ + "2025-11-25T23:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997238646733568 + ], + [ + "2025-11-25T23:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997347691066918 + ], + [ + "2025-11-25T23:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002072274632847 + ], + [ + "2025-11-25T23:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999263933478111 + ], + [ + "2025-11-25T23:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997451077478194 + ], + [ + "2025-11-25T23:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998089585340177 + ], + [ + "2025-11-25T23:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997426867687471 + ], + [ + "2025-11-25T23:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997287965006512 + ], + [ + "2025-11-25T23:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997444850357957 + ], + [ + "2025-11-25T23:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997875650818193 + ], + [ + "2025-11-25T23:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997150568533771 + ], + [ + "2025-11-25T23:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998514691613115 + ], + [ + "2025-11-25T23:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002947026835998 + ], + [ + "2025-11-25T23:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999819107013759 + ], + [ + "2025-11-25T23:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998309367261093 + ], + [ + "2025-11-25T23:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999860345731084 + ], + [ + "2025-11-25T23:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007404180841295 + ], + [ + "2025-11-25T23:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997682776732236 + ], + [ + "2025-11-25T23:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998656537543463 + ], + [ + "2025-11-25T23:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998508094603751 + ], + [ + "2025-11-25T23:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996101097858489 + ], + [ + "2025-11-25T23:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996110880627449 + ], + [ + "2025-11-25T23:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997551442023004 + ], + [ + "2025-11-25T23:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998265613692391 + ], + [ + "2025-11-25T23:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995859018707154 + ], + [ + "2025-11-25T22:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997889877016393 + ], + [ + "2025-11-25T22:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998452760325784 + ], + [ + "2025-11-25T22:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997676072182236 + ], + [ + "2025-11-25T22:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997806561556104 + ], + [ + "2025-11-25T22:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997647288021507 + ], + [ + "2025-11-25T22:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997971646650989 + ], + [ + "2025-11-25T22:47:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996430816871948 + ], + [ + "2025-11-25T22:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999786470296556 + ], + [ + "2025-11-25T22:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995386693446084 + ], + [ + "2025-11-25T22:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998910128306773 + ], + [ + "2025-11-25T22:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998326850445499 + ], + [ + "2025-11-25T22:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998263798121627 + ], + [ + "2025-11-25T22:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997003246485014 + ], + [ + "2025-11-25T22:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999458150130983 + ], + [ + "2025-11-25T22:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994663178488665 + ], + [ + "2025-11-25T22:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998051398884983 + ], + [ + "2025-11-25T22:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997778211086326 + ], + [ + "2025-11-25T22:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000767206533203 + ], + [ + "2025-11-25T22:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997547965277152 + ], + [ + "2025-11-25T22:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009313260018577 + ], + [ + "2025-11-25T22:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000333146863776 + ], + [ + "2025-11-25T22:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003143054987496 + ], + [ + "2025-11-25T22:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997340735236713 + ], + [ + "2025-11-25T22:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996938319674609 + ], + [ + "2025-11-25T22:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999762797152505 + ], + [ + "2025-11-25T22:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004394140777948 + ], + [ + "2025-11-25T22:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998108580786356 + ], + [ + "2025-11-25T22:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997803178602522 + ], + [ + "2025-11-25T22:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997685031951493 + ], + [ + "2025-11-25T22:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010328400495172 + ], + [ + "2025-11-25T21:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997445112656844 + ], + [ + "2025-11-25T21:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996154611039434 + ], + [ + "2025-11-25T21:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999756822806708 + ], + [ + "2025-11-25T21:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997208994934466 + ], + [ + "2025-11-25T21:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997730617313436 + ], + [ + "2025-11-25T21:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996958467528241 + ], + [ + "2025-11-25T21:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001190041894494 + ], + [ + "2025-11-25T21:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997779363403356 + ], + [ + "2025-11-25T21:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001440554505152 + ], + [ + "2025-11-25T21:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996738484835662 + ], + [ + "2025-11-25T21:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012330341934137 + ], + [ + "2025-11-25T21:38:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004380289405574 + ], + [ + "2025-11-25T21:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007298474394166 + ], + [ + "2025-11-25T21:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997717792954275 + ], + [ + "2025-11-25T21:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997070881635174 + ], + [ + "2025-11-25T21:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998039172141308 + ], + [ + "2025-11-25T21:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998599181111485 + ], + [ + "2025-11-25T21:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999340698911114 + ], + [ + "2025-11-25T21:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996446585629865 + ], + [ + "2025-11-25T21:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999769246852974 + ], + [ + "2025-11-25T21:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997367788576416 + ], + [ + "2025-11-25T21:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999675578844796 + ], + [ + "2025-11-25T21:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995937667604671 + ], + [ + "2025-11-25T21:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999842755636638 + ], + [ + "2025-11-25T21:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997155261211521 + ], + [ + "2025-11-25T21:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996906808197678 + ], + [ + "2025-11-25T21:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998691740141862 + ], + [ + "2025-11-25T21:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997213259017618 + ], + [ + "2025-11-25T21:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997876192478263 + ], + [ + "2025-11-25T21:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996759273129757 + ], + [ + "2025-11-25T20:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997098324056207 + ], + [ + "2025-11-25T20:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996594360542362 + ], + [ + "2025-11-25T20:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997210813567421 + ], + [ + "2025-11-25T20:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997251563031688 + ], + [ + "2025-11-25T20:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996640911382552 + ], + [ + "2025-11-25T20:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997791462165742 + ], + [ + "2025-11-25T20:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995382497315682 + ], + [ + "2025-11-25T20:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997503396574499 + ], + [ + "2025-11-25T20:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9992772652993548 + ], + [ + "2025-11-25T20:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997264593952254 + ], + [ + "2025-11-25T20:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994893284432598 + ], + [ + "2025-11-25T20:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997234652035419 + ], + [ + "2025-11-25T20:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999375005604837 + ], + [ + "2025-11-25T20:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998759300718616 + ], + [ + "2025-11-25T20:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995836715808167 + ], + [ + "2025-11-25T20:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999611419502628 + ], + [ + "2025-11-25T20:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99947689662861 + ], + [ + "2025-11-25T20:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997261367524656 + ], + [ + "2025-11-25T20:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996546098739593 + ], + [ + "2025-11-25T20:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995970732920295 + ], + [ + "2025-11-25T20:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995094163923016 + ], + [ + "2025-11-25T20:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996698465961774 + ], + [ + "2025-11-25T20:15:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996623172149655 + ], + [ + "2025-11-25T20:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996071307775082 + ], + [ + "2025-11-25T20:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997129048493086 + ], + [ + "2025-11-25T20:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999378690978973 + ], + [ + "2025-11-25T20:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997148098891812 + ], + [ + "2025-11-25T20:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996025503325859 + ], + [ + "2025-11-25T20:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996326031589171 + ], + [ + "2025-11-25T20:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994145925401084 + ], + [ + "2025-11-25T19:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997112945982749 + ], + [ + "2025-11-25T19:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995671277329661 + ], + [ + "2025-11-25T19:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995431598211489 + ], + [ + "2025-11-25T19:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994267600454952 + ], + [ + "2025-11-25T19:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995271046302686 + ], + [ + "2025-11-25T19:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996099704482266 + ], + [ + "2025-11-25T19:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996245285342245 + ], + [ + "2025-11-25T19:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995171087317197 + ], + [ + "2025-11-25T19:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995979036988054 + ], + [ + "2025-11-25T19:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996039795112999 + ], + [ + "2025-11-25T19:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996326284847838 + ], + [ + "2025-11-25T19:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999590416122589 + ], + [ + "2025-11-25T19:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996398246767312 + ], + [ + "2025-11-25T19:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997451773209135 + ], + [ + "2025-11-25T19:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996581873908864 + ], + [ + "2025-11-25T19:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996755655690054 + ], + [ + "2025-11-25T19:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996751177621485 + ], + [ + "2025-11-25T19:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997386661552634 + ], + [ + "2025-11-25T19:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997291255599017 + ], + [ + "2025-11-25T19:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996734098288382 + ], + [ + "2025-11-25T19:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996244604992763 + ], + [ + "2025-11-25T19:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998877834357514 + ], + [ + "2025-11-25T19:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996605476985366 + ], + [ + "2025-11-25T19:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996361924479605 + ], + [ + "2025-11-25T19:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996718130128084 + ], + [ + "2025-11-25T19:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997746279917613 + ], + [ + "2025-11-25T19:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997418856833808 + ], + [ + "2025-11-25T19:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998323923369045 + ], + [ + "2025-11-25T19:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997633017357 + ], + [ + "2025-11-25T19:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997268071597651 + ], + [ + "2025-11-25T18:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999698940504223 + ], + [ + "2025-11-25T18:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998791557563108 + ], + [ + "2025-11-25T18:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99978937520059 + ], + [ + "2025-11-25T18:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998542293399321 + ], + [ + "2025-11-25T18:52:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998115626234292 + ], + [ + "2025-11-25T18:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997842353874543 + ], + [ + "2025-11-25T18:48:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996884562386789 + ], + [ + "2025-11-25T18:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999721803745494 + ], + [ + "2025-11-25T18:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997628978580415 + ], + [ + "2025-11-25T18:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999020355928134 + ], + [ + "2025-11-25T18:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997158206306186 + ], + [ + "2025-11-25T18:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998690276378012 + ], + [ + "2025-11-25T18:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999154168844038 + ], + [ + "2025-11-25T18:33:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999087187515915 + ], + [ + "2025-11-25T18:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998825785977489 + ], + [ + "2025-11-25T18:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997723846345422 + ], + [ + "2025-11-25T18:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996824587099645 + ], + [ + "2025-11-25T18:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997515437388605 + ], + [ + "2025-11-25T18:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997850655908643 + ], + [ + "2025-11-25T18:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998457575256567 + ], + [ + "2025-11-25T18:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998317716789504 + ], + [ + "2025-11-25T18:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999757736021985 + ], + [ + "2025-11-25T18:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998514980655275 + ], + [ + "2025-11-25T18:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997733983174766 + ], + [ + "2025-11-25T18:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999875206384566 + ], + [ + "2025-11-25T18:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999084825399163 + ], + [ + "2025-11-25T18:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000343187358849 + ], + [ + "2025-11-25T18:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998185914905924 + ], + [ + "2025-11-25T18:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998690576953362 + ], + [ + "2025-11-25T18:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998532220126016 + ], + [ + "2025-11-25T17:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998452802582276 + ], + [ + "2025-11-25T17:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999855048776546 + ], + [ + "2025-11-25T17:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999816592840171 + ], + [ + "2025-11-25T17:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999632071891708 + ], + [ + "2025-11-25T17:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998381540102848 + ], + [ + "2025-11-25T17:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001777034116457 + ], + [ + "2025-11-25T17:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998056014537163 + ], + [ + "2025-11-25T17:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000407114074757 + ], + [ + "2025-11-25T17:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998814728200986 + ], + [ + "2025-11-25T17:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004531060907809 + ], + [ + "2025-11-25T17:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997632315990614 + ], + [ + "2025-11-25T17:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997035365412266 + ], + [ + "2025-11-25T17:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000468606930122 + ], + [ + "2025-11-25T17:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997616430551373 + ], + [ + "2025-11-25T17:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997374908612776 + ], + [ + "2025-11-25T17:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998208724347499 + ], + [ + "2025-11-25T17:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999826276188958 + ], + [ + "2025-11-25T17:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998149055905272 + ], + [ + "2025-11-25T17:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997668639922435 + ], + [ + "2025-11-25T17:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997751596024553 + ], + [ + "2025-11-25T17:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998001059043033 + ], + [ + "2025-11-25T17:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996239021990432 + ], + [ + "2025-11-25T17:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997990572351707 + ], + [ + "2025-11-25T17:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998086463333634 + ], + [ + "2025-11-25T17:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002010245731838 + ], + [ + "2025-11-25T17:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996577728455719 + ], + [ + "2025-11-25T17:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999904811605048 + ], + [ + "2025-11-25T17:06:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999734389475902 + ], + [ + "2025-11-25T17:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003273488925366 + ], + [ + "2025-11-25T17:02:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999563741692226 + ], + [ + "2025-11-25T16:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001778793737695 + ], + [ + "2025-11-25T16:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000088288699756 + ], + [ + "2025-11-25T16:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002001450195555 + ], + [ + "2025-11-25T16:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998504908057506 + ], + [ + "2025-11-25T16:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000119485242306 + ], + [ + "2025-11-25T16:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999912930885142 + ], + [ + "2025-11-25T16:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001367661921547 + ], + [ + "2025-11-25T16:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999777433228827 + ], + [ + "2025-11-25T16:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998546143672851 + ], + [ + "2025-11-25T16:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999499553241141 + ], + [ + "2025-11-25T16:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001468868956294 + ], + [ + "2025-11-25T16:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998842840380875 + ], + [ + "2025-11-25T16:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005386288930866 + ], + [ + "2025-11-25T16:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999105008872737 + ], + [ + "2025-11-25T16:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006516306055664 + ], + [ + "2025-11-25T16:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998595679647033 + ], + [ + "2025-11-25T16:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006918127050148 + ], + [ + "2025-11-25T16:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998439650961811 + ], + [ + "2025-11-25T16:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999969993123113 + ], + [ + "2025-11-25T16:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000628310226858 + ], + [ + "2025-11-25T16:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009151602111976 + ], + [ + "2025-11-25T16:18:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000587361502656 + ], + [ + "2025-11-25T16:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003979267222074 + ], + [ + "2025-11-25T16:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002664057239838 + ], + [ + "2025-11-25T16:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008432227595732 + ], + [ + "2025-11-25T16:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000933217201766 + ], + [ + "2025-11-25T16:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000939047436292 + ], + [ + "2025-11-25T16:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999105148652725 + ], + [ + "2025-11-25T16:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000462607401248 + ], + [ + "2025-11-25T16:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996887539785648 + ], + [ + "2025-11-25T15:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999900261814223 + ], + [ + "2025-11-25T15:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001674215675675 + ], + [ + "2025-11-25T15:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000118866356986 + ], + [ + "2025-11-25T15:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996331185729971 + ], + [ + "2025-11-25T15:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002367315832157 + ], + [ + "2025-11-25T15:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000754305936713 + ], + [ + "2025-11-25T15:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000005383672552 + ], + [ + "2025-11-25T15:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997498446716232 + ], + [ + "2025-11-25T15:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007232320811985 + ], + [ + "2025-11-25T15:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008559798871661 + ], + [ + "2025-11-25T15:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999213642263018 + ], + [ + "2025-11-25T15:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998559216417409 + ], + [ + "2025-11-25T15:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999158135891382 + ], + [ + "2025-11-25T15:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997905779003547 + ], + [ + "2025-11-25T15:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998156941732693 + ], + [ + "2025-11-25T15:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998104132028932 + ], + [ + "2025-11-25T15:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998619579369918 + ], + [ + "2025-11-25T15:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997523100894427 + ], + [ + "2025-11-25T15:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999031888105901 + ], + [ + "2025-11-25T15:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997027028771012 + ], + [ + "2025-11-25T15:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996761902066672 + ], + [ + "2025-11-25T15:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996463468222035 + ], + [ + "2025-11-25T15:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997192795545162 + ], + [ + "2025-11-25T15:13:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999764531287436 + ], + [ + "2025-11-25T15:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996974083297895 + ], + [ + "2025-11-25T15:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997705523852748 + ], + [ + "2025-11-25T15:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999237069431882 + ], + [ + "2025-11-25T15:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997280318944726 + ], + [ + "2025-11-25T15:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999721279774264 + ], + [ + "2025-11-25T15:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999136543997681 + ], + [ + "2025-11-25T14:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998756245880513 + ], + [ + "2025-11-25T14:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997833011928285 + ], + [ + "2025-11-25T14:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997405804228449 + ], + [ + "2025-11-25T14:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998137648927774 + ], + [ + "2025-11-25T14:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998628824799666 + ], + [ + "2025-11-25T14:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997345779958798 + ], + [ + "2025-11-25T14:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996964637764033 + ], + [ + "2025-11-25T14:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998360035197452 + ], + [ + "2025-11-25T14:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003289276407392 + ], + [ + "2025-11-25T14:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996524084006575 + ], + [ + "2025-11-25T14:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997026533992894 + ], + [ + "2025-11-25T14:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997514459224848 + ], + [ + "2025-11-25T14:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999225552166675 + ], + [ + "2025-11-25T14:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998421638516737 + ], + [ + "2025-11-25T14:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998363482262804 + ], + [ + "2025-11-25T14:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999165972995457 + ], + [ + "2025-11-25T14:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997530847169749 + ], + [ + "2025-11-25T14:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997681982094769 + ], + [ + "2025-11-25T14:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999476434982355 + ], + [ + "2025-11-25T14:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999070679516029 + ], + [ + "2025-11-25T14:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996476688321364 + ], + [ + "2025-11-25T14:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997898300372541 + ], + [ + "2025-11-25T14:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998357774064759 + ], + [ + "2025-11-25T14:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998374587854704 + ], + [ + "2025-11-25T14:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997412424770756 + ], + [ + "2025-11-25T14:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000082338981152 + ], + [ + "2025-11-25T14:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999791673392784 + ], + [ + "2025-11-25T14:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000404613483833 + ], + [ + "2025-11-25T14:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998695792097113 + ], + [ + "2025-11-25T13:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000029563122041 + ], + [ + "2025-11-25T13:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999850710131545 + ], + [ + "2025-11-25T13:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000922031346005 + ], + [ + "2025-11-25T13:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000015861735768 + ], + [ + "2025-11-25T13:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999771673380934 + ], + [ + "2025-11-25T13:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999437939219907 + ], + [ + "2025-11-25T13:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000071746009097 + ], + [ + "2025-11-25T13:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001181658903733 + ], + [ + "2025-11-25T13:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997814666114943 + ], + [ + "2025-11-25T13:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999902639444433 + ], + [ + "2025-11-25T13:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000481116922981 + ], + [ + "2025-11-25T13:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000589927226473 + ], + [ + "2025-11-25T13:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999719299351997 + ], + [ + "2025-11-25T13:34:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000356254987417 + ], + [ + "2025-11-25T13:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001371770685765 + ], + [ + "2025-11-25T13:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000100756993861 + ], + [ + "2025-11-25T13:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998243343852736 + ], + [ + "2025-11-25T13:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002178581446541 + ], + [ + "2025-11-25T13:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998986363171932 + ], + [ + "2025-11-25T13:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998806677445958 + ], + [ + "2025-11-25T13:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999018063948866 + ], + [ + "2025-11-25T13:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000243078206277 + ], + [ + "2025-11-25T13:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997975640000795 + ], + [ + "2025-11-25T13:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000317021937135 + ], + [ + "2025-11-25T13:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998945716784543 + ], + [ + "2025-11-25T13:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997070898544925 + ], + [ + "2025-11-25T13:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002223723742723 + ], + [ + "2025-11-25T13:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001169261948526 + ], + [ + "2025-11-25T13:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002756682483953 + ], + [ + "2025-11-25T13:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998354492894196 + ], + [ + "2025-11-25T12:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000128090654129 + ], + [ + "2025-11-25T12:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000501397845305 + ], + [ + "2025-11-25T12:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005762995184113 + ], + [ + "2025-11-25T12:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000549461626569 + ], + [ + "2025-11-25T12:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001324557638125 + ], + [ + "2025-11-25T12:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000070415609301 + ], + [ + "2025-11-25T12:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007346944674727 + ], + [ + "2025-11-25T12:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000051032375082 + ], + [ + "2025-11-25T12:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001250782394948 + ], + [ + "2025-11-25T12:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002604007672304 + ], + [ + "2025-11-25T12:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012915551319994 + ], + [ + "2025-11-25T12:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001642237549038 + ], + [ + "2025-11-25T12:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996448604796635 + ], + [ + "2025-11-25T12:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997638161595134 + ], + [ + "2025-11-25T12:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003361268536082 + ], + [ + "2025-11-25T12:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99969508910734 + ], + [ + "2025-11-25T12:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996965485260113 + ], + [ + "2025-11-25T12:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998209091624646 + ], + [ + "2025-11-25T12:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997339798585799 + ], + [ + "2025-11-25T12:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997203335335771 + ], + [ + "2025-11-25T12:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996759164192436 + ], + [ + "2025-11-25T12:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997805406419735 + ], + [ + "2025-11-25T12:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998724054047563 + ], + [ + "2025-11-25T12:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998297857188243 + ], + [ + "2025-11-25T12:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997647827723251 + ], + [ + "2025-11-25T12:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999402387100382 + ], + [ + "2025-11-25T12:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998596504453765 + ], + [ + "2025-11-25T12:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998534661300733 + ], + [ + "2025-11-25T12:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998758344181069 + ], + [ + "2025-11-25T12:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997012505856075 + ], + [ + "2025-11-25T11:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001220963059365 + ], + [ + "2025-11-25T11:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999623954407934 + ], + [ + "2025-11-25T11:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999495391797149 + ], + [ + "2025-11-25T11:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998424663709391 + ], + [ + "2025-11-25T11:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006706993680625 + ], + [ + "2025-11-25T11:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997820150401096 + ], + [ + "2025-11-25T11:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997159203429552 + ], + [ + "2025-11-25T11:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998252549045875 + ], + [ + "2025-11-25T11:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997026272903416 + ], + [ + "2025-11-25T11:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996999182913423 + ], + [ + "2025-11-25T11:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997503010708712 + ], + [ + "2025-11-25T11:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997190064556432 + ], + [ + "2025-11-25T11:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997692380818617 + ], + [ + "2025-11-25T11:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998159011568069 + ], + [ + "2025-11-25T11:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997426838329855 + ], + [ + "2025-11-25T11:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997531737167039 + ], + [ + "2025-11-25T11:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999216544245004 + ], + [ + "2025-11-25T11:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998193633101737 + ], + [ + "2025-11-25T11:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996941997979918 + ], + [ + "2025-11-25T11:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997547782443266 + ], + [ + "2025-11-25T11:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999855162864122 + ], + [ + "2025-11-25T11:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998114957418397 + ], + [ + "2025-11-25T11:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999786659362046 + ], + [ + "2025-11-25T11:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999144475301809 + ], + [ + "2025-11-25T11:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998366296508255 + ], + [ + "2025-11-25T11:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998938889688843 + ], + [ + "2025-11-25T11:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998255718808234 + ], + [ + "2025-11-25T11:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999964621944718 + ], + [ + "2025-11-25T11:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000148001614968 + ], + [ + "2025-11-25T11:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998997113293315 + ], + [ + "2025-11-25T10:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998423067858055 + ], + [ + "2025-11-25T10:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999617883966497 + ], + [ + "2025-11-25T10:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999889034183407 + ], + [ + "2025-11-25T10:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999117787309573 + ], + [ + "2025-11-25T10:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999956504756852 + ], + [ + "2025-11-25T10:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998898766247147 + ], + [ + "2025-11-25T10:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999256342516373 + ], + [ + "2025-11-25T10:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998460826804448 + ], + [ + "2025-11-25T10:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000734865358891 + ], + [ + "2025-11-25T10:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999269853428571 + ], + [ + "2025-11-25T10:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998979859329341 + ], + [ + "2025-11-25T10:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006870434785156 + ], + [ + "2025-11-25T10:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005265034501283 + ], + [ + "2025-11-25T10:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999473789909145 + ], + [ + "2025-11-25T10:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007032221487615 + ], + [ + "2025-11-25T10:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000639594346967 + ], + [ + "2025-11-25T10:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999836038941605 + ], + [ + "2025-11-25T10:25:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997957332315006 + ], + [ + "2025-11-25T10:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99973676654172 + ], + [ + "2025-11-25T10:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002164123024937 + ], + [ + "2025-11-25T10:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999598806978303 + ], + [ + "2025-11-25T10:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996962267792666 + ], + [ + "2025-11-25T10:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997618411712353 + ], + [ + "2025-11-25T10:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999024746504538 + ], + [ + "2025-11-25T10:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998157246588525 + ], + [ + "2025-11-25T10:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997400143324677 + ], + [ + "2025-11-25T10:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998735183655526 + ], + [ + "2025-11-25T10:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00010005900279 + ], + [ + "2025-11-25T10:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998652625797496 + ], + [ + "2025-11-25T10:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998103288936468 + ], + [ + "2025-11-25T09:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997835084666115 + ], + [ + "2025-11-25T09:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001165170965325 + ], + [ + "2025-11-25T09:56:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999212241299856 + ], + [ + "2025-11-25T09:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999425810592942 + ], + [ + "2025-11-25T09:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998623252272273 + ], + [ + "2025-11-25T09:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001601354922653 + ], + [ + "2025-11-25T09:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999993099225433 + ], + [ + "2025-11-25T09:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999978085779935 + ], + [ + "2025-11-25T09:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003260426801108 + ], + [ + "2025-11-25T09:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002029879307746 + ], + [ + "2025-11-25T09:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999848263268778 + ], + [ + "2025-11-25T09:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999895919592659 + ], + [ + "2025-11-25T09:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999280998814742 + ], + [ + "2025-11-25T09:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997918348590663 + ], + [ + "2025-11-25T09:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005907510402758 + ], + [ + "2025-11-25T09:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000214874369013 + ], + [ + "2025-11-25T09:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996964126468548 + ], + [ + "2025-11-25T09:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001126394190794 + ], + [ + "2025-11-25T09:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998825871164587 + ], + [ + "2025-11-25T09:22:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999882624075251 + ], + [ + "2025-11-25T09:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998033571815589 + ], + [ + "2025-11-25T09:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000532363565868 + ], + [ + "2025-11-25T09:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999009366057116 + ], + [ + "2025-11-25T09:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999786703271549 + ], + [ + "2025-11-25T09:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002879463462344 + ], + [ + "2025-11-25T09:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999717197224279 + ], + [ + "2025-11-25T09:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999825956569259 + ], + [ + "2025-11-25T09:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999923528811809 + ], + [ + "2025-11-25T09:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000368087181388 + ], + [ + "2025-11-25T09:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997227716919461 + ], + [ + "2025-11-25T08:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001409651558815 + ], + [ + "2025-11-25T08:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996399445677887 + ], + [ + "2025-11-25T08:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998888368308698 + ], + [ + "2025-11-25T08:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998252315868031 + ], + [ + "2025-11-25T08:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999023363701179 + ], + [ + "2025-11-25T08:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996436481643088 + ], + [ + "2025-11-25T08:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996818476055696 + ], + [ + "2025-11-25T08:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003553317323977 + ], + [ + "2025-11-25T08:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000169419810528 + ], + [ + "2025-11-25T08:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999896044381827 + ], + [ + "2025-11-25T08:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998730827125494 + ], + [ + "2025-11-25T08:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000097953547702 + ], + [ + "2025-11-25T08:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005400376698805 + ], + [ + "2025-11-25T08:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999825933341235 + ], + [ + "2025-11-25T08:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999834624942703 + ], + [ + "2025-11-25T08:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998962600509664 + ], + [ + "2025-11-25T08:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999207846140583 + ], + [ + "2025-11-25T08:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999925588359293 + ], + [ + "2025-11-25T08:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999138918447997 + ], + [ + "2025-11-25T08:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998628021252892 + ], + [ + "2025-11-25T08:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996857703615186 + ], + [ + "2025-11-25T08:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999258666904651 + ], + [ + "2025-11-25T08:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999340872844722 + ], + [ + "2025-11-25T08:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999172108744665 + ], + [ + "2025-11-25T08:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999369900636309 + ], + [ + "2025-11-25T08:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999756608850557 + ], + [ + "2025-11-25T08:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996596155222288 + ], + [ + "2025-11-25T08:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004121053012862 + ], + [ + "2025-11-25T08:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998024182481343 + ], + [ + "2025-11-25T08:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998749569749845 + ], + [ + "2025-11-25T07:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997937326055791 + ], + [ + "2025-11-25T07:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000488833429475 + ], + [ + "2025-11-25T07:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995713963995898 + ], + [ + "2025-11-25T07:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00019171878003 + ], + [ + "2025-11-25T07:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000652422904621 + ], + [ + "2025-11-25T07:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008462015840947 + ], + [ + "2025-11-25T07:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999900873804339 + ], + [ + "2025-11-25T07:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006528995489352 + ], + [ + "2025-11-25T07:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001640625711092 + ], + [ + "2025-11-25T07:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998408872296669 + ], + [ + "2025-11-25T07:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999932084585219 + ], + [ + "2025-11-25T07:38:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998975464017207 + ], + [ + "2025-11-25T07:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999089356287039 + ], + [ + "2025-11-25T07:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995108883096794 + ], + [ + "2025-11-25T07:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999764997136218 + ], + [ + "2025-11-25T07:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997788445498289 + ], + [ + "2025-11-25T07:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997516089573643 + ], + [ + "2025-11-25T07:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996826774671643 + ], + [ + "2025-11-25T07:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997823267174938 + ], + [ + "2025-11-25T07:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997930883894427 + ], + [ + "2025-11-25T07:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999973605878155 + ], + [ + "2025-11-25T07:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997790068299932 + ], + [ + "2025-11-25T07:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999617662264547 + ], + [ + "2025-11-25T07:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000450175175306 + ], + [ + "2025-11-25T07:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000188290966956 + ], + [ + "2025-11-25T07:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998305862690327 + ], + [ + "2025-11-25T07:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999223827726709 + ], + [ + "2025-11-25T07:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003996200025849 + ], + [ + "2025-11-25T07:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000877845704337 + ], + [ + "2025-11-25T07:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998055421821485 + ], + [ + "2025-11-25T06:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999072953421505 + ], + [ + "2025-11-25T06:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005101612231757 + ], + [ + "2025-11-25T06:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999452175608993 + ], + [ + "2025-11-25T06:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997147562801103 + ], + [ + "2025-11-25T06:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999411843184446 + ], + [ + "2025-11-25T06:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999586086745672 + ], + [ + "2025-11-25T06:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000230808054032 + ], + [ + "2025-11-25T06:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008299949023753 + ], + [ + "2025-11-25T06:44:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997453860794695 + ], + [ + "2025-11-25T06:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997582070237642 + ], + [ + "2025-11-25T06:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999586903639561 + ], + [ + "2025-11-25T06:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000058299064502 + ], + [ + "2025-11-25T06:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999094222000259 + ], + [ + "2025-11-25T06:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999467147929023 + ], + [ + "2025-11-25T06:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000158455376735 + ], + [ + "2025-11-25T06:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999995431504848 + ], + [ + "2025-11-25T06:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997792462208347 + ], + [ + "2025-11-25T06:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007545094503758 + ], + [ + "2025-11-25T06:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999722819740314 + ], + [ + "2025-11-25T06:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008244449231625 + ], + [ + "2025-11-25T06:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998131481139538 + ], + [ + "2025-11-25T06:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999731946515104 + ], + [ + "2025-11-25T06:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998784824274244 + ], + [ + "2025-11-25T06:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998215899656077 + ], + [ + "2025-11-25T06:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997720477830874 + ], + [ + "2025-11-25T06:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996674284105077 + ], + [ + "2025-11-25T06:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999933286431849 + ], + [ + "2025-11-25T06:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997767318835222 + ], + [ + "2025-11-25T06:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999941267510296 + ], + [ + "2025-11-25T06:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999127491584417 + ], + [ + "2025-11-25T05:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999843753197558 + ], + [ + "2025-11-25T05:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995785170096922 + ], + [ + "2025-11-25T05:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001675676508681 + ], + [ + "2025-11-25T05:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998668162088488 + ], + [ + "2025-11-25T05:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999350834923392 + ], + [ + "2025-11-25T05:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999803239517531 + ], + [ + "2025-11-25T05:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999622530736283 + ], + [ + "2025-11-25T05:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999445196558229 + ], + [ + "2025-11-25T05:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000720273856378 + ], + [ + "2025-11-25T05:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999769650989305 + ], + [ + "2025-11-25T05:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997714469977556 + ], + [ + "2025-11-25T05:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998901022125671 + ], + [ + "2025-11-25T05:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999949961205405 + ], + [ + "2025-11-25T05:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999711450817733 + ], + [ + "2025-11-25T05:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996929221130985 + ], + [ + "2025-11-25T05:30:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997865579425824 + ], + [ + "2025-11-25T05:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996673830801456 + ], + [ + "2025-11-25T05:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000198645300535 + ], + [ + "2025-11-25T05:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997879587385796 + ], + [ + "2025-11-25T05:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997589147478794 + ], + [ + "2025-11-25T05:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001865759026913 + ], + [ + "2025-11-25T05:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013723466102702 + ], + [ + "2025-11-25T05:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997280027614847 + ], + [ + "2025-11-25T05:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000156824820454 + ], + [ + "2025-11-25T05:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000024156117815 + ], + [ + "2025-11-25T05:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010038234601901 + ], + [ + "2025-11-25T05:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000030760548093 + ], + [ + "2025-11-25T05:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006725088610744 + ], + [ + "2025-11-25T05:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000584066165876 + ], + [ + "2025-11-25T05:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000078368035353 + ], + [ + "2025-11-25T04:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999626593452313 + ], + [ + "2025-11-25T04:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999928357657034 + ], + [ + "2025-11-25T04:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000047236487585 + ], + [ + "2025-11-25T04:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005468114753666 + ], + [ + "2025-11-25T04:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998455949507384 + ], + [ + "2025-11-25T04:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001201793769852 + ], + [ + "2025-11-25T04:47:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001089298744392 + ], + [ + "2025-11-25T04:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002482394604142 + ], + [ + "2025-11-25T04:43:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998896669074044 + ], + [ + "2025-11-25T04:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000114195644247 + ], + [ + "2025-11-25T04:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013872844679046 + ], + [ + "2025-11-25T04:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003082324521237 + ], + [ + "2025-11-25T04:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999137500099519 + ], + [ + "2025-11-25T04:33:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002482771222883 + ], + [ + "2025-11-25T04:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003939651787501 + ], + [ + "2025-11-25T04:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999902069572972 + ], + [ + "2025-11-25T04:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997583435192137 + ], + [ + "2025-11-25T04:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002232476338637 + ], + [ + "2025-11-25T04:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00017453354854 + ], + [ + "2025-11-25T04:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999916708964566 + ], + [ + "2025-11-25T04:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999190176490911 + ], + [ + "2025-11-25T04:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006081879364495 + ], + [ + "2025-11-25T04:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001656822121394 + ], + [ + "2025-11-25T04:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999247289462997 + ], + [ + "2025-11-25T04:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999395058218663 + ], + [ + "2025-11-25T04:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997916611422637 + ], + [ + "2025-11-25T04:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002929785775936 + ], + [ + "2025-11-25T04:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998858595449218 + ], + [ + "2025-11-25T04:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998269543957289 + ], + [ + "2025-11-25T04:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007139806932108 + ], + [ + "2025-11-25T03:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003979257530422 + ], + [ + "2025-11-25T03:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998779734135187 + ], + [ + "2025-11-25T03:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998651112924306 + ], + [ + "2025-11-25T03:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005634029766703 + ], + [ + "2025-11-25T03:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000009594041898 + ], + [ + "2025-11-25T03:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999990467764656 + ], + [ + "2025-11-25T03:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999839155950075 + ], + [ + "2025-11-25T03:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008714780273078 + ], + [ + "2025-11-25T03:44:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998451695165224 + ], + [ + "2025-11-25T03:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998779396734724 + ], + [ + "2025-11-25T03:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998849747668849 + ], + [ + "2025-11-25T03:37:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996585977454119 + ], + [ + "2025-11-25T03:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999915089435465 + ], + [ + "2025-11-25T03:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000542790119158 + ], + [ + "2025-11-25T03:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999903218108491 + ], + [ + "2025-11-25T03:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998484324430432 + ], + [ + "2025-11-25T03:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997443525867452 + ], + [ + "2025-11-25T03:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002204775025947 + ], + [ + "2025-11-25T03:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999091775949076 + ], + [ + "2025-11-25T03:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999078642266491 + ], + [ + "2025-11-25T03:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997462051751898 + ], + [ + "2025-11-25T03:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005396172472987 + ], + [ + "2025-11-25T03:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999730940969751 + ], + [ + "2025-11-25T03:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997316476315483 + ], + [ + "2025-11-25T03:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000765005875025 + ], + [ + "2025-11-25T03:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006571202498313 + ], + [ + "2025-11-25T03:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997524114315863 + ], + [ + "2025-11-25T03:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997865068589137 + ], + [ + "2025-11-25T03:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002197474505707 + ], + [ + "2025-11-25T03:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001159533364347 + ], + [ + "2025-11-25T02:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998958172084063 + ], + [ + "2025-11-25T02:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998504140937391 + ], + [ + "2025-11-25T02:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999702690931724 + ], + [ + "2025-11-25T02:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997828003440016 + ], + [ + "2025-11-25T02:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998123745304572 + ], + [ + "2025-11-25T02:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998288214912373 + ], + [ + "2025-11-25T02:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999805549577991 + ], + [ + "2025-11-25T02:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998292756905325 + ], + [ + "2025-11-25T02:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999777407600883 + ], + [ + "2025-11-25T02:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998202844563009 + ], + [ + "2025-11-25T02:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997889555517199 + ], + [ + "2025-11-25T02:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998288238576005 + ], + [ + "2025-11-25T02:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998124217914061 + ], + [ + "2025-11-25T02:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998315808974336 + ], + [ + "2025-11-25T02:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998420614022057 + ], + [ + "2025-11-25T02:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997569517745589 + ], + [ + "2025-11-25T02:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012127422344212 + ], + [ + "2025-11-25T02:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997507223357013 + ], + [ + "2025-11-25T02:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997672015600358 + ], + [ + "2025-11-25T02:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99981664060273 + ], + [ + "2025-11-25T02:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012585727592953 + ], + [ + "2025-11-25T02:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996938445316056 + ], + [ + "2025-11-25T02:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999860423149419 + ], + [ + "2025-11-25T02:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997733014869201 + ], + [ + "2025-11-25T02:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004074195710604 + ], + [ + "2025-11-25T02:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997902096166185 + ], + [ + "2025-11-25T02:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004999021522683 + ], + [ + "2025-11-25T02:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996956664656128 + ], + [ + "2025-11-25T02:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000870803108405 + ], + [ + "2025-11-25T02:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998879235370624 + ], + [ + "2025-11-25T01:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997907833508526 + ], + [ + "2025-11-25T01:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997757113453217 + ], + [ + "2025-11-25T01:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996892125946018 + ], + [ + "2025-11-25T01:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000297842177249 + ], + [ + "2025-11-25T01:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999383595203599 + ], + [ + "2025-11-25T01:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998620099260775 + ], + [ + "2025-11-25T01:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997668475228488 + ], + [ + "2025-11-25T01:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996826631664992 + ], + [ + "2025-11-25T01:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006295055685026 + ], + [ + "2025-11-25T01:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002242335515095 + ], + [ + "2025-11-25T01:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998303994012582 + ], + [ + "2025-11-25T01:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997741454770352 + ], + [ + "2025-11-25T01:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996975901804193 + ], + [ + "2025-11-25T01:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997485792992096 + ], + [ + "2025-11-25T01:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997731300849305 + ], + [ + "2025-11-25T01:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997316345078016 + ], + [ + "2025-11-25T01:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996575300175901 + ], + [ + "2025-11-25T01:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999800512602948 + ], + [ + "2025-11-25T01:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998231343572225 + ], + [ + "2025-11-25T01:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997669152496887 + ], + [ + "2025-11-25T01:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002258936446544 + ], + [ + "2025-11-25T01:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998653313596826 + ], + [ + "2025-11-25T01:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998481625556015 + ], + [ + "2025-11-25T01:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998472241512868 + ], + [ + "2025-11-25T01:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998248948176998 + ], + [ + "2025-11-25T01:10:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997331094460805 + ], + [ + "2025-11-25T01:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99981178393896 + ], + [ + "2025-11-25T01:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997929881776643 + ], + [ + "2025-11-25T01:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998193231007878 + ], + [ + "2025-11-25T01:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997758944208843 + ], + [ + "2025-11-25T00:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998529089850573 + ], + [ + "2025-11-25T00:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998766533099487 + ], + [ + "2025-11-25T00:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998604053939159 + ], + [ + "2025-11-25T00:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998225629810131 + ], + [ + "2025-11-25T00:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998844845273691 + ], + [ + "2025-11-25T00:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997844173191583 + ], + [ + "2025-11-25T00:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998431096144001 + ], + [ + "2025-11-25T00:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998126567851991 + ], + [ + "2025-11-25T00:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998520542970188 + ], + [ + "2025-11-25T00:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999468945557937 + ], + [ + "2025-11-25T00:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998164878316389 + ], + [ + "2025-11-25T00:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997609021292004 + ], + [ + "2025-11-25T00:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99994637370298 + ], + [ + "2025-11-25T00:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000545156375585 + ], + [ + "2025-11-25T00:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998492523542697 + ], + [ + "2025-11-25T00:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997548896701074 + ], + [ + "2025-11-25T00:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003527983715597 + ], + [ + "2025-11-25T00:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008036833940979 + ], + [ + "2025-11-25T00:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000020223032355 + ], + [ + "2025-11-25T00:22:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996993691810718 + ], + [ + "2025-11-25T00:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997597025244418 + ], + [ + "2025-11-25T00:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99985846292348 + ], + [ + "2025-11-25T00:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997478159073802 + ], + [ + "2025-11-25T00:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997632903502918 + ], + [ + "2025-11-25T00:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997880895994082 + ], + [ + "2025-11-25T00:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002194229001127 + ], + [ + "2025-11-25T00:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998123510927671 + ], + [ + "2025-11-25T00:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997280031042762 + ], + [ + "2025-11-25T00:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000035501698424 + ], + [ + "2025-11-25T00:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006631013107907 + ], + [ + "2025-11-24T23:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999003120252966 + ], + [ + "2025-11-24T23:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997718259817189 + ], + [ + "2025-11-24T23:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996763358856926 + ], + [ + "2025-11-24T23:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997064928323484 + ], + [ + "2025-11-24T23:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996907608544406 + ], + [ + "2025-11-24T23:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997083031766208 + ], + [ + "2025-11-24T23:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997189241749952 + ], + [ + "2025-11-24T23:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996878474928492 + ], + [ + "2025-11-24T23:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997629625322771 + ], + [ + "2025-11-24T23:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997009901171348 + ], + [ + "2025-11-24T23:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997615945874126 + ], + [ + "2025-11-24T23:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997627682133654 + ], + [ + "2025-11-24T23:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997171110068892 + ], + [ + "2025-11-24T23:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999759162848746 + ], + [ + "2025-11-24T23:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996375452866083 + ], + [ + "2025-11-24T23:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997059325187019 + ], + [ + "2025-11-24T23:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999711788902636 + ], + [ + "2025-11-24T23:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997230988242067 + ], + [ + "2025-11-24T23:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99969350043931 + ], + [ + "2025-11-24T23:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996365197219423 + ], + [ + "2025-11-24T23:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997752382798157 + ], + [ + "2025-11-24T23:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996160379138328 + ], + [ + "2025-11-24T23:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996662133128308 + ], + [ + "2025-11-24T23:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996630985067517 + ], + [ + "2025-11-24T23:11:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996435999461822 + ], + [ + "2025-11-24T23:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996692966793406 + ], + [ + "2025-11-24T23:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996609333633009 + ], + [ + "2025-11-24T23:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996757349144674 + ], + [ + "2025-11-24T23:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996688264580759 + ], + [ + "2025-11-24T23:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996497485729577 + ], + [ + "2025-11-24T22:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997208489026581 + ], + [ + "2025-11-24T22:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997568928367778 + ], + [ + "2025-11-24T22:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997146447294347 + ], + [ + "2025-11-24T22:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996962132757453 + ], + [ + "2025-11-24T22:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997440731983092 + ], + [ + "2025-11-24T22:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997491318903812 + ], + [ + "2025-11-24T22:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996975425856318 + ], + [ + "2025-11-24T22:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997775587901938 + ], + [ + "2025-11-24T22:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999790095060674 + ], + [ + "2025-11-24T22:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999759444839689 + ], + [ + "2025-11-24T22:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996712869628156 + ], + [ + "2025-11-24T22:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999652151782347 + ], + [ + "2025-11-24T22:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997854767594816 + ], + [ + "2025-11-24T22:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999738230970859 + ], + [ + "2025-11-24T22:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999709341465187 + ], + [ + "2025-11-24T22:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997289571639857 + ], + [ + "2025-11-24T22:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998563513982714 + ], + [ + "2025-11-24T22:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999836041756057 + ], + [ + "2025-11-24T22:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999700865949215 + ], + [ + "2025-11-24T22:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997899117738775 + ], + [ + "2025-11-24T22:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998709429786827 + ], + [ + "2025-11-24T22:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999776798251224 + ], + [ + "2025-11-24T22:15:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999699994766623 + ], + [ + "2025-11-24T22:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996153489345726 + ], + [ + "2025-11-24T22:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998821334956745 + ], + [ + "2025-11-24T22:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999667956202331 + ], + [ + "2025-11-24T22:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996827815229955 + ], + [ + "2025-11-24T22:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996979947311494 + ], + [ + "2025-11-24T22:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997124274593818 + ], + [ + "2025-11-24T22:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997889487418589 + ], + [ + "2025-11-24T21:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996307867362114 + ], + [ + "2025-11-24T21:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997073003189867 + ], + [ + "2025-11-24T21:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997706981984201 + ], + [ + "2025-11-24T21:54:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997694883883237 + ], + [ + "2025-11-24T21:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997163044003868 + ], + [ + "2025-11-24T21:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998752173093101 + ], + [ + "2025-11-24T21:48:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996730882013569 + ], + [ + "2025-11-24T21:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997866727033785 + ], + [ + "2025-11-24T21:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999803493813276 + ], + [ + "2025-11-24T21:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997275238415734 + ], + [ + "2025-11-24T21:40:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997976315197833 + ], + [ + "2025-11-24T21:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997472639775618 + ], + [ + "2025-11-24T21:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997378219617727 + ], + [ + "2025-11-24T21:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997051548725566 + ], + [ + "2025-11-24T21:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996680886261721 + ], + [ + "2025-11-24T21:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996144115780253 + ], + [ + "2025-11-24T21:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996027740639821 + ], + [ + "2025-11-24T21:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996739230765634 + ], + [ + "2025-11-24T21:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996929291586201 + ], + [ + "2025-11-24T21:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999601393919439 + ], + [ + "2025-11-24T21:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997453094365828 + ], + [ + "2025-11-24T21:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996290731376426 + ], + [ + "2025-11-24T21:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995136835542417 + ], + [ + "2025-11-24T21:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997028804725188 + ], + [ + "2025-11-24T21:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995864669278725 + ], + [ + "2025-11-24T21:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996899131959786 + ], + [ + "2025-11-24T21:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995505598852755 + ], + [ + "2025-11-24T21:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996874022501808 + ], + [ + "2025-11-24T21:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996954862685996 + ], + [ + "2025-11-24T21:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996654249887755 + ], + [ + "2025-11-24T20:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996354254673392 + ], + [ + "2025-11-24T20:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996375761322271 + ], + [ + "2025-11-24T20:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997406386954036 + ], + [ + "2025-11-24T20:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997408562674245 + ], + [ + "2025-11-24T20:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997504419099711 + ], + [ + "2025-11-24T20:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996414920548692 + ], + [ + "2025-11-24T20:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999681802800454 + ], + [ + "2025-11-24T20:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000023139425052 + ], + [ + "2025-11-24T20:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004808457465293 + ], + [ + "2025-11-24T20:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996434138994191 + ], + [ + "2025-11-24T20:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996709356351485 + ], + [ + "2025-11-24T20:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996775240689153 + ], + [ + "2025-11-24T20:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996900283135495 + ], + [ + "2025-11-24T20:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996635910684937 + ], + [ + "2025-11-24T20:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996911975988715 + ], + [ + "2025-11-24T20:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996121699753155 + ], + [ + "2025-11-24T20:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997284250910853 + ], + [ + "2025-11-24T20:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996614056170724 + ], + [ + "2025-11-24T20:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996453254803394 + ], + [ + "2025-11-24T20:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996646029716071 + ], + [ + "2025-11-24T20:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000758461743795 + ], + [ + "2025-11-24T20:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996889376043622 + ], + [ + "2025-11-24T20:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997282852199255 + ], + [ + "2025-11-24T20:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999742343652301 + ], + [ + "2025-11-24T20:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996887305322179 + ], + [ + "2025-11-24T20:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997599052561437 + ], + [ + "2025-11-24T20:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997127609759738 + ], + [ + "2025-11-24T20:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997454815747859 + ], + [ + "2025-11-24T20:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997155988783019 + ], + [ + "2025-11-24T20:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999756482601901 + ], + [ + "2025-11-24T19:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996693751844015 + ], + [ + "2025-11-24T19:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997160549987253 + ], + [ + "2025-11-24T19:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997598335767502 + ], + [ + "2025-11-24T19:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996606541355852 + ], + [ + "2025-11-24T19:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999721265707463 + ], + [ + "2025-11-24T19:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997511681108506 + ], + [ + "2025-11-24T19:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997675103315893 + ], + [ + "2025-11-24T19:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997191720897411 + ], + [ + "2025-11-24T19:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004138596668553 + ], + [ + "2025-11-24T19:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003100053703147 + ], + [ + "2025-11-24T19:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005383130809646 + ], + [ + "2025-11-24T19:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997114121509725 + ], + [ + "2025-11-24T19:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997498862606122 + ], + [ + "2025-11-24T19:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997352902192235 + ], + [ + "2025-11-24T19:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997143870015479 + ], + [ + "2025-11-24T19:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997394802083769 + ], + [ + "2025-11-24T19:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998090864438641 + ], + [ + "2025-11-24T19:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997203525626169 + ], + [ + "2025-11-24T19:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997115928031791 + ], + [ + "2025-11-24T19:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996236502135689 + ], + [ + "2025-11-24T19:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996048004169806 + ], + [ + "2025-11-24T19:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994838086500506 + ], + [ + "2025-11-24T19:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996039306547345 + ], + [ + "2025-11-24T19:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996319233195673 + ], + [ + "2025-11-24T19:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996243240201027 + ], + [ + "2025-11-24T19:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996176043112949 + ], + [ + "2025-11-24T19:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996245866775735 + ], + [ + "2025-11-24T19:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997089862494368 + ], + [ + "2025-11-24T19:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995001143953619 + ], + [ + "2025-11-24T19:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995366883890868 + ], + [ + "2025-11-24T18:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999542956648914 + ], + [ + "2025-11-24T18:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996194123367643 + ], + [ + "2025-11-24T18:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996124853954896 + ], + [ + "2025-11-24T18:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996350712943358 + ], + [ + "2025-11-24T18:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995587721455054 + ], + [ + "2025-11-24T18:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997260496548246 + ], + [ + "2025-11-24T18:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997168692455376 + ], + [ + "2025-11-24T18:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995697316617368 + ], + [ + "2025-11-24T18:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999731890535294 + ], + [ + "2025-11-24T18:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003741581550578 + ], + [ + "2025-11-24T18:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997134712223374 + ], + [ + "2025-11-24T18:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996982805180572 + ], + [ + "2025-11-24T18:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999711719282266 + ], + [ + "2025-11-24T18:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997410796480392 + ], + [ + "2025-11-24T18:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997139665349352 + ], + [ + "2025-11-24T18:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997113010511306 + ], + [ + "2025-11-24T18:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997115399582119 + ], + [ + "2025-11-24T18:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999577427943348 + ], + [ + "2025-11-24T18:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996819081989934 + ], + [ + "2025-11-24T18:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996841558561386 + ], + [ + "2025-11-24T18:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996129956959405 + ], + [ + "2025-11-24T18:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996244864003844 + ], + [ + "2025-11-24T18:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996638473631282 + ], + [ + "2025-11-24T18:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996723145718018 + ], + [ + "2025-11-24T18:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996559156883007 + ], + [ + "2025-11-24T18:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999664734761893 + ], + [ + "2025-11-24T18:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996847141098407 + ], + [ + "2025-11-24T18:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999677160942809 + ], + [ + "2025-11-24T18:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996587116194665 + ], + [ + "2025-11-24T18:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996011237620125 + ], + [ + "2025-11-24T17:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998565281966344 + ], + [ + "2025-11-24T17:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997246338679006 + ], + [ + "2025-11-24T17:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997636564739518 + ], + [ + "2025-11-24T17:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997123013665195 + ], + [ + "2025-11-24T17:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997203413230513 + ], + [ + "2025-11-24T17:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997836336316911 + ], + [ + "2025-11-24T17:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998644357371907 + ], + [ + "2025-11-24T17:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997256952742963 + ], + [ + "2025-11-24T17:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996956954948668 + ], + [ + "2025-11-24T17:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999746608126818 + ], + [ + "2025-11-24T17:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998202309461306 + ], + [ + "2025-11-24T17:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999664739046024 + ], + [ + "2025-11-24T17:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997777194079608 + ], + [ + "2025-11-24T17:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997498798969076 + ], + [ + "2025-11-24T17:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997591079976786 + ], + [ + "2025-11-24T17:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997309725012486 + ], + [ + "2025-11-24T17:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996941263152105 + ], + [ + "2025-11-24T17:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997638670818152 + ], + [ + "2025-11-24T17:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997513072535044 + ], + [ + "2025-11-24T17:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999759021705809 + ], + [ + "2025-11-24T17:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996743397199644 + ], + [ + "2025-11-24T17:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997079195930939 + ], + [ + "2025-11-24T17:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999763478326249 + ], + [ + "2025-11-24T17:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999743773347173 + ], + [ + "2025-11-24T17:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998099952916257 + ], + [ + "2025-11-24T17:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998034240615771 + ], + [ + "2025-11-24T17:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998203312652586 + ], + [ + "2025-11-24T17:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00004214163827 + ], + [ + "2025-11-24T17:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999677488129148 + ], + [ + "2025-11-24T17:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999414701146441 + ], + [ + "2025-11-24T16:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998502805937298 + ], + [ + "2025-11-24T16:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997541300711973 + ], + [ + "2025-11-24T16:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999304324884942 + ], + [ + "2025-11-24T16:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998765998455328 + ], + [ + "2025-11-24T16:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998208435314343 + ], + [ + "2025-11-24T16:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997336866662815 + ], + [ + "2025-11-24T16:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999472510593522 + ], + [ + "2025-11-24T16:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997830907276037 + ], + [ + "2025-11-24T16:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998515967701769 + ], + [ + "2025-11-24T16:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998017678248361 + ], + [ + "2025-11-24T16:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003016543914593 + ], + [ + "2025-11-24T16:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012466076149844 + ], + [ + "2025-11-24T16:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997514546895899 + ], + [ + "2025-11-24T16:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999243217575611 + ], + [ + "2025-11-24T16:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998252683226134 + ], + [ + "2025-11-24T16:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998588346898859 + ], + [ + "2025-11-24T16:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997177701887897 + ], + [ + "2025-11-24T16:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998205779976485 + ], + [ + "2025-11-24T16:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998646828425839 + ], + [ + "2025-11-24T16:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998041594326349 + ], + [ + "2025-11-24T16:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996108962562695 + ], + [ + "2025-11-24T16:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997989886802167 + ], + [ + "2025-11-24T16:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000555220132274 + ], + [ + "2025-11-24T16:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998513947348632 + ], + [ + "2025-11-24T16:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997557504178415 + ], + [ + "2025-11-24T16:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999327993514006 + ], + [ + "2025-11-24T16:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002848067592602 + ], + [ + "2025-11-24T16:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999428617384495 + ], + [ + "2025-11-24T16:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997573627936304 + ], + [ + "2025-11-24T16:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996692683964423 + ], + [ + "2025-11-24T15:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998263917001072 + ], + [ + "2025-11-24T15:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998179313410307 + ], + [ + "2025-11-24T15:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996762810197987 + ], + [ + "2025-11-24T15:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997295855179302 + ], + [ + "2025-11-24T15:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998202947003588 + ], + [ + "2025-11-24T15:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998841210029129 + ], + [ + "2025-11-24T15:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997235809835262 + ], + [ + "2025-11-24T15:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999720405741602 + ], + [ + "2025-11-24T15:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999001070904213 + ], + [ + "2025-11-24T15:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000143526723885 + ], + [ + "2025-11-24T15:40:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999612977936392 + ], + [ + "2025-11-24T15:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997668507315136 + ], + [ + "2025-11-24T15:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999774765100382 + ], + [ + "2025-11-24T15:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002870809487012 + ], + [ + "2025-11-24T15:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003182793764847 + ], + [ + "2025-11-24T15:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999800577360581 + ], + [ + "2025-11-24T15:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003648161386165 + ], + [ + "2025-11-24T15:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007018125279712 + ], + [ + "2025-11-24T15:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0019162519242768 + ], + [ + "2025-11-24T15:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000821416152654 + ], + [ + "2025-11-24T15:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000260822583793 + ], + [ + "2025-11-24T15:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000275137219603 + ], + [ + "2025-11-24T15:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0017142861104429 + ], + [ + "2025-11-24T15:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002150364936768 + ], + [ + "2025-11-24T15:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000255141382387 + ], + [ + "2025-11-24T15:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997101194140923 + ], + [ + "2025-11-24T15:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002593298159483 + ], + [ + "2025-11-24T15:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005535573960533 + ], + [ + "2025-11-24T15:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000440782688402 + ], + [ + "2025-11-24T15:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999033907320946 + ], + [ + "2025-11-24T14:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007450996005731 + ], + [ + "2025-11-24T14:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003062767588253 + ], + [ + "2025-11-24T14:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010920455218073 + ], + [ + "2025-11-24T14:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000468504154887 + ], + [ + "2025-11-24T14:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009699860949952 + ], + [ + "2025-11-24T14:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997912480834152 + ], + [ + "2025-11-24T14:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009062513510778 + ], + [ + "2025-11-24T14:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006659908726112 + ], + [ + "2025-11-24T14:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997966983034261 + ], + [ + "2025-11-24T14:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997992106187238 + ], + [ + "2025-11-24T14:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998459517751694 + ], + [ + "2025-11-24T14:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998200832184333 + ], + [ + "2025-11-24T14:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999777653100801 + ], + [ + "2025-11-24T14:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998744561304728 + ], + [ + "2025-11-24T14:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998196158522012 + ], + [ + "2025-11-24T14:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997914841656454 + ], + [ + "2025-11-24T14:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998339243991885 + ], + [ + "2025-11-24T14:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999813278821053 + ], + [ + "2025-11-24T14:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997976558536721 + ], + [ + "2025-11-24T14:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999835043188132 + ], + [ + "2025-11-24T14:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998785206946139 + ], + [ + "2025-11-24T14:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998124686886642 + ], + [ + "2025-11-24T14:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997868871375716 + ], + [ + "2025-11-24T14:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997569261521295 + ], + [ + "2025-11-24T14:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997361656306807 + ], + [ + "2025-11-24T14:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997616548663679 + ], + [ + "2025-11-24T14:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998362985899616 + ], + [ + "2025-11-24T14:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997738212626782 + ], + [ + "2025-11-24T14:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997199151243231 + ], + [ + "2025-11-24T13:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998149209772741 + ], + [ + "2025-11-24T13:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997699418374086 + ], + [ + "2025-11-24T13:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998118392469992 + ], + [ + "2025-11-24T13:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997682863076421 + ], + [ + "2025-11-24T13:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998103355120513 + ], + [ + "2025-11-24T13:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997559640707672 + ], + [ + "2025-11-24T13:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998277938853812 + ], + [ + "2025-11-24T13:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998319440364529 + ], + [ + "2025-11-24T13:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998468406942423 + ], + [ + "2025-11-24T13:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996437334453651 + ], + [ + "2025-11-24T13:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997993551359147 + ], + [ + "2025-11-24T13:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000033785009818 + ], + [ + "2025-11-24T13:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998285094873851 + ], + [ + "2025-11-24T13:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999770309591307 + ], + [ + "2025-11-24T13:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998475673173296 + ], + [ + "2025-11-24T13:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003608570148812 + ], + [ + "2025-11-24T13:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997073077359301 + ], + [ + "2025-11-24T13:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016391075556652 + ], + [ + "2025-11-24T13:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997053364353508 + ], + [ + "2025-11-24T13:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997951276938967 + ], + [ + "2025-11-24T13:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997329679526685 + ], + [ + "2025-11-24T13:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996704434544519 + ], + [ + "2025-11-24T13:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997689068600146 + ], + [ + "2025-11-24T13:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999682719156051 + ], + [ + "2025-11-24T13:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997260300101589 + ], + [ + "2025-11-24T13:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997182315731291 + ], + [ + "2025-11-24T13:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998767902359199 + ], + [ + "2025-11-24T13:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997731208219157 + ], + [ + "2025-11-24T13:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996511230141126 + ], + [ + "2025-11-24T13:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998766194954318 + ], + [ + "2025-11-24T12:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999921992880544 + ], + [ + "2025-11-24T12:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999441145350053 + ], + [ + "2025-11-24T12:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998653382203865 + ], + [ + "2025-11-24T12:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999873413202622 + ], + [ + "2025-11-24T12:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999772285674037 + ], + [ + "2025-11-24T12:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000437071762708 + ], + [ + "2025-11-24T12:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998472571386473 + ], + [ + "2025-11-24T12:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998552935839665 + ], + [ + "2025-11-24T12:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007994409611645 + ], + [ + "2025-11-24T12:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998437263450438 + ], + [ + "2025-11-24T12:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998610057413493 + ], + [ + "2025-11-24T12:38:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000131724806037 + ], + [ + "2025-11-24T12:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014370659459255 + ], + [ + "2025-11-24T12:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001423233759936 + ], + [ + "2025-11-24T12:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999035584619088 + ], + [ + "2025-11-24T12:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997230078639379 + ], + [ + "2025-11-24T12:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997939671767618 + ], + [ + "2025-11-24T12:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997466597152662 + ], + [ + "2025-11-24T12:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999957308335926 + ], + [ + "2025-11-24T12:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997567163700225 + ], + [ + "2025-11-24T12:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996693817069018 + ], + [ + "2025-11-24T12:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996742290015774 + ], + [ + "2025-11-24T12:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997071360341325 + ], + [ + "2025-11-24T12:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998611225566775 + ], + [ + "2025-11-24T12:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997125474233701 + ], + [ + "2025-11-24T12:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996758478976334 + ], + [ + "2025-11-24T12:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998039883115585 + ], + [ + "2025-11-24T12:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998596300773791 + ], + [ + "2025-11-24T12:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998327541241901 + ], + [ + "2025-11-24T12:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996833164257473 + ], + [ + "2025-11-24T11:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998086747531374 + ], + [ + "2025-11-24T11:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997512061670629 + ], + [ + "2025-11-24T11:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998234759206678 + ], + [ + "2025-11-24T11:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997118456416884 + ], + [ + "2025-11-24T11:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997732165407873 + ], + [ + "2025-11-24T11:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996334235223182 + ], + [ + "2025-11-24T11:48:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998747350084359 + ], + [ + "2025-11-24T11:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999637024499314 + ], + [ + "2025-11-24T11:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998408877060435 + ], + [ + "2025-11-24T11:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997202292784702 + ], + [ + "2025-11-24T11:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999563252813499 + ], + [ + "2025-11-24T11:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000379822064505 + ], + [ + "2025-11-24T11:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000898817701587 + ], + [ + "2025-11-24T11:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003274294106645 + ], + [ + "2025-11-24T11:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000030863088991 + ], + [ + "2025-11-24T11:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002604724536583 + ], + [ + "2025-11-24T11:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002690686349094 + ], + [ + "2025-11-24T11:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003563461163991 + ], + [ + "2025-11-24T11:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000314213854098 + ], + [ + "2025-11-24T11:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997695596721125 + ], + [ + "2025-11-24T11:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000509154052664 + ], + [ + "2025-11-24T11:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996782647439321 + ], + [ + "2025-11-24T11:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997247649667707 + ], + [ + "2025-11-24T11:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001530432864143 + ], + [ + "2025-11-24T11:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000590785384899 + ], + [ + "2025-11-24T11:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996767680832397 + ], + [ + "2025-11-24T11:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997405341673337 + ], + [ + "2025-11-24T11:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002565309563713 + ], + [ + "2025-11-24T11:04:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006500756130035 + ], + [ + "2025-11-24T11:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997096081961514 + ], + [ + "2025-11-24T10:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999965883210342 + ], + [ + "2025-11-24T10:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998012636218696 + ], + [ + "2025-11-24T10:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999908065593665 + ], + [ + "2025-11-24T10:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997422881068195 + ], + [ + "2025-11-24T10:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001001830420693 + ], + [ + "2025-11-24T10:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998725326610728 + ], + [ + "2025-11-24T10:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000203579674639 + ], + [ + "2025-11-24T10:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002492229360254 + ], + [ + "2025-11-24T10:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005565695726817 + ], + [ + "2025-11-24T10:42:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999623218172451 + ], + [ + "2025-11-24T10:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999236771280983 + ], + [ + "2025-11-24T10:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007167287196135 + ], + [ + "2025-11-24T10:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005843583929483 + ], + [ + "2025-11-24T10:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002023282906538 + ], + [ + "2025-11-24T10:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012711523200153 + ], + [ + "2025-11-24T10:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005233545776946 + ], + [ + "2025-11-24T10:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004838840135843 + ], + [ + "2025-11-24T10:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996761902130452 + ], + [ + "2025-11-24T10:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998061553228427 + ], + [ + "2025-11-24T10:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000459353056612 + ], + [ + "2025-11-24T10:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997906767149377 + ], + [ + "2025-11-24T10:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997425321294384 + ], + [ + "2025-11-24T10:15:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998070592160061 + ], + [ + "2025-11-24T10:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000705382513952 + ], + [ + "2025-11-24T10:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997916424651295 + ], + [ + "2025-11-24T10:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997124543394119 + ], + [ + "2025-11-24T10:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997873283782264 + ], + [ + "2025-11-24T10:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996932589860567 + ], + [ + "2025-11-24T10:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998238242486667 + ], + [ + "2025-11-24T10:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998421207696112 + ], + [ + "2025-11-24T09:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997427173946616 + ], + [ + "2025-11-24T09:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997807791782133 + ], + [ + "2025-11-24T09:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997585800907947 + ], + [ + "2025-11-24T09:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000078834965478 + ], + [ + "2025-11-24T09:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998307940278205 + ], + [ + "2025-11-24T09:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997529498613199 + ], + [ + "2025-11-24T09:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997174435331134 + ], + [ + "2025-11-24T09:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005519345379104 + ], + [ + "2025-11-24T09:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997932960040292 + ], + [ + "2025-11-24T09:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998482327726823 + ], + [ + "2025-11-24T09:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997865884271424 + ], + [ + "2025-11-24T09:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999916925131713 + ], + [ + "2025-11-24T09:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998141493596303 + ], + [ + "2025-11-24T09:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001789134647272 + ], + [ + "2025-11-24T09:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000210598972028 + ], + [ + "2025-11-24T09:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999046457228977 + ], + [ + "2025-11-24T09:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999641033158682 + ], + [ + "2025-11-24T09:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016609336867806 + ], + [ + "2025-11-24T09:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010527364112267 + ], + [ + "2025-11-24T09:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999458568132683 + ], + [ + "2025-11-24T09:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001770097462648 + ], + [ + "2025-11-24T09:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016107049721248 + ], + [ + "2025-11-24T09:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998527641951177 + ], + [ + "2025-11-24T09:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000108998957266 + ], + [ + "2025-11-24T09:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002532921882121 + ], + [ + "2025-11-24T09:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014743669705672 + ], + [ + "2025-11-24T09:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000223581401466 + ], + [ + "2025-11-24T09:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005750890805862 + ], + [ + "2025-11-24T09:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004213736105152 + ], + [ + "2025-11-24T09:01:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013214435579 + ], + [ + "2025-11-24T08:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003124945492845 + ], + [ + "2025-11-24T08:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014609413885214 + ], + [ + "2025-11-24T08:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010038542220485 + ], + [ + "2025-11-24T08:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007146003483505 + ], + [ + "2025-11-24T08:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005573521525852 + ], + [ + "2025-11-24T08:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00064090624419 + ], + [ + "2025-11-24T08:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009610488350822 + ], + [ + "2025-11-24T08:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998894282060007 + ], + [ + "2025-11-24T08:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998883762651213 + ], + [ + "2025-11-24T08:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004716861640743 + ], + [ + "2025-11-24T08:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998744026773533 + ], + [ + "2025-11-24T08:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000010992790209 + ], + [ + "2025-11-24T08:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999210700597044 + ], + [ + "2025-11-24T08:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004493698348065 + ], + [ + "2025-11-24T08:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999110261840115 + ], + [ + "2025-11-24T08:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999175859966675 + ], + [ + "2025-11-24T08:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003223883569246 + ], + [ + "2025-11-24T08:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999605961758107 + ], + [ + "2025-11-24T08:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997615755102264 + ], + [ + "2025-11-24T08:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002149017899689 + ], + [ + "2025-11-24T08:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994806633667196 + ], + [ + "2025-11-24T08:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999932912770382 + ], + [ + "2025-11-24T08:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998297834270072 + ], + [ + "2025-11-24T08:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003479646424378 + ], + [ + "2025-11-24T08:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996155872216596 + ], + [ + "2025-11-24T08:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996859349916503 + ], + [ + "2025-11-24T08:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999795290258233 + ], + [ + "2025-11-24T08:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998815075798528 + ], + [ + "2025-11-24T08:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997239111279621 + ], + [ + "2025-11-24T08:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997824313149445 + ], + [ + "2025-11-24T07:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000154235390722 + ], + [ + "2025-11-24T07:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999564236385371 + ], + [ + "2025-11-24T07:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998296235459336 + ], + [ + "2025-11-24T07:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001661791624117 + ], + [ + "2025-11-24T07:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002965702592752 + ], + [ + "2025-11-24T07:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000095041863335 + ], + [ + "2025-11-24T07:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998821295453815 + ], + [ + "2025-11-24T07:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004311221436515 + ], + [ + "2025-11-24T07:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004683314302627 + ], + [ + "2025-11-24T07:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007615806621237 + ], + [ + "2025-11-24T07:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005423551325212 + ], + [ + "2025-11-24T07:37:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996849934330788 + ], + [ + "2025-11-24T07:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996496711233455 + ], + [ + "2025-11-24T07:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008611621043848 + ], + [ + "2025-11-24T07:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000229077501617 + ], + [ + "2025-11-24T07:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000242768490008 + ], + [ + "2025-11-24T07:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997065170955198 + ], + [ + "2025-11-24T07:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000085087015393 + ], + [ + "2025-11-24T07:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998490770954896 + ], + [ + "2025-11-24T07:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000367070887052 + ], + [ + "2025-11-24T07:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995875290932719 + ], + [ + "2025-11-24T07:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00041505852004 + ], + [ + "2025-11-24T07:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997926425844472 + ], + [ + "2025-11-24T07:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004812687609737 + ], + [ + "2025-11-24T07:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996181249708667 + ], + [ + "2025-11-24T07:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005756900161227 + ], + [ + "2025-11-24T07:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996967714352555 + ], + [ + "2025-11-24T07:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006461755681038 + ], + [ + "2025-11-24T07:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995503765854061 + ], + [ + "2025-11-24T07:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002160002318716 + ], + [ + "2025-11-24T06:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996768956520341 + ], + [ + "2025-11-24T06:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006171675884823 + ], + [ + "2025-11-24T06:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996267938662325 + ], + [ + "2025-11-24T06:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999687938347641 + ], + [ + "2025-11-24T06:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996103894543086 + ], + [ + "2025-11-24T06:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002578206714534 + ], + [ + "2025-11-24T06:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997367889861748 + ], + [ + "2025-11-24T06:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000427649482388 + ], + [ + "2025-11-24T06:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996804188271048 + ], + [ + "2025-11-24T06:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998252478906474 + ], + [ + "2025-11-24T06:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997315983542113 + ], + [ + "2025-11-24T06:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999646168261319 + ], + [ + "2025-11-24T06:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998257200135644 + ], + [ + "2025-11-24T06:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998366683258992 + ], + [ + "2025-11-24T06:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997983403262679 + ], + [ + "2025-11-24T06:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002253758173139 + ], + [ + "2025-11-24T06:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999759805310997 + ], + [ + "2025-11-24T06:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997702844531365 + ], + [ + "2025-11-24T06:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999811663523878 + ], + [ + "2025-11-24T06:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000515946462641 + ], + [ + "2025-11-24T06:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998767983275306 + ], + [ + "2025-11-24T06:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997290750848619 + ], + [ + "2025-11-24T06:16:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997597311806167 + ], + [ + "2025-11-24T06:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008543762109892 + ], + [ + "2025-11-24T06:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999050372475629 + ], + [ + "2025-11-24T06:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997506089361843 + ], + [ + "2025-11-24T06:07:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997167260886372 + ], + [ + "2025-11-24T06:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997016322379113 + ], + [ + "2025-11-24T06:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998887041836393 + ], + [ + "2025-11-24T06:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998663824967984 + ], + [ + "2025-11-24T05:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999817841493457 + ], + [ + "2025-11-24T05:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000524668667088 + ], + [ + "2025-11-24T05:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999390115502285 + ], + [ + "2025-11-24T05:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998431446628764 + ], + [ + "2025-11-24T05:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998962193867801 + ], + [ + "2025-11-24T05:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000564064950964 + ], + [ + "2025-11-24T05:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000002146900297 + ], + [ + "2025-11-24T05:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000074125459664 + ], + [ + "2025-11-24T05:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999768704389942 + ], + [ + "2025-11-24T05:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000665494028465 + ], + [ + "2025-11-24T05:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999715979340449 + ], + [ + "2025-11-24T05:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000239941105853 + ], + [ + "2025-11-24T05:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998174685148493 + ], + [ + "2025-11-24T05:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006717707997934 + ], + [ + "2025-11-24T05:31:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001911472033458 + ], + [ + "2025-11-24T05:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998511120889658 + ], + [ + "2025-11-24T05:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998470520334114 + ], + [ + "2025-11-24T05:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999536104210003 + ], + [ + "2025-11-24T05:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000150575669919 + ], + [ + "2025-11-24T05:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998790132925374 + ], + [ + "2025-11-24T05:19:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006002888478969 + ], + [ + "2025-11-24T05:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000635758193848 + ], + [ + "2025-11-24T05:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999806325701417 + ], + [ + "2025-11-24T05:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001226258264189 + ], + [ + "2025-11-24T05:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004453248252563 + ], + [ + "2025-11-24T05:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001597642590332 + ], + [ + "2025-11-24T05:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000852108652931 + ], + [ + "2025-11-24T05:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000894404851857 + ], + [ + "2025-11-24T05:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005010449324907 + ], + [ + "2025-11-24T05:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006442922495042 + ], + [ + "2025-11-24T04:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001007968216478 + ], + [ + "2025-11-24T04:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000323431168532 + ], + [ + "2025-11-24T04:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001229421396887 + ], + [ + "2025-11-24T04:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998687874154143 + ], + [ + "2025-11-24T04:51:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001097277611266 + ], + [ + "2025-11-24T04:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000745477563804 + ], + [ + "2025-11-24T04:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003093681489212 + ], + [ + "2025-11-24T04:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996614283060631 + ], + [ + "2025-11-24T04:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001897320596698 + ], + [ + "2025-11-24T04:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001756488737805 + ], + [ + "2025-11-24T04:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006071443919171 + ], + [ + "2025-11-24T04:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997066972494215 + ], + [ + "2025-11-24T04:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003884586134786 + ], + [ + "2025-11-24T04:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001208545461004 + ], + [ + "2025-11-24T04:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998116174883409 + ], + [ + "2025-11-24T04:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998313610579858 + ], + [ + "2025-11-24T04:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999302613701702 + ], + [ + "2025-11-24T04:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001550288953203 + ], + [ + "2025-11-24T04:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997990095673897 + ], + [ + "2025-11-24T04:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998506987978061 + ], + [ + "2025-11-24T04:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998909318493714 + ], + [ + "2025-11-24T04:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009765334559737 + ], + [ + "2025-11-24T04:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002212403154338 + ], + [ + "2025-11-24T04:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998350788418706 + ], + [ + "2025-11-24T04:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000487080143317 + ], + [ + "2025-11-24T04:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007972984579803 + ], + [ + "2025-11-24T04:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003046205802273 + ], + [ + "2025-11-24T04:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008170604107214 + ], + [ + "2025-11-24T04:03:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001058112364192 + ], + [ + "2025-11-24T04:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006065055893174 + ], + [ + "2025-11-24T03:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002550936602512 + ], + [ + "2025-11-24T03:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002881884729684 + ], + [ + "2025-11-24T03:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003688598035436 + ], + [ + "2025-11-24T03:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008085322922498 + ], + [ + "2025-11-24T03:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005971444726447 + ], + [ + "2025-11-24T03:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002108502440052 + ], + [ + "2025-11-24T03:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001984635042842 + ], + [ + "2025-11-24T03:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999922912011306 + ], + [ + "2025-11-24T03:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010574736389344 + ], + [ + "2025-11-24T03:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008769227462764 + ], + [ + "2025-11-24T03:40:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001096328229413 + ], + [ + "2025-11-24T03:37:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000524948817142 + ], + [ + "2025-11-24T03:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004138020215425 + ], + [ + "2025-11-24T03:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001344402135423 + ], + [ + "2025-11-24T03:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004546285515887 + ], + [ + "2025-11-24T03:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006038874158336 + ], + [ + "2025-11-24T03:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014090127009778 + ], + [ + "2025-11-24T03:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012446515980509 + ], + [ + "2025-11-24T03:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0015584311394066 + ], + [ + "2025-11-24T03:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008355775734539 + ], + [ + "2025-11-24T03:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001307892605568 + ], + [ + "2025-11-24T03:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014459043988524 + ], + [ + "2025-11-24T03:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001873527344747 + ], + [ + "2025-11-24T03:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007846865811088 + ], + [ + "2025-11-24T03:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014462205601091 + ], + [ + "2025-11-24T03:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013031643752017 + ], + [ + "2025-11-24T03:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001965635132834 + ], + [ + "2025-11-24T03:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000830209659346 + ], + [ + "2025-11-24T03:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011785519685412 + ], + [ + "2025-11-24T03:01:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009319990428047 + ], + [ + "2025-11-24T02:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00127353155282 + ], + [ + "2025-11-24T02:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000803094883938 + ], + [ + "2025-11-24T02:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006360948414892 + ], + [ + "2025-11-24T02:53:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009925568735958 + ], + [ + "2025-11-24T02:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000835200744271 + ], + [ + "2025-11-24T02:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007035381113116 + ], + [ + "2025-11-24T02:47:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008950744503884 + ], + [ + "2025-11-24T02:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006578691680243 + ], + [ + "2025-11-24T02:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000127065858309 + ], + [ + "2025-11-24T02:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005421101192675 + ], + [ + "2025-11-24T02:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000762656909643 + ], + [ + "2025-11-24T02:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000622162516017 + ], + [ + "2025-11-24T02:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000370467769615 + ], + [ + "2025-11-24T02:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000262804863866 + ], + [ + "2025-11-24T02:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000089735613967 + ], + [ + "2025-11-24T02:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001312514965427 + ], + [ + "2025-11-24T02:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000519525474891 + ], + [ + "2025-11-24T02:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999623674765705 + ], + [ + "2025-11-24T02:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003526124735806 + ], + [ + "2025-11-24T02:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012652864084965 + ], + [ + "2025-11-24T02:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001531667173482 + ], + [ + "2025-11-24T02:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002132796834546 + ], + [ + "2025-11-24T02:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000126949017742 + ], + [ + "2025-11-24T02:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000600715041692 + ], + [ + "2025-11-24T02:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000695233692059 + ], + [ + "2025-11-24T02:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004590242833158 + ], + [ + "2025-11-24T02:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000306330005038 + ], + [ + "2025-11-24T02:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999260043919298 + ], + [ + "2025-11-24T02:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008168224564349 + ], + [ + "2025-11-24T02:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000794479976147 + ], + [ + "2025-11-24T01:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000598199368016 + ], + [ + "2025-11-24T01:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000985989152298 + ], + [ + "2025-11-24T01:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999988338781677 + ], + [ + "2025-11-24T01:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001015585578577 + ], + [ + "2025-11-24T01:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001564843753352 + ], + [ + "2025-11-24T01:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003328406293732 + ], + [ + "2025-11-24T01:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000320499103543 + ], + [ + "2025-11-24T01:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999979322472045 + ], + [ + "2025-11-24T01:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999955434288761 + ], + [ + "2025-11-24T01:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007001594991682 + ], + [ + "2025-11-24T01:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001008639142754 + ], + [ + "2025-11-24T01:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000730698695959 + ], + [ + "2025-11-24T01:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999675433810737 + ], + [ + "2025-11-24T01:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000144392200511 + ], + [ + "2025-11-24T01:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999764219161318 + ], + [ + "2025-11-24T01:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002247759073286 + ], + [ + "2025-11-24T01:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000067759330315 + ], + [ + "2025-11-24T01:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010542842673642 + ], + [ + "2025-11-24T01:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999408641377397 + ], + [ + "2025-11-24T01:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999977824645465 + ], + [ + "2025-11-24T01:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000091828773132 + ], + [ + "2025-11-24T01:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000197544645206 + ], + [ + "2025-11-24T01:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999666646034451 + ], + [ + "2025-11-24T01:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999259995616676 + ], + [ + "2025-11-24T01:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000106054627018 + ], + [ + "2025-11-24T01:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000085722035982 + ], + [ + "2025-11-24T01:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999589422283706 + ], + [ + "2025-11-24T01:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999930278171483 + ], + [ + "2025-11-24T01:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000063493869059 + ], + [ + "2025-11-24T01:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002249649793244 + ], + [ + "2025-11-24T00:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999040342485569 + ], + [ + "2025-11-24T00:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999593648042039 + ], + [ + "2025-11-24T00:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000224861094213 + ], + [ + "2025-11-24T00:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011322639739026 + ], + [ + "2025-11-24T00:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004759195265915 + ], + [ + "2025-11-24T00:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999326756447848 + ], + [ + "2025-11-24T00:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000357324747173 + ], + [ + "2025-11-24T00:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001537070062088 + ], + [ + "2025-11-24T00:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007096811057243 + ], + [ + "2025-11-24T00:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997853758755323 + ], + [ + "2025-11-24T00:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003158472106 + ], + [ + "2025-11-24T00:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000002561658621 + ], + [ + "2025-11-24T00:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003370201864101 + ], + [ + "2025-11-24T00:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999761410142493 + ], + [ + "2025-11-24T00:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999195169004261 + ], + [ + "2025-11-24T00:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999679012689344 + ], + [ + "2025-11-24T00:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999288478835064 + ], + [ + "2025-11-24T00:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998042058258654 + ], + [ + "2025-11-24T00:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999224208441774 + ], + [ + "2025-11-24T00:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000047313833893 + ], + [ + "2025-11-24T00:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999749085975451 + ], + [ + "2025-11-24T00:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999124072476229 + ], + [ + "2025-11-24T00:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999993266757512 + ], + [ + "2025-11-24T00:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999962968329202 + ], + [ + "2025-11-24T00:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998968966367187 + ], + [ + "2025-11-24T00:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998370213708953 + ], + [ + "2025-11-24T00:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999996994907187 + ], + [ + "2025-11-24T00:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999003321814833 + ], + [ + "2025-11-24T00:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999320770761366 + ], + [ + "2025-11-24T00:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000636244141254 + ], + [ + "2025-11-23T23:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003179715114685 + ], + [ + "2025-11-23T23:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999994978662487 + ], + [ + "2025-11-23T23:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999990415631702 + ], + [ + "2025-11-23T23:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000536893388812 + ], + [ + "2025-11-23T23:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004702534764314 + ], + [ + "2025-11-23T23:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001001796702194 + ], + [ + "2025-11-23T23:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00011783580791 + ], + [ + "2025-11-23T23:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000935400078563 + ], + [ + "2025-11-23T23:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001143695335442 + ], + [ + "2025-11-23T23:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001108266026093 + ], + [ + "2025-11-23T23:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001094476140877 + ], + [ + "2025-11-23T23:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000124686906834 + ], + [ + "2025-11-23T23:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001642707215783 + ], + [ + "2025-11-23T23:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000659725815848 + ], + [ + "2025-11-23T23:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000766018780853 + ], + [ + "2025-11-23T23:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001244530694535 + ], + [ + "2025-11-23T23:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000115703082685 + ], + [ + "2025-11-23T23:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999902862477823 + ], + [ + "2025-11-23T23:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000128798339651 + ], + [ + "2025-11-23T23:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00015004397212 + ], + [ + "2025-11-23T23:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001011488733667 + ], + [ + "2025-11-23T23:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001065847035606 + ], + [ + "2025-11-23T23:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000754593355345 + ], + [ + "2025-11-23T23:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001520646001147 + ], + [ + "2025-11-23T23:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001371474571938 + ], + [ + "2025-11-23T23:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002650552691887 + ], + [ + "2025-11-23T23:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999733418046615 + ], + [ + "2025-11-23T23:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000689623090646 + ], + [ + "2025-11-23T23:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000086601097355 + ], + [ + "2025-11-23T23:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998363052147878 + ], + [ + "2025-11-23T22:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998825215710082 + ], + [ + "2025-11-23T22:58:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999697114844143 + ], + [ + "2025-11-23T22:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997172599987606 + ], + [ + "2025-11-23T22:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997351361237667 + ], + [ + "2025-11-23T22:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999761637519287 + ], + [ + "2025-11-23T22:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998624761796863 + ], + [ + "2025-11-23T22:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998039879029658 + ], + [ + "2025-11-23T22:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008378705346903 + ], + [ + "2025-11-23T22:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999302318095296 + ], + [ + "2025-11-23T22:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998422822608783 + ], + [ + "2025-11-23T22:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998612916759436 + ], + [ + "2025-11-23T22:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000031727691436 + ], + [ + "2025-11-23T22:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000142824155853 + ], + [ + "2025-11-23T22:33:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999110337587555 + ], + [ + "2025-11-23T22:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998498324990083 + ], + [ + "2025-11-23T22:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000370024548724 + ], + [ + "2025-11-23T22:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000008611869858 + ], + [ + "2025-11-23T22:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999423070463883 + ], + [ + "2025-11-23T22:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999949894531672 + ], + [ + "2025-11-23T22:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003026462742115 + ], + [ + "2025-11-23T22:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001158801425252 + ], + [ + "2025-11-23T22:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000137109352016 + ], + [ + "2025-11-23T22:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000206245318455 + ], + [ + "2025-11-23T22:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004812190619774 + ], + [ + "2025-11-23T22:11:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012189010289885 + ], + [ + "2025-11-23T22:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997680499864224 + ], + [ + "2025-11-23T22:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000043670572117 + ], + [ + "2025-11-23T22:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004417312508673 + ], + [ + "2025-11-23T22:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011764227512774 + ], + [ + "2025-11-23T22:01:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998950685072655 + ], + [ + "2025-11-23T21:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998987347418805 + ], + [ + "2025-11-23T21:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014435754594608 + ], + [ + "2025-11-23T21:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999988759035085 + ], + [ + "2025-11-23T21:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000414389394605 + ], + [ + "2025-11-23T21:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001340282146158 + ], + [ + "2025-11-23T21:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005812993970613 + ], + [ + "2025-11-23T21:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999613442479893 + ], + [ + "2025-11-23T21:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000567197015502 + ], + [ + "2025-11-23T21:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000303359337598 + ], + [ + "2025-11-23T21:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0015341027370308 + ], + [ + "2025-11-23T21:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999559631211803 + ], + [ + "2025-11-23T21:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000398678365339 + ], + [ + "2025-11-23T21:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009788073458106 + ], + [ + "2025-11-23T21:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999820860258559 + ], + [ + "2025-11-23T21:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999942793434696 + ], + [ + "2025-11-23T21:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999699210592216 + ], + [ + "2025-11-23T21:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009760072037193 + ], + [ + "2025-11-23T21:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001728380466226 + ], + [ + "2025-11-23T21:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999393748823054 + ], + [ + "2025-11-23T21:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000146809606658 + ], + [ + "2025-11-23T21:20:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012698254956447 + ], + [ + "2025-11-23T21:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002790462697857 + ], + [ + "2025-11-23T21:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999285400478738 + ], + [ + "2025-11-23T21:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999741687342683 + ], + [ + "2025-11-23T21:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013065589019963 + ], + [ + "2025-11-23T21:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001232469731813 + ], + [ + "2025-11-23T21:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999871791243046 + ], + [ + "2025-11-23T21:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999837089284426 + ], + [ + "2025-11-23T21:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996235805319383 + ], + [ + "2025-11-23T21:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000248791778654 + ], + [ + "2025-11-23T20:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999356676424369 + ], + [ + "2025-11-23T20:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000014070536667 + ], + [ + "2025-11-23T20:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000497286291983 + ], + [ + "2025-11-23T20:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000027021541703 + ], + [ + "2025-11-23T20:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000181912097663 + ], + [ + "2025-11-23T20:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000134461563166 + ], + [ + "2025-11-23T20:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999850838547026 + ], + [ + "2025-11-23T20:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001438586926965 + ], + [ + "2025-11-23T20:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000318677468514 + ], + [ + "2025-11-23T20:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000158210410233 + ], + [ + "2025-11-23T20:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999939076959711 + ], + [ + "2025-11-23T20:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002376861254014 + ], + [ + "2025-11-23T20:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000039568115887 + ], + [ + "2025-11-23T20:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999373932565038 + ], + [ + "2025-11-23T20:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000543976831828 + ], + [ + "2025-11-23T20:30:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000208056513692 + ], + [ + "2025-11-23T20:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999444505043094 + ], + [ + "2025-11-23T20:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000104261427323 + ], + [ + "2025-11-23T20:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000680079377624 + ], + [ + "2025-11-23T20:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004643052248772 + ], + [ + "2025-11-23T20:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999102081115662 + ], + [ + "2025-11-23T20:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999464319115885 + ], + [ + "2025-11-23T20:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000131587649748 + ], + [ + "2025-11-23T20:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998921115483306 + ], + [ + "2025-11-23T20:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999155799227538 + ], + [ + "2025-11-23T20:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999995689886185 + ], + [ + "2025-11-23T20:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000095346418636 + ], + [ + "2025-11-23T20:05:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999211919835205 + ], + [ + "2025-11-23T20:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999922057695766 + ], + [ + "2025-11-23T20:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000207880797014 + ], + [ + "2025-11-23T19:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000686713474165 + ], + [ + "2025-11-23T19:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000090312544565 + ], + [ + "2025-11-23T19:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000309824229372 + ], + [ + "2025-11-23T19:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000468004010716 + ], + [ + "2025-11-23T19:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000930252810891 + ], + [ + "2025-11-23T19:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001348761547617 + ], + [ + "2025-11-23T19:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000358242615037 + ], + [ + "2025-11-23T19:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000436610628036 + ], + [ + "2025-11-23T19:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007135306123467 + ], + [ + "2025-11-23T19:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007646907996153 + ], + [ + "2025-11-23T19:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999957428288921 + ], + [ + "2025-11-23T19:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000127281228235 + ], + [ + "2025-11-23T19:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000010598462772 + ], + [ + "2025-11-23T19:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007378955213995 + ], + [ + "2025-11-23T19:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999785425430333 + ], + [ + "2025-11-23T19:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000492241128582 + ], + [ + "2025-11-23T19:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999199151994497 + ], + [ + "2025-11-23T19:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006117859096222 + ], + [ + "2025-11-23T19:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000075087366933 + ], + [ + "2025-11-23T19:21:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000570441962011 + ], + [ + "2025-11-23T19:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997943010087635 + ], + [ + "2025-11-23T19:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010338826808245 + ], + [ + "2025-11-23T19:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000380566272769 + ], + [ + "2025-11-23T19:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000157885871637 + ], + [ + "2025-11-23T19:11:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997780457881256 + ], + [ + "2025-11-23T19:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000442920636725 + ], + [ + "2025-11-23T19:07:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999842927511368 + ], + [ + "2025-11-23T19:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999168621965916 + ], + [ + "2025-11-23T19:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000073493021761 + ], + [ + "2025-11-23T19:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002045783566729 + ], + [ + "2025-11-23T18:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000329461852233 + ], + [ + "2025-11-23T18:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005357626572575 + ], + [ + "2025-11-23T18:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003718651358775 + ], + [ + "2025-11-23T18:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003152068779935 + ], + [ + "2025-11-23T18:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000226828266786 + ], + [ + "2025-11-23T18:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008384655598621 + ], + [ + "2025-11-23T18:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004088119144685 + ], + [ + "2025-11-23T18:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012730746762113 + ], + [ + "2025-11-23T18:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000290985877754 + ], + [ + "2025-11-23T18:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014715654391029 + ], + [ + "2025-11-23T18:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004198282939734 + ], + [ + "2025-11-23T18:37:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000263110071002 + ], + [ + "2025-11-23T18:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000361153602981 + ], + [ + "2025-11-23T18:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000375213014525 + ], + [ + "2025-11-23T18:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997112280065664 + ], + [ + "2025-11-23T18:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999969577380603 + ], + [ + "2025-11-23T18:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999821121254866 + ], + [ + "2025-11-23T18:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998844778251585 + ], + [ + "2025-11-23T18:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998614331035466 + ], + [ + "2025-11-23T18:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999716285108861 + ], + [ + "2025-11-23T18:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000193899411635 + ], + [ + "2025-11-23T18:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999541610546588 + ], + [ + "2025-11-23T18:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999948805995159 + ], + [ + "2025-11-23T18:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997807291923428 + ], + [ + "2025-11-23T18:12:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002274597106224 + ], + [ + "2025-11-23T18:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999103443728107 + ], + [ + "2025-11-23T18:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000050785864343 + ], + [ + "2025-11-23T18:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006842757502699 + ], + [ + "2025-11-23T18:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002768239468764 + ], + [ + "2025-11-23T18:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999950131952732 + ], + [ + "2025-11-23T17:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001408239692786 + ], + [ + "2025-11-23T17:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016113700371754 + ], + [ + "2025-11-23T17:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000814018957256 + ], + [ + "2025-11-23T17:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999426115337119 + ], + [ + "2025-11-23T17:52:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000169816086348 + ], + [ + "2025-11-23T17:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012799097291134 + ], + [ + "2025-11-23T17:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000078405461984 + ], + [ + "2025-11-23T17:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000858503886807 + ], + [ + "2025-11-23T17:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001103023773246 + ], + [ + "2025-11-23T17:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001658808826142 + ], + [ + "2025-11-23T17:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000057509054943 + ], + [ + "2025-11-23T17:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000864205089417 + ], + [ + "2025-11-23T17:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999553493291757 + ], + [ + "2025-11-23T17:33:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000179154453643 + ], + [ + "2025-11-23T17:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999566637709889 + ], + [ + "2025-11-23T17:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008855930815876 + ], + [ + "2025-11-23T17:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999787453730925 + ], + [ + "2025-11-23T17:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000381154885511 + ], + [ + "2025-11-23T17:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999926563373277 + ], + [ + "2025-11-23T17:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000757692314326 + ], + [ + "2025-11-23T17:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998151649285413 + ], + [ + "2025-11-23T17:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999779427670231 + ], + [ + "2025-11-23T17:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999841329549597 + ], + [ + "2025-11-23T17:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998707566597919 + ], + [ + "2025-11-23T17:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998096865921892 + ], + [ + "2025-11-23T17:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998065856241363 + ], + [ + "2025-11-23T17:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999549992773579 + ], + [ + "2025-11-23T17:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999904938148471 + ], + [ + "2025-11-23T17:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998624559163604 + ], + [ + "2025-11-23T17:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000011604277093 + ], + [ + "2025-11-23T16:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999910484812414 + ], + [ + "2025-11-23T16:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999416756727036 + ], + [ + "2025-11-23T16:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999979565896028 + ], + [ + "2025-11-23T16:54:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005831135928487 + ], + [ + "2025-11-23T16:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998574072589659 + ], + [ + "2025-11-23T16:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999397742539192 + ], + [ + "2025-11-23T16:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999058598440959 + ], + [ + "2025-11-23T16:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998496817494276 + ], + [ + "2025-11-23T16:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012968836437022 + ], + [ + "2025-11-23T16:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000916817085234 + ], + [ + "2025-11-23T16:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009832482960979 + ], + [ + "2025-11-23T16:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999730100151388 + ], + [ + "2025-11-23T16:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996974595185565 + ], + [ + "2025-11-23T16:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999753425541715 + ], + [ + "2025-11-23T16:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001962079560784 + ], + [ + "2025-11-23T16:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005170097157259 + ], + [ + "2025-11-23T16:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002843401957637 + ], + [ + "2025-11-23T16:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996492163355961 + ], + [ + "2025-11-23T16:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999391285589623 + ], + [ + "2025-11-23T16:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999914082315287 + ], + [ + "2025-11-23T16:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999295795766455 + ], + [ + "2025-11-23T16:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007177625820798 + ], + [ + "2025-11-23T16:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999606019431467 + ], + [ + "2025-11-23T16:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999185375676553 + ], + [ + "2025-11-23T16:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000080457314313 + ], + [ + "2025-11-23T16:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000745730692787 + ], + [ + "2025-11-23T16:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000302976575163 + ], + [ + "2025-11-23T16:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999636905590821 + ], + [ + "2025-11-23T16:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003601240798172 + ], + [ + "2025-11-23T16:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000789193693085 + ], + [ + "2025-11-23T15:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008427163488505 + ], + [ + "2025-11-23T15:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000027549606316 + ], + [ + "2025-11-23T15:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006873837510784 + ], + [ + "2025-11-23T15:54:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011508313447115 + ], + [ + "2025-11-23T15:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011569552983404 + ], + [ + "2025-11-23T15:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000726467506091 + ], + [ + "2025-11-23T15:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996880852545884 + ], + [ + "2025-11-23T15:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999466518121135 + ], + [ + "2025-11-23T15:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998036015531451 + ], + [ + "2025-11-23T15:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001238303658332 + ], + [ + "2025-11-23T15:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998929634824182 + ], + [ + "2025-11-23T15:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998836894575477 + ], + [ + "2025-11-23T15:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999281879299845 + ], + [ + "2025-11-23T15:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005678752015563 + ], + [ + "2025-11-23T15:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002652254444706 + ], + [ + "2025-11-23T15:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009068997119923 + ], + [ + "2025-11-23T15:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999914582117816 + ], + [ + "2025-11-23T15:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002775436292481 + ], + [ + "2025-11-23T15:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999850015340132 + ], + [ + "2025-11-23T15:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999743088297524 + ], + [ + "2025-11-23T15:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999111265632041 + ], + [ + "2025-11-23T15:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016394107059554 + ], + [ + "2025-11-23T15:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998573149570058 + ], + [ + "2025-11-23T15:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998942452043573 + ], + [ + "2025-11-23T15:11:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999444397168659 + ], + [ + "2025-11-23T15:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999983579673685 + ], + [ + "2025-11-23T15:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999488140614845 + ], + [ + "2025-11-23T15:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998279758199028 + ], + [ + "2025-11-23T15:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999172197650309 + ], + [ + "2025-11-23T15:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998777249920072 + ], + [ + "2025-11-23T14:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999806674602075 + ], + [ + "2025-11-23T14:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997379794389344 + ], + [ + "2025-11-23T14:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999147167014474 + ], + [ + "2025-11-23T14:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997706370574165 + ], + [ + "2025-11-23T14:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998225702498669 + ], + [ + "2025-11-23T14:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998857465951428 + ], + [ + "2025-11-23T14:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995919547170091 + ], + [ + "2025-11-23T14:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997574060615751 + ], + [ + "2025-11-23T14:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998992802746948 + ], + [ + "2025-11-23T14:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999533288480669 + ], + [ + "2025-11-23T14:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997274496808798 + ], + [ + "2025-11-23T14:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996578459452757 + ], + [ + "2025-11-23T14:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998948477855025 + ], + [ + "2025-11-23T14:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000173537858954 + ], + [ + "2025-11-23T14:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998683490022336 + ], + [ + "2025-11-23T14:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998078714392162 + ], + [ + "2025-11-23T14:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999405926227406 + ], + [ + "2025-11-23T14:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001149115396573 + ], + [ + "2025-11-23T14:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998658567368127 + ], + [ + "2025-11-23T14:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999146193217034 + ], + [ + "2025-11-23T14:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999538369306932 + ], + [ + "2025-11-23T14:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000667418231605 + ], + [ + "2025-11-23T14:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998450908639874 + ], + [ + "2025-11-23T14:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000188951254339 + ], + [ + "2025-11-23T14:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999527271760793 + ], + [ + "2025-11-23T14:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000958710090506 + ], + [ + "2025-11-23T14:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998432009212176 + ], + [ + "2025-11-23T14:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000709431944674 + ], + [ + "2025-11-23T14:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999320744942702 + ], + [ + "2025-11-23T14:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007186863959276 + ], + [ + "2025-11-23T13:59:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997155007136421 + ], + [ + "2025-11-23T13:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008583090367709 + ], + [ + "2025-11-23T13:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998414708029962 + ], + [ + "2025-11-23T13:53:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016351121891622 + ], + [ + "2025-11-23T13:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997610863556663 + ], + [ + "2025-11-23T13:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010271284826553 + ], + [ + "2025-11-23T13:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001204310187688 + ], + [ + "2025-11-23T13:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012536780449777 + ], + [ + "2025-11-23T13:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013909081732126 + ], + [ + "2025-11-23T13:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012409789711367 + ], + [ + "2025-11-23T13:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996749893682855 + ], + [ + "2025-11-23T13:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011372308493336 + ], + [ + "2025-11-23T13:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998599607926998 + ], + [ + "2025-11-23T13:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000349182453964 + ], + [ + "2025-11-23T13:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998766077374768 + ], + [ + "2025-11-23T13:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001743977186925 + ], + [ + "2025-11-23T13:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998341182727902 + ], + [ + "2025-11-23T13:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999859602478612 + ], + [ + "2025-11-23T13:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999190336100224 + ], + [ + "2025-11-23T13:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998253583232906 + ], + [ + "2025-11-23T13:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997692798366168 + ], + [ + "2025-11-23T13:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998022904410508 + ], + [ + "2025-11-23T13:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999194862655144 + ], + [ + "2025-11-23T13:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999888083457334 + ], + [ + "2025-11-23T13:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997769502634115 + ], + [ + "2025-11-23T13:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997620714963356 + ], + [ + "2025-11-23T13:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998591782645023 + ], + [ + "2025-11-23T13:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999163633296358 + ], + [ + "2025-11-23T13:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999184454571592 + ], + [ + "2025-11-23T13:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998341973551921 + ], + [ + "2025-11-23T12:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998485431492058 + ], + [ + "2025-11-23T12:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999216671006619 + ], + [ + "2025-11-23T12:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999882012585479 + ], + [ + "2025-11-23T12:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998771592162348 + ], + [ + "2025-11-23T12:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999846781522589 + ], + [ + "2025-11-23T12:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999273661405996 + ], + [ + "2025-11-23T12:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998483673953484 + ], + [ + "2025-11-23T12:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999470414800404 + ], + [ + "2025-11-23T12:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998532467372595 + ], + [ + "2025-11-23T12:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999375064888113 + ], + [ + "2025-11-23T12:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000018815833873 + ], + [ + "2025-11-23T12:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000058511890202 + ], + [ + "2025-11-23T12:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998074197463679 + ], + [ + "2025-11-23T12:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999644190310353 + ], + [ + "2025-11-23T12:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000398007603153 + ], + [ + "2025-11-23T12:30:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000326080295359 + ], + [ + "2025-11-23T12:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000561196576694 + ], + [ + "2025-11-23T12:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000032272938137 + ], + [ + "2025-11-23T12:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000033386666664 + ], + [ + "2025-11-23T12:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000048777566135 + ], + [ + "2025-11-23T12:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000046603151724 + ], + [ + "2025-11-23T12:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000066131171094 + ], + [ + "2025-11-23T12:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000443157407203 + ], + [ + "2025-11-23T12:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000041103475028 + ], + [ + "2025-11-23T12:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999938982370405 + ], + [ + "2025-11-23T12:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002632547121402 + ], + [ + "2025-11-23T12:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011541741615082 + ], + [ + "2025-11-23T12:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000211044939273 + ], + [ + "2025-11-23T12:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999777773160307 + ], + [ + "2025-11-23T12:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001414463732796 + ], + [ + "2025-11-23T11:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008465610129627 + ], + [ + "2025-11-23T11:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999956850864157 + ], + [ + "2025-11-23T11:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999231085009396 + ], + [ + "2025-11-23T11:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001438620420071 + ], + [ + "2025-11-23T11:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003668552321192 + ], + [ + "2025-11-23T11:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998667176024453 + ], + [ + "2025-11-23T11:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000698105596516 + ], + [ + "2025-11-23T11:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001370168924344 + ], + [ + "2025-11-23T11:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000887563184122 + ], + [ + "2025-11-23T11:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997455481629673 + ], + [ + "2025-11-23T11:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0019306961271464 + ], + [ + "2025-11-23T11:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014073042403122 + ], + [ + "2025-11-23T11:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000023280993168 + ], + [ + "2025-11-23T11:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006961600675979 + ], + [ + "2025-11-23T11:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001281889460483 + ], + [ + "2025-11-23T11:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012409543752683 + ], + [ + "2025-11-23T11:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997732753446098 + ], + [ + "2025-11-23T11:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008023238137433 + ], + [ + "2025-11-23T11:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002935526968095 + ], + [ + "2025-11-23T11:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011439186808078 + ], + [ + "2025-11-23T11:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997109047985789 + ], + [ + "2025-11-23T11:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999161863802677 + ], + [ + "2025-11-23T11:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008167283589007 + ], + [ + "2025-11-23T11:13:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997249938091918 + ], + [ + "2025-11-23T11:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000027735336414 + ], + [ + "2025-11-23T11:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000051192934667 + ], + [ + "2025-11-23T11:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000055086984454 + ], + [ + "2025-11-23T11:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996546048299565 + ], + [ + "2025-11-23T11:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997541166521631 + ], + [ + "2025-11-23T11:01:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997564424161112 + ], + [ + "2025-11-23T10:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000555336630814 + ], + [ + "2025-11-23T10:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998997897503276 + ], + [ + "2025-11-23T10:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016298942453823 + ], + [ + "2025-11-23T10:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998471314051548 + ], + [ + "2025-11-23T10:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005078496270525 + ], + [ + "2025-11-23T10:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999008983089455 + ], + [ + "2025-11-23T10:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999859190711745 + ], + [ + "2025-11-23T10:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001766121577669 + ], + [ + "2025-11-23T10:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009832666284777 + ], + [ + "2025-11-23T10:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009317770451338 + ], + [ + "2025-11-23T10:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000097180888285 + ], + [ + "2025-11-23T10:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001449228980252 + ], + [ + "2025-11-23T10:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999614621960101 + ], + [ + "2025-11-23T10:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000980883718971 + ], + [ + "2025-11-23T10:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999682414789813 + ], + [ + "2025-11-23T10:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999040319428735 + ], + [ + "2025-11-23T10:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999831217211972 + ], + [ + "2025-11-23T10:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00090872661843 + ], + [ + "2025-11-23T10:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00014051389251 + ], + [ + "2025-11-23T10:21:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999491396142565 + ], + [ + "2025-11-23T10:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000014072420105 + ], + [ + "2025-11-23T10:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009811969350157 + ], + [ + "2025-11-23T10:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004910640735054 + ], + [ + "2025-11-23T10:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999826049881645 + ], + [ + "2025-11-23T10:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997944516707762 + ], + [ + "2025-11-23T10:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001000684343484 + ], + [ + "2025-11-23T10:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002192706514235 + ], + [ + "2025-11-23T10:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999220898375581 + ], + [ + "2025-11-23T10:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000512095719833 + ], + [ + "2025-11-23T10:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007933756459997 + ], + [ + "2025-11-23T09:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011146627175673 + ], + [ + "2025-11-23T09:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998626489726059 + ], + [ + "2025-11-23T09:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008301659539285 + ], + [ + "2025-11-23T09:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998533734253771 + ], + [ + "2025-11-23T09:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999180314577211 + ], + [ + "2025-11-23T09:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997288789133275 + ], + [ + "2025-11-23T09:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999400427757583 + ], + [ + "2025-11-23T09:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999414882499279 + ], + [ + "2025-11-23T09:44:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998487752369637 + ], + [ + "2025-11-23T09:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997952841276312 + ], + [ + "2025-11-23T09:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000392086370444 + ], + [ + "2025-11-23T09:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999822528996908 + ], + [ + "2025-11-23T09:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999462476121825 + ], + [ + "2025-11-23T09:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998013628871153 + ], + [ + "2025-11-23T09:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000136310577434 + ], + [ + "2025-11-23T09:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999213554789993 + ], + [ + "2025-11-23T09:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000031631466137 + ], + [ + "2025-11-23T09:25:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998964399050653 + ], + [ + "2025-11-23T09:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998378944737755 + ], + [ + "2025-11-23T09:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000133233574648 + ], + [ + "2025-11-23T09:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998834406936353 + ], + [ + "2025-11-23T09:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000173043219354 + ], + [ + "2025-11-23T09:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000002771180788 + ], + [ + "2025-11-23T09:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001225820641253 + ], + [ + "2025-11-23T09:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999302489334699 + ], + [ + "2025-11-23T09:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000011483368705 + ], + [ + "2025-11-23T09:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003454038465724 + ], + [ + "2025-11-23T09:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000132950819629 + ], + [ + "2025-11-23T09:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999934395167027 + ], + [ + "2025-11-23T09:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001174045689674 + ], + [ + "2025-11-23T08:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005264354575896 + ], + [ + "2025-11-23T08:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001409078751966 + ], + [ + "2025-11-23T08:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000014214040916 + ], + [ + "2025-11-23T08:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001004609627704 + ], + [ + "2025-11-23T08:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010977448713156 + ], + [ + "2025-11-23T08:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002239728321056 + ], + [ + "2025-11-23T08:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001892213794883 + ], + [ + "2025-11-23T08:46:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000111779562381 + ], + [ + "2025-11-23T08:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011226727001805 + ], + [ + "2025-11-23T08:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003431818946034 + ], + [ + "2025-11-23T08:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001388106733358 + ], + [ + "2025-11-23T08:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000167730257612 + ], + [ + "2025-11-23T08:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999586740821108 + ], + [ + "2025-11-23T08:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000172840071646 + ], + [ + "2025-11-23T08:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000108076226805 + ], + [ + "2025-11-23T08:29:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007145047995005 + ], + [ + "2025-11-23T08:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999746253303998 + ], + [ + "2025-11-23T08:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999785065811181 + ], + [ + "2025-11-23T08:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000059686950966 + ], + [ + "2025-11-23T08:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010877295798484 + ], + [ + "2025-11-23T08:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00005978122636 + ], + [ + "2025-11-23T08:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999211896531186 + ], + [ + "2025-11-23T08:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009074619764962 + ], + [ + "2025-11-23T08:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0012786510364136 + ], + [ + "2025-11-23T08:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006060965296868 + ], + [ + "2025-11-23T08:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999156031426162 + ], + [ + "2025-11-23T08:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000289419669651 + ], + [ + "2025-11-23T08:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009477675924632 + ], + [ + "2025-11-23T08:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000969870186866 + ], + [ + "2025-11-23T08:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999359375986615 + ], + [ + "2025-11-23T07:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000418365586287 + ], + [ + "2025-11-23T07:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003815735300845 + ], + [ + "2025-11-23T07:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996522323756282 + ], + [ + "2025-11-23T07:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999979266056732 + ], + [ + "2025-11-23T07:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999846896893624 + ], + [ + "2025-11-23T07:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999697573700329 + ], + [ + "2025-11-23T07:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999773487311745 + ], + [ + "2025-11-23T07:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999445465217123 + ], + [ + "2025-11-23T07:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000877482857329 + ], + [ + "2025-11-23T07:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003864544882304 + ], + [ + "2025-11-23T07:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000277263252564 + ], + [ + "2025-11-23T07:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999958757525745 + ], + [ + "2025-11-23T07:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000103556989078 + ], + [ + "2025-11-23T07:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998938754924569 + ], + [ + "2025-11-23T07:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999725381701475 + ], + [ + "2025-11-23T07:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999438955589155 + ], + [ + "2025-11-23T07:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005449924879501 + ], + [ + "2025-11-23T07:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998703949273053 + ], + [ + "2025-11-23T07:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999698968986209 + ], + [ + "2025-11-23T07:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000303572579188 + ], + [ + "2025-11-23T07:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008149462213967 + ], + [ + "2025-11-23T07:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999868350092175 + ], + [ + "2025-11-23T07:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999162092356646 + ], + [ + "2025-11-23T07:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000007727537064 + ], + [ + "2025-11-23T07:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008882312078686 + ], + [ + "2025-11-23T07:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998742991860814 + ], + [ + "2025-11-23T07:07:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999551150828562 + ], + [ + "2025-11-23T07:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000362817216575 + ], + [ + "2025-11-23T07:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000025744021293 + ], + [ + "2025-11-23T07:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998231680800486 + ], + [ + "2025-11-23T06:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996564580718162 + ], + [ + "2025-11-23T06:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999077791763095 + ], + [ + "2025-11-23T06:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996871632109637 + ], + [ + "2025-11-23T06:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998494358190875 + ], + [ + "2025-11-23T06:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997277421502894 + ], + [ + "2025-11-23T06:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999775416782943 + ], + [ + "2025-11-23T06:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998456488125088 + ], + [ + "2025-11-23T06:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999411713095439 + ], + [ + "2025-11-23T06:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002670402996106 + ], + [ + "2025-11-23T06:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998429130545754 + ], + [ + "2025-11-23T06:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998274938885047 + ], + [ + "2025-11-23T06:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999679454137637 + ], + [ + "2025-11-23T06:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005129393302243 + ], + [ + "2025-11-23T06:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997447144390482 + ], + [ + "2025-11-23T06:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999981386819835 + ], + [ + "2025-11-23T06:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999988405763836 + ], + [ + "2025-11-23T06:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011590674870154 + ], + [ + "2025-11-23T06:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999469869449256 + ], + [ + "2025-11-23T06:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008685270107465 + ], + [ + "2025-11-23T06:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998452616235481 + ], + [ + "2025-11-23T06:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999905470006938 + ], + [ + "2025-11-23T06:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000770636111393 + ], + [ + "2025-11-23T06:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003958516462803 + ], + [ + "2025-11-23T06:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9995757808249941 + ], + [ + "2025-11-23T06:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001499072964255 + ], + [ + "2025-11-23T06:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000446398245726 + ], + [ + "2025-11-23T06:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997354605828528 + ], + [ + "2025-11-23T06:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996106466097886 + ], + [ + "2025-11-23T06:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011809599490311 + ], + [ + "2025-11-23T06:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996869897900753 + ], + [ + "2025-11-23T05:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998734503876516 + ], + [ + "2025-11-23T05:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998134235556375 + ], + [ + "2025-11-23T05:56:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007223017064297 + ], + [ + "2025-11-23T05:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.99968604492015 + ], + [ + "2025-11-23T05:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997787400423884 + ], + [ + "2025-11-23T05:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997401415926112 + ], + [ + "2025-11-23T05:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997805569993706 + ], + [ + "2025-11-23T05:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997129006386914 + ], + [ + "2025-11-23T05:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996565982583193 + ], + [ + "2025-11-23T05:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997837905329808 + ], + [ + "2025-11-23T05:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999484365993977 + ], + [ + "2025-11-23T05:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997938985115454 + ], + [ + "2025-11-23T05:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999762094135496 + ], + [ + "2025-11-23T05:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998991188355535 + ], + [ + "2025-11-23T05:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000726300235352 + ], + [ + "2025-11-23T05:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998387577327984 + ], + [ + "2025-11-23T05:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998131547079075 + ], + [ + "2025-11-23T05:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998806665848764 + ], + [ + "2025-11-23T05:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007540603227134 + ], + [ + "2025-11-23T05:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997848508192207 + ], + [ + "2025-11-23T05:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000061417967544 + ], + [ + "2025-11-23T05:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997537899797905 + ], + [ + "2025-11-23T05:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009097969720364 + ], + [ + "2025-11-23T05:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998225590758292 + ], + [ + "2025-11-23T05:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997597010076391 + ], + [ + "2025-11-23T05:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999096955671488 + ], + [ + "2025-11-23T05:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999077799116544 + ], + [ + "2025-11-23T05:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997330777514959 + ], + [ + "2025-11-23T05:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998534303406507 + ], + [ + "2025-11-23T05:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999926920263359 + ], + [ + "2025-11-23T04:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999653177245998 + ], + [ + "2025-11-23T04:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998084379236747 + ], + [ + "2025-11-23T04:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996494182710589 + ], + [ + "2025-11-23T04:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000989036276864 + ], + [ + "2025-11-23T04:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999566813041261 + ], + [ + "2025-11-23T04:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998487506731639 + ], + [ + "2025-11-23T04:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999648867485715 + ], + [ + "2025-11-23T04:46:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005881741424907 + ], + [ + "2025-11-23T04:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998097163807872 + ], + [ + "2025-11-23T04:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996541190387794 + ], + [ + "2025-11-23T04:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997035241661173 + ], + [ + "2025-11-23T04:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999778152065504 + ], + [ + "2025-11-23T04:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999988687711491 + ], + [ + "2025-11-23T04:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997416770544721 + ], + [ + "2025-11-23T04:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998287516937456 + ], + [ + "2025-11-23T04:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000055118042381 + ], + [ + "2025-11-23T04:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999264588474728 + ], + [ + "2025-11-23T04:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998427853365287 + ], + [ + "2025-11-23T04:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998160947030272 + ], + [ + "2025-11-23T04:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999312396019966 + ], + [ + "2025-11-23T04:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999713595482976 + ], + [ + "2025-11-23T04:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000239700190032 + ], + [ + "2025-11-23T04:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000064525924417 + ], + [ + "2025-11-23T04:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000017284004109 + ], + [ + "2025-11-23T04:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999896913027666 + ], + [ + "2025-11-23T04:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001627294218518 + ], + [ + "2025-11-23T04:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000045354279788 + ], + [ + "2025-11-23T04:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999771643746023 + ], + [ + "2025-11-23T04:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998932400181185 + ], + [ + "2025-11-23T04:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003105209965981 + ], + [ + "2025-11-23T03:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999197399709512 + ], + [ + "2025-11-23T03:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997163129064334 + ], + [ + "2025-11-23T03:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997158550446545 + ], + [ + "2025-11-23T03:53:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004738751667628 + ], + [ + "2025-11-23T03:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999165030341517 + ], + [ + "2025-11-23T03:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997510187932329 + ], + [ + "2025-11-23T03:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997186777677298 + ], + [ + "2025-11-23T03:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013543188259049 + ], + [ + "2025-11-23T03:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999280229619392 + ], + [ + "2025-11-23T03:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999985329173636 + ], + [ + "2025-11-23T03:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998083015085288 + ], + [ + "2025-11-23T03:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005481381672034 + ], + [ + "2025-11-23T03:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000026364802178 + ], + [ + "2025-11-23T03:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999240754933982 + ], + [ + "2025-11-23T03:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999843956815076 + ], + [ + "2025-11-23T03:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998909336776789 + ], + [ + "2025-11-23T03:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001137422708186 + ], + [ + "2025-11-23T03:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999524324812231 + ], + [ + "2025-11-23T03:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999626145888063 + ], + [ + "2025-11-23T03:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999868661138728 + ], + [ + "2025-11-23T03:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002135174915778 + ], + [ + "2025-11-23T03:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000184822079757 + ], + [ + "2025-11-23T02:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999367507727775 + ], + [ + "2025-11-23T02:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999626677963632 + ], + [ + "2025-11-23T02:55:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00096218333622 + ], + [ + "2025-11-23T02:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999176414706007 + ], + [ + "2025-11-23T02:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001032540161825 + ], + [ + "2025-11-23T02:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000281775073612 + ], + [ + "2025-11-23T02:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000002970171575 + ], + [ + "2025-11-23T02:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999698709410065 + ], + [ + "2025-11-23T02:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999902911371601 + ], + [ + "2025-11-23T02:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004462518176334 + ], + [ + "2025-11-23T02:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004086709104183 + ], + [ + "2025-11-23T02:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998977482582865 + ], + [ + "2025-11-23T02:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997046687444472 + ], + [ + "2025-11-23T02:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010619506200122 + ], + [ + "2025-11-23T02:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007172982817854 + ], + [ + "2025-11-23T02:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998371982975716 + ], + [ + "2025-11-23T02:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997802971444764 + ], + [ + "2025-11-23T02:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007725633708136 + ], + [ + "2025-11-23T02:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999252396871241 + ], + [ + "2025-11-23T02:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999669945767725 + ], + [ + "2025-11-23T02:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999016369774415 + ], + [ + "2025-11-23T02:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999956189137873 + ], + [ + "2025-11-23T02:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999839781353164 + ], + [ + "2025-11-23T02:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000157888433257 + ], + [ + "2025-11-23T02:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999237586222735 + ], + [ + "2025-11-23T02:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998233015245879 + ], + [ + "2025-11-23T02:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011754106776973 + ], + [ + "2025-11-23T02:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999973432655327 + ], + [ + "2025-11-23T02:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999546137728583 + ], + [ + "2025-11-23T02:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999912141082423 + ], + [ + "2025-11-23T01:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999057354951937 + ], + [ + "2025-11-23T01:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999364644976326 + ], + [ + "2025-11-23T01:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999095418720347 + ], + [ + "2025-11-23T01:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998968791448 + ], + [ + "2025-11-23T01:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999828985696834 + ], + [ + "2025-11-23T01:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999484002464682 + ], + [ + "2025-11-23T01:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999792030255412 + ], + [ + "2025-11-23T01:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000188927913378 + ], + [ + "2025-11-23T01:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006277407406872 + ], + [ + "2025-11-23T01:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999992379517336 + ], + [ + "2025-11-23T01:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000111939322415 + ], + [ + "2025-11-23T01:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004808081986318 + ], + [ + "2025-11-23T01:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999884646152823 + ], + [ + "2025-11-23T01:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001167504889932 + ], + [ + "2025-11-23T01:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007618306048467 + ], + [ + "2025-11-23T01:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009030994945904 + ], + [ + "2025-11-23T01:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999608234998215 + ], + [ + "2025-11-23T01:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000480129499604 + ], + [ + "2025-11-23T01:24:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000229642912151 + ], + [ + "2025-11-23T01:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001654624154959 + ], + [ + "2025-11-23T01:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999910590551507 + ], + [ + "2025-11-23T01:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0011001359882121 + ], + [ + "2025-11-23T01:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000855367882274 + ], + [ + "2025-11-23T01:14:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008913514903501 + ], + [ + "2025-11-23T01:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999576279140083 + ], + [ + "2025-11-23T01:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999798321657294 + ], + [ + "2025-11-23T01:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999787078392784 + ], + [ + "2025-11-23T01:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999665044347229 + ], + [ + "2025-11-23T01:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999940589485234 + ], + [ + "2025-11-23T01:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000027923136527 + ], + [ + "2025-11-23T00:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999782232320642 + ], + [ + "2025-11-23T00:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999878707310052 + ], + [ + "2025-11-23T00:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003999797427956 + ], + [ + "2025-11-23T00:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004323166826932 + ], + [ + "2025-11-23T00:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998747958778946 + ], + [ + "2025-11-23T00:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000018200261527 + ], + [ + "2025-11-23T00:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009148226632294 + ], + [ + "2025-11-23T00:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997772220476396 + ], + [ + "2025-11-23T00:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999118342225124 + ], + [ + "2025-11-23T00:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000442691269291 + ], + [ + "2025-11-23T00:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014961232248276 + ], + [ + "2025-11-23T00:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998083057437482 + ], + [ + "2025-11-23T00:19:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000060997255951 + ], + [ + "2025-11-23T00:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005302265739253 + ], + [ + "2025-11-23T00:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001627776250153 + ], + [ + "2025-11-23T00:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996572264690686 + ], + [ + "2025-11-23T00:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0015345593875344 + ], + [ + "2025-11-23T00:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997341397685136 + ], + [ + "2025-11-23T00:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006234895136297 + ], + [ + "2025-11-23T00:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998433540149779 + ], + [ + "2025-11-23T00:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006203428222162 + ], + [ + "2025-11-23T00:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998108086782626 + ], + [ + "2025-11-22T23:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0014961501515858 + ], + [ + "2025-11-22T23:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997794567131123 + ], + [ + "2025-11-22T23:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999914953015882 + ], + [ + "2025-11-22T23:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0009069731990043 + ], + [ + "2025-11-22T23:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000819386031687 + ], + [ + "2025-11-22T23:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998963099196656 + ], + [ + "2025-11-22T23:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000039067301281 + ], + [ + "2025-11-22T23:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0013074795887722 + ], + [ + "2025-11-22T23:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999291982670581 + ], + [ + "2025-11-22T23:41:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998251599289828 + ], + [ + "2025-11-22T23:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000185428576453 + ], + [ + "2025-11-22T23:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997318831799958 + ], + [ + "2025-11-22T23:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999223405185248 + ], + [ + "2025-11-22T23:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999205702198701 + ], + [ + "2025-11-22T23:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010506522298162 + ], + [ + "2025-11-22T23:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997879124910942 + ], + [ + "2025-11-22T23:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999867569314566 + ], + [ + "2025-11-22T23:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002396046335547 + ], + [ + "2025-11-22T23:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997983696025294 + ], + [ + "2025-11-22T23:21:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998506415744008 + ], + [ + "2025-11-22T23:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998901797927424 + ], + [ + "2025-11-22T23:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004285698975577 + ], + [ + "2025-11-22T23:16:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997438022108484 + ], + [ + "2025-11-22T23:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999568897629705 + ], + [ + "2025-11-22T23:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998347518362584 + ], + [ + "2025-11-22T23:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016575970944275 + ], + [ + "2025-11-22T23:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999719961690946 + ], + [ + "2025-11-22T23:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000468292877125 + ], + [ + "2025-11-22T23:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999484146959995 + ], + [ + "2025-11-22T23:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997249792981966 + ], + [ + "2025-11-22T22:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001442921725443 + ], + [ + "2025-11-22T22:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010694522273291 + ], + [ + "2025-11-22T22:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998024804566819 + ], + [ + "2025-11-22T22:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007478646884695 + ], + [ + "2025-11-22T22:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001701822616287 + ], + [ + "2025-11-22T22:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999222603988757 + ], + [ + "2025-11-22T22:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998024333731773 + ], + [ + "2025-11-22T22:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997973487131924 + ], + [ + "2025-11-22T22:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999434341919391 + ], + [ + "2025-11-22T22:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998514184629899 + ], + [ + "2025-11-22T22:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998092265352428 + ], + [ + "2025-11-22T22:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999084005253489 + ], + [ + "2025-11-22T22:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999798412460225 + ], + [ + "2025-11-22T22:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999935461060746 + ], + [ + "2025-11-22T22:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000173646308971 + ], + [ + "2025-11-22T22:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999961255866873 + ], + [ + "2025-11-22T22:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002031165336511 + ], + [ + "2025-11-22T22:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001476650805576 + ], + [ + "2025-11-22T22:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000046402456227 + ], + [ + "2025-11-22T22:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000750170001125 + ], + [ + "2025-11-22T22:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008850428314229 + ], + [ + "2025-11-22T22:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000476632814621 + ], + [ + "2025-11-22T22:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998013606688646 + ], + [ + "2025-11-22T22:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003725880069139 + ], + [ + "2025-11-22T22:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999480408801065 + ], + [ + "2025-11-22T22:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001060365780834 + ], + [ + "2025-11-22T22:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999081432459223 + ], + [ + "2025-11-22T22:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005180393118083 + ], + [ + "2025-11-22T22:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000226822031495 + ], + [ + "2025-11-22T22:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999364451662001 + ], + [ + "2025-11-22T21:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000152517452234 + ], + [ + "2025-11-22T21:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004039964983782 + ], + [ + "2025-11-22T21:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004294764443746 + ], + [ + "2025-11-22T21:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000032634841138 + ], + [ + "2025-11-22T21:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001754083564847 + ], + [ + "2025-11-22T21:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005103774455288 + ], + [ + "2025-11-22T21:47:57Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008166594133754 + ], + [ + "2025-11-22T21:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999597202974415 + ], + [ + "2025-11-22T21:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004185985418441 + ], + [ + "2025-11-22T21:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007582248984446 + ], + [ + "2025-11-22T21:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999011138435968 + ], + [ + "2025-11-22T21:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999527387891453 + ], + [ + "2025-11-22T21:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998969537391185 + ], + [ + "2025-11-22T21:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999828160820985 + ], + [ + "2025-11-22T21:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999573464564904 + ], + [ + "2025-11-22T21:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999889294645241 + ], + [ + "2025-11-22T21:27:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000007520187591 + ], + [ + "2025-11-22T21:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999778360422654 + ], + [ + "2025-11-22T21:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999893527308201 + ], + [ + "2025-11-22T21:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999869936769308 + ], + [ + "2025-11-22T21:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000236505583402 + ], + [ + "2025-11-22T21:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999868173762444 + ], + [ + "2025-11-22T21:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999430065564889 + ], + [ + "2025-11-22T21:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000523570822029 + ], + [ + "2025-11-22T21:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999023695255731 + ], + [ + "2025-11-22T21:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997518475853279 + ], + [ + "2025-11-22T21:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998191117322668 + ], + [ + "2025-11-22T21:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999980510702733 + ], + [ + "2025-11-22T21:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998393851651205 + ], + [ + "2025-11-22T21:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999660344839861 + ], + [ + "2025-11-22T20:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998892071703509 + ], + [ + "2025-11-22T20:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999610948403977 + ], + [ + "2025-11-22T20:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998724929334873 + ], + [ + "2025-11-22T20:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998692802912333 + ], + [ + "2025-11-22T20:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998829282732209 + ], + [ + "2025-11-22T20:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999262420605878 + ], + [ + "2025-11-22T20:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998657588646309 + ], + [ + "2025-11-22T20:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997986800518365 + ], + [ + "2025-11-22T20:44:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999871490044171 + ], + [ + "2025-11-22T20:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998876447856797 + ], + [ + "2025-11-22T20:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999612417749124 + ], + [ + "2025-11-22T20:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999849701002633 + ], + [ + "2025-11-22T20:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999197921929102 + ], + [ + "2025-11-22T20:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999449722687413 + ], + [ + "2025-11-22T20:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999841368472566 + ], + [ + "2025-11-22T20:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999817809620092 + ], + [ + "2025-11-22T20:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999943037851245 + ], + [ + "2025-11-22T20:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999555957634871 + ], + [ + "2025-11-22T20:23:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999981860745828 + ], + [ + "2025-11-22T20:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999339521950409 + ], + [ + "2025-11-22T20:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999800791116259 + ], + [ + "2025-11-22T20:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000025409906206 + ], + [ + "2025-11-22T20:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996886379397556 + ], + [ + "2025-11-22T20:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999935942523682 + ], + [ + "2025-11-22T20:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000160900555306 + ], + [ + "2025-11-22T20:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000072520795213 + ], + [ + "2025-11-22T20:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998493465396533 + ], + [ + "2025-11-22T20:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999958581217895 + ], + [ + "2025-11-22T20:04:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000301358837058 + ], + [ + "2025-11-22T20:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000352467871818 + ], + [ + "2025-11-22T19:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998560035686724 + ], + [ + "2025-11-22T19:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999921988205134 + ], + [ + "2025-11-22T19:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999753442741282 + ], + [ + "2025-11-22T19:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000784127947462 + ], + [ + "2025-11-22T19:51:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999921887712328 + ], + [ + "2025-11-22T19:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999178487632909 + ], + [ + "2025-11-22T19:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999963591243852 + ], + [ + "2025-11-22T19:45:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000272524886253 + ], + [ + "2025-11-22T19:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999186209175097 + ], + [ + "2025-11-22T19:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999774927906356 + ], + [ + "2025-11-22T19:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998744189947711 + ], + [ + "2025-11-22T19:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000093705288842 + ], + [ + "2025-11-22T19:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998543682995312 + ], + [ + "2025-11-22T19:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999590706934978 + ], + [ + "2025-11-22T19:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997628095997602 + ], + [ + "2025-11-22T19:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999734621115853 + ], + [ + "2025-11-22T19:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997812072153569 + ], + [ + "2025-11-22T19:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999415857328791 + ], + [ + "2025-11-22T19:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998156856659642 + ], + [ + "2025-11-22T19:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998310906039689 + ], + [ + "2025-11-22T19:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998560478742291 + ], + [ + "2025-11-22T19:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998020670404352 + ], + [ + "2025-11-22T19:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999872479034228 + ], + [ + "2025-11-22T19:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998478092699414 + ], + [ + "2025-11-22T19:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999261088206056 + ], + [ + "2025-11-22T19:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998026522256354 + ], + [ + "2025-11-22T19:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998359173025547 + ], + [ + "2025-11-22T19:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999865453552148 + ], + [ + "2025-11-22T18:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997921410182907 + ], + [ + "2025-11-22T18:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000055481909014 + ], + [ + "2025-11-22T18:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997676813489489 + ], + [ + "2025-11-22T18:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998752778790869 + ], + [ + "2025-11-22T18:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998687763414177 + ], + [ + "2025-11-22T18:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998673864799709 + ], + [ + "2025-11-22T18:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997752241936135 + ], + [ + "2025-11-22T18:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998650809692273 + ], + [ + "2025-11-22T18:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998385159608129 + ], + [ + "2025-11-22T18:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998462644922212 + ], + [ + "2025-11-22T18:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998438332852453 + ], + [ + "2025-11-22T18:22:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998915346006709 + ], + [ + "2025-11-22T18:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998068502274334 + ], + [ + "2025-11-22T18:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997937137085301 + ], + [ + "2025-11-22T18:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997949093209048 + ], + [ + "2025-11-22T18:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998880425556416 + ], + [ + "2025-11-22T18:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998599697520503 + ], + [ + "2025-11-22T18:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998331194539659 + ], + [ + "2025-11-22T18:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999895589562365 + ], + [ + "2025-11-22T18:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998367649245621 + ], + [ + "2025-11-22T18:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997983671202305 + ], + [ + "2025-11-22T18:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998525026541402 + ], + [ + "2025-11-22T17:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998792918411684 + ], + [ + "2025-11-22T17:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998219925502707 + ], + [ + "2025-11-22T17:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998352135665691 + ], + [ + "2025-11-22T17:53:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997273245429493 + ], + [ + "2025-11-22T17:52:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997265325204258 + ], + [ + "2025-11-22T17:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998253835703039 + ], + [ + "2025-11-22T17:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998462930670555 + ], + [ + "2025-11-22T17:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999768391107075 + ], + [ + "2025-11-22T17:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999753542908528 + ], + [ + "2025-11-22T17:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998853831209651 + ], + [ + "2025-11-22T17:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997003976729588 + ], + [ + "2025-11-22T17:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998380836797203 + ], + [ + "2025-11-22T17:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998611587399369 + ], + [ + "2025-11-22T17:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0005694997271868 + ], + [ + "2025-11-22T17:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997489853320657 + ], + [ + "2025-11-22T17:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997915659213827 + ], + [ + "2025-11-22T17:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998556036164602 + ], + [ + "2025-11-22T17:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998276874124074 + ], + [ + "2025-11-22T17:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998317077420759 + ], + [ + "2025-11-22T17:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998042652717524 + ], + [ + "2025-11-22T17:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998642765769561 + ], + [ + "2025-11-22T17:17:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998247179648099 + ], + [ + "2025-11-22T17:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997993543791612 + ], + [ + "2025-11-22T17:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997976651931934 + ], + [ + "2025-11-22T17:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998188220109894 + ], + [ + "2025-11-22T17:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999811900838663 + ], + [ + "2025-11-22T17:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997463461700192 + ], + [ + "2025-11-22T17:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997863922127369 + ], + [ + "2025-11-22T17:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997038274007445 + ], + [ + "2025-11-22T17:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998116345127379 + ], + [ + "2025-11-22T16:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997872303698464 + ], + [ + "2025-11-22T16:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997045408783566 + ], + [ + "2025-11-22T16:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997791627993982 + ], + [ + "2025-11-22T16:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998106267951052 + ], + [ + "2025-11-22T16:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997287197511137 + ], + [ + "2025-11-22T16:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997658342793176 + ], + [ + "2025-11-22T16:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997261780030828 + ], + [ + "2025-11-22T16:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998359317449639 + ], + [ + "2025-11-22T16:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998015526642442 + ], + [ + "2025-11-22T16:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998113766927985 + ], + [ + "2025-11-22T16:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998335089150179 + ], + [ + "2025-11-22T16:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998074959717367 + ], + [ + "2025-11-22T16:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997503188513677 + ], + [ + "2025-11-22T16:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999825816001285 + ], + [ + "2025-11-22T16:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997643202405186 + ], + [ + "2025-11-22T16:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998152442454907 + ], + [ + "2025-11-22T16:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998059616742495 + ], + [ + "2025-11-22T16:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998337516455089 + ], + [ + "2025-11-22T16:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998021698618412 + ], + [ + "2025-11-22T16:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000032451994811 + ], + [ + "2025-11-22T16:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997561526985248 + ], + [ + "2025-11-22T16:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999038296803183 + ], + [ + "2025-11-22T16:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999896879214113 + ], + [ + "2025-11-22T16:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0008271022815756 + ], + [ + "2025-11-22T16:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998334047300338 + ], + [ + "2025-11-22T16:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998309295055889 + ], + [ + "2025-11-22T16:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999905141044421 + ], + [ + "2025-11-22T16:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997899260478582 + ], + [ + "2025-11-22T16:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998504691866007 + ], + [ + "2025-11-22T16:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999769342550034 + ], + [ + "2025-11-22T15:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998402559102472 + ], + [ + "2025-11-22T15:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001832853600243 + ], + [ + "2025-11-22T15:56:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997795583821556 + ], + [ + "2025-11-22T15:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998126531467371 + ], + [ + "2025-11-22T15:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998330533181574 + ], + [ + "2025-11-22T15:49:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004602805067104 + ], + [ + "2025-11-22T15:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996875904972785 + ], + [ + "2025-11-22T15:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997262014954812 + ], + [ + "2025-11-22T15:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000550659484973 + ], + [ + "2025-11-22T15:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006697934060633 + ], + [ + "2025-11-22T15:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997835902221862 + ], + [ + "2025-11-22T15:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998081537996035 + ], + [ + "2025-11-22T15:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997494259423166 + ], + [ + "2025-11-22T15:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998039022053683 + ], + [ + "2025-11-22T15:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999756495288041 + ], + [ + "2025-11-22T15:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997292121473187 + ], + [ + "2025-11-22T15:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997361573253576 + ], + [ + "2025-11-22T15:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998082418617803 + ], + [ + "2025-11-22T15:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998095354902072 + ], + [ + "2025-11-22T15:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996994023197516 + ], + [ + "2025-11-22T15:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997314612119778 + ], + [ + "2025-11-22T15:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997495923225436 + ], + [ + "2025-11-22T15:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997548621990258 + ], + [ + "2025-11-22T15:13:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997548459707932 + ], + [ + "2025-11-22T15:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996403379324642 + ], + [ + "2025-11-22T15:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997520980278868 + ], + [ + "2025-11-22T15:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997710313681428 + ], + [ + "2025-11-22T15:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998330525045814 + ], + [ + "2025-11-22T15:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997243428939567 + ], + [ + "2025-11-22T15:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998634212035847 + ], + [ + "2025-11-22T14:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998394480123242 + ], + [ + "2025-11-22T14:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998568164903222 + ], + [ + "2025-11-22T14:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998464795763409 + ], + [ + "2025-11-22T14:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000537436477481 + ], + [ + "2025-11-22T14:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999931767937235 + ], + [ + "2025-11-22T14:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998706158679601 + ], + [ + "2025-11-22T14:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999768162286512 + ], + [ + "2025-11-22T14:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0003191014446335 + ], + [ + "2025-11-22T14:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999422370811337 + ], + [ + "2025-11-22T14:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999535439692198 + ], + [ + "2025-11-22T14:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999725454754167 + ], + [ + "2025-11-22T14:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0010332157084607 + ], + [ + "2025-11-22T14:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999350184996965 + ], + [ + "2025-11-22T14:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999642653200431 + ], + [ + "2025-11-22T14:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999292295440066 + ], + [ + "2025-11-22T14:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000302114037407 + ], + [ + "2025-11-22T14:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999919705605782 + ], + [ + "2025-11-22T14:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000004658922618 + ], + [ + "2025-11-22T14:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999360448528654 + ], + [ + "2025-11-22T14:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001042355523428 + ], + [ + "2025-11-22T14:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999780659340938 + ], + [ + "2025-11-22T14:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000278321130023 + ], + [ + "2025-11-22T14:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999032424941461 + ], + [ + "2025-11-22T14:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001978191453802 + ], + [ + "2025-11-22T14:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999802000611882 + ], + [ + "2025-11-22T14:10:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999808116185785 + ], + [ + "2025-11-22T14:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999690529740067 + ], + [ + "2025-11-22T14:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00105664910904 + ], + [ + "2025-11-22T14:04:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999418544073392 + ], + [ + "2025-11-22T14:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000319425282973 + ], + [ + "2025-11-22T13:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999566038909473 + ], + [ + "2025-11-22T13:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999633058173961 + ], + [ + "2025-11-22T13:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999396498094111 + ], + [ + "2025-11-22T13:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000078066079405 + ], + [ + "2025-11-22T13:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000212642448756 + ], + [ + "2025-11-22T13:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999868824092855 + ], + [ + "2025-11-22T13:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000005279476809 + ], + [ + "2025-11-22T13:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999953898462951 + ], + [ + "2025-11-22T13:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000024269915141 + ], + [ + "2025-11-22T13:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999459486426179 + ], + [ + "2025-11-22T13:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999380649797757 + ], + [ + "2025-11-22T13:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999817143477493 + ], + [ + "2025-11-22T13:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000017530469004 + ], + [ + "2025-11-22T13:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999785820481083 + ], + [ + "2025-11-22T13:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000145452032048 + ], + [ + "2025-11-22T13:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000255515692054 + ], + [ + "2025-11-22T13:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999926813282737 + ], + [ + "2025-11-22T13:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999978047096216 + ], + [ + "2025-11-22T13:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999571993032558 + ], + [ + "2025-11-22T13:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002918270491097 + ], + [ + "2025-11-22T13:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999933963450515 + ], + [ + "2025-11-22T13:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999281550152779 + ], + [ + "2025-11-22T13:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999632149428941 + ], + [ + "2025-11-22T13:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016113817371186 + ], + [ + "2025-11-22T13:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000679330649767 + ], + [ + "2025-11-22T13:09:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999050537875201 + ], + [ + "2025-11-22T13:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997527105258794 + ], + [ + "2025-11-22T13:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996949928314859 + ], + [ + "2025-11-22T13:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998422061412298 + ], + [ + "2025-11-22T13:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997180773712704 + ], + [ + "2025-11-22T12:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997929450650964 + ], + [ + "2025-11-22T12:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998643513083755 + ], + [ + "2025-11-22T12:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997266047280615 + ], + [ + "2025-11-22T12:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000005700486116 + ], + [ + "2025-11-22T12:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999924371247013 + ], + [ + "2025-11-22T12:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998210842343856 + ], + [ + "2025-11-22T12:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0007583902976782 + ], + [ + "2025-11-22T12:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999650240551233 + ], + [ + "2025-11-22T12:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999589389744945 + ], + [ + "2025-11-22T12:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000721842037343 + ], + [ + "2025-11-22T12:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998116199942813 + ], + [ + "2025-11-22T12:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998066461442215 + ], + [ + "2025-11-22T12:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998441351761722 + ], + [ + "2025-11-22T12:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996313342375991 + ], + [ + "2025-11-22T12:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998968861101516 + ], + [ + "2025-11-22T12:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999809818743353 + ], + [ + "2025-11-22T12:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998043504313151 + ], + [ + "2025-11-22T12:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996234405924262 + ], + [ + "2025-11-22T12:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998650572164521 + ], + [ + "2025-11-22T12:21:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998619125734516 + ], + [ + "2025-11-22T12:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998375770904521 + ], + [ + "2025-11-22T12:18:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996834583459275 + ], + [ + "2025-11-22T12:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998827852272673 + ], + [ + "2025-11-22T12:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999956665266741 + ], + [ + "2025-11-22T12:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000064312115147 + ], + [ + "2025-11-22T12:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998384494862979 + ], + [ + "2025-11-22T12:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998707096026573 + ], + [ + "2025-11-22T12:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999941227455828 + ], + [ + "2025-11-22T12:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000181279287834 + ], + [ + "2025-11-22T12:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998501394778735 + ], + [ + "2025-11-22T11:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999305850977248 + ], + [ + "2025-11-22T11:57:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999828067086285 + ], + [ + "2025-11-22T11:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002134750517002 + ], + [ + "2025-11-22T11:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999184202050897 + ], + [ + "2025-11-22T11:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999602053116604 + ], + [ + "2025-11-22T11:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999402178655429 + ], + [ + "2025-11-22T11:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002711983575892 + ], + [ + "2025-11-22T11:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999539324136274 + ], + [ + "2025-11-22T11:43:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999387123270301 + ], + [ + "2025-11-22T11:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998042551411237 + ], + [ + "2025-11-22T11:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006742049564978 + ], + [ + "2025-11-22T11:37:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999465492808643 + ], + [ + "2025-11-22T11:35:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999786573743963 + ], + [ + "2025-11-22T11:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997529478979107 + ], + [ + "2025-11-22T11:31:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996688442649257 + ], + [ + "2025-11-22T11:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999579793500869 + ], + [ + "2025-11-22T11:28:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000010982077464 + ], + [ + "2025-11-22T11:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997808434641452 + ], + [ + "2025-11-22T11:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997052314828981 + ], + [ + "2025-11-22T11:22:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999394423983278 + ], + [ + "2025-11-22T11:19:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000023207192186 + ], + [ + "2025-11-22T11:17:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998433247423024 + ], + [ + "2025-11-22T11:15:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996774779636146 + ], + [ + "2025-11-22T11:13:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000103258341069 + ], + [ + "2025-11-22T11:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001862856747967 + ], + [ + "2025-11-22T11:09:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999515494941421 + ], + [ + "2025-11-22T11:07:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996948036005658 + ], + [ + "2025-11-22T11:05:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.00007615463092 + ], + [ + "2025-11-22T11:03:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0002059528351688 + ], + [ + "2025-11-22T11:01:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999848255152454 + ], + [ + "2025-11-22T10:59:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997241756526897 + ], + [ + "2025-11-22T10:57:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000299287302967 + ], + [ + "2025-11-22T10:55:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0004619165704989 + ], + [ + "2025-11-22T10:53:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.999932115795213 + ], + [ + "2025-11-22T10:51:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998054804560648 + ], + [ + "2025-11-22T10:49:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0016882171776922 + ], + [ + "2025-11-22T10:47:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998897733863816 + ], + [ + "2025-11-22T10:45:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.000021243381761 + ], + [ + "2025-11-22T10:43:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998937011711151 + ], + [ + "2025-11-22T10:41:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001752242365653 + ], + [ + "2025-11-22T10:39:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999946270550053 + ], + [ + "2025-11-22T10:37:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0001095125172499 + ], + [ + "2025-11-22T10:35:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998459865513988 + ], + [ + "2025-11-22T10:33:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0018388555252709 + ], + [ + "2025-11-22T10:31:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0006475666765764 + ], + [ + "2025-11-22T10:29:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999585743240587 + ], + [ + "2025-11-22T10:27:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997972412319274 + ], + [ + "2025-11-22T10:25:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.001863731771352 + ], + [ + "2025-11-22T10:23:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0019007000000013 + ], + [ + "2025-11-22T08:29:48Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998397255000017 + ], + [ + "2025-11-22T07:41:12Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9994295961425436 + ], + [ + "2025-11-22T06:51:29Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997296150000017 + ], + [ + "2025-11-22T06:11:26Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997296150000042 + ], + [ + "2025-11-22T05:30:28Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997296150000021 + ], + [ + "2025-11-22T04:47:29Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997296150000013 + ], + [ + "2025-11-22T04:12:00Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998745972259941 + ], + [ + "2025-11-22T04:11:59Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9996890029367008 + ], + [ + "2025-11-22T03:21:26Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9999897449999995 + ], + [ + "2025-11-22T02:32:21Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 1.0000292639485056 + ], + [ + "2025-11-22T01:39:58Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9997696350000039 + ], + [ + "2025-11-22T00:58:16Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998046720670152 + ], + [ + "2025-11-22T00:19:05Z", + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "Ethereum", + null, + "MAIR120", + "USDC", + 0.9998295379999973 + ] + ] + } + ], + "Messages": null + } + ] +} diff --git a/apps/indexer/lib/indexer/memory/monitor.ex b/apps/indexer/lib/indexer/memory/monitor.ex index b25b2aaf0e67..c78898fd539a 100644 --- a/apps/indexer/lib/indexer/memory/monitor.ex +++ b/apps/indexer/lib/indexer/memory/monitor.ex @@ -105,7 +105,7 @@ defmodule Indexer.Memory.Monitor do default_limit = 1 <<< 30 percentage = - case Application.get_env(:explorer, :mode) do + case Explorer.mode() do :indexer -> 100 _ -> Application.get_env(:indexer, :system_memory_percentage) end diff --git a/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex b/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex index 5574da0a5117..cff45a22c31b 100644 --- a/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex +++ b/apps/nft_media_handler/lib/nft_media_handler/dispatcher_interface.ex @@ -17,7 +17,25 @@ defmodule NFTMediaHandler.DispatcherInterface do """ @impl true def init(_) do - {:ok, %{used_nodes: [], unused_nodes: []}} + {:ok, nil, {:continue, 1}} + end + + @impl true + def handle_continue(attempt, _state) do + attempt |> Kernel.**(3) |> :timer.seconds() |> :timer.sleep() + + get_indexer_nodes() + |> case do + [] -> + if attempt < 5 do + {:noreply, nil, {:continue, attempt + 1}} + else + raise "No indexer nodes discovered after #{attempt} attempts" + end + + nodes -> + {:noreply, %{used_nodes: [], unused_nodes: nodes}} + end end @doc """ @@ -29,8 +47,7 @@ defmodule NFTMediaHandler.DispatcherInterface do {used, unused, node_to_call} = case unused_nodes do [] -> - Node.list() - |> Enum.filter(&Helper.indexer_node?/1) + get_indexer_nodes() |> case do [] -> raise "No indexer nodes discovered" @@ -88,4 +105,9 @@ defmodule NFTMediaHandler.DispatcherInterface do defp remote_call(args, function, _node, false) do apply(Indexer.NFTMediaHandler.Queue, function, args) end + + defp get_indexer_nodes do + Node.list() + |> Enum.filter(&Helper.indexer_node?/1) + end end diff --git a/config/config_helper.exs b/config/config_helper.exs index 48cd6a9e67bb..d298ea084e72 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -275,6 +275,7 @@ defmodule ConfigHelper do | Source.CryptoCompare | Source.CryptoRank | Source.DefiLlama + | Source.DIA | Source.Mobula | nil def market_source(env_var) do @@ -284,6 +285,7 @@ defmodule ConfigHelper do "crypto_compare" => Source.CryptoCompare, "crypto_rank" => Source.CryptoRank, "defillama" => Source.DefiLlama, + "dia" => Source.DIA, "mobula" => Source.Mobula, "" => nil, nil => nil diff --git a/config/runtime.exs b/config/runtime.exs index 54eeac3b9404..6b5d08f09b0a 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -401,7 +401,7 @@ config :explorer, Explorer.Chain.Cache.Counters.AverageBlockTime, num_of_blocks: ConfigHelper.parse_integer_env_var("CACHE_AVERAGE_BLOCK_TIME_WINDOW", 100) config :explorer, Explorer.Market.MarketHistoryCache, - cache_period: ConfigHelper.parse_time_env_var("CACHE_MARKET_HISTORY_PERIOD", "6h") + cache_period: ConfigHelper.parse_time_env_var("CACHE_MARKET_HISTORY_PERIOD", "1h") config :explorer, Explorer.Chain.Cache.Counters.AddressTransactionsCount, cache_period: ConfigHelper.parse_time_env_var("CACHE_ADDRESS_TRANSACTIONS_COUNTER_PERIOD", "1h") @@ -427,6 +427,8 @@ config :explorer, Explorer.Chain.Cache.Counters.NewPendingTransactionsCount, cache_period: ConfigHelper.parse_time_env_var("CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD", "5m"), enable_consolidation: true +config :explorer, Explorer.Market, enabled: !disable_exchange_rates? + config :explorer, Explorer.Market.Source, native_coin_source: ConfigHelper.market_source("MARKET_NATIVE_COIN_SOURCE") || ConfigHelper.market_source("EXCHANGE_RATES_SOURCE"), @@ -508,6 +510,12 @@ config :explorer, Explorer.Market.Source.Mobula, secondary_coin_id: System.get_env("MARKET_MOBULA_SECONDARY_COIN_ID") || System.get_env("EXCHANGE_RATES_MOBULA_SECONDARY_COIN_ID") +config :explorer, Explorer.Market.Source.DIA, + blockchain: System.get_env("MARKET_DIA_BLOCKCHAIN"), + base_url: System.get_env("MARKET_DIA_BASE_URL", "https://api.diadata.org/v1"), + coin_address_hash: System.get_env("MARKET_DIA_COIN_ADDRESS_HASH"), + secondary_coin_address_hash: System.get_env("MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH") + config :explorer, Explorer.Market.Fetcher.Coin, store: :ets, enabled: !disable_exchange_rates? && ConfigHelper.parse_bool_env_var("MARKET_COIN_FETCHER_ENABLED", "true"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 714eb49872b5..d1ac31ed252f 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -82,6 +82,10 @@ DISABLE_MARKET=true # MARKET_MOBULA_API_KEY= # MARKET_MOBULA_COIN_ID= # MARKET_MOBULA_SECONDARY_COIN_ID= +# MARKET_DIA_BLOCKCHAIN= +# MARKET_DIA_BASE_URL= +# MARKET_DIA_COIN_ADDRESS_HASH= +# MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH= # MARKET_COIN_FETCHER_ENABLED= # MARKET_COIN_CACHE_PERIOD= # MARKET_TOKENS_FETCHER_ENABLED= From fbb16d5b0b71df34a177a1fd57b8928bf898d323 Mon Sep 17 00:00:00 2001 From: StackOverflowExcept1on <109800286+StackOverflowExcept1on@users.noreply.github.com> Date: Fri, 28 Nov 2025 16:45:46 +0300 Subject: [PATCH 0506/1006] chore: use chain id `31337` for `anvil` (#13644) --- docker-compose/anvil.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose/anvil.yml b/docker-compose/anvil.yml index 04cd475a6f18..af91124f099c 100644 --- a/docker-compose/anvil.yml +++ b/docker-compose/anvil.yml @@ -33,7 +33,7 @@ services: ETHEREUM_JSONRPC_WS_URL: ws://host.docker.internal:8545/ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER: 'true' INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER: 'true' - CHAIN_ID: '1337' + CHAIN_ID: '31337' visualizer: extends: @@ -52,7 +52,7 @@ services: file: ./services/frontend.yml service: frontend environment: - NEXT_PUBLIC_NETWORK_ID: '1337' + NEXT_PUBLIC_NETWORK_ID: '31337' NEXT_PUBLIC_NETWORK_RPC_URL: http://host.docker.internal:8545/ stats-db-init: From c93b592f4b95f6130fd6a12bd623ae2a164f16de Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 28 Nov 2025 19:18:13 +0300 Subject: [PATCH 0507/1006] Update common-blockscout.env --- docker-compose/envs/common-blockscout.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index d1ac31ed252f..45a2bc24808e 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -154,14 +154,14 @@ API_V1_WRITE_METHODS_DISABLED=false # API_RATE_LIMIT_BY_WHITELISTED_IP_TIME_INTERVAL=1s # API_RATE_LIMIT_UI_V2_WITH_TOKEN_TIME_INTERVAL=1s # API_RATE_LIMIT_BY_ACCOUNT_API_KEY_TIME_INTERVAL=1s -# API_RATE_LIMIT_BY_IP_TIME_INTERVAL=5m +# API_RATE_LIMIT_BY_IP_TIME_INTERVAL=1m # API_RATE_LIMIT=50 # API_RATE_LIMIT_BY_KEY=10 -# API_RATE_LIMIT_BY_WHITELISTED_IP=50 +# API_RATE_LIMIT_BY_WHITELISTED_IP=25 # API_RATE_LIMIT_WHITELISTED_IPS= # API_RATE_LIMIT_STATIC_API_KEY= # API_RATE_LIMIT_UI_V2_WITH_TOKEN=5 -# API_RATE_LIMIT_BY_IP=3000 +# API_RATE_LIMIT_BY_IP=300 # API_RATE_LIMIT_CONFIG_URL= # API_RATE_LIMIT_REMOTE_IP_HEADERS= # API_RATE_LIMIT_REMOTE_IP_KNOWN_PROXIES= From 437a4b73517cf5f02bb20b7de01c516eb9f421e5 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Dec 2025 12:48:38 +0300 Subject: [PATCH 0508/1006] 9.3.0 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- ...h-docker-image-for-optimism-worldchain.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 76 +++++++++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 62 files changed, 137 insertions(+), 61 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 6fa536069ee3..f49a32419cd8 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index d5f6f99c4e16..b8e5800efc20 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 50251f287a01..c41dffa21249 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index a6f664835633..34935fa4b42b 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index cb51492706c3..85ed3d662009 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 9cdbc7f7178d..71c19872260c 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index af8421e492cf..ef4fdc6ea23f 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 72b9c97752bd..5db3e5f288bf 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 4a98bda61744..912b936408b3 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 4c4cf34d5b72..69e40557d542 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 12fbdb8fcbbc..d153e5f52f99 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 4666f2493e0d..595270a627cf 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 0a6282db6382..2945f32d7f9d 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 2ba719dda421..307edef1ee33 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 810fea2195c6..53276c2a3a19 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index cb14bda0ce4f..8c723245e8a0 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 54536eb6e7ed..7a337ba907f6 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 9af3f85d2e4a..fa5a88414b22 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 1041c1b4d5a4..1e3e7615b39e 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 2f8538dbeb77..936fc44b5e96 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index ac02731804fa..c7ac4d9b40b1 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 178596481857..0b4242d041e0 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index edc5dcf91bbc..a89d9faa83b9 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 66aa400af280..62e6842f52e5 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 88976b6c6636..9302e07650c1 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index c3a310d21e84..f1b0a578a6f6 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index 600edab111c5..98328d91e129 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index ea544d4f0038..442e2474036c 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 48eb2975100f..d1652d7d29d3 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index e183f176af1b..d74844c8f0e3 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index c0d4e3e73d04..4db8b8c0a83e 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 5bca7d319212..2b9af018a4e7 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index f2c8d41300af..1f4466402baf 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index bac29c965387..09429c7dbb26 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index c6c2a77df141..4dc18558a052 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index be9a4c503ea2..d5a6af2b7d2f 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 27386dfd4a6b..210d3c1ef3d7 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: ${{ vars.OTP_VERSION }} ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index ba17ab56de03..1f2075858b7b 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index a9df88603646..a788ecbd74ea 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index e7627dfcf414..c4e900c18167 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index bab71a34a080..01c788947295 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 0fb1fb8b0dc3..814821d8920a 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index e144a483db34..3b5110bceae7 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index b50018ee61ed..17f2bf33fefd 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 0b159478ffd8..9eff900f67ba 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 2e451a9c1c61..d6fc9e35fdc2 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 0202d2bab00b..f23d4b4f134a 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index ba5f14520767..4beed960095d 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index d426b1a838c2..e95fdf172bee 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 7c8d8964d577..4818ab7ea8f8 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 9dd51b3960ce..2456a899624c 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.2.2 + RELEASE_VERSION: 9.3.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index e8503f4904d0..422da8d06597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,81 @@ # Changelog +## 9.3.0 + +### 🚀 Features + +- Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) +- Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) +- Support ZRC-2 tokens for `zilliqa` chain type ([#13443](https://github.com/blockscout/blockscout/issues/13443)) +- Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539)) +- Hot smart-contracts ([#13471](https://github.com/blockscout/blockscout/issues/13471)) +- Support OP Jovian upgrade, other enhancements ([#13538](https://github.com/blockscout/blockscout/issues/13538)) +- Scope celo under optimism chain type ([#13375](https://github.com/blockscout/blockscout/issues/13375)) + +### 🐛 Bug Fixes + +- `Mix.env()` in `runtime.exs` ([#13641](https://github.com/blockscout/blockscout/issues/13641)) +- Celo aggregated election rewards migrator test ([#13639](https://github.com/blockscout/blockscout/issues/13639)) +- Fix filecoin web tests ([#13634](https://github.com/blockscout/blockscout/issues/13634)) +- Fix dialyzer test for filecoin chain type ([#13623](https://github.com/blockscout/blockscout/issues/13623)) +- Handle deposit status statement too complex ([#13588](https://github.com/blockscout/blockscout/issues/13588)) +- Beacon deposits: fallback to node ([#13425](https://github.com/blockscout/blockscout/issues/13425)) +- Fix logic of disable token exchange rate ([#13414](https://github.com/blockscout/blockscout/issues/13414)) +- Null-checks for distribution field in celo epochs api ([#13457](https://github.com/blockscout/blockscout/issues/13457)) +- Reset ResetSanitizeDuplicatedLogsMigration status ([#13556](https://github.com/blockscout/blockscout/issues/13556)) +- Duplicated block numbers in int txs queue ([#13554](https://github.com/blockscout/blockscout/issues/13554)) +- Fix coin balance history - related normalize_balances_by_day/2 function ([#13515](https://github.com/blockscout/blockscout/issues/13515)) + +### 📚 Documentation + +- Update API endpoints descriptions in OpenAPI ([#13647](https://github.com/blockscout/blockscout/issues/13647)) + +### ⚡ Performance + +- Implement celo aggregated election rewards ([#13418](https://github.com/blockscout/blockscout/issues/13418)) + +### ⚙️ Miscellaneous Tasks + +- Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) +- Update devcontainer image to use Elixir 1.19.4 ([#13645](https://github.com/blockscout/blockscout/issues/13645)) +- Elixir 1.19.3 -> 1.19.4 ([#13643](https://github.com/blockscout/blockscout/issues/13643)) +- Update devcontainer image to use Elixir 1.19 ([#13637](https://github.com/blockscout/blockscout/issues/13637)) +- Internal transaction, Token transfer, Withdrawal, Smart-contracts, Main Page, Stats, Config and Search controllers OpenAPI specs ([#13557](https://github.com/blockscout/blockscout/issues/13557)) +- Using own runner for build ([#13624](https://github.com/blockscout/blockscout/issues/13624)) +- Drop token_instances_token_id_index index ([#13598](https://github.com/blockscout/blockscout/issues/13598)) +- Add migration to drop unique tokens_contract_address_hash_index index ([#13596](https://github.com/blockscout/blockscout/issues/13596)) +- Elixir 1.17 -> 1.19 ([#13566](https://github.com/blockscout/blockscout/issues/13566)) +- Handle `NativeCoin*ed` events on Arc chain to make dual token balances synced ([#13452](https://github.com/blockscout/blockscout/issues/13452)) +- Improve DeleteZeroValueInternalTransactions migration ([#13569](https://github.com/blockscout/blockscout/issues/13569)) +- Remove address-related props from sending to multichain service ([#13584](https://github.com/blockscout/blockscout/issues/13584)) +- Move not auth cookies to headers ([#13478](https://github.com/blockscout/blockscout/issues/13478)) +- Transaction controller OpenAPI spec ([#13419](https://github.com/blockscout/blockscout/issues/13419)) +- Increase genesis file content fetch timeout ([#13527](https://github.com/blockscout/blockscout/issues/13527)) + +### New ENV variables +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `INDEXER_DISABLE_HOT_SMART_CONTRACTS_FETCHER` | If `true`, `Indexer.Fetcher.Stats.HotSmartContracts` won't be started. Implemented in [#13471](https://github.com/blockscout/blockscout/pull/13471). | Version: v9.3.0\+
Default: `false`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED` | Enable of delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.3.0\+
Default: `false`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE` | Specifies the block batch size selected for the delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.3.0\+
Default: `100`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` | Specifies the period for which recent zero-value calls won't be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.3.0\+
Default: `30`
Applications: Indexer | +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL` | Specifies the interval between checking of new zero-value calls to be deleted in delete zero-value calls migration. Implemented in [#13305](https://github.com/blockscout/blockscout/pull/13305). | Version: v9.3.0\+
Default: `1m`
Applications: Indexer | +| `MARKET_DIA_BLOCKCHAIN` | Sets DIA platform from https://www.diadata.org/docs/reference/apis/token-prices/api-endpoints/blockchains. Implemented in [#12678](https://github.com/blockscout/blockscout/pull/12678). | Version: v9.3.0\+
Default: (empty)
Applications: Indexer | +| `MARKET_DIA_BASE_URL` | If set, overrides the DIA API url. Implemented in [#12678](https://github.com/blockscout/blockscout/pull/12678). | Version: v9.3.0\+
Default: `https://api.diadata.org/v1`
Applications: API, Indexer | +| `MARKET_DIA_COIN_ADDRESS_HASH` | Sets address hash for native coin in DIA. Implemented in [#12678](https://github.com/blockscout/blockscout/pull/12678). | Version: v9.3.0\+
Default: (empty)
Applications: API | +| `MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH` | Sets address hash for secondary coin in DIA. Implemented in [#12678](https://github.com/blockscout/blockscout/pull/12678). | Version: v9.3.0\+
Default: (empty)
Applications: API | +| `INDEXER_METRICS_ENABLED` | Flag to enable base indexer metrics. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: true
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT` | Flag to enable indexer metric: the count of token instances not uploaded to CDN. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: false
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT` | Flag to enable indexer metric: the count of token instances with failed metadata fetches. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: true
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT` | Flag to enable indexer metric: the count of token instances pending to fetch. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: true
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT` | Flag to enable indexer metric: the count of current token balances with missing values. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: true
Applications: Indexer | +| `INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT` | Flag to enable indexer metric: the count of archival token balances with missing values. Implemented in [#13539](https://github.com/blockscout/blockscout/pull/13539). | Version: v9.3.0\+
Default: true
Applications: Indexer | +| `INDEXER_OPTIMISM_L2_JOVIAN_TIMESTAMP` | Jovian upgrade L2 block timestamp. If set to `0`, the Jovian is assumed to be active from genesis block. Implemented in [#13538](https://github.com/blockscout/blockscout/pull/13538). | Version: v9.3.0+
Default: (empty)
Applications: API, Indexer | +| `INDEXER_ARC_NATIVE_TOKEN_DECIMALS` | Defines the number of decimals for Arc chain native token (e.g. USDC). Implemented in [#13452](https://github.com/blockscout/blockscout/pull/13452). | Version: v9.3.0+
Default: `6`
Applications: Indexer | +| `INDEXER_ARC_NATIVE_TOKEN_CONTRACT` | Arc chain native token contract address. Implemented in [#13452](https://github.com/blockscout/blockscout/pull/13452). | Version: v9.3.0+
Default: `0x3600000000000000000000000000000000000000`
Applications: Indexer | +| `INDEXER_ARC_NATIVE_TOKEN_SYSTEM_CONTRACT` | Arc chain system contract address emitting `NativeCoinTransferred` event. Implemented in [#13452](https://github.com/blockscout/blockscout/pull/13452). | Version: v9.3.0+
Default: `0x1800000000000000000000000000000000000000`
Applications: Indexer | + + ## 9.2.2 ### 🐛 Bug Fixes diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 3acc56b6b3f2..738851261884 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.2.2", + version: "9.3.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index ab655560dcaf..857a1afa7939 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -19,7 +19,7 @@ defmodule EthereumJSONRPC.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.2.2" + version: "9.3.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 70a3761fab13..431ccbe37b25 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.2.2"}] + @default_headers [{"User-Agent", "blockscout-9.3.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index b873945aa7a8..21bb576dc31b 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -20,7 +20,7 @@ defmodule Explorer.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.2.2", + version: "9.3.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 65f7637b9625..b9d67bf8d207 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.2.2", + version: "9.3.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 0fc135b4aad7..a314adb137c8 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.2.2", + version: "9.3.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index 7dd3598e1a77..addda8865dbb 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.2.2", + version: "9.3.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 3a6653f78529..e51a7fc2ccf8 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.2.2' +RELEASE_VERSION ?= '9.3.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index f00d258ab37c..255b2297c17f 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.2.2", + version: "9.3.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 0d6743c05104..eb883626e146 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.2.2" + set version: "9.3.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From 9b6d80072e3f30613ff7945d3cb30228ed7e2ad8 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 1 Dec 2025 15:37:25 +0300 Subject: [PATCH 0509/1006] fix: Enhance drop_tokens_contract_address_hash_index DB migration (#13655) --- CHANGELOG.md | 2 +- ...rop_tokens_contract_address_hash_index.exs | 41 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 422da8d06597..df29e996bd71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ - Internal transaction, Token transfer, Withdrawal, Smart-contracts, Main Page, Stats, Config and Search controllers OpenAPI specs ([#13557](https://github.com/blockscout/blockscout/issues/13557)) - Using own runner for build ([#13624](https://github.com/blockscout/blockscout/issues/13624)) - Drop token_instances_token_id_index index ([#13598](https://github.com/blockscout/blockscout/issues/13598)) -- Add migration to drop unique tokens_contract_address_hash_index index ([#13596](https://github.com/blockscout/blockscout/issues/13596)) +- Add migration to drop unique tokens_contract_address_hash_index index ([#13596](https://github.com/blockscout/blockscout/issues/13596), [#13655](https://github.com/blockscout/blockscout/pull/13655)) - Elixir 1.17 -> 1.19 ([#13566](https://github.com/blockscout/blockscout/issues/13566)) - Handle `NativeCoin*ed` events on Arc chain to make dual token balances synced ([#13452](https://github.com/blockscout/blockscout/issues/13452)) - Improve DeleteZeroValueInternalTransactions migration ([#13569](https://github.com/blockscout/blockscout/issues/13569)) diff --git a/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs b/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs index 0410ca660104..abd0461c3be1 100644 --- a/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs +++ b/apps/explorer/priv/repo/migrations/20251115202635_drop_tokens_contract_address_hash_index.exs @@ -15,7 +15,7 @@ defmodule Explorer.Repo.Migrations.DropTokensContractAddressHashIndex do ) THEN RAISE NOTICE 'Index tokens_contract_address_hash_index exists. Proceeding...'; - -- 2. Drop the FK constraint (only if it exists) + -- 2. Drop the token_instances_token_contract_address_hash_fkey FK constraint (only if it exists) IF EXISTS ( SELECT 1 FROM pg_constraint @@ -29,11 +29,31 @@ defmodule Explorer.Repo.Migrations.DropTokensContractAddressHashIndex do '; END IF; - -- 3. Drop the redundant index + -- 3. Drop the bridged_tokens_home_token_contract_address_hash_fkey FK constraint (only if it exists) + IF EXISTS ( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'bridged_tokens' AND n.nspname = 'public' + ) THEN + IF EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'bridged_tokens_home_token_contract_address_hash_fkey' + AND conrelid = 'public.bridged_tokens'::regclass + ) THEN + RAISE NOTICE 'Dropping foreign key bridged_tokens_home_token_contract_address_hash_fkey...'; + EXECUTE ' + ALTER TABLE public.bridged_tokens + DROP CONSTRAINT bridged_tokens_home_token_contract_address_hash_fkey + '; + END IF; + END IF; + + -- 4. Drop the redundant index RAISE NOTICE 'Dropping index tokens_contract_address_hash_index...'; EXECUTE 'DROP INDEX public.tokens_contract_address_hash_index'; - -- 4. Recreate the FK constraint + -- 5. Recreate the token_instances_token_contract_address_hash_fkey FK constraint RAISE NOTICE 'Recreating foreign key token_instances_token_contract_address_hash_fkey...'; EXECUTE ' ALTER TABLE public.token_instances @@ -41,6 +61,21 @@ defmodule Explorer.Repo.Migrations.DropTokensContractAddressHashIndex do FOREIGN KEY (token_contract_address_hash) REFERENCES public.tokens(contract_address_hash) '; + + -- 6. Recreate the bridged_tokens_home_token_contract_address_hash_fkey FK constraint (only if table exists) + IF EXISTS ( + SELECT 1 FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relname = 'bridged_tokens' AND n.nspname = 'public' + ) THEN + RAISE NOTICE 'Recreating foreign key bridged_tokens_home_token_contract_address_hash_fkey...'; + EXECUTE ' + ALTER TABLE public.bridged_tokens + ADD CONSTRAINT bridged_tokens_home_token_contract_address_hash_fkey + FOREIGN KEY (home_token_contract_address_hash) + REFERENCES public.tokens(contract_address_hash) + '; + END IF; ELSE RAISE NOTICE 'Index tokens_contract_address_hash_index does NOT exist. Nothing to do.'; END IF; From 031b1c53ed88417549e9fdf9cd5406be4158cc48 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:44:42 +0300 Subject: [PATCH 0510/1006] fix: deposits constraints drop deadlock (#13656) Co-authored-by: Victor Baranov --- CHANGELOG.md | 2 +- .../migrations/20251029090030_deposits_remove_foreign_keys.exs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df29e996bd71..ba56729c104f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,7 +19,7 @@ - Fix filecoin web tests ([#13634](https://github.com/blockscout/blockscout/issues/13634)) - Fix dialyzer test for filecoin chain type ([#13623](https://github.com/blockscout/blockscout/issues/13623)) - Handle deposit status statement too complex ([#13588](https://github.com/blockscout/blockscout/issues/13588)) -- Beacon deposits: fallback to node ([#13425](https://github.com/blockscout/blockscout/issues/13425)) +- Beacon deposits: fallback to node ([#13425](https://github.com/blockscout/blockscout/issues/13425), [#13656](https://github.com/blockscout/blockscout/pull/13656)) - Fix logic of disable token exchange rate ([#13414](https://github.com/blockscout/blockscout/issues/13414)) - Null-checks for distribution field in celo epochs api ([#13457](https://github.com/blockscout/blockscout/issues/13457)) - Reset ResetSanitizeDuplicatedLogsMigration status ([#13556](https://github.com/blockscout/blockscout/issues/13556)) diff --git a/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs b/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs index eca7de796530..dbd0056c8caf 100644 --- a/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs +++ b/apps/explorer/priv/beacon/migrations/20251029090030_deposits_remove_foreign_keys.exs @@ -1,6 +1,9 @@ defmodule Explorer.Repo.Beacon.Migrations.DepositsRemoveForeignKeys do use Ecto.Migration + @disable_ddl_transaction true + @disable_migration_lock true + def change do drop_if_exists(constraint(:beacon_deposits, :beacon_deposits_block_hash_fkey)) drop_if_exists(constraint(:beacon_deposits, :beacon_deposits_from_address_hash_fkey)) From 5ccc68bfdf4ddd1fd4f69803878084ca8f4ae8b7 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 2 Dec 2025 18:44:34 +0300 Subject: [PATCH 0511/1006] chore: Enhance unique index for (identifier, abi) bundle in `contract_methods` table (#13659) --- .../lib/explorer/chain/contract_method.ex | 5 ++++- .../lib/explorer/chain/smart_contract.ex | 2 +- .../lib/explorer/smart_contract/helper.ex | 8 ++++++-- ...0_update_contract_methods_unique_index.exs | 19 +++++++++++++++++++ apps/explorer/test/support/factory.ex | 2 +- 5 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20251202092200_update_contract_methods_unique_index.exs diff --git a/apps/explorer/lib/explorer/chain/contract_method.ex b/apps/explorer/lib/explorer/chain/contract_method.ex index 1bf5d6f865cc..993dcfc954cd 100644 --- a/apps/explorer/lib/explorer/chain/contract_method.ex +++ b/apps/explorer/lib/explorer/chain/contract_method.ex @@ -44,7 +44,10 @@ defmodule Explorer.Chain.ContractMethod do # Enforce ContractMethod ShareLocks order (see docs: sharelocks.md) ordered_successes = Enum.sort_by(successes, &{&1.identifier, &1.abi}) - Repo.insert_all(__MODULE__, ordered_successes, on_conflict: :nothing, conflict_target: [:identifier, :abi]) + Repo.insert_all(__MODULE__, ordered_successes, + on_conflict: :nothing, + conflict_target: {:unsafe_fragment, ~s<(identifier, md5(abi::text))>} + ) end def import_all do diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index dc0195912741..f9173392cac6 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -821,7 +821,7 @@ defmodule Explorer.Chain.SmartContract do contract_code_md5 = target_address.contract_code.bytes - |> Helper.contract_code_md5() + |> Helper.md5() verified_bytecode_twin_contract_query = from( diff --git a/apps/explorer/lib/explorer/smart_contract/helper.ex b/apps/explorer/lib/explorer/smart_contract/helper.ex index 769502b0d6a0..b9d0ea58b858 100644 --- a/apps/explorer/lib/explorer/smart_contract/helper.ex +++ b/apps/explorer/lib/explorer/smart_contract/helper.ex @@ -66,7 +66,11 @@ defmodule Explorer.SmartContract.Helper do def add_contract_code_md5(attrs), do: attrs - def contract_code_md5(bytes) do + @doc """ + Calculates MD5 hash of given binary. + """ + @spec md5(binary()) :: String.t() + def md5(bytes) do :md5 |> :crypto.hash(bytes) |> Base.encode16(case: :lower) @@ -74,7 +78,7 @@ defmodule Explorer.SmartContract.Helper do defp attrs_extend_with_contract_code_md5(attrs, address) do if address.contract_code do - contract_code_md5 = contract_code_md5(address.contract_code.bytes) + contract_code_md5 = md5(address.contract_code.bytes) attrs |> Map.put_new(:contract_code_md5, contract_code_md5) diff --git a/apps/explorer/priv/repo/migrations/20251202092200_update_contract_methods_unique_index.exs b/apps/explorer/priv/repo/migrations/20251202092200_update_contract_methods_unique_index.exs new file mode 100644 index 000000000000..47b923892b85 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251202092200_update_contract_methods_unique_index.exs @@ -0,0 +1,19 @@ +defmodule Explorer.Repo.Migrations.UpdateContractMethodsUniqueIndex do + use Ecto.Migration + + def up do + create( + unique_index(:contract_methods, [:identifier, "md5(abi::text)"], name: :contract_methods_identifier_md5_abi_index) + ) + + drop(unique_index(:contract_methods, [:identifier, :abi], name: :contract_methods_identifier_abi_index)) + end + + def down do + create(unique_index(:contract_methods, [:identifier, :abi], name: :contract_methods_identifier_abi_index)) + + drop( + unique_index(:contract_methods, [:identifier, "md5(abi::text)"], name: :contract_methods_identifier_md5_abi_index) + ) + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index ffb9a736a13a..d4a4b09cbec5 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1105,7 +1105,7 @@ defmodule Explorer.Factory do contract_code_info = contract_code_info() {:ok, data} = Explorer.Chain.Data.cast(contract_code_info.bytecode) - bytecode_md5 = Helper.contract_code_md5(data.bytes) + bytecode_md5 = Helper.md5(data.bytes) %SmartContract{ address_hash: insert(:address, contract_code: contract_code_info.bytecode, verified: true).hash, From 47fd3446fa3e6736095783446082ac33ff4461f9 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 3 Dec 2025 20:47:36 +0300 Subject: [PATCH 0512/1006] fix: Fix 500 for pending tx in `gettxinfo` RPC API endpoint (#13665) --- CHANGELOG.md | 1 + .../lib/block_scout_web/views/api/rpc/transaction_view.ex | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba56729c104f..691b8ced35bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ ### ⚙️ Miscellaneous Tasks +- Fix 500 for pending tx in gettxinfo RPC API endpoint([#13665](https://github.com/blockscout/blockscout/pull/13665)) - Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) - Update devcontainer image to use Elixir 1.19.4 ([#13645](https://github.com/blockscout/blockscout/issues/13645)) - Elixir 1.19.3 -> 1.19.4 ([#13643](https://github.com/blockscout/blockscout/issues/13643)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 74c8ba5a32e1..3ddaed62b8b6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -61,7 +61,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do "hash" => "#{transaction.hash}", "timeStamp" => "#{DateTime.to_unix(Transaction.block_timestamp(transaction))}", "blockNumber" => "#{transaction.block_number}", - "confirmations" => "#{block_height - transaction.block_number}", + "confirmations" => + if(is_nil(transaction.block_number), do: "0", else: "#{block_height - transaction.block_number}"), "success" => if(transaction.status == :ok, do: true, else: false), "from" => "#{transaction.from_address_hash}", "to" => "#{transaction.to_address_hash}", From 76b245b9e9c8da8df573a5fb6d78bae35bf68d70 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 3 Dec 2025 21:34:26 +0300 Subject: [PATCH 0513/1006] fix: Fix 500 for pending tx in `tokentx` RPC API endpoint (#13666) --- CHANGELOG.md | 3 ++- .../views/api/rpc/address_view.ex | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 691b8ced35bf..640f9d7cee6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ ### 🐛 Bug Fixes +- Fix 500 for pending tx in tokentx RPC API endpoint ([#13666](https://github.com/blockscout/blockscout/pull/13666)) +- Fix 500 for pending tx in gettxinfo RPC API endpoint([#13665](https://github.com/blockscout/blockscout/pull/13665)) - `Mix.env()` in `runtime.exs` ([#13641](https://github.com/blockscout/blockscout/issues/13641)) - Celo aggregated election rewards migrator test ([#13639](https://github.com/blockscout/blockscout/issues/13639)) - Fix filecoin web tests ([#13634](https://github.com/blockscout/blockscout/issues/13634)) @@ -36,7 +38,6 @@ ### ⚙️ Miscellaneous Tasks -- Fix 500 for pending tx in gettxinfo RPC API endpoint([#13665](https://github.com/blockscout/blockscout/pull/13665)) - Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) - Update devcontainer image to use Elixir 1.19.4 ([#13645](https://github.com/blockscout/blockscout/issues/13645)) - Elixir 1.19.3 -> 1.19.4 ([#13643](https://github.com/blockscout/blockscout/issues/13643)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 94d24120f93f..58e7c18f240b 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -157,9 +157,21 @@ defmodule BlockScoutWeb.API.RPC.AddressView do defp prepare_common_token_transfer(token_transfer, max_block_number, decoded_input) do tt_denormalization_fields = if DenormalizationHelper.tt_denormalization_finished?() do - %{"timeStamp" => to_string(DateTime.to_unix(token_transfer.transaction.block_timestamp))} + %{ + "timeStamp" => + if(token_transfer.transaction.block_timestamp, + do: to_string(DateTime.to_unix(token_transfer.transaction.block_timestamp)), + else: "" + ) + } else - %{"timeStamp" => to_string(DateTime.to_unix(token_transfer.block.timestamp))} + %{ + "timeStamp" => + if(token_transfer.block.timestamp, + do: to_string(DateTime.to_unix(token_transfer.block.timestamp)), + else: "" + ) + } end %{ From c5e3980c304d086812d8e8c96ea5eeb82a292a13 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Dec 2025 12:24:13 +0300 Subject: [PATCH 0514/1006] fix: Indexer Prometheus metrics infinite timeout (#13668) --- CHANGELOG.md | 2 +- .../chain/metrics/queries/indexer_metrics.ex | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640f9d7cee6e..4b3c217addf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) - Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) - Support ZRC-2 tokens for `zilliqa` chain type ([#13443](https://github.com/blockscout/blockscout/issues/13443)) -- Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539)) +- Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539), [#13668](https://github.com/blockscout/blockscout/pull/13668)) - Hot smart-contracts ([#13471](https://github.com/blockscout/blockscout/issues/13471)) - Support OP Jovian upgrade, other enhancements ([#13538](https://github.com/blockscout/blockscout/issues/13538)) - Scope celo under optimism chain type ([#13375](https://github.com/blockscout/blockscout/issues/13375)) diff --git a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex index 30caccbfb3af..fcb0d9d9b600 100644 --- a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex +++ b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex @@ -30,7 +30,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ); """ - case SQL.query(Repo, sql_string) do + case SQL.query(Repo, sql_string, [], timeout: :infinity) do {:ok, %Postgrex.Result{command: :select, columns: ["missing_blocks_count"], rows: [[missing_blocks_count]]}} -> missing_blocks_count @@ -44,7 +44,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do """ @spec missing_internal_transactions_count() :: integer() def missing_internal_transactions_count do - Repo.aggregate(PendingBlockOperation, :count, :block_hash) + Repo.aggregate(PendingBlockOperation, :count, :block_hash, timeout: :infinity) end @doc """ @@ -66,7 +66,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ); """ - case SQL.query(Repo, sql_string) do + case SQL.query(Repo, sql_string, [], timeout: :infinity) do {:ok, %Postgrex.Result{ command: :select, @@ -92,7 +92,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ) query - |> Repo.aggregate(:count, :id) + |> Repo.aggregate(:count, :id, timeout: :infinity) end @doc """ @@ -107,7 +107,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ) query - |> Repo.aggregate(:count, :token_id) + |> Repo.aggregate(:count, :token_id, timeout: :infinity) end @doc """ @@ -137,7 +137,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ) AS a; """ - case SQL.query(Repo, sql_string) do + case SQL.query(Repo, sql_string, [], timeout: :infinity) do {:ok, %Postgrex.Result{ command: :select, @@ -163,7 +163,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do FROM token_instances WHERE metadata IS NOT NULL AND thumbnails IS NULL AND cdn_upload_error IS NULL; """ - case SQL.query(Repo, sql_string) do + case SQL.query(Repo, sql_string, [], timeout: :infinity) do {:ok, %Postgrex.Result{ command: :select, @@ -182,7 +182,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do """ @spec multichain_search_db_export_balances_queue_count() :: integer() def multichain_search_db_export_balances_queue_count do - Repo.aggregate(MultichainSearchDbBalancesExportQueue, :count, :id) + Repo.aggregate(MultichainSearchDbBalancesExportQueue, :count, :id, timeout: :infinity) end @doc """ @@ -190,7 +190,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do """ @spec multichain_search_db_export_counters_queue_count() :: integer() def multichain_search_db_export_counters_queue_count do - Repo.aggregate(MultichainSearchDbCountersExportQueue, :count, :timestamp) + Repo.aggregate(MultichainSearchDbCountersExportQueue, :count, :timestamp, timeout: :infinity) end @doc """ @@ -198,7 +198,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do """ @spec multichain_search_db_export_token_info_queue_count() :: integer() def multichain_search_db_export_token_info_queue_count do - Repo.aggregate(MultichainSearchDbTokenInfoExportQueue, :count, :address_hash) + Repo.aggregate(MultichainSearchDbTokenInfoExportQueue, :count, :address_hash, timeout: :infinity) end @doc """ @@ -206,6 +206,6 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do """ @spec multichain_search_db_main_export_queue_count() :: integer() def multichain_search_db_main_export_queue_count do - Repo.aggregate(MultichainSearchDbMainExportQueue, :count, :hash) + Repo.aggregate(MultichainSearchDbMainExportQueue, :count, :hash, timeout: :infinity) end end From f315f6cbe75f34901be09e63a21b1aa72b27004e Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 4 Dec 2025 13:26:35 +0300 Subject: [PATCH 0515/1006] fix: Add missing infinity timeout to hot contracts requests (#13669) Co-authored-by: Victor Baranov --- CHANGELOG.md | 2 +- apps/explorer/lib/explorer/stats/hot_smart_contracts.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3c217addf9..6e2120deb9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) - Support ZRC-2 tokens for `zilliqa` chain type ([#13443](https://github.com/blockscout/blockscout/issues/13443)) - Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539), [#13668](https://github.com/blockscout/blockscout/pull/13668)) -- Hot smart-contracts ([#13471](https://github.com/blockscout/blockscout/issues/13471)) +- Hot smart-contracts ([#13471](https://github.com/blockscout/blockscout/issues/13471), [#13669](https://github.com/blockscout/blockscout/pull/13669)) - Support OP Jovian upgrade, other enhancements ([#13538](https://github.com/blockscout/blockscout/issues/13538)) - Scope celo under optimism chain type ([#13375](https://github.com/blockscout/blockscout/issues/13375)) diff --git a/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex index 16ff4c8a7d47..abdc06480a1e 100644 --- a/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex +++ b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex @@ -50,7 +50,7 @@ defmodule Explorer.Stats.HotSmartContracts do {:ok, from_block |> aggregate_hot_smart_contracts_for_block_interval_query(to_block) - |> Chain.select_repo(options).all() + |> Chain.select_repo(options).all(timeout: :infinity) |> Enum.map(&Map.put(&1, :date, date))} else error -> {:error, error} @@ -80,7 +80,7 @@ defmodule Explorer.Stats.HotSmartContracts do __MODULE__ |> select([hot_smart_contracts_daily], hot_smart_contracts_daily.date) |> distinct(true) - |> Chain.select_repo(options).all() + |> Chain.select_repo(options).all(timeout: :infinity) end @spec delete_older_than(Date.t(), keyword()) :: {non_neg_integer(), nil} From f27dd1c484990f0642f53de06b69a9b0add1d22c Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:02:01 +0300 Subject: [PATCH 0516/1006] chore: Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env (#13671) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + .../test/explorer/config_helper_test.exs | 53 +++++++++++++++++++ config/config_helper.exs | 12 +++++ config/runtime.exs | 2 +- 4 files changed, 67 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e2120deb9f2..15ac27e55e24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ ### ⚙️ Miscellaneous Tasks +- Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env ([#13671](https://github.com/blockscout/blockscout/pull/13671)) - Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) - Update devcontainer image to use Elixir 1.19.4 ([#13645](https://github.com/blockscout/blockscout/issues/13645)) - Elixir 1.19.3 -> 1.19.4 ([#13643](https://github.com/blockscout/blockscout/issues/13643)) diff --git a/apps/explorer/test/explorer/config_helper_test.exs b/apps/explorer/test/explorer/config_helper_test.exs index 08fe9728ca8e..017473298b69 100644 --- a/apps/explorer/test/explorer/config_helper_test.exs +++ b/apps/explorer/test/explorer/config_helper_test.exs @@ -40,6 +40,59 @@ defmodule ConfigHelperTest do end end + describe "parse_path_env_var/2" do + test "common case" do + System.put_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "test") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER") == "/test" + end + + test "don't use defined default" do + System.put_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "test") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") == "/test" + end + + test "don't prepend / if path already has it" do + System.put_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "/test") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") == "/test" + end + + test "using defined fallback default" do + System.delete_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") == "/default" + end + + test "invalid path" do + System.put_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "//test") + + assert_raise RuntimeError, "Invalid path in environment variable NFT_MEDIA_HANDLER_BUCKET_FOLDER: //test", fn -> + ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER") + end + end + + test "invalid path with default" do + System.put_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "//test") + + assert_raise RuntimeError, "Invalid path in environment variable NFT_MEDIA_HANDLER_BUCKET_FOLDER: //test", fn -> + ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") + end + end + + test "empty path with default" do + System.delete_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") == "/default" + end + + test "nil path" do + System.delete_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER") == nil + end + + test "nil path with default" do + System.delete_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER") + assert ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER", "default") == "/default" + end + end + defp clear_env_variables do System.delete_env("ETHEREUM_JSONRPC_HTTP_URLS") System.delete_env("ETHEREUM_JSONRPC_HTTP_URL") diff --git a/config/config_helper.exs b/config/config_helper.exs index d298ea084e72..a26621cdb43e 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -246,6 +246,18 @@ defmodule ConfigHelper do def parse_bool_env_var(env_var, default_value \\ "false"), do: String.downcase(safe_get_env(env_var, default_value)) == "true" + @spec parse_path_env_var(String.t(), String.t() | nil) :: String.t() | nil + def parse_path_env_var(env_var, default_value \\ nil) do + env_var + |> System.get_env(default_value) + |> case do + "//" <> _ = path -> raise "Invalid path in environment variable #{env_var}: #{path}" + "/" <> _ = path -> path + path when is_binary(path) -> "/" <> path + other -> other + end + end + @spec cache_ttl_check_interval(boolean()) :: non_neg_integer() | false def cache_ttl_check_interval(disable_indexer?) do if(disable_indexer?, do: :timer.seconds(1), else: false) diff --git a/config/runtime.exs b/config/runtime.exs index 6b5d08f09b0a..a47645850e92 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1643,7 +1643,7 @@ config :nft_media_handler, tmp_dir: "./temp", remote?: nmh_remote?, worker?: nmh_worker?, - r2_folder: System.get_env("NFT_MEDIA_HANDLER_BUCKET_FOLDER"), + r2_folder: ConfigHelper.parse_path_env_var("NFT_MEDIA_HANDLER_BUCKET_FOLDER"), standalone_media_worker?: nmh_enabled? && nmh_remote? && nmh_worker?, worker_concurrency: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_WORKER_CONCURRENCY", 10), worker_batch_size: ConfigHelper.parse_integer_env_var("NFT_MEDIA_HANDLER_WORKER_BATCH_SIZE", 10), From ffdd97616a835b7c376841bb1b495926147aafce Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Dec 2025 14:04:13 +0300 Subject: [PATCH 0517/1006] chore: Pin indexer monitor metrics to indexer pod (#13670) --- CHANGELOG.md | 2 +- .../lib/block_scout_web/application.ex | 106 ++++++++---------- .../lib/explorer/prometheus/instrumenter.ex | 90 --------------- apps/indexer/lib/indexer/application.ex | 5 +- .../lib/indexer/prometheus/instrumenter.ex | 90 +++++++++++++++ .../lib/indexer/prometheus/metrics.ex} | 8 +- config/runtime.exs | 30 ++--- 7 files changed, 161 insertions(+), 170 deletions(-) rename apps/{explorer/lib/explorer/chain/metrics/indexer_metrics.ex => indexer/lib/indexer/prometheus/metrics.ex} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ac27e55e24..fd48ffe18e74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) - Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) - Support ZRC-2 tokens for `zilliqa` chain type ([#13443](https://github.com/blockscout/blockscout/issues/13443)) -- Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539), [#13668](https://github.com/blockscout/blockscout/pull/13668)) +- Indexer monitor Prometheus metrics ([#13539](https://github.com/blockscout/blockscout/issues/13539), [#13668](https://github.com/blockscout/blockscout/pull/13668), [#13670](https://github.com/blockscout/blockscout/pull/13670)) - Hot smart-contracts ([#13471](https://github.com/blockscout/blockscout/issues/13471), [#13669](https://github.com/blockscout/blockscout/pull/13669)) - Support OP Jovian upgrade, other enhancements ([#13538](https://github.com/blockscout/blockscout/issues/13538)) - Scope celo under optimism chain type ([#13375](https://github.com/blockscout/blockscout/issues/13375)) diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index cef8ca91acca..f90e617e3e3c 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -4,52 +4,32 @@ defmodule BlockScoutWeb.Application do """ use Application - use Utils.CompileTimeEnvHelper, disable_api?: [:block_scout_web, :disable_api?] alias BlockScoutWeb.{Endpoint, HealthEndpoint, RateLimit.Hammer} alias BlockScoutWeb.Utility.RateLimitConfigHelper + alias Explorer - if @disable_api? do - def start(_type, _args) do - opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] - - if Application.get_env(:nft_media_handler, :standalone_media_worker?) do - Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) - else - # Endpoint must be the last child in the supervision tree - # since it must be started after all of the other processes - # (to be sure that application is ready to handle traffic) - # and stopped before them for the same reason. - # However, some processes may depend on Endpoint - # so they need to be started after. - base_children = [Supervisor.child_spec(Endpoint, [])] - {first_api_children, last_api_children} = setup_and_define_children() - all_children = first_api_children ++ base_children ++ last_api_children - - Supervisor.start_link(all_children, opts) - end - end - else - def start(_type, _args) do - opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + def start(_type, _args) do + opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + if Explorer.mode() in [:api, :all] do RateLimitConfigHelper.store_rate_limit_config() + end - if Application.get_env(:nft_media_handler, :standalone_media_worker?) do - Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) - else - # Endpoint must be the last child in the supervision tree - # since it must be started after all of the other processes - # (to be sure that application is ready to handle traffic) - # and stopped before them for the same reason. - # However, some processes may depend on Endpoint - # so they need to be started after. - base_children = [Supervisor.child_spec(Endpoint, [])] - {first_api_children, last_api_children} = setup_and_define_children() - all_children = first_api_children ++ base_children ++ last_api_children - - Supervisor.start_link(all_children, opts) - end + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) + else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. + base_children = [Supervisor.child_spec(Endpoint, [])] + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children + + Supervisor.start_link(all_children, opts) end end @@ -60,13 +40,21 @@ defmodule BlockScoutWeb.Application do :ok end - if @disable_api? do - defp setup_and_define_children do - BlockScoutWeb.Prometheus.Exporter.setup() - {[], []} + defp indexer_metric_worker do + alias Indexer.Prometheus.Metrics, as: IndexerMetrics + + if Explorer.mode() in [:indexer, :all] do + [{IndexerMetrics, []}] + else + [] end - else - defp setup_and_define_children do + end + + defp setup_and_define_children do + if Explorer.mode() == :indexer do + BlockScoutWeb.Prometheus.Exporter.setup() + {indexer_metric_worker(), []} + else alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.Counters.{BlocksIndexedCounter, InternalTransactionsIndexedCounter} alias BlockScoutWeb.Prometheus.{Exporter, PublicExporter} @@ -79,7 +67,6 @@ defmodule BlockScoutWeb.Application do } alias BlockScoutWeb.Utility.EventHandlersMetrics - alias Explorer.Chain.Metrics.IndexerMetrics alias Explorer.Chain.Metrics.PublicMetrics, as: PublicChainMetrics Exporter.setup() @@ -93,21 +80,22 @@ defmodule BlockScoutWeb.Application do "Current API rate limit by IP #{inspect(Application.get_env(:block_scout_web, :api_rate_limit)[:limit_by_ip])} reqs/sec" ) + base_workers = [ + {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, + {MainPage, name: MainPage}, + {Main, name: Main}, + {SmartContract, name: SmartContract}, + {TokenTransfer, name: TokenTransfer}, + {BlocksIndexedCounter, name: BlocksIndexedCounter}, + {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, + {EventHandlersMetrics, []}, + {PublicChainMetrics, []}, + Hammer.child_for_supervisor() + ] + # Define workers and child supervisors to be supervised { - [ - {Phoenix.PubSub, name: BlockScoutWeb.PubSub}, - {MainPage, name: MainPage}, - {Main, name: Main}, - {SmartContract, name: SmartContract}, - {TokenTransfer, name: TokenTransfer}, - {BlocksIndexedCounter, name: BlocksIndexedCounter}, - {InternalTransactionsIndexedCounter, name: InternalTransactionsIndexedCounter}, - {EventHandlersMetrics, []}, - {PublicChainMetrics, []}, - {IndexerMetrics, []}, - Hammer.child_for_supervisor() - ], + base_workers ++ indexer_metric_worker(), [ {Absinthe.Subscription, Endpoint} ] diff --git a/apps/explorer/lib/explorer/prometheus/instrumenter.ex b/apps/explorer/lib/explorer/prometheus/instrumenter.ex index 41704a36cb6f..6bc350bd4eed 100644 --- a/apps/explorer/lib/explorer/prometheus/instrumenter.ex +++ b/apps/explorer/lib/explorer/prometheus/instrumenter.ex @@ -66,22 +66,6 @@ defmodule Explorer.Prometheus.Instrumenter do registry: :public ] - # metrics of indexing monitor - @gauge [name: :missing_blocks_count, help: "Number of blocks missing in the chain"] - @gauge [ - name: :missing_internal_transactions_count, - help: "Number of blocks with not yet fetched internal transactions" - ] - @gauge [name: :missing_current_token_balances_count, help: "Number of missing current token balances"] - @gauge [name: :missing_archival_token_balances_count, help: "Number of missing token balances in history"] - @gauge [name: :unfetched_token_instances_count, help: "Number of unfetched token instances"] - @gauge [name: :failed_token_instances_metadata_count, help: "Number of failed token instances metadata"] - @gauge [name: :token_instances_not_uploaded_to_cdn_count, help: "Token instances not uploaded to CDN"] - @gauge [name: :multichain_search_db_main_export_queue_count, help: "Size of the main multichain export queue"] - @gauge [name: :multichain_search_db_export_balances_queue_count, help: "Size of the balances export queue"] - @gauge [name: :multichain_search_db_export_counters_queue_count, help: "Size of the counters export queue"] - @gauge [name: :multichain_search_db_export_token_info_queue_count, help: "Size of the token info export queue"] - # metrics of NFT media handler @histogram [ @@ -199,80 +183,6 @@ defmodule Explorer.Prometheus.Instrumenter do Gauge.set([name: :active_addresses_number, registry: :public], number) end - @doc """ - Defines the metric for the number of blocks missing in the chain. - """ - @spec missing_blocks_count(integer()) :: :ok - def missing_blocks_count(value), do: Gauge.set([name: :missing_blocks_count], value) - - @doc """ - Defines the metric for the number of blocks with not yet fetched internal transactions. - """ - @spec missing_internal_transactions_count(integer()) :: :ok - def missing_internal_transactions_count(value), do: Gauge.set([name: :missing_internal_transactions_count], value) - - @doc """ - Defines the metric for the number of missing current token balances. - """ - @spec missing_current_token_balances_count(integer()) :: :ok - def missing_current_token_balances_count(value), - do: Gauge.set([name: :missing_current_token_balances_count], value) - - @doc """ - Defines the metric for the number of missing token balances in history. - """ - @spec missing_archival_token_balances_count(integer()) :: :ok - def missing_archival_token_balances_count(value), do: Gauge.set([name: :missing_archival_token_balances_count], value) - - @doc """ - Defines the metric for the number of unfetched token instances. - """ - @spec unfetched_token_instances_count(integer()) :: :ok - def unfetched_token_instances_count(value), - do: Gauge.set([name: :unfetched_token_instances_count], value) - - @doc """ - Defines the metric for the number of failed token instances metadata. - """ - @spec failed_token_instances_metadata_count(integer()) :: :ok - def failed_token_instances_metadata_count(value), - do: Gauge.set([name: :failed_token_instances_metadata_count], value) - - @doc """ - Defines the metric for the number of token instances not uploaded to CDN. - """ - @spec token_instances_not_uploaded_to_cdn_count(integer()) :: :ok - def token_instances_not_uploaded_to_cdn_count(value), - do: Gauge.set([name: :token_instances_not_uploaded_to_cdn_count], value) - - @doc """ - Defines the metric for the size of the main multichain export queue. - """ - @spec multichain_search_db_main_export_queue_count(integer()) :: :ok - def multichain_search_db_main_export_queue_count(value), - do: Gauge.set([name: :multichain_search_db_main_export_queue_count], value) - - @doc """ - Defines the metric for the size of the balances export queue. - """ - @spec multichain_search_db_export_balances_queue_count(integer()) :: :ok - def multichain_search_db_export_balances_queue_count(value), - do: Gauge.set([name: :multichain_search_db_export_balances_queue_count], value) - - @doc """ - Defines the metric for the size of the counters export queue. - """ - @spec multichain_search_db_export_counters_queue_count(integer()) :: :ok - def multichain_search_db_export_counters_queue_count(value), - do: Gauge.set([name: :multichain_search_db_export_counters_queue_count], value) - - @doc """ - Defines the metric for the size of the token info export queue. - """ - @spec multichain_search_db_export_token_info_queue_count(integer()) :: :ok - def multichain_search_db_export_token_info_queue_count(value), - do: Gauge.set([name: :multichain_search_db_export_token_info_queue_count], value) - @doc """ Defines the metric for time taken for media resizing and uploading (in seconds). """ diff --git a/apps/indexer/lib/indexer/application.ex b/apps/indexer/lib/indexer/application.ex index 19ce93a01230..e533f603bf53 100644 --- a/apps/indexer/lib/indexer/application.ex +++ b/apps/indexer/lib/indexer/application.ex @@ -20,7 +20,10 @@ defmodule Indexer.Application do @impl Application def start(_type, _args) do - Instrumenter.setup() + if Explorer.mode() in [:indexer, :all] do + Instrumenter.setup() + end + memory_monitor_name = Memory.Monitor json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) diff --git a/apps/indexer/lib/indexer/prometheus/instrumenter.ex b/apps/indexer/lib/indexer/prometheus/instrumenter.ex index c8fc996b2a4a..9de7269f938b 100644 --- a/apps/indexer/lib/indexer/prometheus/instrumenter.ex +++ b/apps/indexer/lib/indexer/prometheus/instrumenter.ex @@ -44,6 +44,22 @@ defmodule Indexer.Prometheus.Instrumenter do @gauge [name: :latest_block_timestamp, help: "Latest block timestamp"] + # metrics of indexing monitor + @gauge [name: :missing_blocks_count, help: "Number of blocks missing in the chain"] + @gauge [ + name: :missing_internal_transactions_count, + help: "Number of blocks with not yet fetched internal transactions" + ] + @gauge [name: :missing_current_token_balances_count, help: "Number of missing current token balances"] + @gauge [name: :missing_archival_token_balances_count, help: "Number of missing token balances in history"] + @gauge [name: :unfetched_token_instances_count, help: "Number of unfetched token instances"] + @gauge [name: :failed_token_instances_metadata_count, help: "Number of failed token instances metadata"] + @gauge [name: :token_instances_not_uploaded_to_cdn_count, help: "Token instances not uploaded to CDN"] + @gauge [name: :multichain_search_db_main_export_queue_count, help: "Size of the main multichain export queue"] + @gauge [name: :multichain_search_db_export_balances_queue_count, help: "Size of the balances export queue"] + @gauge [name: :multichain_search_db_export_counters_queue_count, help: "Size of the counters export queue"] + @gauge [name: :multichain_search_db_export_token_info_queue_count, help: "Size of the token info export queue"] + @spec setup() :: :ok def setup do min_blockchain_block_number = @@ -163,4 +179,78 @@ defmodule Indexer.Prometheus.Instrumenter do :ok end end + + @doc """ + Defines the metric for the number of blocks missing in the chain. + """ + @spec missing_blocks_count(integer()) :: :ok + def missing_blocks_count(value), do: Gauge.set([name: :missing_blocks_count], value) + + @doc """ + Defines the metric for the number of blocks with not yet fetched internal transactions. + """ + @spec missing_internal_transactions_count(integer()) :: :ok + def missing_internal_transactions_count(value), do: Gauge.set([name: :missing_internal_transactions_count], value) + + @doc """ + Defines the metric for the number of missing current token balances. + """ + @spec missing_current_token_balances_count(integer()) :: :ok + def missing_current_token_balances_count(value), + do: Gauge.set([name: :missing_current_token_balances_count], value) + + @doc """ + Defines the metric for the number of missing token balances in history. + """ + @spec missing_archival_token_balances_count(integer()) :: :ok + def missing_archival_token_balances_count(value), do: Gauge.set([name: :missing_archival_token_balances_count], value) + + @doc """ + Defines the metric for the number of unfetched token instances. + """ + @spec unfetched_token_instances_count(integer()) :: :ok + def unfetched_token_instances_count(value), + do: Gauge.set([name: :unfetched_token_instances_count], value) + + @doc """ + Defines the metric for the number of failed token instances metadata. + """ + @spec failed_token_instances_metadata_count(integer()) :: :ok + def failed_token_instances_metadata_count(value), + do: Gauge.set([name: :failed_token_instances_metadata_count], value) + + @doc """ + Defines the metric for the number of token instances not uploaded to CDN. + """ + @spec token_instances_not_uploaded_to_cdn_count(integer()) :: :ok + def token_instances_not_uploaded_to_cdn_count(value), + do: Gauge.set([name: :token_instances_not_uploaded_to_cdn_count], value) + + @doc """ + Defines the metric for the size of the main multichain export queue. + """ + @spec multichain_search_db_main_export_queue_count(integer()) :: :ok + def multichain_search_db_main_export_queue_count(value), + do: Gauge.set([name: :multichain_search_db_main_export_queue_count], value) + + @doc """ + Defines the metric for the size of the balances export queue. + """ + @spec multichain_search_db_export_balances_queue_count(integer()) :: :ok + def multichain_search_db_export_balances_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_balances_queue_count], value) + + @doc """ + Defines the metric for the size of the counters export queue. + """ + @spec multichain_search_db_export_counters_queue_count(integer()) :: :ok + def multichain_search_db_export_counters_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_counters_queue_count], value) + + @doc """ + Defines the metric for the size of the token info export queue. + """ + @spec multichain_search_db_export_token_info_queue_count(integer()) :: :ok + def multichain_search_db_export_token_info_queue_count(value), + do: Gauge.set([name: :multichain_search_db_export_token_info_queue_count], value) end diff --git a/apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex b/apps/indexer/lib/indexer/prometheus/metrics.ex similarity index 89% rename from apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex rename to apps/indexer/lib/indexer/prometheus/metrics.ex index 7e8363639286..c0dfa1d2c6ea 100644 --- a/apps/explorer/lib/explorer/chain/metrics/indexer_metrics.ex +++ b/apps/indexer/lib/indexer/prometheus/metrics.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain.Metrics.IndexerMetrics do +defmodule Indexer.Prometheus.Metrics do @moduledoc """ Module responsible for periodically setting indexer metrics. """ @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Metrics.IndexerMetrics do use GenServer alias Explorer.Chain.Metrics.Queries.IndexerMetrics, as: IndexerMetricsQueries - alias Explorer.Prometheus.Instrumenter + alias Indexer.Prometheus.Instrumenter @interval :timer.hours(1) @default_metrics_list [ @@ -24,7 +24,7 @@ defmodule Explorer.Chain.Metrics.IndexerMetrics do end def init(_) do - if Application.get_env(:explorer, __MODULE__)[:enabled] do + if Application.get_env(:indexer, __MODULE__)[:enabled] do send(self(), :set_metrics) {:ok, %{}} else @@ -41,7 +41,7 @@ defmodule Explorer.Chain.Metrics.IndexerMetrics do defp metrics_list do additional_metrics = - Application.get_env(:explorer, __MODULE__)[:specific_metrics_enabled?] + Application.get_env(:indexer, __MODULE__)[:specific_metrics_enabled?] |> Enum.filter(fn {_, enabled?} -> enabled? == true end) |> Enum.map(fn {metric, _} -> metric end) diff --git a/config/runtime.exs b/config/runtime.exs index a47645850e92..74106b88d97e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -880,21 +880,6 @@ config :explorer, Explorer.Chain.Metrics.PublicMetrics, enabled: ConfigHelper.parse_bool_env_var("PUBLIC_METRICS_ENABLED", "false"), update_period_hours: ConfigHelper.parse_integer_env_var("PUBLIC_METRICS_UPDATE_PERIOD_HOURS", 24) -config :explorer, Explorer.Chain.Metrics.IndexerMetrics, - enabled: ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED", "true"), - specific_metrics_enabled?: %{ - token_instances_not_uploaded_to_cdn_count: - ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT", "false"), - failed_token_instances_metadata_count: - ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT", "true"), - unfetched_token_instances_count: - ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT", "true"), - missing_current_token_balances_count: - ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT", "true"), - missing_archival_token_balances_count: - ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT", "true") - } - config :explorer, Explorer.Chain.Filecoin.NativeAddress, network_prefix: ConfigHelper.parse_catalog_value("FILECOIN_NETWORK_PREFIX", ["f", "t"], true, "f") @@ -1622,6 +1607,21 @@ config :indexer, Indexer.Utils.EventNotificationsCleaner, app_mode == :indexer && ConfigHelper.parse_bool_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED", "true"), max_age: ConfigHelper.parse_time_env_var("INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE", "5m") +config :indexer, Indexer.Prometheus.Metrics, + enabled: app_mode in [:indexer, :all] && ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED", "true"), + specific_metrics_enabled?: %{ + token_instances_not_uploaded_to_cdn_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_TOKEN_INSTANCES_NOT_UPLOADED_TO_CDN_COUNT", "false"), + failed_token_instances_metadata_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_FAILED_TOKEN_INSTANCES_METADATA_COUNT", "true"), + unfetched_token_instances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_UNFETCHED_TOKEN_INSTANCES_COUNT", "true"), + missing_current_token_balances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_CURRENT_TOKEN_BALANCES_COUNT", "true"), + missing_archival_token_balances_count: + ConfigHelper.parse_bool_env_var("INDEXER_METRICS_ENABLED_MISSING_ARCHIVAL_TOKEN_BALANCES_COUNT", "true") + } + config :ex_aws, json_codec: Jason, access_key_id: System.get_env("NFT_MEDIA_HANDLER_AWS_ACCESS_KEY_ID"), From d849521b700d54c3399b0c9715d1a6ad730bb66e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Dec 2025 18:34:51 +0300 Subject: [PATCH 0518/1006] chore: Enhance RPC API errors logging (#13664) --- CHANGELOG.md | 1 + .../controllers/api/rpc/rpc_translator.ex | 15 ++++++++++++++- .../lib/block_scout_web/plug/logger.ex | 4 +++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd48ffe18e74..6f93e945d04a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ ### ⚙️ Miscellaneous Tasks - Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env ([#13671](https://github.com/blockscout/blockscout/pull/13671)) +- Enhance RPC API errors logging ([#13664](https://github.com/blockscout/blockscout/pull/13664)) - Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) - Update devcontainer image to use Elixir 1.19.4 ([#13645](https://github.com/blockscout/blockscout/issues/13645)) - Elixir 1.19.3 -> 1.19.4 ([#13643](https://github.com/blockscout/blockscout/issues/13643)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index 931d751e400a..9000fde8650f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -94,7 +94,12 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do {:error, error} -> APILogger.error(fn -> - ["Error while calling RPC action", inspect(error, limit: :infinity, printable_limit: :infinity)] + redacted_query_string = redact_apikey(conn.query_string) + + [ + "Error while calling RPC action #{action} in module #{module} with query string #{redacted_query_string}", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] end) conn @@ -127,6 +132,14 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do |> halt() end + @doc """ + Redacts the API key from the query string. + """ + @spec redact_apikey(String.t()) :: String.t() + def redact_apikey(query_string) do + String.replace(query_string, ~r/apikey=[^&]*/i, "apikey=[REDACTED]") + end + @doc false @spec translate_module(map(), String.t()) :: {:ok, {module(), list(atom())}} | {:error, :no_action} defp translate_module(translations, module) do diff --git a/apps/block_scout_web/lib/block_scout_web/plug/logger.ex b/apps/block_scout_web/lib/block_scout_web/plug/logger.ex index b61c9cafe4c6..f7b56d6f2b99 100644 --- a/apps/block_scout_web/lib/block_scout_web/plug/logger.ex +++ b/apps/block_scout_web/lib/block_scout_web/plug/logger.ex @@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Plug.Logger do """ require Logger + alias BlockScoutWeb.API.RPC.RPCTranslator alias Plug.Conn @behaviour Plug @@ -50,7 +51,8 @@ defmodule BlockScoutWeb.Plug.Logger do defp endpoint(conn) do if conn.query_string do - "#{conn.request_path}?#{conn.query_string}" + redacted_query_string = RPCTranslator.redact_apikey(conn.query_string) + "#{conn.request_path}?#{redacted_query_string}" else conn.request_path end From 9141429acea77fbae91e93da194dda2ec3190f29 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 4 Dec 2025 23:57:03 +0300 Subject: [PATCH 0519/1006] chore: Filter out empty addresses from multichain export (#13674) --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain.ex | 6 + .../multichain_search.ex | 26 +++- apps/explorer/test/explorer/chain_test.exs | 112 ++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f93e945d04a..3335b8f8b2ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ ### ⚙️ Miscellaneous Tasks +- Filter out empty addresses from multichain export ([#13674](https://github.com/blockscout/blockscout/pull/13674)) - Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env ([#13671](https://github.com/blockscout/blockscout/pull/13671)) - Enhance RPC API errors logging ([#13664](https://github.com/blockscout/blockscout/pull/13664)) - Use chain id `31337` for `anvil` ([#13644](https://github.com/blockscout/blockscout/issues/13644)) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 8a4848aefca1..1b9298dc8c1b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1138,7 +1138,13 @@ defmodule Explorer.Chain do address_current_token_balances: imported[:address_current_token_balances] || [] } + filtered_addresses_to_import = + MultichainSearch.filter_addresses_to_multichain_import(assets_to_import[:addresses], options[:broadcast]) + + assets_to_import = Map.put(assets_to_import, :addresses, filtered_addresses_to_import) + MultichainSearch.send_data_to_queue(assets_to_import) + result other_result -> diff --git a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex index 4ac4527cb2c4..0e9cb31f2970 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/multichain_search.ex @@ -4,7 +4,7 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do """ alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.{Block, Hash, Token, Transaction, Wei} + alias Explorer.Chain.{Address, Block, Hash, Token, Transaction, Wei} alias Explorer.Chain.Block.Range alias Explorer.Chain.Cache.ChainId @@ -697,6 +697,30 @@ defmodule Explorer.MicroserviceInterfaces.MultichainSearch do end end + @spec filter_addresses_to_multichain_import( + [Address.t()], + atom() | nil + ) :: [Address.t()] + def filter_addresses_to_multichain_import(addresses, :on_demand) do + addresses + |> Enum.filter(fn %Address{ + fetched_coin_balance: fetched_coin_balance, + transactions_count: transactions_count, + token_transfers_count: token_transfers_count + } -> + case fetched_coin_balance do + %Wei{value: value} -> Decimal.compare(value, 0) == :gt + _ -> false + end || + (is_number(transactions_count) and transactions_count > 0) || + (is_number(token_transfers_count) and token_transfers_count > 0) + end) + end + + def filter_addresses_to_multichain_import(addresses, _broadcast) do + addresses + end + defp token_optional_field(data, metadata, key, convert_to_string \\ false) do case Map.get(metadata, key) do nil -> diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index dede80fdfea0..fe012ad0f667 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1431,6 +1431,118 @@ defmodule Explorer.ChainTest do assert Repo.aggregate(MainExportQueue, :count, :hash) == 5 end + test "doesn't populate main multichain export queue from on_demand fetcher, if the address is inactive on the chain" do + Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) + Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) + multichain_configuration = Application.get_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, multichain_configuration) + end) + + bypass = Bypass.open() + + Application.put_env(:explorer, Explorer.MicroserviceInterfaces.MultichainSearch, + service_url: "http://localhost:#{bypass.port}", + addresses_chunk_size: 7_000 + ) + + import_data_1 = %{ + addresses: %{ + params: [ + %{ + hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca", + transactions_count: 0, + token_transfers_count: 0, + fetched_coin_balance: nil + } + ] + }, + broadcast: :on_demand + } + + TestHelper.get_chain_id_mock() + Chain.import(import_data_1) + + # non-active address isn't imported from on-demand fetcher + assert Repo.aggregate(MainExportQueue, :count, :hash) == 0 + + import_data_2 = %{ + addresses: %{ + params: [ + %{ + hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987acb", + transactions_count: 0, + token_transfers_count: 0, + fetched_coin_balance: nil + } + ] + } + } + + Chain.import(import_data_2) + + # "non-active" address is imported from regular fetcher + assert Repo.aggregate(MainExportQueue, :count, :hash) == 1 + + import_data_3 = %{ + addresses: %{ + params: [ + %{ + hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987acd", + transactions_count: 0, + token_transfers_count: 0, + fetched_coin_balance: %Wei{value: Decimal.new(10)} + } + ] + }, + broadcast: :on_demand + } + + Chain.import(import_data_3) + + # address with a non-zero balance is imported from on-demand fetcher + assert Repo.aggregate(MainExportQueue, :count, :hash) == 2 + + import_data_4 = %{ + addresses: %{ + params: [ + %{ + hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987ace", + transactions_count: 1, + token_transfers_count: 0, + fetched_coin_balance: %Wei{value: Decimal.new(0)} + } + ] + }, + broadcast: :on_demand + } + + Chain.import(import_data_4) + + # address with non-zero transactions counter is imported from on-demand fetcher + assert Repo.aggregate(MainExportQueue, :count, :hash) == 3 + + import_data_5 = %{ + addresses: %{ + params: [ + %{ + hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987acc", + transactions_count: 1, + token_transfers_count: nil, + fetched_coin_balance: nil + } + ] + }, + broadcast: :on_demand + } + + Chain.import(import_data_5) + + # address with non-zero token transfers counter is imported from on-demand fetcher + assert Repo.aggregate(MainExportQueue, :count, :hash) == 4 + end + test "populates balances multichain export queue and updates it, if the multichain service is enabled" do Supervisor.terminate_child(Explorer.Supervisor, ChainId.child_id()) Supervisor.restart_child(Explorer.Supervisor, ChainId.child_id()) From 0e12fb81d130730751d39768de97c122a5fa2a38 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 5 Dec 2025 15:12:59 +0300 Subject: [PATCH 0520/1006] feat: Indexer config API endpoint (#13679) --- CHANGELOG.md | 1 + .../lib/block_scout_web/application.ex | 69 ++++++++++++------- .../controllers/api/v2/config_controller.ex | 56 +++++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 1 + 4 files changed, 104 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3335b8f8b2ab..bec2f2cc1a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Indexer config API endpoint ([#13679](https://github.com/blockscout/blockscout/pull/13679)) - Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) - Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) - Support ZRC-2 tokens for `zilliqa` chain type ([#13443](https://github.com/blockscout/blockscout/issues/13443)) diff --git a/apps/block_scout_web/lib/block_scout_web/application.ex b/apps/block_scout_web/lib/block_scout_web/application.ex index f90e617e3e3c..2b8d56646090 100644 --- a/apps/block_scout_web/lib/block_scout_web/application.ex +++ b/apps/block_scout_web/lib/block_scout_web/application.ex @@ -4,32 +4,53 @@ defmodule BlockScoutWeb.Application do """ use Application + use Utils.CompileTimeEnvHelper, disable_api?: [:block_scout_web, :disable_api?] alias BlockScoutWeb.{Endpoint, HealthEndpoint, RateLimit.Hammer} alias BlockScoutWeb.Utility.RateLimitConfigHelper alias Explorer - def start(_type, _args) do - opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + if @disable_api? do + def start(_type, _args) do + opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] + + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) + else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. + base_children = [Supervisor.child_spec(Endpoint, [])] + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children + + Supervisor.start_link(all_children, opts) + end + end + else + def start(_type, _args) do + opts = [strategy: :one_for_one, name: BlockScoutWeb.Supervisor, max_restarts: 1_000] - if Explorer.mode() in [:api, :all] do RateLimitConfigHelper.store_rate_limit_config() - end - if Application.get_env(:nft_media_handler, :standalone_media_worker?) do - Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) - else - # Endpoint must be the last child in the supervision tree - # since it must be started after all of the other processes - # (to be sure that application is ready to handle traffic) - # and stopped before them for the same reason. - # However, some processes may depend on Endpoint - # so they need to be started after. - base_children = [Supervisor.child_spec(Endpoint, [])] - {first_api_children, last_api_children} = setup_and_define_children() - all_children = first_api_children ++ base_children ++ last_api_children - - Supervisor.start_link(all_children, opts) + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + Supervisor.start_link([Supervisor.child_spec(HealthEndpoint, [])], opts) + else + # Endpoint must be the last child in the supervision tree + # since it must be started after all of the other processes + # (to be sure that application is ready to handle traffic) + # and stopped before them for the same reason. + # However, some processes may depend on Endpoint + # so they need to be started after. + base_children = [Supervisor.child_spec(Endpoint, [])] + {first_api_children, last_api_children} = setup_and_define_children() + all_children = first_api_children ++ base_children ++ last_api_children + + Supervisor.start_link(all_children, opts) + end end end @@ -40,9 +61,9 @@ defmodule BlockScoutWeb.Application do :ok end - defp indexer_metric_worker do - alias Indexer.Prometheus.Metrics, as: IndexerMetrics + alias Indexer.Prometheus.Metrics, as: IndexerMetrics + defp indexer_metric_worker do if Explorer.mode() in [:indexer, :all] do [{IndexerMetrics, []}] else @@ -50,11 +71,13 @@ defmodule BlockScoutWeb.Application do end end - defp setup_and_define_children do - if Explorer.mode() == :indexer do + if @disable_api? do + defp setup_and_define_children do BlockScoutWeb.Prometheus.Exporter.setup() {indexer_metric_worker(), []} - else + end + else + defp setup_and_define_children do alias BlockScoutWeb.API.APILogger alias BlockScoutWeb.Counters.{BlocksIndexedCounter, InternalTransactionsIndexedCounter} alias BlockScoutWeb.Prometheus.{Exporter, PublicExporter} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 187816378e74..507ca4c8b8da 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -47,6 +47,62 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> json(%{limit: limit}) end + operation :indexer, + summary: "Indexer configuration", + description: "Returns config of indexer.", + parameters: base_params(), + responses: [ + ok: + {"Indexer configuration.", "application/json", + %Schema{ + type: :object, + properties: %{ + indexer_enabled: %Schema{type: :boolean}, + internal_transactions_fetcher_enabled: %Schema{type: :boolean}, + pending_transactions_fetcher_enabled: %Schema{type: :boolean}, + token_instance_retry_fetcher_enabled: %Schema{type: :boolean}, + token_instance_sanitize_fetcher_enabled: %Schema{type: :boolean}, + block_reward_fetcher_enabled: %Schema{type: :boolean} + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + def indexer(conn, _params) do + indexer_enabled? = Application.get_env(:indexer, Indexer.Supervisor)[:enabled] + + internal_transactions_fetcher_enabled? = + indexer_enabled? && + not Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor)[:disabled?] + + pending_transactions_fetcher_enabled? = + indexer_enabled? && + not Application.get_env(:indexer, Indexer.Fetcher.PendingTransaction.Supervisor)[:disabled?] + + block_reward_fetcher_enabled? = + indexer_enabled? && + not Application.get_env(:indexer, Indexer.Fetcher.BlockReward.Supervisor)[:disabled?] + + token_instance_retry_fetcher_enabled? = + indexer_enabled? && + not Application.get_env(:indexer, Indexer.Fetcher.TokenInstance.Retry.Supervisor)[:disabled?] + + token_instance_sanitize_fetcher_enabled? = + indexer_enabled? && + not Application.get_env(:indexer, Indexer.Fetcher.TokenInstance.Sanitize.Supervisor)[:disabled?] + + conn + |> put_status(200) + |> json(%{ + indexer_enabled: indexer_enabled?, + internal_transactions_fetcher_enabled: internal_transactions_fetcher_enabled?, + pending_transactions_fetcher_enabled: pending_transactions_fetcher_enabled?, + block_reward_fetcher_enabled: block_reward_fetcher_enabled?, + token_instance_retry_fetcher_enabled: token_instance_retry_fetcher_enabled?, + token_instance_sanitize_fetcher_enabled: token_instance_sanitize_fetcher_enabled? + }) + end + operation :public_metrics, summary: "Public metrics configuration", description: "Returns update period / configuration for public metrics.", diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 5f301bb94b8d..ee1c0555b2b4 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -140,6 +140,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do scope "/config" do get("/backend-version", V2.ConfigController, :backend_version) get("/csv-export", V2.ConfigController, :csv_export) + get("/indexer", V2.ConfigController, :indexer) get("/public-metrics", V2.ConfigController, :public_metrics) get("/smart-contracts/languages", V2.ConfigController, :languages_list) From 872212972dffa76a6b25f116c174e988ca1b2966 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Fri, 5 Dec 2025 16:30:55 +0400 Subject: [PATCH 0521/1006] fix: JSON RPC encoding for signed authorizations (#13678) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/eth_rpc.ex | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bec2f2cc1a1e..789405a3ddd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### 🐛 Bug Fixes +- JSON RPC encoding for signed authorizations ([#13678](https://github.com/blockscout/blockscout/pull/13678)) - Fix 500 for pending tx in tokentx RPC API endpoint ([#13666](https://github.com/blockscout/blockscout/pull/13666)) - Fix 500 for pending tx in gettxinfo RPC API endpoint([#13665](https://github.com/blockscout/blockscout/pull/13665)) - `Mix.env()` in `runtime.exs` ([#13641](https://github.com/blockscout/blockscout/issues/13641)) diff --git a/apps/explorer/lib/explorer/eth_rpc.ex b/apps/explorer/lib/explorer/eth_rpc.ex index b51e5ed13f3c..41b2f63381c4 100644 --- a/apps/explorer/lib/explorer/eth_rpc.ex +++ b/apps/explorer/lib/explorer/eth_rpc.ex @@ -4,8 +4,6 @@ defmodule Explorer.EthRPC do """ import Explorer.EthRpcHelper - import EthereumJSONRPC, only: [integer_to_quantity: 1] - use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias Ecto.Type, as: EctoType @@ -965,8 +963,8 @@ defmodule Explorer.EthRPC do signed_authorizations |> Enum.map(fn signed_authorization -> %{ - "chainId" => String.downcase(integer_to_quantity(signed_authorization.chain_id)), - "nonce" => Helper.integer_to_hex(Decimal.to_integer(signed_authorization.nonce)), + "chainId" => Helper.decimal_to_hex(signed_authorization.chain_id), + "nonce" => Helper.decimal_to_hex(signed_authorization.nonce), "address" => to_string(signed_authorization.address), "r" => Helper.decimal_to_hex(signed_authorization.r), "s" => Helper.decimal_to_hex(signed_authorization.s), From 51b46f19cefbbbddcfed9c2f6a11634cb75c2280 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 5 Dec 2025 15:59:43 +0300 Subject: [PATCH 0522/1006] fix: Restore fetcher name to dev console output (#13681) --- CHANGELOG.md | 1 + config/runtime.exs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 789405a3ddd7..6b01490fc431 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### 🐛 Bug Fixes +- Restore fetcher name to dev console output ([#13681](https://github.com/blockscout/blockscout/pull/13681)) - JSON RPC encoding for signed authorizations ([#13678](https://github.com/blockscout/blockscout/pull/13678)) - Fix 500 for pending tx in tokentx RPC API endpoint ([#13666](https://github.com/blockscout/blockscout/pull/13666)) - Fix 500 for pending tx in gettxinfo RPC API endpoint([#13665](https://github.com/blockscout/blockscout/pull/13665)) diff --git a/config/runtime.exs b/config/runtime.exs index 74106b88d97e..fe70b6459ba0 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -12,7 +12,7 @@ config :logger, :default_handler, (if config_env() == :prod do LoggerJSON.Formatters.Basic.new(metadata: ConfigHelper.logger_backend_metadata()) else - Logger.Formatter.new() + Logger.Formatter.new(metadata: ConfigHelper.logger_backend_metadata()) end) config :logger, :api, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api] From b918b36027a9b43d567c69938fb4f5bc2b6ac538 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 5 Dec 2025 16:57:28 +0300 Subject: [PATCH 0523/1006] chore: Add `osaka` to the default list of supported EVM versions (#13680) --- CHANGELOG.md | 1 + config/runtime.exs | 4 ++-- docker-compose/envs/common-blockscout.env | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b01490fc431..3bfa3c71a03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ ### ⚙️ Miscellaneous Tasks +- Add osaka to the default list of supported EVM versions ([#13680](https://github.com/blockscout/blockscout/pull/13680)) - Filter out empty addresses from multichain export ([#13674](https://github.com/blockscout/blockscout/pull/13674)) - Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env ([#13671](https://github.com/blockscout/blockscout/pull/13671)) - Enhance RPC API errors logging ([#13664](https://github.com/blockscout/blockscout/pull/13664)) diff --git a/config/runtime.exs b/config/runtime.exs index fe70b6459ba0..79e92b2f372c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -292,10 +292,10 @@ config :explorer, coin_name: System.get_env("COIN_NAME") || "ETH", allowed_solidity_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS") || - "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,default", + "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,osaka,default", allowed_vyper_evm_versions: System.get_env("CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS") || - "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default", + "byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,osaka,default", include_uncles_in_average_block_time: ConfigHelper.parse_bool_env_var("UNCLES_IN_AVERAGE_BLOCK_TIME"), realtime_events_sender: (case app_mode do diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 45a2bc24808e..558af0ff02d2 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -135,8 +135,8 @@ RELEASE_LINK= # ADDRESSES_TABS_COUNTERS_TTL=10m # TOKEN_METADATA_UPDATE_INTERVAL=48h # TOKEN_COUNTERS_UPDATE_INTERVAL=3h -# CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,default -# CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,default +# CONTRACT_VERIFICATION_ALLOWED_SOLIDITY_EVM_VERSIONS=homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,paris,shanghai,cancun,prague,osaka,default +# CONTRACT_VERIFICATION_ALLOWED_VYPER_EVM_VERSIONS=byzantium,constantinople,petersburg,istanbul,berlin,paris,shanghai,cancun,osaka,default # CONTRACT_VERIFICATION_MAX_LIBRARIES=10 # CONTRACT_AUDIT_REPORTS_AIRTABLE_URL= # CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY= From 51bfbc1309cf4454f476d18727da69b20d84e609 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Sun, 7 Dec 2025 21:15:42 +0400 Subject: [PATCH 0524/1006] feat: Internal transactions on demand fetcher (#13604) --- .../lib/block_scout_web/chain.ex | 280 ++++++- ...address_internal_transaction_controller.ex | 15 +- .../controllers/api/rpc/address_controller.ex | 3 +- .../controllers/api/v2/address_controller.ex | 7 +- .../controllers/api/v2/block_controller.ex | 10 +- .../api/v2/csv_export_controller.ex | 2 +- .../api/v2/internal_transaction_controller.ex | 6 +- .../api/v2/transaction_controller.ex | 10 +- ...saction_internal_transaction_controller.ex | 10 +- .../address/internal_transactions.ex | 8 +- .../graphql/resolvers/internal_transaction.ex | 37 +- .../transaction_interpretation.ex | 6 +- .../api/rpc/address_controller_test.exs | 3 +- .../api/v2/block_controller_test.exs | 2 +- .../address/internal_transactions_test.exs | 4 +- .../import/runner/internal_transactions.ex | 40 + .../explorer/chain/internal_transaction.ex | 186 +++-- .../zero_value_delete_queue.ex | 14 + apps/explorer/lib/explorer/etherscan.ex | 33 +- ...delete_zero_value_internal_transactions.ex | 116 ++- .../utility/address_id_to_address_hash.ex | 18 + ...l_transactions_zero_value_delete_queue.exs | 11 + apps/explorer/test/support/factory.ex | 24 +- .../indexer/fetcher/internal_transaction.ex | 12 +- .../fetcher/on_demand/internal_transaction.ex | 731 ++++++++++++++++++ .../on_demand/internal_transaction_test.exs | 353 +++++++++ 26 files changed, 1781 insertions(+), 160 deletions(-) rename apps/{explorer/lib/explorer/chain => block_scout_web/lib/block_scout_web}/csv_export/address/internal_transactions.ex (92%) rename apps/{explorer/test/explorer/chain => block_scout_web/test/block_scout_web}/csv_export/address/internal_transactions_test.exs (98%) create mode 100644 apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20251128143146_create_internal_transactions_zero_value_delete_queue.exs create mode 100644 apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex create mode 100644 apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 30d51474bc5b..5fd9139fd974 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -13,6 +13,8 @@ defmodule BlockScoutWeb.Chain do string_to_full_hash: 1 ] + import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] + import Explorer.PagingOptions, only: [ default_paging_options: 0, @@ -55,7 +57,9 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.Optimism.InteropMessage, as: OptimismInteropMessage alias Explorer.Chain.Optimism.OutputRoot, as: OptimismOutputRoot alias Explorer.Chain.Scroll.Bridge, as: ScrollBridge - alias Explorer.PagingOptions + alias Explorer.{Etherscan, PagingOptions} + alias Explorer.Migrator.DeleteZeroValueInternalTransactions + alias Indexer.Fetcher.OnDemand.InternalTransaction, as: InternalTransactionOnDemand alias Plug.Conn @page_size page_size() @@ -512,6 +516,26 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: {block_number}}] end + def paging_options(%{"transaction_index" => transaction_index_string, "index" => index_string}) + when is_binary(transaction_index_string) and is_binary(index_string) do + with {transaction_index, ""} <- Integer.parse(transaction_index_string), + {index, ""} <- Integer.parse(index_string) do + [paging_options: %{@default_paging_options | key: %{transaction_index: transaction_index, index: index}}] + else + _ -> + [paging_options: @default_paging_options] + end + end + + def paging_options(%{"transaction_index" => transaction_index, "index" => index}) + when is_integer(transaction_index) and is_integer(index) do + [paging_options: %{@default_paging_options | key: %{transaction_index: transaction_index, index: index}}] + end + + def paging_options(%{transaction_index: transaction_index, index: index}) do + [paging_options: %{@default_paging_options | key: %{transaction_index: transaction_index, index: index}}] + end + def paging_options(%{"index" => index_string}) when is_binary(index_string) do case Integer.parse(index_string) do {index, ""} -> @@ -943,8 +967,11 @@ defmodule BlockScoutWeb.Chain do %{block_number: number} end - defp paging_params(%InternalTransaction{index: index, transaction_hash: transaction_hash}) do - {:ok, %Transaction{block_number: block_number, index: transaction_index}} = hash_to_transaction(transaction_hash) + defp paging_params(%InternalTransaction{ + index: index, + transaction_index: transaction_index, + block_number: block_number + }) do %{block_number: block_number, transaction_index: transaction_index, index: index} end @@ -1224,6 +1251,253 @@ defmodule BlockScoutWeb.Chain do ) end + @doc """ + Fetches latest internal transactions, routing to either the database or on-demand RPC source. + + When internal transactions are present in the database (for recent blocks + within the storage period), they are fetched from the DB. For older blocks + where zero-value internal transactions have been deleted, the function + falls back to fetching on-demand from the JSON-RPC node. + + ## Parameters + - `options`: Keyword list with optional keys: + - `:paging_options` - pagination options including page_size and key + - `:transaction_hash` - filter by specific transaction option + + ## Returns + - List of InternalTransaction structs + """ + @spec fetch_internal_transactions(Keyword.t()) :: [InternalTransaction.t()] + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + def fetch_internal_transactions(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + transaction_hash = Keyword.get(options, :transaction_hash) + + necessity_by_association = + %{ + :block => :optional, + [from_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, proxy_implementations_association()]] => :optional + } + + options_with_necessity = Keyword.put_new(options, :necessity_by_association, necessity_by_association) + + cond do + match?(%PagingOptions{key: {0, 0, 0}}, paging_options) or + match?(%PagingOptions{key: %{transaction_index: 0, index: 0}}, paging_options) -> + [] + + not is_nil(transaction_hash) -> + case hash_to_transaction(transaction_hash) do + {:ok, transaction} -> + transaction_to_internal_transactions(transaction, options_with_necessity) + + {:error, :not_found} -> + [] + end + + match?(%PagingOptions{key: {_, _, _}}, paging_options) and + not InternalTransaction.present_in_db?(elem(paging_options.key, 0)) -> + InternalTransactionOnDemand.fetch_latest(options_with_necessity) + + Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled] -> + from_db = InternalTransaction.fetch(options) + + from_node = + if InternalTransactionOnDemand.should_fetch?(from_db, paging_options.page_size) do + InternalTransactionOnDemand.fetch_latest(options_with_necessity) + else + [] + end + + merge_internal_transactions(from_db, from_node, paging_options.page_size) + + true -> + InternalTransaction.fetch(options) + end + end + + @doc """ + Fetches internal transactions for the given transaction, routing to either the database or on-demand RPC source. + + When internal transactions are present in the database (for recent blocks + within the storage period), they are fetched from the DB. For older blocks + where zero-value internal transactions have been deleted, the function + falls back to fetching on-demand from the JSON-RPC node. + + ## Parameters + - `transaction`: The transaction struct to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + + ## Returns + - List of InternalTransaction structs for the given transaction + """ + @spec transaction_to_internal_transactions(Transaction.t(), Keyword.t()) :: [InternalTransaction.t()] + def transaction_to_internal_transactions(transaction, options \\ []) do + if InternalTransaction.present_in_db?(transaction.block_number) do + InternalTransaction.transaction_to_internal_transactions(transaction.hash, options) + else + InternalTransactionOnDemand.fetch_by_transaction(transaction, options) + end + end + + @doc """ + Fetches internal transactions for the given block, routing to either the database or on-demand RPC source. + + When internal transactions are present in the database (for recent blocks + within the storage period), they are fetched from the DB. For older blocks + where zero-value internal transactions have been deleted, the function + falls back to fetching on-demand from the JSON-RPC node. + + ## Parameters + - `block`: The block struct to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + - `:type` - filter by transaction type + - `:call_type` - filter by call type + + ## Returns + - List of InternalTransaction structs for the given block + """ + @spec block_to_internal_transactions(Block.t(), Keyword.t()) :: [InternalTransaction.t()] + def block_to_internal_transactions(block, options \\ []) do + if InternalTransaction.present_in_db?(block.number) do + InternalTransaction.block_to_internal_transactions(block.hash, options) + else + InternalTransactionOnDemand.fetch_by_block(block, options) + end + end + + @doc """ + Fetches internal transactions for the given address by combining DB and on-demand sources. + + It first loads DB-backed internal transactions for the requested page, then + fetches additional items on-demand via JSON-RPC if needed. The merged list is + deduplicated, sorted in descending order, and trimmed to the requested page size. + + ## Parameters + - `address_hash`: The address hash to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:paging_options` - pagination options including page_size and key + - `:necessity_by_association` - associations to preload as required or optional + + ## Returns + - List of InternalTransaction structs for the given address + """ + @spec address_to_internal_transactions(Hash.Address.t(), Keyword.t()) :: [InternalTransaction.t()] + def address_to_internal_transactions(address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, default_paging_options()) + + case paging_options do + %PagingOptions{key: {0, 0, 0}} -> + [] + + _ -> + from_db = InternalTransaction.fetch_from_db_by_address(address_hash, options) + + from_node = + if InternalTransactionOnDemand.should_fetch?(from_db, paging_options.page_size) do + InternalTransactionOnDemand.fetch_by_address(address_hash, options) + else + [] + end + + merge_internal_transactions(from_db, from_node, paging_options.page_size) + end + end + + @doc """ + Works similar to `Explorer.Etherscan.list_internal_transactions/2` + but using DB or on-demand RPC based on internal transactions presence in DB. + + When internal transactions are present in the database (for recent blocks + within the storage period), they are fetched from the DB. For older blocks + where zero-value internal transactions have been deleted, the function + falls back to fetching on-demand from the JSON-RPC node. + + ## Parameters + - `transaction_or_address_hash_param_or_no_param`: Transaction or address hash or `:all` as a source to fetching internal transactions + - `options`: Map of options + + ## Returns + - List of InternalTransaction fields maps for the given param + """ + @spec list_internal_transactions(Hash.Full.t() | Hash.Address.t() | :all, map()) :: [map()] + def list_internal_transactions(%Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash, raw_options) do + options = Map.merge(Etherscan.default_options(), raw_options) + + case hash_to_transaction(transaction_hash) do + {:ok, transaction} -> + if not options.include_zero_value or InternalTransaction.present_in_db?(transaction.block_number) do + Etherscan.list_internal_transactions(transaction.hash, options) + else + InternalTransactionOnDemand.etherscan_fetch_by_transaction(transaction, options) + end + + {:error, :not_found} -> + [] + end + end + + def list_internal_transactions(%Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash, raw_options) do + from_db = Etherscan.list_internal_transactions(address_hash, raw_options) + + options = Map.merge(Etherscan.default_options(), raw_options) + + from_node = + if options.include_zero_value and InternalTransactionOnDemand.should_fetch?(from_db, options.page_size) do + InternalTransactionOnDemand.etherscan_fetch_by_address(address_hash, options) + else + [] + end + + merge_internal_transactions(from_db, from_node, options.page_size, options.order_by_direction) + end + + def list_internal_transactions(:all, raw_options) do + options = Map.merge(Etherscan.default_options(), raw_options) + + cond do + not options.include_zero_value -> + Etherscan.list_internal_transactions(:all, options) + + not is_nil(options[:endblock]) and not InternalTransaction.present_in_db?(options[:endblock]) -> + InternalTransactionOnDemand.etherscan_fetch_latest(options) + + Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled] -> + from_db = Etherscan.list_internal_transactions(:all, options) + + from_node = + if InternalTransactionOnDemand.should_fetch?(from_db, options.page_size) do + InternalTransactionOnDemand.etherscan_fetch_latest(options) + else + [] + end + + merge_internal_transactions(from_db, from_node, options.page_size, options.order_by_direction) + + true -> + Etherscan.list_internal_transactions(:all, options) + end + end + + defp merge_internal_transactions(first_list, second_list, limit, sort_direction \\ :desc) do + sort_func = + case sort_direction do + :asc -> &<=/2 + _ -> &>=/2 + end + + first_list + |> Enum.concat(second_list) + |> Enum.uniq_by(&{&1.block_number, &1.transaction_index, &1.index}) + |> Enum.sort_by(&{&1.block_number, &1.transaction_index, &1.index}, sort_func) + |> Enum.take(limit) + end + defp map_to_string_keys(map) do map |> Map.keys() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex index dd029269f00b..d3444389019d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex @@ -6,12 +6,21 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + import BlockScoutWeb.Chain, + only: [ + current_filter: 1, + paging_options: 1, + next_page_params: 3, + split_list_by_page: 1, + address_to_internal_transactions: 2 + ] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView} alias Explorer.{Chain, Market} - alias Explorer.Chain.{Address, InternalTransaction, Wei} + alias Explorer.Chain.{Address, Wei} alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Phoenix.View @@ -34,7 +43,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do |> Keyword.merge(paging_options(params)) |> Keyword.merge(current_filter(params)) - internal_transactions_plus_one = InternalTransaction.address_to_internal_transactions(address_hash, full_options) + internal_transactions_plus_one = address_to_internal_transactions(address_hash, full_options) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) next_page_path = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index fb786e155419..5082b44a56ae 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.API.RPC.Helper + alias BlockScoutWeb.Chain, as: BlockScoutWebChain alias Explorer.{Chain, Etherscan} alias Explorer.Chain.{Address, Wei} alias Explorer.Etherscan.{Addresses, Blocks} @@ -545,7 +546,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do end defp list_internal_transactions(transaction_or_address_hash_param_or_no_param, options) do - case Etherscan.list_internal_transactions(transaction_or_address_hash_param_or_no_param, options) do + case BlockScoutWebChain.list_internal_transactions(transaction_or_address_hash_param_or_no_param, options) do [] -> {:error, :not_found} internal_transactions -> {:ok, internal_transactions} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 925f6952b652..0729290a84df 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -13,7 +13,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do split_list_by_page: 1, current_filter: 1, paging_params_with_fiat_value: 1, - fetch_scam_token_toggle: 2 + fetch_scam_token_toggle: 2, + address_to_internal_transactions: 2 ] import BlockScoutWeb.PagingHelper, @@ -42,7 +43,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.Schemas.Helper, as: SchemasHelper alias Explorer.{Chain, Market, PagingOptions} - alias Explorer.Chain.{Address, Beacon.Deposit, Hash, InternalTransaction, Transaction} + alias Explorer.Chain.{Address, Beacon.Deposit, Hash, Transaction} alias Explorer.Chain.Address.{CoinBalance, Counters} alias Explorer.Chain.Token.Instance @@ -602,7 +603,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> Keyword.merge(current_filter(params)) |> Keyword.merge(@api_true) - results_plus_one = InternalTransaction.address_to_internal_transactions(address_hash, full_options) + results_plus_one = address_to_internal_transactions(address_hash, full_options) {internal_transactions, next_page} = split_list_by_page(results_plus_one) next_page_params = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 6dc2d5fd5bdd..aa3becd1560e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -15,7 +15,8 @@ defmodule BlockScoutWeb.API.V2.BlockController do param_to_block_number: 1, put_key_value_to_paging_options: 3, split_list_by_page: 1, - parse_block_hash_or_number_param: 1 + parse_block_hash_or_number_param: 1, + block_to_internal_transactions: 2 ] import BlockScoutWeb.PagingHelper, @@ -438,14 +439,15 @@ defmodule BlockScoutWeb.API.V2.BlockController do parameters: base_params() ++ [block_hash_or_number_param(), internal_transaction_type_param(), internal_transaction_call_type_param()] ++ - define_paging_params(["block_index", "items_count"]), + define_paging_params(["transaction_index", "index", "items_count"]), responses: [ ok: {"Internal transactions in the specified block.", "application/json", paginated_response( items: Schemas.InternalTransaction, next_page_params_example: %{ - "block_index" => 8, + "transaction_index" => 3, + "index" => 8, "items_count" => 50 }, title_prefix: "BlockInternalTransactions" @@ -474,7 +476,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> Keyword.merge(internal_transaction_type_options(params)) |> Keyword.merge(internal_transaction_call_type_options(params)) - internal_transactions_plus_one = InternalTransaction.block_to_internal_transactions(block.hash, full_options) + internal_transactions_plus_one = block_to_internal_transactions(block, full_options) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index 309c792be6e0..b2e5f7f4f13c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -3,11 +3,11 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do use OpenApiSpex.ControllerSpecs alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.CsvExport.Address.InternalTransactions, as: AddressInternalTransactionsCsvExporter alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.Chain alias Explorer.Chain.Address alias Explorer.Chain.Address.CurrentTokenBalance - alias Explorer.Chain.CsvExport.Address.InternalTransactions, as: AddressInternalTransactionsCsvExporter alias Explorer.Chain.CsvExport.Address.Logs, as: AddressLogsCsvExporter alias Explorer.Chain.CsvExport.Address.TokenTransfers, as: AddressTokenTransfersCsvExporter alias Explorer.Chain.CsvExport.Address.Transactions, as: AddressTransactionsCsvExporter diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 6864942cf0d9..5e702d68138a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -5,13 +5,13 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.InternalTransaction import BlockScoutWeb.Chain, only: [ split_list_by_page: 1, paging_options: 1, - next_page_params: 3 + next_page_params: 3, + fetch_internal_transactions: 1 ] import Explorer.PagingOptions, only: [default_paging_options: 0] @@ -62,7 +62,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do result = options - |> InternalTransaction.fetch() + |> fetch_internal_transactions() |> split_list_by_page() {internal_transactions, next_page} = result diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 0ad364358ac9..33f5165397fc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -17,7 +17,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do token_transfers_next_page_params: 3, paging_options: 1, split_list_by_page: 1, - fetch_scam_token_toggle: 2 + fetch_scam_token_toggle: 2, + transaction_to_internal_transactions: 2 ] import BlockScoutWeb.PagingHelper, @@ -53,7 +54,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit alias Explorer.Chain.Beacon.Reader, as: BeaconReader alias Explorer.Chain.Cache.Counters.{NewPendingTransactionsCount, Transactions24hCount} - alias Explorer.Chain.{Hash, InternalTransaction, Transaction} + alias Explorer.Chain.{Hash, Transaction} alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.PolygonZkevm.Reader, as: PolygonZkevmReader alias Explorer.Chain.Scroll.Reader, as: ScrollReader @@ -758,14 +759,13 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec internal_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def internal_transactions(conn, %{transaction_hash_param: transaction_hash_string} = params) do - with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do + with {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = @internal_transaction_necessity_by_association |> Keyword.merge(paging_options(params)) |> Keyword.merge(@api_true) - internal_transactions_plus_one = - InternalTransaction.transaction_to_internal_transactions(transaction_hash, full_options) + internal_transactions_plus_one = transaction_to_internal_transactions(transaction, full_options) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index 5ed7024a99c5..ae16db75e0bb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -2,13 +2,16 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + import BlockScoutWeb.Chain, + only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1, transaction_to_internal_transactions: 2] + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView, TransactionController} alias Explorer.{Chain, Market} - alias Explorer.Chain.{DenormalizationHelper, InternalTransaction} + alias Explorer.Chain.DenormalizationHelper alias Phoenix.View def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do @@ -32,8 +35,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do |> DenormalizationHelper.extend_transaction_block_necessity(:optional) |> Keyword.merge(paging_options(params)) - internal_transactions_plus_one = - InternalTransaction.transaction_to_internal_transactions(transaction_hash, full_options) + internal_transactions_plus_one = transaction_to_internal_transactions(transaction, full_options) {internal_transactions, next_page} = split_list_by_page(internal_transactions_plus_one) diff --git a/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex similarity index 92% rename from apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex rename to apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex index 5003d24b6cd9..f423529e1e2e 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address/internal_transactions.ex +++ b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex @@ -1,9 +1,11 @@ -defmodule Explorer.Chain.CsvExport.Address.InternalTransactions do +defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do @moduledoc """ Exports internal transactions to a csv file. """ - alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction, Wei} + import BlockScoutWeb.Chain, only: [address_to_internal_transactions: 2] + + alias Explorer.Chain.{Address, Hash, Transaction, Wei} alias Explorer.Chain.CsvExport.Helper @spec export(Hash.Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() @@ -39,7 +41,7 @@ defmodule Explorer.Chain.CsvExport.Address.InternalTransactions do else: &1 )).() - InternalTransaction.address_to_internal_transactions(address_hash, options) + address_to_internal_transactions(address_hash, options) end defp to_csv_format(internal_transactions) do diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/internal_transaction.ex index 14e0093eef47..33971212e21f 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/internal_transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/resolvers/internal_transaction.ex @@ -1,23 +1,46 @@ defmodule BlockScoutWeb.GraphQL.Resolvers.InternalTransaction do @moduledoc false + import Explorer.Chain, only: [hash_to_transaction: 1] + alias Absinthe.Relay.Connection - alias Explorer.Chain.Transaction - alias Explorer.{GraphQL, Repo} + alias BlockScoutWeb.Chain + alias Explorer.Chain.{InternalTransaction, Transaction} + alias Explorer.{GraphQL, PagingOptions} + alias Indexer.Fetcher.OnDemand.InternalTransaction, as: InternalTransactionOnDemand + + def get_by(%{transaction_hash: transaction_hash, index: index} = args, _) do + case hash_to_transaction(transaction_hash) do + {:ok, transaction} -> + if InternalTransaction.present_in_db?(transaction.block_number) do + GraphQL.get_internal_transaction(args) + else + options = [paging_options: %PagingOptions{page_size: index + 1}] + + transaction + |> InternalTransactionOnDemand.fetch_by_transaction(options) + |> Enum.find(&(&1.index == index)) + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + |> case do + nil -> {:error, "Internal transaction not found."} + internal_transaction -> {:ok, internal_transaction} + end + end - def get_by(%{transaction_hash: _, index: _} = args, _) do - GraphQL.get_internal_transaction(args) + _ -> + {:error, "Internal transaction not found."} + end end def get_by(%Transaction{} = transaction, args, _) do transaction - |> GraphQL.transaction_to_internal_transactions_query() - |> Connection.from_query(&Repo.all/1, args, options(args)) + |> Chain.transaction_to_internal_transactions(options(args)) + |> Connection.from_list(args, options(args)) end defp options(%{before: _}), do: [] - defp options(%{count: count}), do: [count: count] + defp options(%{count: count}), do: [paging_options: %PagingOptions{page_size: count}] defp options(_), do: [] end diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index bc8c17d66b45..19c471cdae1a 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -3,6 +3,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do Module to interact with Transaction Interpretation Service """ + import BlockScoutWeb.Chain, only: [transaction_to_internal_transactions: 2] + alias BlockScoutWeb.API.V2.{Helper, InternalTransactionView, TokenTransferView, TokenView, TransactionView} alias Ecto.Association.NotLoaded alias Explorer.{Chain, HttpClient} @@ -229,8 +231,8 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do @internal_transaction_necessity_by_association |> Keyword.merge(@api_true) - transaction.hash - |> InternalTransaction.transaction_to_internal_transactions(full_options) + transaction + |> transaction_to_internal_transactions(full_options) |> Enum.take(@items_limit) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index e686fd636117..66519a43acbe 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -2118,7 +2118,8 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do value: 1, from_address: address, block_hash: transaction.block_hash, - block_index: 0 + block_index: 0, + block_number: transaction.block_number ) |> with_contract_creation(contract_address) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index 46203e37fa59..afcca5e7a7fa 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -882,7 +882,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do transaction = :transaction |> insert() - |> with_block(block) + |> with_block(block, index: index) insert(:internal_transaction, transaction: transaction, diff --git a/apps/explorer/test/explorer/chain/csv_export/address/internal_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs similarity index 98% rename from apps/explorer/test/explorer/chain/csv_export/address/internal_transactions_test.exs rename to apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs index 93cb100fcb15..fe9f014f21b3 100644 --- a/apps/explorer/test/explorer/chain/csv_export/address/internal_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.Chain.CsvExport.Address.InternalTransactionsTest do +defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do use Explorer.DataCase - alias Explorer.Chain.CsvExport.Address.InternalTransactions, as: AddressInternalTransactionsCsvExporter + alias BlockScoutWeb.CsvExport.Address.InternalTransactions, as: AddressInternalTransactionsCsvExporter alias Explorer.Chain.{Address, Wei} describe "export/3" do diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index cbc0b22c8ac9..ca089d70796d 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -22,6 +22,8 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Import.Runner + alias Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue + alias Explorer.Migrator.DeleteZeroValueInternalTransactions alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo alias Explorer.Utility.MissingRangesManipulator @@ -191,6 +193,14 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do :update_pending_blocks_status ) end) + |> Multi.run(:save_zero_value_to_delete, fn repo, %{internal_transactions: internal_transactions} -> + Instrumenter.block_import_stage_runner( + fn -> save_zero_value_to_delete(repo, internal_transactions, insert_options) end, + :block_pending, + :internal_transactions, + :save_zero_value_to_delete + ) + end) end def run_insert_only(changes_list, %{timestamps: timestamps} = options) when is_map(options) do @@ -861,6 +871,36 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end + defp save_zero_value_to_delete(repo, internal_transactions, %{timeout: timeout, timestamps: timestamps}) do + with true <- Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled], + border_number when is_integer(border_number) <- DeleteZeroValueInternalTransactions.border_number() do + internal_transactions + |> Enum.map(& &1.block_number) + |> Enum.uniq() + |> Enum.filter(&(not is_nil(&1) and &1 <= border_number)) + |> Enum.map(&Map.put(timestamps, :block_number, &1)) + |> case do + [] -> + {:ok, []} + + insert_params -> + {_total, result} = + repo.insert_all( + ZeroValueDeleteQueue, + insert_params, + conflict_target: [:block_number], + on_conflict: {:replace, [:updated_at]}, + returning: [:block_number], + timeout: timeout + ) + + {:ok, result} + end + else + _ -> {:ok, []} + end + end + defp traceable_blocks_dynamic_query do if RangesHelper.trace_ranges_present?() do block_ranges = RangesHelper.get_trace_block_ranges() diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 82116a660e23..d1ec988c8560 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -9,6 +9,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{CallType, Type} + alias Explorer.Migrator.DeleteZeroValueInternalTransactions import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] @@ -679,7 +680,7 @@ defmodule Explorer.Chain.InternalTransaction do |> filter_by_type(type_filter, call_type_filter) |> filter_by_call_type(call_type_filter) |> limit(^paging_options.page_size) - |> order_by([internal_transaction], asc: internal_transaction.block_index) + |> order_by([internal_transaction], asc: internal_transaction.transaction_index, asc: internal_transaction.index) |> Chain.select_repo(options).all() end @@ -708,67 +709,9 @@ defmodule Explorer.Chain.InternalTransaction do __MODULE__.t() ] def address_to_internal_transactions(hash, options \\ []) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - direction = Keyword.get(options, :direction) - - from_block = Chain.from_block(options) - to_block = Chain.to_block(options) - - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - case paging_options do - %PagingOptions{key: {0, 0, 0}} -> - [] - - _ -> - if direction == nil || direction == "" do - query_to_address_hash_wrapped = - __MODULE__ - |> where_nonpending_block() - |> where_address_fields_match(hash, :to_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> where_is_different_from_parent_transaction() - |> common_where_limit_order(paging_options) - |> Chain.wrapped_union_subquery() - - query_from_address_hash_wrapped = - __MODULE__ - |> where_nonpending_block() - |> where_address_fields_match(hash, :from_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> where_is_different_from_parent_transaction() - |> common_where_limit_order(paging_options) - |> Chain.wrapped_union_subquery() - - query_created_contract_address_hash_wrapped = - __MODULE__ - |> where_nonpending_block() - |> where_address_fields_match(hash, :created_contract_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> where_is_different_from_parent_transaction() - |> common_where_limit_order(paging_options) - |> Chain.wrapped_union_subquery() - - query_to_address_hash_wrapped - |> union_all(^query_from_address_hash_wrapped) - |> union_all(^query_created_contract_address_hash_wrapped) - |> Chain.wrapped_union_subquery() - |> common_where_and_order(paging_options) - |> preload(:block) - |> Chain.join_associations(necessity_by_association) - |> Chain.select_repo(options).all() - |> deduplicate_and_trim_internal_transactions(paging_options) - else - __MODULE__ - |> where_nonpending_block() - |> where_address_fields_match(hash, direction) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> where_is_different_from_parent_transaction() - |> common_where_limit_order(paging_options) - |> preload(:block) - |> Chain.join_associations(necessity_by_association) - |> Chain.select_repo(options).all() - end + case Keyword.get(options, :paging_options, @default_paging_options) do + %PagingOptions{key: {0, 0, 0}} -> [] + _ -> fetch_from_db_by_address(hash, options) end end @@ -784,6 +727,113 @@ defmodule Explorer.Chain.InternalTransaction do |> Enum.take(paging_options.page_size) end + @doc """ + Determines whether internal transactions for the given block number are present in the database. + + When the DeleteZeroValueInternalTransactions migration is enabled, internal + transactions for blocks older than the border_number may have been removed + from the database. This function returns false for those old blocks, + indicating that the data must be fetched on-demand from RPC instead. + + ## Parameters + - `block_number`: The block number to check, or nil for pending transactions + + ## Returns + - `true` if internal transactions are present in the database + - `false` if internal transactions have been deleted and must be fetched on-demand + """ + @spec present_in_db?(non_neg_integer() | nil) :: boolean() + def present_in_db?(nil), do: true + + def present_in_db?(block_number) do + if Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled] do + border_number = DeleteZeroValueInternalTransactions.border_number() + is_nil(border_number) or border_number < block_number + else + true + end + end + + @doc """ + Fetches internal transactions from the database for the given address hash with specified options. + + When direction is nil or empty, the function performs a union query across + all three address roles (to_address, from_address, created_contract_address) + and deduplicates the results. When direction is specified, it performs a + single query filtering by the specified address field. + + ## Parameters + - `hash`: The address hash to query internal transactions for + - `options`: Keyword list with the following keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:direction` - filter by address role (:to, :from, :to_address_hash, :from_address_hash, :created_contract_address_hash, or nil for all) + - `:from_block` - starting block number for the query range + - `:to_block` - ending block number for the query range + - `:paging_options` - pagination options including page_size and key + + ## Returns + - List of InternalTransaction structs matching the query, with deduplication applied when direction is nil + """ + @spec fetch_from_db_by_address(Hash.Address.t(), Keyword.t()) :: [__MODULE__.t()] + def fetch_from_db_by_address(hash, options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + direction = Keyword.get(options, :direction) + + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + if direction == nil || direction == "" do + query_to_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :to_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_from_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :from_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_created_contract_address_hash_wrapped = + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, :created_contract_address_hash) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() + |> common_where_limit_order(paging_options) + |> Chain.wrapped_union_subquery() + + query_to_address_hash_wrapped + |> union_all(^query_from_address_hash_wrapped) + |> union_all(^query_created_contract_address_hash_wrapped) + |> Chain.wrapped_union_subquery() + |> common_where_and_order(paging_options) + |> preload(:block) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + |> deduplicate_and_trim_internal_transactions(paging_options) + else + __MODULE__ + |> where_nonpending_block() + |> where_address_fields_match(hash, direction) + |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) + |> where_is_different_from_parent_transaction() + |> common_where_limit_order(paging_options) + |> preload(:block) + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).all() + end + end + defp common_where_limit_order(query, paging_options) do query |> common_where_and_order(paging_options) @@ -970,15 +1020,19 @@ defmodule Explorer.Chain.InternalTransaction do end end - defp page_block_internal_transaction(query, %PagingOptions{key: %{block_index: block_index}}) do + defp page_block_internal_transaction(query, %PagingOptions{key: %{transaction_index: transaction_index, index: index}}) do query - |> where([internal_transaction], internal_transaction.block_index > ^block_index) + |> where( + [internal_transaction], + (internal_transaction.transaction_index == ^transaction_index and internal_transaction.index > ^index) or + internal_transaction.transaction_index > ^transaction_index + ) end defp page_block_internal_transaction(query, _), do: query - def internal_transaction_to_block_paging_options(%__MODULE__{block_index: block_index}) do - %{"block_index" => block_index} + def internal_transaction_to_block_paging_options(%__MODULE__{transaction_index: transaction_index, index: index}) do + %{"transaction_index" => transaction_index, "index" => index} end defp where_consensus_transactions(query) do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex b/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex new file mode 100644 index 000000000000..bc47724d1d1f --- /dev/null +++ b/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex @@ -0,0 +1,14 @@ +defmodule Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue do + @moduledoc """ + Stores numbers for blocks, whose zero-value internal transactions should be deleted + """ + + use Explorer.Schema + + @primary_key false + typed_schema "internal_transactions_zero_value_delete_queue" do + field(:block_number, :integer, primary_key: true) + + timestamps() + end +end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index aed462e27f59..c8a4291bcbc5 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -33,6 +33,14 @@ defmodule Explorer.Etherscan do @default_options.page_size end + @doc """ + Returns the default options map used for querying operations. + + The map includes default values for pagination, ordering, block ranges, and filtering options. + """ + @spec default_options() :: map() + def default_options, do: @default_options + @doc """ Gets a list of transactions for a given `t:Explorer.Chain.Hash.Address.t/0`. @@ -73,6 +81,7 @@ defmodule Explorer.Etherscan do end @internal_transaction_fields ~w( + block_number from_address_hash to_address_hash transaction_hash @@ -88,6 +97,15 @@ defmodule Explorer.Etherscan do error )a + @doc """ + Returns the list of internal transaction fields used in query selections. + + These fields represent the core attributes of internal transactions that are + consistently retrieved across different query operations. + """ + @spec internal_transaction_fields() :: [atom()] + def internal_transaction_fields, do: @internal_transaction_fields + @doc """ Gets a list of all internal transactions (with :all option) or for a given address hash (`t:Explorer.Chain.Hash.Address.t/0`) or transaction hash @@ -117,8 +135,7 @@ defmodule Explorer.Etherscan do limit: 10_000, select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: transaction.block_timestamp, - block_number: transaction.block_number + block_timestamp: transaction.block_timestamp }) ) else @@ -130,8 +147,7 @@ defmodule Explorer.Etherscan do limit: 10_000, select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: b.timestamp, - block_number: b.number + block_timestamp: b.timestamp }) ) end @@ -212,8 +228,7 @@ defmodule Explorer.Etherscan do limit: ^options_to_limit_for_inner_query(options), select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: transaction.block_timestamp, - block_number: transaction.block_number + block_timestamp: transaction.block_timestamp }) ) else @@ -231,8 +246,7 @@ defmodule Explorer.Etherscan do limit: ^options_to_limit_for_inner_query(options), select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: b.timestamp, - block_number: b.number + block_timestamp: b.timestamp }) ) end @@ -253,8 +267,7 @@ defmodule Explorer.Etherscan do offset: ^options_to_offset(options), select: merge(map(it, ^@internal_transaction_fields), %{ - block_timestamp: block.timestamp, - block_number: block.number + block_timestamp: block.timestamp }) ) end diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 1d6ac78a8af3..094ed1129f39 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -11,6 +11,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do alias Explorer.Chain.{Block, InternalTransaction} alias Explorer.Chain.Cache.Counters.AverageBlockTime + alias Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue alias Explorer.Migrator.MigrationStatus alias Explorer.Repo alias Explorer.Utility.{AddressIdToAddressHash, InternalTransactionsAddressPlaceholder} @@ -27,6 +28,13 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do GenServer.start_link(__MODULE__, :ok, name: __MODULE__) end + @doc """ + Returns the border block number. All internal transactions with zero value (and no contract creation) and + block number less than or equal to the border number are subject to deletion. + """ + @spec border_number() :: non_neg_integer() | nil + def border_number, do: get_border_number() + @impl true def init(_) do {:ok, %{}, {:continue, :ok}} @@ -56,6 +64,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do border_number = get_border_number() to_number = border_number && min(max_number + batch_size(), border_number) clear_internal_transactions(max_number, to_number) + clear_from_delete_queue() completed? = to_number == border_number new_max_number = (to_number && to_number + 1) || max_number @@ -72,18 +81,42 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do {:noreply, new_state} end - @smallint_max_value 32767 + defp clear_from_delete_queue do + batch_size = batch_size() + + ZeroValueDeleteQueue + |> order_by([z], z.block_number) + |> select([z], z.block_number) + |> limit(^batch_size) + |> Repo.all() + |> clear_internal_transactions() + end + defp clear_internal_transactions(from_number, to_number) when is_integer(from_number) and is_integer(to_number) and from_number < to_number do + dynamic_condition = dynamic([it], it.block_number >= ^from_number and it.block_number <= ^to_number) + + do_clear_internal_transactions(dynamic_condition) + end + + defp clear_internal_transactions(_from, _to), do: :ok + + defp clear_internal_transactions(block_numbers) when is_list(block_numbers) do + dynamic_condition = dynamic([it], it.block_number in ^block_numbers) + + do_clear_internal_transactions(dynamic_condition) + end + + @smallint_max_value 32767 + defp do_clear_internal_transactions(dynamic_condition) do Repo.transaction(fn -> + condition = dynamic([it], ^dynamic_condition and it.type == ^:call and it.value == ^0) + locked_internal_transactions_to_delete_query = from( it in InternalTransaction, select: select_ctid(it), - where: it.block_number >= ^from_number, - where: it.block_number <= ^to_number, - where: it.type == ^:call, - where: it.value == ^0, + where: ^condition, order_by: [asc: it.transaction_hash, asc: it.index], lock: "FOR UPDATE" ) @@ -96,12 +129,17 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do select: %{ from_address_hash: it.from_address_hash, to_address_hash: it.to_address_hash, - block_number: it.block_number + block_number: it.block_number, + index: it.index } ) {_count, deleted_internal_transactions} = Repo.delete_all(delete_query, timeout: :infinity) + ZeroValueDeleteQueue + |> where([it], ^dynamic_condition) + |> Repo.delete_all(timeout: :infinity) + address_hashes = deleted_internal_transactions |> Enum.flat_map(&[&1.from_address_hash, &1.to_address_hash]) @@ -124,36 +162,40 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do |> Enum.group_by(& &1.block_number) |> Enum.flat_map(fn {block_number, internal_transactions} -> internal_transactions - |> Enum.reduce(%{}, fn internal_transaction, inner_acc -> - from_address_hash = internal_transaction.from_address_hash - to_address_hash = internal_transaction.to_address_hash - - inner_acc - |> Map.update( - from_address_hash, - %{ - address_id: address_to_id_map[from_address_hash], - block_number: block_number, - count_tos: 0, - count_froms: 1 - }, - fn existing_params -> - %{existing_params | count_froms: min(existing_params.count_froms + 1, @smallint_max_value)} - end - ) - |> Map.update( - to_address_hash, - %{ - address_id: address_to_id_map[to_address_hash], - block_number: block_number, - count_tos: 1, - count_froms: 0 - }, - # credo:disable-for-next-line Credo.Check.Refactor.Nesting - fn existing_params -> - %{existing_params | count_tos: min(existing_params.count_tos + 1, @smallint_max_value)} - end - ) + |> Enum.reduce(%{}, fn + %{index: 0}, inner_acc -> + inner_acc + + internal_transaction, inner_acc -> + from_address_hash = internal_transaction.from_address_hash + to_address_hash = internal_transaction.to_address_hash + + inner_acc + |> Map.update( + from_address_hash, + %{ + address_id: address_to_id_map[from_address_hash], + block_number: block_number, + count_tos: 0, + count_froms: 1 + }, + fn existing_params -> + %{existing_params | count_froms: min(existing_params.count_froms + 1, @smallint_max_value)} + end + ) + |> Map.update( + to_address_hash, + %{ + address_id: address_to_id_map[to_address_hash], + block_number: block_number, + count_tos: 1, + count_froms: 0 + }, + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + fn existing_params -> + %{existing_params | count_tos: min(existing_params.count_tos + 1, @smallint_max_value)} + end + ) end) |> Map.values() |> Enum.reject(&is_nil(&1.address_id)) @@ -164,8 +206,6 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end) end - defp clear_internal_transactions(_from, _to), do: :ok - defp get_border_number do storage_period = Application.get_env(:explorer, __MODULE__)[:storage_period_days] || @default_storage_period_days border_timestamp = Timex.shift(Timex.now(), days: -storage_period) diff --git a/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex b/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex index b363d3f220a8..5ee9ffa2d981 100644 --- a/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex +++ b/apps/explorer/lib/explorer/utility/address_id_to_address_hash.ex @@ -6,6 +6,7 @@ defmodule Explorer.Utility.AddressIdToAddressHash do use Explorer.Schema alias Explorer.Chain.{Address, Hash} + alias Explorer.Repo @primary_key false typed_schema "address_ids_to_address_hashes" do @@ -22,4 +23,21 @@ defmodule Explorer.Utility.AddressIdToAddressHash do def changeset(address_id_to_address_hash \\ %__MODULE__{}, params) do cast(address_id_to_address_hash, params, [:address_id, :address_hash]) end + + @doc """ + Retrieves the address_id for a given address_hash. + + ## Parameters + - `hash`: The address hash to look up + + ## Returns + - The address_id if found, nil otherwise + """ + @spec hash_to_id(Hash.Address.t()) :: integer() | nil + def hash_to_id(hash) do + __MODULE__ + |> where([a], a.address_hash == ^hash) + |> select([a], a.address_id) + |> Repo.one() + end end diff --git a/apps/explorer/priv/repo/migrations/20251128143146_create_internal_transactions_zero_value_delete_queue.exs b/apps/explorer/priv/repo/migrations/20251128143146_create_internal_transactions_zero_value_delete_queue.exs new file mode 100644 index 000000000000..87875d128ba2 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20251128143146_create_internal_transactions_zero_value_delete_queue.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.CreateInternalTransactionsZeroValueDeleteQueue do + use Ecto.Migration + + def change do + create table(:internal_transactions_zero_value_delete_queue, primary_key: false) do + add(:block_number, :bigint, primary_key: true) + + timestamps() + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index d4a4b09cbec5..298ce072f84a 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -70,7 +70,13 @@ defmodule Explorer.Factory do alias Explorer.Market.MarketHistory alias Explorer.Repo - alias Explorer.Utility.{EventNotification, MissingBalanceOfToken, MissingBlockRange} + alias Explorer.Utility.{ + AddressIdToAddressHash, + EventNotification, + InternalTransactionsAddressPlaceholder, + MissingBalanceOfToken, + MissingBlockRange + } alias Ueberauth.Strategy.Auth0 alias Ueberauth.Auth.{Extra, Info} @@ -319,6 +325,22 @@ defmodule Explorer.Factory do |> Map.merge(address_factory_chain_type_fields()) end + def address_id_to_address_hash_factory do + %AddressIdToAddressHash{ + address_id: sequence("address_id", & &1), + address_hash: address_hash() + } + end + + def deleted_internal_transactions_address_placeholder_factory do + %InternalTransactionsAddressPlaceholder{ + address_id: sequence("address_id", & &1), + block_number: block_number(), + count_tos: 1, + count_froms: 1 + } + end + case @chain_type do :zksync -> defp address_factory_chain_type_fields() do diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index 166be6c0d0d7..fe421f8244df 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -229,7 +229,11 @@ defmodule Indexer.Fetcher.InternalTransaction do EthereumJSONRPC.RSK, EthereumJSONRPC.Filecoin ] - defp block_traceable_variants do + @doc """ + Returns the list of JSON-RPC variants that support block-traceable internal transactions. + """ + @spec block_traceable_variants() :: [module()] + def block_traceable_variants do if Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth)[:block_traceable?] do [EthereumJSONRPC.Geth | @default_block_traceable_variants] else @@ -326,7 +330,11 @@ defmodule Indexer.Fetcher.InternalTransaction do # TODO: should we cover this with tests? @zetachain_non_traceable_type 88 - defp filter_non_traceable_transactions(transactions) do + @doc """ + Filters out transactions that are known to not have traceable internal transactions. + """ + @spec filter_non_traceable_transactions([Transaction.t()]) :: [Transaction.t()] + def filter_non_traceable_transactions(transactions) do case Application.get_env(:explorer, :chain_type) do :zetachain -> Enum.reject(transactions, &(&1.type == @zetachain_non_traceable_type)) :zilliqa -> Enum.reject(transactions, &ZilliqaHelper.scilla_transaction?/1) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex new file mode 100644 index 000000000000..0ad4d2e5b8c6 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex @@ -0,0 +1,731 @@ +# credo:disable-for-this-file +defmodule Indexer.Fetcher.OnDemand.InternalTransaction do + @moduledoc """ + Fetches internal transactions from node. + """ + + require Logger + + import Ecto.Query + + alias Explorer.{Chain, Etherscan, PagingOptions} + alias Explorer.Chain.{Block, BlockNumberHelper, Hash, InternalTransaction, Transaction} + alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Repo + alias Explorer.Utility.{AddressIdToAddressHash, InternalTransactionsAddressPlaceholder} + alias Indexer.Fetcher.InternalTransaction, as: InternalTransactionFetcher + + @default_paging_options %PagingOptions{page_size: 50} + + @doc """ + Determines whether internal transactions should be fetched on-demand based on DB records and limit. + + ## Parameters + - `records_from_db`: List of internal transaction records from the database + - `limit`: The number of records requested + + ## Returns + - `true` if on-demand fetching is needed + - `false` if DB records are sufficient + """ + @spec should_fetch?([InternalTransaction.t()], non_neg_integer()) :: boolean() + def should_fetch?(_records, 0), do: false + + def should_fetch?(records_from_db, limit) do + with true <- Enum.count(records_from_db) >= limit, + %{block_number: min_block_number} <- Enum.min_by(records_from_db, & &1.block_number), + true <- InternalTransaction.present_in_db?(min_block_number) do + false + else + _ -> true + end + end + + @doc """ + Fetches latest internal transactions. + + This function acts like `Explorer.Chain.InternalTransaction.fetch/2` without `transaction_hash` + which means that it applies paging and associations preloading and returning list of DB model records. + + ## Parameters + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + + ## Returns + - List of latest InternalTransaction structs + """ + @spec fetch_latest(Keyword.t()) :: [InternalTransaction.t()] + def fetch_latest(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + + to_block_number = + case {paging_options, to_block} do + {%PagingOptions{key: {block_number, _, _}}, _} -> block_number + {_, block_number} when is_integer(block_number) -> block_number + _ -> BlockNumber.get_max() + end + + sort_direction = + case Keyword.get(options, :sort_direction) do + :asc -> &<=/2 + _ -> &>=/2 + end + + index_internal_transaction_desc_order = Keyword.get(options, :index_internal_transaction_desc_order, true) + + to_block_number + |> fetch_enough(from_block || 0, paging_options.page_size, options) + |> Enum.sort_by(&{&1.block_number, &1.transaction_index, &1.index}, sort_direction) + |> page_internal_transaction(paging_options, %{ + index_internal_transaction_desc_order: index_internal_transaction_desc_order + }) + |> Enum.take(paging_options.page_size) + end + + @doc """ + Fetches internal transactions for the given transaction from node. + + This function acts like `Explorer.Chain.InternalTransaction.transaction_to_internal_transactions/2` + which means that it applies paging and associations preloading and returning list of DB model records. + + ## Parameters + - `transaction`: The transaction struct to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + + ## Returns + - List of InternalTransaction structs for the given transaction + """ + @spec fetch_by_transaction(Transaction.t(), Keyword.t()) :: [InternalTransaction.t()] + def fetch_by_transaction(transaction, options \\ []) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + params = [ + %{ + block_number: transaction.block_number, + hash_data: to_string(transaction.hash), + transaction_index: transaction.index + } + ] + + case EthereumJSONRPC.fetch_internal_transactions(params, json_rpc_named_arguments) do + {:ok, internal_transactions_params} -> + internal_transactions_params + |> Enum.map(&serialize/1) + |> different_from_parent_transaction() + |> Enum.sort_by(& &1.index) + |> add_block_hashes(transaction.block_hash) + |> join_associations(necessity_by_association) + |> page_internal_transaction(paging_options) + |> Enum.take(paging_options.page_size) + |> Repo.preload(:block) + + :ignore -> + [transaction.block_number] + |> fetch_block_internal_transactions() + |> Enum.map(&serialize/1) + |> Enum.filter(&(&1.transaction_hash == transaction.hash)) + |> different_from_parent_transaction() + |> Enum.sort_by(& &1.index) + |> add_block_hashes(transaction.block_hash) + |> join_associations(necessity_by_association) + |> page_internal_transaction(paging_options) + |> Enum.take(paging_options.page_size) + |> Repo.preload(:block) + + error -> + Logger.error( + "Failed to fetch internal transactions for transaction #{inspect(transaction.hash)}: #{inspect(error)}" + ) + + [] + end + end + + @doc """ + Fetches internal transactions for the given block from node. + + This function acts like `Explorer.Chain.InternalTransaction.block_to_internal_transactions/2` + which means that it applies paging and associations preloading and returning list of DB model records. + + ## Parameters + - `block_number`: The block number to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + - `:type` - filter by transaction type + - `:call_type` - filter by call type + + ## Returns + - List of InternalTransaction structs for the given block + """ + @spec fetch_by_block(map() | non_neg_integer(), Keyword.t()) :: [InternalTransaction.t()] + def fetch_by_block(block, options \\ []) + + def fetch_by_block(block_number, options) when is_integer(block_number) do + fetch_by_block(%Block{number: block_number, hash: nil}, options) + end + + def fetch_by_block(%Block{} = block, options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + type_filter = Keyword.get(options, :type) + call_type_filter = Keyword.get(options, :call_type) + unlimited? = Keyword.get(options, :unlimited) + + [block.number] + |> fetch_block_internal_transactions() + |> Enum.map(&serialize/1) + |> different_from_parent_transaction() + |> filter_by_type(type_filter, call_type_filter) + |> filter_by_call_type(call_type_filter) + |> page_block_internal_transaction(paging_options) + |> Enum.sort_by(&{&1.transaction_index, &1.index}) + |> then(&if unlimited?, do: &1, else: Enum.take(&1, paging_options.page_size)) + |> add_block_hashes(block.hash) + |> join_associations(necessity_by_association) + end + + @doc """ + Fetches internal transactions for the given address from node. + + This function acts like `Explorer.Chain.InternalTransaction.address_to_internal_transactions/2` + which means that it applies paging and associations preloading and returning list of DB model records. + + ## Parameters + - `address_hash`: The address hash to fetch internal transactions for + - `options`: Keyword list with optional keys: + - `:necessity_by_association` - associations to preload as required or optional + - `:paging_options` - pagination options including page_size and key + - `:direction` - if specified, will filter internal transactions by address type. If `:to` is specified, only + internal transactions where the "to" address matches will be returned. Likewise, if `:from` is specified, only + internal transactions where the "from" address matches will be returned. If `:direction` is omitted, internal + transactions either to or from the address will be returned. + - `:from_block` - lower boundary for block number + - `:to_block` - upper boundary for block number + + ## Returns + - List of InternalTransaction structs for the given address + """ + @spec fetch_by_address(Hash.Address.t(), Keyword.t()) :: [InternalTransaction.t()] + def fetch_by_address(address_hash, options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + direction = Keyword.get(options, :direction) + + from_block = Chain.from_block(options) + to_block = Chain.to_block(options) + + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + address_id = AddressIdToAddressHash.hash_to_id(address_hash) + + block_number_from_paging_options = + case paging_options do + %{key: {block_number, _, _}} -> block_number + _ -> nil + end + + max_block_number = + case {to_block, block_number_from_paging_options} do + {nil, nil} -> nil + {nil, key} -> key + {to, nil} -> to + {to, key} -> min(to, key) + end + + sum_mode = + case direction do + d when d in [:to, :to_address_hash] -> "tos" + d when d in [:from, :from_address_hash] -> "froms" + _ -> "both" + end + + sort_direction = Keyword.get(options, :sort_direction, :desc) + + sort_func = + case sort_direction do + :asc -> &<=/2 + _ -> &>=/2 + end + + index_internal_transaction_desc_order = Keyword.get(options, :index_internal_transaction_desc_order, true) + + address_id + |> do_fetch_for_address(max_block_number, from_block, paging_options.page_size, sum_mode, sort_direction) + |> Enum.map(&serialize/1) + |> filter_by_address(address_hash, direction) + |> different_from_parent_transaction() + |> page_internal_transaction(paging_options, %{ + index_internal_transaction_desc_order: index_internal_transaction_desc_order + }) + |> Enum.sort_by(&{&1.block_number, &1.transaction_index, &1.index}, sort_func) + |> Enum.take(paging_options.page_size) + |> add_block_hashes() + |> join_associations(necessity_by_association) + |> Repo.preload(:block) + end + + defp do_fetch_for_address(address_id, to_block, from_block, limit, sum_mode, sort_direction, acc \\ []) + + defp do_fetch_for_address(_, to_block, from_block, _, _, _, acc) + when is_integer(from_block) and is_integer(to_block) and from_block >= to_block, do: acc + + defp do_fetch_for_address(address_id, to_block, from_block, limit, sum_mode, sort_direction, acc) do + internal_transactions = + address_id + |> get_block_numbers_for_address(to_block, from_block, limit, sum_mode, sort_direction) + |> fetch_block_internal_transactions() + + result = Enum.concat(internal_transactions, acc) + + count = + internal_transactions + |> different_from_parent_transaction() + |> Enum.count() + + if count > 0 and count < limit do + case sort_direction do + :desc -> + do_fetch_for_address( + address_id, + List.last(internal_transactions).block_number - 1, + from_block, + limit - count, + sum_mode, + sort_direction, + result + ) + + :asc -> + do_fetch_for_address( + address_id, + to_block, + List.last(internal_transactions).block_number + 1, + limit - count, + sum_mode, + sort_direction, + result + ) + end + else + result + end + end + + @doc """ + Fetches internal transactions for the given transaction from node, formatted for Etherscan API compatibility. + + ## Parameters + - `transaction`: The transaction struct to fetch internal transactions for + - `raw_options`: Map of Etherscan-compatible options including page_size + + ## Returns + - List of internal transactions serialized in Etherscan format + """ + @spec etherscan_fetch_by_transaction(Transaction.t(), map()) :: [map()] + def etherscan_fetch_by_transaction(transaction, raw_options) do + options = Map.merge(Etherscan.default_options(), raw_options) + + transaction + |> fetch_by_transaction(paging_options: %PagingOptions{page_size: options.page_size}) + |> Enum.map(ðerscan_serialize/1) + end + + @doc """ + Fetches internal transactions for the given address from node, formatted for Etherscan API compatibility. + + ## Parameters + - `address_hash`: The address hash to fetch internal transactions for + - `raw_options`: Map of Etherscan-compatible options including page_size, direction filter, and block range + + ## Returns + - List of internal transactions serialized in Etherscan format + """ + @spec etherscan_fetch_by_address(Hash.Address.t(), map()) :: [map()] + def etherscan_fetch_by_address(address_hash, raw_options) do + options = Map.merge(Etherscan.default_options(), raw_options) + + direction = + case options do + %{filter_by: "to"} -> :to + %{filter_by: "from"} -> :from + _ -> :both + end + + prepared_options = [ + paging_options: %PagingOptions{page_size: options.page_size, page_number: options.page_number}, + direction: direction, + from_block: Map.get(options, :startblock), + to_block: Map.get(options, :endblock), + sort_direction: Map.get(options, :order_by_direction), + index_internal_transaction_desc_order: Map.get(options, :order_by_direction) != :asc + ] + + address_hash + |> fetch_by_address(prepared_options) + |> Repo.preload(:block) + |> Enum.map(ðerscan_serialize/1) + end + + @doc """ + Fetches latest internal transactions from node, formatted for Etherscan API compatibility. + + ## Parameters + - `raw_options`: Map of Etherscan-compatible options including page_size, page_number, and block range + + ## Returns + - List of internal transactions serialized in Etherscan format + """ + @spec etherscan_fetch_latest(map()) :: [map()] + def etherscan_fetch_latest(raw_options) do + options = Map.merge(Etherscan.default_options(), raw_options) + + prepared_options = [ + paging_options: %PagingOptions{page_size: options.page_size, page_number: options.page_number}, + from_block: Map.get(options, :startblock), + to_block: Map.get(options, :endblock), + sort_direction: Map.get(options, :order_by_direction), + index_internal_transaction_desc_order: Map.get(options, :order_by_direction) != :asc + ] + + prepared_options + |> fetch_latest() + |> different_from_parent_transaction() + |> Repo.preload(:block) + |> Enum.map(ðerscan_serialize/1) + end + + defp fetch_enough(start_block_number, end_block_number, count, options, acc \\ []) + + defp fetch_enough(start_number, end_number, count, options, acc) when start_number >= end_number do + internal_transactions = fetch_by_block(start_number, Keyword.put(options, :unlimited, true)) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + fetched_count = + internal_transactions + |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) + |> Enum.count() + + result = Enum.concat(acc, internal_transactions) + + if fetched_count >= count or start_number == 0 do + result + else + start_number + |> BlockNumberHelper.previous_block_number() + |> fetch_enough(end_number, count - fetched_count, options, result) + end + end + + defp fetch_enough(_start_number, _end_number, _count, _options, acc), do: acc + + defp get_block_numbers_for_address(nil, _end_block, _start_block, _limit, _sum_mode, _order), do: [] + + defp get_block_numbers_for_address(address_id, end_block, start_block, limit, sum_mode, order) do + ranked_query = + InternalTransactionsAddressPlaceholder + |> where([q], q.address_id == ^address_id) + |> then(fn query -> + if is_nil(end_block) do + query + else + where(query, [q], q.block_number <= ^end_block) + end + end) + |> then(fn query -> + if is_nil(start_block) do + query + else + where(query, [q], q.block_number >= ^start_block) + end + end) + |> windows([q], w: [order_by: [{^order, :block_number}]]) + |> select([q], %{ + block_number: q.block_number, + running_sum: + fragment( + """ + sum( + CASE ? + WHEN 'froms' THEN ? + WHEN 'tos' THEN ? + ELSE ? + ? + END + ) OVER w + """, + ^sum_mode, + q.count_froms, + q.count_tos, + q.count_froms, + q.count_tos + ) + }) + + cut_query = + from(r in subquery(ranked_query), + select: %{ + block_number: r.block_number, + running_sum: r.running_sum, + cutoff_block: + fragment( + "max(CASE WHEN ? >= ? THEN ? END) OVER ()", + r.running_sum, + ^limit, + r.block_number + ) + } + ) + + condition = + case order do + :desc -> dynamic([c], is_nil(c.cutoff_block) or c.block_number >= c.cutoff_block) + :asc -> dynamic([c], is_nil(c.cutoff_block) or c.block_number <= c.cutoff_block) + end + + final_query = + from(c in subquery(cut_query), + where: ^condition, + order_by: [{^order, c.block_number}], + select: c.block_number + ) + + Repo.all(final_query) + end + + defp filter_by_address(internal_transactions, address_hash, direction) do + Enum.filter(internal_transactions, fn internal_transaction -> + case direction do + d when d in [:to, :to_address_hash] -> + internal_transaction.to_address_hash == address_hash + + d when d in [:from, :from_address_hash] -> + internal_transaction.from_address_hash == address_hash + + _ -> + internal_transaction.to_address_hash == address_hash or + internal_transaction.from_address_hash == address_hash or + internal_transaction.created_contract_address_hash == address_hash + end + end) + end + + defp fetch_block_internal_transactions([]), do: [] + + defp fetch_block_internal_transactions(block_numbers) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + variant = Keyword.fetch!(json_rpc_named_arguments, :variant) + + if variant in InternalTransactionFetcher.block_traceable_variants() do + case EthereumJSONRPC.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) do + {:ok, result} -> + result + + error -> + Logger.error("Failed to fetch internal transactions for blocks #{inspect(block_numbers)}: #{inspect(error)}") + [] + end + else + Enum.reduce(block_numbers, [], fn block_number, acc_list -> + block_number + |> Chain.get_transactions_of_block_number() + |> InternalTransactionFetcher.filter_non_traceable_transactions() + |> Enum.map( + &%{ + block_number: &1.block_number, + hash_data: to_string(&1.hash), + transaction_index: &1.index + } + ) + |> case do + [] -> + {:ok, []} + + transactions -> + try do + EthereumJSONRPC.fetch_internal_transactions(transactions, json_rpc_named_arguments) + catch + :exit, error -> + {:error, error, __STACKTRACE__} + end + end + |> case do + {:ok, internal_transactions} -> + internal_transactions ++ acc_list + + error_or_ignore -> + Logger.error("Failed to fetch internal transactions for block #{block_number}: #{inspect(error_or_ignore)}") + + acc_list + end + end) + end + end + + defp page_internal_transaction(_, _, _ \\ %{index_internal_transaction_desc_order: false}) + + defp page_internal_transaction(internal_transactions, %PagingOptions{key: nil}, _), do: internal_transactions + + defp page_internal_transaction( + internal_transactions, + %PagingOptions{key: {block_number, transaction_index, index}}, + %{ + index_internal_transaction_desc_order: false + } + ) do + Stream.filter( + internal_transactions, + &(&1.block_number < block_number or (&1.block_number == block_number and &1.transaction_index < transaction_index) or + (&1.block_number == block_number and &1.transaction_index == transaction_index and &1.index > index)) + ) + end + + defp page_internal_transaction( + internal_transactions, + %PagingOptions{key: {block_number, transaction_index, index}}, + %{ + index_internal_transaction_desc_order: true + } + ) do + Stream.filter( + internal_transactions, + &(&1.block_number < block_number or (&1.block_number == block_number and &1.transaction_index < transaction_index) or + (&1.block_number == block_number and &1.transaction_index == transaction_index and &1.index < index)) + ) + end + + defp page_internal_transaction(internal_transactions, %PagingOptions{key: {0}}, %{ + index_internal_transaction_desc_order: desc_order + }) do + if desc_order do + internal_transactions + else + Stream.filter(internal_transactions, &(&1.index > 0)) + end + end + + defp page_internal_transaction(internal_transactions, %PagingOptions{key: {index}}, %{ + index_internal_transaction_desc_order: desc_order + }) do + if desc_order do + Stream.filter(internal_transactions, &(&1.index < index)) + else + Stream.filter(internal_transactions, &(&1.index > index)) + end + end + + defp page_block_internal_transaction(internal_transactions, %PagingOptions{ + key: %{transaction_index: transaction_index, index: index} + }) do + Stream.filter( + internal_transactions, + &((&1.transaction_index == transaction_index and &1.index > index) or &1.transaction_index > transaction_index) + ) + end + + defp page_block_internal_transaction(internal_transactions, _), do: internal_transactions + + # filter by `type` is automatically ignored if `call_type_filter` is not empty, + # as applying both filter simultaneously have no sense + defp filter_by_type(internal_transactions, _, [_ | _]), do: internal_transactions + defp filter_by_type(internal_transactions, nil, _), do: internal_transactions + defp filter_by_type(internal_transactions, [], _), do: internal_transactions + + defp filter_by_type(internal_transactions, types, _) do + Stream.filter(internal_transactions, &(&1.type in types)) + end + + defp filter_by_call_type(internal_transactions, []), do: internal_transactions + defp filter_by_call_type(internal_transactions, nil), do: internal_transactions + + defp filter_by_call_type(internal_transactions, call_types) do + Stream.filter(internal_transactions, &(&1.call_type in call_types)) + end + + defp join_associations(records, necessity_by_association) + when is_list(records) and is_map(necessity_by_association) do + Enum.reduce(necessity_by_association, records, fn {association, necessity}, acc -> + join_association(acc, association, necessity) + end) + end + + defp join_association(records, [{association, nested_preload}], :optional) + when is_atom(association) do + Repo.preload(records, [{association, nested_preload}]) + end + + defp join_association(records, [{association, nested_preload}], :required) + when is_atom(association) do + records + |> Repo.preload([{association, nested_preload}]) + |> Enum.filter(fn struct -> + case Map.fetch(struct, association) do + {:ok, value} -> not is_nil(value) + :error -> true + end + end) + end + + defp join_association(records, association, :optional) do + Repo.preload(records, association) + end + + defp join_association(records, association, :required) do + records + |> Repo.preload(association) + |> Enum.filter(fn struct -> + case Map.fetch(struct, association) do + {:ok, value} -> + case value do + [] -> false + nil -> false + _ -> true + end + + :error -> + true + end + end) + end + + defp add_block_hashes(internal_transactions, block_hash \\ nil) do + block_number_to_hash_map = + case block_hash do + nil -> + internal_transactions + |> Enum.map(& &1.transaction_hash) + |> Enum.uniq() + |> Transaction.by_hashes_query() + |> select([t], {t.block_number, t.block_hash}) + |> Repo.all() + |> Map.new() + + _ -> + %{} + end + + Enum.map(internal_transactions, &%{&1 | block_hash: block_hash || block_number_to_hash_map[&1.block_number]}) + end + + defp different_from_parent_transaction(internal_transactions) do + Enum.reject(internal_transactions, &(&1.type == :call and &1.index == 0)) + end + + defp serialize(internal_transaction_params) do + %InternalTransaction{} + |> InternalTransaction.changeset(internal_transaction_params) + |> Ecto.Changeset.apply_changes() + end + + defp etherscan_serialize(internal_transaction) do + internal_transaction + |> Map.take(Etherscan.internal_transaction_fields()) + |> Map.put(:block_timestamp, internal_transaction.block.timestamp) + end +end diff --git a/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs new file mode 100644 index 000000000000..56da961be67f --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs @@ -0,0 +1,353 @@ +defmodule Indexer.Fetcher.OnDemand.InternalTransactionTest do + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import Mox + + alias Explorer.Chain.InternalTransaction + alias Explorer.PagingOptions + alias Indexer.Fetcher.OnDemand.InternalTransaction, as: InternalTransactionOnDemand + + setup :set_mox_global + + setup :verify_on_exit! + + setup do + initial_env = Application.get_all_env(:ethereum_jsonrpc) + on_exit(fn -> Application.put_all_env([{:ethereum_jsonrpc, initial_env}]) end) + EthereumJSONRPC.Case.Geth.Mox.setup() + end + + test "fetch_by_transaction/2" do + transaction = :transaction |> insert() |> with_block() + block_hash = transaction.block_hash + + expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn + [%{id: id, params: _}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "type" => "create", + "from" => "0x117b358218da5a4f647072ddb50ded038ed63d17", + "to" => "0x205a6b72ce16736c9d87172568a9c0cb9304de0d", + "value" => "0x0", + "gas" => "0x106f5", + "gasUsed" => "0x106f5", + "input" => + "0x608060405234801561001057600080fd5b50610150806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033", + "output" => + "0x608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100d9565b60405180910390f35b610073600480360381019061006e919061009d565b61007e565b005b60008054905090565b8060008190555050565b60008135905061009781610103565b92915050565b6000602082840312156100b3576100b26100fe565b5b60006100c184828501610088565b91505092915050565b6100d3816100f4565b82525050565b60006020820190506100ee60008301846100ca565b92915050565b6000819050919050565b600080fd5b61010c816100f4565b811461011757600080fd5b5056fea26469706673582212209a159a4f3847890f10bfb87871a61eba91c5dbf5ee3cf6398207e292eee22a1664736f6c63430008070033" + } + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, tracer: "call_tracer", debug_trace_timeout: "5s") + + opts = [ + necessity_by_association: %{ + [transaction: [:from_address]] => :required + } + ] + + assert [ + %InternalTransaction{ + created_contract_address_hash: created_contract_address_hash, + from_address_hash: from_address_hash, + transaction: %{from_address: %{}, block_hash: ^block_hash} + } + ] = InternalTransactionOnDemand.fetch_by_transaction(transaction, opts) + + assert to_string(created_contract_address_hash) == "0x205a6b72ce16736c9d87172568a9c0cb9304de0d" + assert to_string(from_address_hash) == "0x117b358218da5a4f647072ddb50ded038ed63d17" + end + + test "fetch_by_block/2" do + block = build(:block) + block_quantity = EthereumJSONRPC.integer_to_quantity(block.number) + block_hash = block.hash + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^block_quantity, _]}], _ -> + {:ok, + [ + %{ + id: id, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + } + ] + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, + tracer: "call_tracer", + debug_trace_timeout: "5s", + block_traceable?: true + ) + + assert [%InternalTransaction{block_hash: ^block_hash, index: 1}] = + InternalTransactionOnDemand.fetch_by_block(block, []) + end + + test "fetch_by_block/2 (block_traceable?: false)" do + transaction = :transaction |> insert() |> with_block() + transaction_hash_str = to_string(transaction.hash) + block_hash = transaction.block_hash + + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, params: [^transaction_hash_str, _]}], _ -> + {:ok, + [ + %{ + id: id, + result: %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x6df83a19647a398d48e77a6835f4a28eb7e2f7c0", + "type" => "DELEGATECALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => "0x4200000000000000000000000000000000000015", + "type" => "CALL", + "value" => "0x0" + } + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, + tracer: "call_tracer", + debug_trace_timeout: "5s", + block_traceable?: false + ) + + assert [%InternalTransaction{block_hash: ^block_hash, index: 1}] = + InternalTransactionOnDemand.fetch_by_block(transaction.block, []) + end + + test "fetch_by_address/2" do + address = insert(:address) + address_hash_str = to_string(address.hash) + id_to_hash = insert(:address_id_to_address_hash, address_hash: address.hash) + + insert(:deleted_internal_transactions_address_placeholder, + address_id: id_to_hash.address_id, + block_number: 1, + count_tos: 1, + count_froms: 1 + ) + + insert(:deleted_internal_transactions_address_placeholder, + address_id: id_to_hash.address_id, + block_number: 2, + count_tos: 2, + count_froms: 2 + ) + + insert(:deleted_internal_transactions_address_placeholder, + address_id: id_to_hash.address_id, + block_number: 3, + count_tos: 3, + count_froms: 3 + ) + + expect(EthereumJSONRPC.Mox, :json_rpc, fn + [ + %{id: id_1, params: ["0x3", _]}, + %{id: id_2, params: ["0x2", _]} + ], + _ -> + {:ok, + [ + %{ + id: id_1, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5c" + }, + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5d" + }, + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5e" + } + ] + }, + %{ + id: id_2, + result: [ + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5f" + }, + %{ + "result" => %{ + "calls" => [ + %{ + "from" => "0x4200000000000000000000000000000000000015", + "gas" => "0xe9a3c", + "gasUsed" => "0x4a28", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + } + ], + "from" => "0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001", + "gas" => "0xf4240", + "gasUsed" => "0xb6f9", + "input" => + "0x015d8eb900000000000000000000000000000000000000000000000000000000009cb0d80000000000000000000000000000000000000000000000000000000065898738000000000000000000000000000000000000000000000000000000000000001b65f7961a6893850c1f001edeaa0aa4f1fb36b67eee61a8623f8f4da81be25c0000000000000000000000000000000000000000000000000000000000000000050000000000000000000000007431310e026b69bfc676c0013e12a1a11411eec9000000000000000000000000000000000000000000000000000000000000083400000000000000000000000000000000000000000000000000000000000f4240", + "to" => address_hash_str, + "type" => "CALL", + "value" => "0x0" + }, + "txHash" => "0x32b17f27ddb546eab3c4c33f31eb22c1cb992d4ccc50dae26922805b717efe5b" + } + ] + } + ]} + end) + + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, + tracer: "call_tracer", + debug_trace_timeout: "5s", + block_traceable?: true + ) + + opts = [ + direction: :to_address_hash, + paging_options: %PagingOptions{page_size: 4} + ] + + assert [%InternalTransaction{}, %InternalTransaction{}, %InternalTransaction{}, %InternalTransaction{}] = + result = InternalTransactionOnDemand.fetch_by_address(address.hash, opts) + + assert result |> Enum.filter(&(&1.block_number == 3)) |> Enum.count() == 3 + assert result |> Enum.filter(&(&1.block_number == 2)) |> Enum.count() == 1 + end +end From 85432ff5f098abdc37f2a0b5279eb30b2bffee5c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Sun, 7 Dec 2025 20:17:01 +0300 Subject: [PATCH 0525/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfa3c71a03f..87e48f6f6df7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Internal transactions on demand fetcher ([#13604](https://github.com/blockscout/blockscout/pull/13604)) - Indexer config API endpoint ([#13679](https://github.com/blockscout/blockscout/pull/13679)) - Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) - Add metadata to eth bytecode DB lookup request ([#13625](https://github.com/blockscout/blockscout/issues/13625)) From e17320baf56137e8ae3feee62437d8aed2021193 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 8 Dec 2025 12:37:03 +0300 Subject: [PATCH 0526/1006] fix: Add smart contract preload to hot contracts query (#13691) --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/stats/hot_smart_contracts.ex | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e48f6f6df7..e2c0535cf1a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Add smart contract preload to hot contracts query ([#13691](https://github.com/blockscout/blockscout/pull/13691)) - Restore fetcher name to dev console output ([#13681](https://github.com/blockscout/blockscout/pull/13681)) - JSON RPC encoding for signed authorizations ([#13678](https://github.com/blockscout/blockscout/pull/13678)) - Fix 500 for pending tx in tokentx RPC API endpoint ([#13666](https://github.com/blockscout/blockscout/pull/13666)) diff --git a/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex index abdc06480a1e..f11d52ae52ab 100644 --- a/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex +++ b/apps/explorer/lib/explorer/stats/hot_smart_contracts.ex @@ -125,7 +125,7 @@ defmodule Explorer.Stats.HotSmartContracts do preloads = Keyword.get(options, :preloads, - contract_address: [:names, proxy_implementations_association(), reputation_association()] + contract_address: [:smart_contract, :names, proxy_implementations_association(), reputation_association()] ) now = DateTime.utc_now() @@ -158,7 +158,7 @@ defmodule Explorer.Stats.HotSmartContracts do preloads = Keyword.get(options, :preloads, - contract_address: [:names, proxy_implementations_association(), reputation_association()] + contract_address: [:smart_contract, :names, proxy_implementations_association(), reputation_association()] ) __MODULE__ From 11b9435b718fd0749ef176ab631285bee2061d48 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:35:15 +0300 Subject: [PATCH 0527/1006] fix: Fix NFTMediaHandler postgres parameters overflow error (#13694) --- apps/explorer/lib/explorer/chain/token/instance.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex index 4bf0b224f404..dc093a3bf40c 100644 --- a/apps/explorer/lib/explorer/chain/token/instance.ex +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -831,7 +831,7 @@ defmodule Explorer.Chain.Token.Instance do def batch_upsert_cdn_results(instances) do {_, result} = - Repo.insert_all(__MODULE__, instances, + Repo.safe_insert_all(__MODULE__, instances, on_conflict: {:replace, [:thumbnails, :media_type, :updated_at, :cdn_upload_error]}, conflict_target: [:token_id, :token_contract_address_hash], returning: true From 51eb3cd0d837277d4730ca0e8d6101bb048d301f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 8 Dec 2025 15:37:12 +0300 Subject: [PATCH 0528/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2c0535cf1a3..f3f1f2aed883 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Fix NFTMediaHandler postgres parameters overflow error ([#13694](https://github.com/blockscout/blockscout/pull/13694)) - Add smart contract preload to hot contracts query ([#13691](https://github.com/blockscout/blockscout/pull/13691)) - Restore fetcher name to dev console output ([#13681](https://github.com/blockscout/blockscout/pull/13681)) - JSON RPC encoding for signed authorizations ([#13678](https://github.com/blockscout/blockscout/pull/13678)) From e02b70f8d25400bed02ab21bb6f3969dc99b4142 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 8 Dec 2025 16:03:07 +0300 Subject: [PATCH 0529/1006] fix: Add missed reputation fetch (#13695) --- CHANGELOG.md | 1 + .../lib/block_scout_web/views/api/v2/helper.ex | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f1f2aed883..d102cc264dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### 🐛 Bug Fixes +- Add missed reputation fetch ([#13695](https://github.com/blockscout/blockscout/pull/13695)) - Fix NFTMediaHandler postgres parameters overflow error ([#13694](https://github.com/blockscout/blockscout/pull/13694)) - Add smart contract preload to hot contracts query ([#13691](https://github.com/blockscout/blockscout/pull/13691)) - Restore fetcher name to dev console output ([#13681](https://github.com/blockscout/blockscout/pull/13681)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 6dce261e43f3..026bcbc93adf 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.V2.Helper do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] alias Ecto.Association.NotLoaded - alias Explorer.Chain.{Address, SmartContract} + alias Explorer.Chain.{Address, Address.Reputation, SmartContract} alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.Transaction.History.TransactionStats @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.Helper do "is_contract" => smart_contract?, "name" => address_name(address), "is_scam" => address_marked_as_scam?(address), - "reputation" => (address_marked_as_scam?(address) && "scam") || "ok", + "reputation" => (address_marked_as_scam?(address) && "scam") || address_reputation_if_loaded(address), "proxy_type" => proxy_implementations && proxy_implementations.proxy_type, "implementations" => Proxy.proxy_object_info(proxy_implementations), "is_verified" => smart_contract_verified?(address) || verified_as_proxy?(proxy_implementations), @@ -122,6 +122,14 @@ defmodule BlockScoutWeb.API.V2.Helper do } end + defp address_reputation_if_loaded(%Address{reputation: %Reputation{reputation: reputation}}) do + reputation + end + + defp address_reputation_if_loaded(_) do + "ok" + end + case @chain_type do :filecoin -> defp address_chain_type_fields(result, address) do From 8587e0d7578139824e68aa16883fe5081e1fb274 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 8 Dec 2025 19:12:43 +0400 Subject: [PATCH 0530/1006] fix: Update InternalTransactionsAddressPlaceholder upserts (#13696) --- .../migrator/delete_zero_value_internal_transactions.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 094ed1129f39..e2c26e956f7c 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -202,7 +202,10 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end) |> Enum.sort_by(&{&1.address_id, &1.block_number}) - Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_params) + Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_params, + on_conflict: :replace_all, + conflict_target: [:address_id, :block_number] + ) end) end From 74fdec9eb671636c1ebb2d947e8141019b34bec1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 8 Dec 2025 18:13:23 +0300 Subject: [PATCH 0531/1006] Add CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d102cc264dab..ec93c5687f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Update InternalTransactionsAddressPlaceholder upserts ([#13696](https://github.com/blockscout/blockscout/pull/13696)) - Internal transactions on demand fetcher ([#13604](https://github.com/blockscout/blockscout/pull/13604)) - Indexer config API endpoint ([#13679](https://github.com/blockscout/blockscout/pull/13679)) - Add DIA market source ([#12678](https://github.com/blockscout/blockscout/issues/12678)) From ff5ddaab6c6260a4748759eebcbe22798114789e Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:52:08 +0400 Subject: [PATCH 0532/1006] fix: Limit batch size for placeholders insertion (#13699) --- CHANGELOG.md | 1 + .../delete_zero_value_internal_transactions.ex | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec93c5687f40..55e9c5087902 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Limit batch size for placeholders insertion ([#13699](https://github.com/blockscout/blockscout/pull/13699)) - Add missed reputation fetch ([#13695](https://github.com/blockscout/blockscout/pull/13695)) - Fix NFTMediaHandler postgres parameters overflow error ([#13694](https://github.com/blockscout/blockscout/pull/13694)) - Add smart contract preload to hot contracts query ([#13691](https://github.com/blockscout/blockscout/pull/13691)) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index e2c26e956f7c..427b4bc5c2a6 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -202,10 +202,14 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end) |> Enum.sort_by(&{&1.address_id, &1.block_number}) - Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_params, - on_conflict: :replace_all, - conflict_target: [:address_id, :block_number] - ) + placeholders_params + |> Enum.chunk_every(1000) + |> Enum.each(fn placeholders_batch -> + Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_batch, + on_conflict: :replace_all, + conflict_target: [:address_id, :block_number] + ) + end) end) end From ca3e23fed90eb85ff9c9668cd1ac63e11dfc4eb4 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 11 Dec 2025 13:52:35 +0400 Subject: [PATCH 0533/1006] fix: Set timeout: :infinity for delete zero value migration (#13708) --- CHANGELOG.md | 1 + ...delete_zero_value_internal_transactions.ex | 203 +++++++++--------- 2 files changed, 104 insertions(+), 100 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55e9c5087902..f8f0ffd6a1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Set timeout: :infinity for delete zero value migration ([#13708](https://github.com/blockscout/blockscout/pull/13708)) - Limit batch size for placeholders insertion ([#13699](https://github.com/blockscout/blockscout/pull/13699)) - Add missed reputation fetch ([#13695](https://github.com/blockscout/blockscout/pull/13695)) - Fix NFTMediaHandler postgres parameters overflow error ([#13694](https://github.com/blockscout/blockscout/pull/13694)) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 427b4bc5c2a6..71ff8c3b5a3d 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -109,108 +109,111 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do @smallint_max_value 32767 defp do_clear_internal_transactions(dynamic_condition) do - Repo.transaction(fn -> - condition = dynamic([it], ^dynamic_condition and it.type == ^:call and it.value == ^0) - - locked_internal_transactions_to_delete_query = - from( - it in InternalTransaction, - select: select_ctid(it), - where: ^condition, - order_by: [asc: it.transaction_hash, asc: it.index], - lock: "FOR UPDATE" - ) - - delete_query = - from( - it in InternalTransaction, - inner_join: locked_it in subquery(locked_internal_transactions_to_delete_query), - on: join_on_ctid(it, locked_it), - select: %{ - from_address_hash: it.from_address_hash, - to_address_hash: it.to_address_hash, - block_number: it.block_number, - index: it.index - } - ) - - {_count, deleted_internal_transactions} = Repo.delete_all(delete_query, timeout: :infinity) - - ZeroValueDeleteQueue - |> where([it], ^dynamic_condition) - |> Repo.delete_all(timeout: :infinity) - - address_hashes = - deleted_internal_transactions - |> Enum.flat_map(&[&1.from_address_hash, &1.to_address_hash]) - |> Enum.uniq() - |> Enum.reject(&is_nil/1) - - id_to_address_params = Enum.map(address_hashes, &%{address_hash: &1}) - - Repo.insert_all(AddressIdToAddressHash, id_to_address_params, on_conflict: :nothing) - - address_to_id_map = - AddressIdToAddressHash - |> where([a], a.address_hash in ^address_hashes) - |> select([a], {a.address_hash, a.address_id}) - |> Repo.all() - |> Map.new() - - placeholders_params = - deleted_internal_transactions - |> Enum.group_by(& &1.block_number) - |> Enum.flat_map(fn {block_number, internal_transactions} -> - internal_transactions - |> Enum.reduce(%{}, fn - %{index: 0}, inner_acc -> - inner_acc - - internal_transaction, inner_acc -> - from_address_hash = internal_transaction.from_address_hash - to_address_hash = internal_transaction.to_address_hash - - inner_acc - |> Map.update( - from_address_hash, - %{ - address_id: address_to_id_map[from_address_hash], - block_number: block_number, - count_tos: 0, - count_froms: 1 - }, - fn existing_params -> - %{existing_params | count_froms: min(existing_params.count_froms + 1, @smallint_max_value)} - end - ) - |> Map.update( - to_address_hash, - %{ - address_id: address_to_id_map[to_address_hash], - block_number: block_number, - count_tos: 1, - count_froms: 0 - }, - # credo:disable-for-next-line Credo.Check.Refactor.Nesting - fn existing_params -> - %{existing_params | count_tos: min(existing_params.count_tos + 1, @smallint_max_value)} - end - ) + Repo.transaction( + fn -> + condition = dynamic([it], ^dynamic_condition and it.type == ^:call and it.value == ^0) + + locked_internal_transactions_to_delete_query = + from( + it in InternalTransaction, + select: select_ctid(it), + where: ^condition, + order_by: [asc: it.transaction_hash, asc: it.index], + lock: "FOR UPDATE" + ) + + delete_query = + from( + it in InternalTransaction, + inner_join: locked_it in subquery(locked_internal_transactions_to_delete_query), + on: join_on_ctid(it, locked_it), + select: %{ + from_address_hash: it.from_address_hash, + to_address_hash: it.to_address_hash, + block_number: it.block_number, + index: it.index + } + ) + + {_count, deleted_internal_transactions} = Repo.delete_all(delete_query, timeout: :infinity) + + ZeroValueDeleteQueue + |> where([it], ^dynamic_condition) + |> Repo.delete_all(timeout: :infinity) + + address_hashes = + deleted_internal_transactions + |> Enum.flat_map(&[&1.from_address_hash, &1.to_address_hash]) + |> Enum.uniq() + |> Enum.reject(&is_nil/1) + + id_to_address_params = Enum.map(address_hashes, &%{address_hash: &1}) + + Repo.insert_all(AddressIdToAddressHash, id_to_address_params, on_conflict: :nothing) + + address_to_id_map = + AddressIdToAddressHash + |> where([a], a.address_hash in ^address_hashes) + |> select([a], {a.address_hash, a.address_id}) + |> Repo.all() + |> Map.new() + + placeholders_params = + deleted_internal_transactions + |> Enum.group_by(& &1.block_number) + |> Enum.flat_map(fn {block_number, internal_transactions} -> + internal_transactions + |> Enum.reduce(%{}, fn + %{index: 0}, inner_acc -> + inner_acc + + internal_transaction, inner_acc -> + from_address_hash = internal_transaction.from_address_hash + to_address_hash = internal_transaction.to_address_hash + + inner_acc + |> Map.update( + from_address_hash, + %{ + address_id: address_to_id_map[from_address_hash], + block_number: block_number, + count_tos: 0, + count_froms: 1 + }, + fn existing_params -> + %{existing_params | count_froms: min(existing_params.count_froms + 1, @smallint_max_value)} + end + ) + |> Map.update( + to_address_hash, + %{ + address_id: address_to_id_map[to_address_hash], + block_number: block_number, + count_tos: 1, + count_froms: 0 + }, + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + fn existing_params -> + %{existing_params | count_tos: min(existing_params.count_tos + 1, @smallint_max_value)} + end + ) + end) + |> Map.values() + |> Enum.reject(&is_nil(&1.address_id)) end) - |> Map.values() - |> Enum.reject(&is_nil(&1.address_id)) + |> Enum.sort_by(&{&1.address_id, &1.block_number}) + + placeholders_params + |> Enum.chunk_every(1000) + |> Enum.each(fn placeholders_batch -> + Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_batch, + on_conflict: :replace_all, + conflict_target: [:address_id, :block_number] + ) end) - |> Enum.sort_by(&{&1.address_id, &1.block_number}) - - placeholders_params - |> Enum.chunk_every(1000) - |> Enum.each(fn placeholders_batch -> - Repo.insert_all(InternalTransactionsAddressPlaceholder, placeholders_batch, - on_conflict: :replace_all, - conflict_target: [:address_id, :block_number] - ) - end) - end) + end, + timeout: :infinity + ) end defp get_border_number do From a6a0a924323b70eda91ed24bb9c098a939cf2409 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Dec 2025 16:00:16 +0300 Subject: [PATCH 0534/1006] production-optimism-experimental workflow --- ...ocker-image-for-optimism-exeperimental.yml | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/publish-docker-image-for-optimism-exeperimental.yml diff --git a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml new file mode 100644 index 000000000000..57700a92bd83 --- /dev/null +++ b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml @@ -0,0 +1,61 @@ +name: Optimism Publish Docker image + +on: + workflow_dispatch: + push: + branches: + - production-optimism-experimental +permissions: + contents: read + packages: write + +jobs: + push_to_registry: + name: Push Docker image to GitHub Container Registry + runs-on: build + env: + RELEASE_VERSION: 9.3.0 + DOCKER_CHAIN_NAME: optimism + steps: + - uses: actions/checkout@v5 + - name: Setup repo + uses: ./.github/actions/setup-repo + id: setup + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + docker-remote-multi-platform: true + docker-arm-host: ${{ secrets.ARM_RUNNER_HOSTNAME }} + docker-arm-host-key: ${{ secrets.ARM_RUNNER_KEY }} + + - name: Build and push Docker image (indexer + API) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-experimental-${{ env.SHORT_SHA }} + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism + + - name: Build and push Docker image (indexer) + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + tags: ghcr.io/blockscout/blockscout-${{ env.DOCKER_CHAIN_NAME }}-private:${{ env.RELEASE_VERSION }}-postrelease-experimental-${{ env.SHORT_SHA }}-indexer + labels: ${{ steps.setup.outputs.docker-labels }} + platforms: | + linux/amd64 + linux/arm64/v8 + build-args: | + DISABLE_API=true + BLOCKSCOUT_VERSION=v${{ env.RELEASE_VERSION }}.+commit.${{ env.SHORT_SHA }} + RELEASE_VERSION=${{ env.RELEASE_VERSION }} + CHAIN_TYPE=optimism From b1cf182d57761a4e53923813ec17b7bf8f4ad61f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 11 Dec 2025 17:34:53 +0300 Subject: [PATCH 0535/1006] chore: Change URL to Solidity binaries list (#13711) --- CHANGELOG.md | 1 + .../controllers/api/v2/verification_controller_test.exs | 8 ++++---- apps/explorer/config/config.exs | 2 +- .../lib/explorer/smart_contract/compiler_version.ex | 2 +- .../lib/explorer/smart_contract/solc_downloader.ex | 2 +- docker-compose/envs/common-smart-contract-verifier.env | 6 +++--- 6 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f0ffd6a1c7..dbe3da9b5841 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ ### ⚙️ Miscellaneous Tasks +- Change URL to Solidity binaries list ([#13711](https://github.com/blockscout/blockscout/pull/13711)) - Add osaka to the default list of supported EVM versions ([#13680](https://github.com/blockscout/blockscout/pull/13680)) - Filter out empty addresses from multichain export ([#13674](https://github.com/blockscout/blockscout/pull/13674)) - Validate NFT_MEDIA_HANDLER_BUCKET_FOLDER env ([#13671](https://github.com/blockscout/blockscout/pull/13671)) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs index f48e003ef6ab..82adbdae3672 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/verification_controller_test.exs @@ -131,7 +131,7 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do test "success verification", %{conn: conn} do before = Application.get_env(:explorer, :solc_bin_api_url) - Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + Application.put_env(:explorer, :solc_bin_api_url, "https://binaries.soliditylang.org") path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/solidity_0.5.9_smart_contract.sol" contract = File.read!(path) @@ -186,7 +186,7 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do test "get error on empty contract name", %{conn: conn} do before = Application.get_env(:explorer, :solc_bin_api_url) - Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + Application.put_env(:explorer, :solc_bin_api_url, "https://binaries.soliditylang.org") contract_address = insert(:contract_address, contract_code: "0x01") @@ -310,7 +310,7 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do test "success verification", %{conn: conn} do before = Application.get_env(:explorer, :solc_bin_api_url) - Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + Application.put_env(:explorer, :solc_bin_api_url, "https://binaries.soliditylang.org") path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/vyper.vy" contract = File.read!(path) @@ -462,7 +462,7 @@ defmodule BlockScoutWeb.API.V2.VerificationControllerTest do test "success verification", %{conn: conn} do before = Application.get_env(:explorer, :solc_bin_api_url) - Application.put_env(:explorer, :solc_bin_api_url, "https://solc-bin.ethereum.org") + Application.put_env(:explorer, :solc_bin_api_url, "https://binaries.soliditylang.org") path = File.cwd!() <> "/../explorer/test/support/fixture/smart_contract/standard_input.json" json = File.read!(path) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index c321b04223fe..a53cf5a39c67 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -204,7 +204,7 @@ config :explorer, Explorer.Tracer, trace_key: :blockscout config :explorer, - solc_bin_api_url: "https://solc-bin.ethereum.org" + solc_bin_api_url: "https://binaries.soliditylang.org" config :explorer, :http_client, Explorer.HttpClient.Tesla diff --git a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex index 412c2eb1b752..0a72a7649023 100644 --- a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex +++ b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex @@ -1,6 +1,6 @@ defmodule Explorer.SmartContract.CompilerVersion do @moduledoc """ - Adapter for fetching compiler versions from https://solc-bin.ethereum.org/bin/list.json. + Adapter for fetching compiler versions from https://binaries.soliditylang.org/bin/list.json. """ alias Explorer.{Helper, HttpClient} diff --git a/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex index f85b2a67b34a..105afa042921 100644 --- a/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex +++ b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex @@ -91,7 +91,7 @@ defmodule Explorer.SmartContract.SolcDownloader do end defp download(version) do - download_path = "https://solc-bin.ethereum.org/bin/soljson-#{version}.js" + download_path = "https://binaries.soliditylang.org/bin/soljson-#{version}.js" download_path |> HttpClient.get!([], timeout: 60_000, recv_timeout: 60_000) diff --git a/docker-compose/envs/common-smart-contract-verifier.env b/docker-compose/envs/common-smart-contract-verifier.env index 5fdf805ec670..7b152838e62f 100644 --- a/docker-compose/envs/common-smart-contract-verifier.env +++ b/docker-compose/envs/common-smart-contract-verifier.env @@ -14,9 +14,9 @@ SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR=/tmp/solidity-compilers SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE=0 0 * * * * * # It depends on the OS you are running the service on -SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/linux-amd64/list.json -#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json -#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json +SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://binaries.soliditylang.org/linux-amd64/list.json +#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://binaries.soliditylang.org/macosx-amd64/list.json +#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://binaries.soliditylang.org/windows-amd64/list.json SMART_CONTRACT_VERIFIER__VYPER__ENABLED=true SMART_CONTRACT_VERIFIER__VYPER__COMPILERS_DIR=/tmp/vyper-compilers From 56d5d0a5404cc517e43bb0d629ea2af92605b30d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 12 Dec 2025 12:45:32 +0300 Subject: [PATCH 0536/1006] Rename experimental GA workflow --- .../publish-docker-image-for-optimism-exeperimental.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml index 57700a92bd83..a95e75e07362 100644 --- a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml +++ b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml @@ -1,4 +1,4 @@ -name: Optimism Publish Docker image +name: Optimism Publish experimental Docker image on: workflow_dispatch: From fff8a84a2c0ec2953d584cc0b93942f92a1b83d3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 12 Dec 2025 14:01:06 +0300 Subject: [PATCH 0537/1006] chore: Eliminate warnings in the Swagger file (#13714) --- CHANGELOG.md | 1 + .../controllers/api/v2/address_controller.ex | 43 ++++++------------- .../controllers/api/v2/block_controller.ex | 24 ++++------- .../api/v2/ethereum/deposit_controller.ex | 4 +- .../api/v2/internal_transaction_controller.ex | 3 +- .../api/v2/main_page_controller.ex | 9 ++-- .../api/v2/smart_contract_controller.ex | 3 +- .../controllers/api/v2/stats_controller.ex | 3 +- .../controllers/api/v2/token_controller.ex | 21 +++------ .../api/v2/token_transfer_controller.ex | 3 +- .../api/v2/transaction_controller.ex | 38 ++++++---------- .../api/v2/withdrawal_controller.ex | 3 +- .../block_scout_web/schemas/api/v2/general.ex | 2 - .../schemas/api/v2/general/decoded_input.ex | 15 ++++++- .../api/v2/general/decoded_log_input.ex | 15 ++++++- 15 files changed, 79 insertions(+), 108 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbe3da9b5841..9b7933f4514d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ ### ⚙️ Miscellaneous Tasks +- Eliminate warnings in the Swagger file ([#13714](https://github.com/blockscout/blockscout/pull/13714)) - Change URL to Solidity binaries list ([#13711](https://github.com/blockscout/blockscout/pull/13711)) - Add osaka to the default list of supported EVM versions ([#13680](https://github.com/blockscout/blockscout/pull/13680)) - Filter out empty addresses from multichain export ([#13674](https://github.com/blockscout/blockscout/pull/13674)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 0729290a84df..cf7f2c8f7549 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -298,7 +298,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do responses: [ ok: {"All token balances for the specified address.", "application/json", - %Schema{title: "AddressTokenBalances", type: :array, items: Schemas.Address.TokenBalance}}, + %Schema{type: :array, items: Schemas.Address.TokenBalance}}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() ] @@ -380,8 +380,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "inserted_at" => "2025-05-26T10:26:51.474448Z", "items_count" => 50, "value" => "24741049597737" - }, - title_prefix: "AddressTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -469,8 +468,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "block_number" => 12_345_678, "index" => 0, "items_count" => 50 - }, - title_prefix: "AddressTokenTransfers" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -563,8 +561,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "index" => 8, "items_count" => 50, "transaction_index" => 8 - }, - title_prefix: "AddressInternalTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -640,8 +637,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {"Event logs for the specified address, with pagination.", "application/json", paginated_response( items: Schemas.Log, - next_page_params_example: %{"block_number" => 22_546_398, "index" => 268, "items_count" => 50}, - title_prefix: "AddressLogs" + next_page_params_example: %{"block_number" => 22_546_398, "index" => 268, "items_count" => 50} )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -714,8 +710,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {"Blocks validated by the specified address, with pagination.", "application/json", paginated_response( items: Schemas.Block, - next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50}, - title_prefix: "AddressBlocksValidated" + next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50} )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -782,8 +777,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do {"Historical coin balance changes for the specified address, with pagination.", "application/json", paginated_response( items: Schemas.CoinBalance, - next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50}, - title_prefix: "AddressCoinBalanceHistory" + next_page_params_example: %{"block_number" => 22_546_398, "items_count" => 50} )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -838,7 +832,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do ok: {"Daily coin balance history for the specified address.", "application/json", %Schema{ - title: "AddressCoinBalanceHistoryByDay", type: :object, properties: %{ days: %Schema{type: :integer, nullable: false}, @@ -905,8 +898,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "id" => 12_519_063_346, "items_count" => 50, "value" => "3750000000000000000000" - }, - title_prefix: "AddressTokens" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -980,8 +972,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "application/json", paginated_response( items: Schemas.Withdrawal, - next_page_params_example: %{"index" => 88_192_653, "items_count" => 50}, - title_prefix: "AddressWithdrawals" + next_page_params_example: %{"index" => 88_192_653, "items_count" => 50} )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -1047,7 +1038,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do items: Schemas.Address.schema() |> SchemasHelper.extend_schema( - title: "AddressWithCoinBalanceAndTransactionsCount", properties: %{ coin_balance: Schemas.General.IntegerStringNullable, transactions_count: %Schema{ @@ -1066,8 +1056,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "hash" => "0x59708733fbbf64378d9293ec56b977c011a08fd2", "items_count" => 50, "transactions_count" => nil - }, - title_prefix: "AddressList" + } ), properties: %{ exchange_rate: Schemas.General.FloatStringNullable, @@ -1208,8 +1197,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "token_contract_address_hash" => "0x1ffe11b9fb7f6ff1b153ab8608cf403ecaf9d44a", "token_id" => "24950", "token_type" => "ERC-721" - }, - title_prefix: "AddressNFTs" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -1285,8 +1273,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "items_count" => 50, "token_contract_address_hash" => "0x1ffe11b9fb7f6ff1b153ab8608cf403ecaf9d44a", "token_type" => "ERC-721" - }, - title_prefix: "AddressNFTCollections" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -1363,8 +1350,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do "associated_account_address_hash" => "0x1234567890123456789012345678901234567890", "type" => "validator", "items_count" => 50 - }, - title_prefix: "AddressCeloElectionRewards" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() @@ -1473,8 +1459,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do next_page_params_example: %{ "index" => 123, "items_count" => 50 - }, - title_prefix: "AddressBeaconDeposits" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index aa3becd1560e..eef82080d69a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -200,8 +200,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "block_number" => 22_566_361, "items_count" => 50 - }, - title_prefix: "Blocks" + } )} ] @@ -245,8 +244,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "block_number" => 22_566_361, "items_count" => 50 - }, - title_prefix: "ArbitrumBatchBlocks" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -293,8 +291,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "block_number" => 22_566_361, "items_count" => 50 - }, - title_prefix: "OptimismBatchBlocks" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -342,8 +339,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "block_number" => 22_566_361, "items_count" => 50 - }, - title_prefix: "ScrollBatchBlocks" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -392,8 +388,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do "block_number" => 12_345_678, "index" => 103, "items_count" => 50 - }, - title_prefix: "BlockTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -449,8 +444,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do "transaction_index" => 3, "index" => 8, "items_count" => 50 - }, - title_prefix: "BlockInternalTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -516,8 +510,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "index" => 88_192_653, "items_count" => 50 - }, - title_prefix: "BlockWithdrawals" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -620,8 +613,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do next_page_params_example: %{ "index" => 123, "items_count" => 50 - }, - title_prefix: "BlockBeaconDeposits" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex index b300b3149694..e6681a08f06b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/ethereum/deposit_controller.ex @@ -27,8 +27,7 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do next_page_params_example: %{ "index" => 123, "items_count" => 50 - }, - title_prefix: "BeaconDeposits" + } )}, forbidden: ForbiddenResponse.response() ] @@ -86,7 +85,6 @@ defmodule BlockScoutWeb.API.V2.Ethereum.DepositController do ok: {"Total count of beacon deposits.", "application/json", %Schema{ - title: "TotalBeaconDepositsCount", type: :object, properties: %{ deposits_count: %Schema{type: :integer, nullable: false} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 5e702d68138a..778283a5be48 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -42,8 +42,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do "transaction_index" => 68, "block_number" => 22_133_247, "items_count" => 50 - }, - title_prefix: "InternalTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 51a32ec2d2e1..a206eb58ca74 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -53,8 +53,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do %Schema{ type: :array, items: Schemas.Block.Response, - nullable: false, - additionalProperties: false + nullable: false }} ] @@ -88,8 +87,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do %Schema{ type: :array, items: Schemas.Transaction.Response, - nullable: false, - additionalProperties: false + nullable: false }}, unprocessable_entity: JsonErrorResponse.response() ] @@ -117,8 +115,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do %Schema{ type: :array, items: Schemas.Transaction.Response, - nullable: false, - additionalProperties: false + nullable: false }}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 169dd23ae5bd..a988f008466b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -105,8 +105,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do next_page_params_example: %{ "smart_contract_id" => 1_947_801, "items_count" => 50 - }, - title_prefix: "SmartContracts" + } )}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index ff7919421731..e649592cc4bc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -272,8 +272,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do "total_gas_used" => "100", "contract_address_hash" => "0x01a2A10583675E0e5dF52DE1b62734109201477a", "items_count" => 50 - }, - title_prefix: "HotSmartContracts" + } )}, unprocessable_entity: JsonErrorResponse.response(), forbidden: ForbiddenResponse.response() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index f386d05f54a0..d972a6f05772 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -148,8 +148,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "batch_block_hash" => "0x789", "batch_transaction_hash" => "0xabc", "index_in_batch" => 2 - }, - title_prefix: "TokenTransfers" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -205,8 +204,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "address_hash" => "0x48bb9b14483e43c7726df702b271d410e7460656", "value" => "200000000000000", "items_count" => 50 - }, - title_prefix: "TokenHolders" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -251,8 +249,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do items: Schemas.TokenInstance, next_page_params_example: %{ "unique_token" => 782_098 - }, - title_prefix: "TokenInstances" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -405,8 +402,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "index" => 920, "block_number" => 23_489_243, "token_id" => "4" - }, - title_prefix: "TokenInstanceTransfers" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -467,8 +463,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "items_count" => 50, "token_id" => "0", "value" => "4217417051704137590935" - }, - title_prefix: "TokenInstanceHolders" + } )}, unprocessable_entity: JsonErrorResponse.response(), not_found: NotFoundResponse.response() @@ -581,8 +576,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "market_cap" => "570958125.135513", "name" => "Wrapped Staked ETH", "items_count" => 50 - }, - title_prefix: "Tokens" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -645,8 +639,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do "market_cap" => "570958125.135513", "name" => "Wrapped Staked ETH", "items_count" => 50 - }, - title_prefix: "BridgedTokens" + } )}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex index e91ed2f9ec1e..406adf2cfb8d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_transfer_controller.ex @@ -53,8 +53,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do next_page_params_example: %{ "index" => 50, "block_number" => 22_133_247 - }, - title_prefix: "TokenTransfers" + } )}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 33f5165397fc..0d9a6c8f8198 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -230,8 +230,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 23_532_302, "index" => 375, "items_count" => 50 - }, - title_prefix: "Transactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -274,7 +273,6 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ok: {"Polygon ZkEVM batch transactions.", "application/json", %Schema{ - title: "PolygonZkEVMBatchTransactions", type: :object, properties: %{ items: %Schema{type: :array, items: Schemas.Transaction.Response} @@ -324,8 +322,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 65_361_291, "index" => 1, "items_count" => 50 - }, - title_prefix: "ZkSyncBatchTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -354,8 +351,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 391_483_842, "index" => 0, "items_count" => 50 - }, - title_prefix: "ArbitrumBatchTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -425,8 +421,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 142_678_440, "index" => 5, "items_count" => 50 - }, - title_prefix: "OptimismBatchTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -460,8 +455,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 14_127_868, "index" => 0, "items_count" => 50 - }, - title_prefix: "ScrollBatchTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -590,8 +584,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 14_127_868, "index" => 0, "items_count" => 50 - }, - title_prefix: "ExecutionNodeTransactions" + } )}, unprocessable_entity: JsonErrorResponse.response() ] @@ -686,8 +679,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params_example: %{ "index" => 442, "block_number" => 21_307_214 - }, - title_prefix: "TransactionTokenTransfers" + } )}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() @@ -747,8 +739,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 22_133_247, "transaction_index" => 68, "items_count" => 50 - }, - title_prefix: "TransactionInternalTransactions" + } )}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() @@ -798,8 +789,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "index" => 124, "block_number" => 21_925_703, "items_count" => 50 - }, - title_prefix: "TransactionLogs" + } )}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() @@ -852,8 +842,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params_example: %{ "state_changes" => nil, "items_count" => 50 - }, - title_prefix: "TransactionStateChanges" + } ) }, not_found: NotFoundResponse.response(), @@ -900,8 +889,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do "block_number" => 23_617_990, "index" => 128, "items_count" => 50 - }, - title_prefix: "WatchlistTransactions" + } )}, forbidden: ForbiddenResponse.response(), unprocessable_entity: JsonErrorResponse.response() @@ -1014,7 +1002,6 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ok: {"Blobs for transaction.", "application/json", %Schema{ - title: "TransactionBlobs", type: :object, properties: %{ items: %Schema{type: :array, items: Schemas.Blob.Response} @@ -1103,8 +1090,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do next_page_params_example: %{ "index" => 2_287_943, "items_count" => 50 - }, - title_prefix: "Transactions" + } )}, not_found: NotFoundResponse.response(), unprocessable_entity: JsonErrorResponse.response() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index 0d36c1d8f10f..33f21b1b350f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -27,8 +27,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do next_page_params_example: %{ "index" => 50, "items_count" => 50 - }, - title_prefix: "Withdrawals" + } )}, unprocessable_entity: JsonErrorResponse.response() ] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 72cb1b524b47..0500d997eb22 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -732,10 +732,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do def paginated_response(options) do items_schema = Keyword.fetch!(options, :items) next_page_params_example = Keyword.fetch!(options, :next_page_params_example) - title_prefix = Keyword.fetch!(options, :title_prefix) %Schema{ - title: "#{title_prefix}PaginatedResponse", type: :object, properties: %{ items: %Schema{type: :array, items: items_schema, nullable: false}, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex index 56726e08b077..909740995500 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_input.ex @@ -16,7 +16,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.General.DecodedInput do name: %Schema{type: :string, nullable: false}, type: %Schema{type: :string, nullable: false}, value: %Schema{ - anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + anyOf: [ + %Schema{type: :object}, + %Schema{ + type: :array, + items: %Schema{ + anyOf: [ + %Schema{type: :object}, + %Schema{type: :array, items: %Schema{type: :string}}, + %Schema{type: :string} + ] + } + }, + %Schema{type: :string} + ], nullable: false } }, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex index 445cd02d2d1d..bdf3d9bb7f4b 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general/decoded_log_input.ex @@ -17,7 +17,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.General.DecodedLogInput do type: %Schema{type: :string, nullable: false}, indexed: %Schema{type: :boolean, nullable: false}, value: %Schema{ - anyOf: [%Schema{type: :object}, %Schema{type: :array}, %Schema{type: :string}], + anyOf: [ + %Schema{type: :object}, + %Schema{ + type: :array, + items: %Schema{ + anyOf: [ + %Schema{type: :object}, + %Schema{type: :array, items: %Schema{type: :string}}, + %Schema{type: :string} + ] + } + }, + %Schema{type: :string} + ], nullable: false } }, From 6db8f300784c4c17e94fe07447a05c9bf90d832b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 12 Dec 2025 14:24:52 +0300 Subject: [PATCH 0538/1006] chore: Refactoring of the application mode config (#13715) --- CHANGELOG.md | 1 + .../controllers/api/v2/config_controller.ex | 2 +- config/runtime.exs | 7 +++---- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b7933f4514d..fdfe8bc3a8a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ ### ⚙️ Miscellaneous Tasks +- Refactoring of the application mode config ([#13715](https://github.com/blockscout/blockscout/pull/13715)) - Eliminate warnings in the Swagger file ([#13714](https://github.com/blockscout/blockscout/pull/13714)) - Change URL to Solidity binaries list ([#13711](https://github.com/blockscout/blockscout/pull/13711)) - Add osaka to the default list of supported EVM versions ([#13680](https://github.com/blockscout/blockscout/pull/13680)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 507ca4c8b8da..300cdd034734 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -69,7 +69,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do ] def indexer(conn, _params) do - indexer_enabled? = Application.get_env(:indexer, Indexer.Supervisor)[:enabled] + indexer_enabled? = System.get_env("DISABLE_INDEXER") !== "true" internal_transactions_fetcher_enabled? = indexer_enabled? && diff --git a/config/runtime.exs b/config/runtime.exs index 79e92b2f372c..647e2681d433 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -272,9 +272,8 @@ config :ethereum_jsonrpc, EthereumJSONRPC.Utility.EndpointAvailabilityChecker, e ### Explorer ### ################ -disable_indexer? = ConfigHelper.parse_bool_env_var("DISABLE_INDEXER") -disable_webapp? = ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP") app_mode = ConfigHelper.mode() +disable_indexer? = app_mode == :api || ConfigHelper.parse_bool_env_var("DISABLE_INDEXER") disable_exchange_rates? = if System.get_env("DISABLE_MARKET"), @@ -715,7 +714,7 @@ config :explorer, Explorer.Account, siwe_message: System.get_env("ACCOUNT_SIWE_MESSAGE", "Sign in to Blockscout Account V2") config :explorer, Explorer.Chain.Cache.MinMissingBlockNumber, - enabled: !ConfigHelper.parse_bool_env_var("DISABLE_INDEXER"), + enabled: !disable_indexer?, batch_size: ConfigHelper.parse_integer_env_var("MIN_MISSING_BLOCK_NUMBER_BATCH_SIZE", 100_000) config :explorer, :spandex, @@ -991,7 +990,7 @@ config :indexer, :arc, arc_native_token_system_address: System.get_env("INDEXER_ARC_NATIVE_TOKEN_SYSTEM_CONTRACT", "0x1800000000000000000000000000000000000000") -config :indexer, Indexer.Supervisor, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_INDEXER") +config :indexer, Indexer.Supervisor, enabled: !disable_indexer? config :indexer, Indexer.Fetcher.TransactionAction.Supervisor, enabled: ConfigHelper.parse_bool_env_var("INDEXER_TX_ACTIONS_ENABLE") From ba84236abf0103aac89db174766b89c3cd7d825a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 12 Dec 2025 17:16:22 +0300 Subject: [PATCH 0539/1006] chore: GitHub Actions workflows: stop using ELIXIR_VERSION & OTP_VERSION from org/repo variables (#13718) --- .github/workflows/config.yml | 4 ++-- .github/workflows/generate-swagger.yml | 4 ++-- .github/workflows/pre-release-arbitrum.yml | 4 ++-- .github/workflows/pre-release-celo.yml | 4 ++-- .github/workflows/pre-release-eth.yml | 4 ++-- .github/workflows/pre-release-filecoin.yml | 4 ++-- .github/workflows/pre-release-fuse.yml | 4 ++-- .github/workflows/pre-release-gnosis.yml | 4 ++-- .github/workflows/pre-release-optimism.yml | 4 ++-- .github/workflows/pre-release-polygon-zkevm.yml | 4 ++-- .github/workflows/pre-release-rootstock.yml | 4 ++-- .github/workflows/pre-release-scroll.yml | 4 ++-- .github/workflows/pre-release-zilliqa.yml | 4 ++-- .github/workflows/pre-release-zksync.yml | 4 ++-- .github/workflows/pre-release.yml | 4 ++-- .github/workflows/publish-docker-image-every-push.yml | 4 ++-- .github/workflows/publish-docker-image-old-ui.yml | 4 ++-- .github/workflows/publish-docker-image-staging-on-demand.yml | 4 ++-- .github/workflows/publish-regular-docker-image-on-demand.yml | 4 ++-- .github/workflows/release-arbitrum.yml | 4 ++-- .github/workflows/release-celo.yml | 4 ++-- .github/workflows/release-default.yml | 4 ++-- .github/workflows/release-eth.yml | 4 ++-- .github/workflows/release-filecoin.yml | 4 ++-- .github/workflows/release-fuse.yml | 4 ++-- .github/workflows/release-gnosis.yml | 4 ++-- .github/workflows/release-optimism.yml | 4 ++-- .github/workflows/release-polygon-zkevm.yml | 4 ++-- .github/workflows/release-rootstock.yml | 4 ++-- .github/workflows/release-scroll.yml | 4 ++-- .github/workflows/release-zetachain.yml | 4 ++-- .github/workflows/release-zilliqa.yml | 4 ++-- .github/workflows/release-zksync.yml | 4 ++-- CHANGELOG.md | 1 + 34 files changed, 67 insertions(+), 66 deletions(-) diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index cdd53359e2e1..afdb51c01855 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -31,8 +31,8 @@ on: env: MIX_ENV: test - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ github.ref_name == '13566/merge' && '1.19.4' || vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' ACCOUNT_AUTH0_DOMAIN: "blockscoutcom.us.auth0.com" jobs: diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index f49a32419cd8..ddf27bfc7aa7 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -15,8 +15,8 @@ on: types: [published] env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' RELEASE_VERSION: 9.3.0 jobs: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index b8e5800efc20..472082362460 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index c41dffa21249..0ffe1df8f82b 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 34935fa4b42b..a1c7cd93cedc 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 85ed3d662009..19b4c07e2278 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 71c19872260c..09548973a789 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index ef4fdc6ea23f..1b0a50540bdf 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 5db3e5f288bf..8385bfe84d44 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 912b936408b3..cd6df3268065 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 69e40557d542..e88fd3dff02e 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index d153e5f52f99..ee1127c0c9a9 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 595270a627cf..50a8d5ff6375 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 2945f32d7f9d..186aebda9806 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 307edef1ee33..516f71a16f10 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -13,8 +13,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 8c723245e8a0..057f2a1fb1ea 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -9,8 +9,8 @@ on: - '**/README.md' - 'docker-compose/*' env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' RELEASE_VERSION: 9.3.0 permissions: diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 4dc18558a052..c2cdfa3eacae 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -4,8 +4,8 @@ on: workflow_dispatch: env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' permissions: contents: read diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index d5a6af2b7d2f..c572c31ced6b 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -10,8 +10,8 @@ on: - '**/README.md' - 'docker-compose/*' env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' RELEASE_VERSION: 9.3.0 permissions: diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index 210d3c1ef3d7..ad87f7dd24dd 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -3,8 +3,8 @@ name: Publish regular Docker image on demand on: workflow_dispatch: env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' RELEASE_VERSION: 9.3.0 permissions: diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 1f2075858b7b..6fa982fe95bd 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index a788ecbd74ea..a960190fb244 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index c4e900c18167..a897b863b120 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 01c788947295..4b3bab79e258 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 814821d8920a..0fb8c633362d 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 3b5110bceae7..460cf5ebb974 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 17f2bf33fefd..b1d7c7da6a24 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 9eff900f67ba..526b201593a2 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index d6fc9e35fdc2..3a445a63b2f7 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index f23d4b4f134a..6200cecf0be2 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 4beed960095d..cb1e7f502fdc 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index e95fdf172bee..3e8a52c61061 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 4818ab7ea8f8..dfb2ec70f726 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 2456a899624c..fcf32047be00 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -10,8 +10,8 @@ permissions: packages: write env: - OTP_VERSION: ${{ vars.OTP_VERSION }} - ELIXIR_VERSION: ${{ vars.ELIXIR_VERSION }} + OTP_VERSION: '27.3.4.6' + ELIXIR_VERSION: '1.19.4' jobs: push_to_registry: diff --git a/CHANGELOG.md b/CHANGELOG.md index fdfe8bc3a8a7..cd52a08089ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ ### ⚙️ Miscellaneous Tasks +- GitHub Actions workflows: stop using ELIXIR_VERSION & OTP_VERSION from org/repo variables ([#13718](https://github.com/blockscout/blockscout/pull/13718)) - Refactoring of the application mode config ([#13715](https://github.com/blockscout/blockscout/pull/13715)) - Eliminate warnings in the Swagger file ([#13714](https://github.com/blockscout/blockscout/pull/13714)) - Change URL to Solidity binaries list ([#13711](https://github.com/blockscout/blockscout/pull/13711)) From dc39f9af42e7f87be5d1a551d17d20480b9b8c07 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Mon, 15 Dec 2025 15:46:50 +0400 Subject: [PATCH 0540/1006] fix: `batch_number` input param is now integer for OP and Scroll API endpoints (#13727) Co-authored-by: POA <33550681+poa@users.noreply.github.com> Co-authored-by: Victor Baranov --- CHANGELOG.md | 2 ++ .../controllers/api/v2/transaction_controller.ex | 8 ++------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd52a08089ce..738cca3a7cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- `batch_number` input param is now integer for OP and Scroll API endpoints ([#13727](https://github.com/blockscout/blockscout/pull/13727)) - Set timeout: :infinity for delete zero value migration ([#13708](https://github.com/blockscout/blockscout/pull/13708)) - Limit batch size for placeholders insertion ([#13699](https://github.com/blockscout/blockscout/pull/13699)) - Add missed reputation fetch ([#13695](https://github.com/blockscout/blockscout/pull/13695)) @@ -73,6 +74,7 @@ - Increase genesis file content fetch timeout ([#13527](https://github.com/blockscout/blockscout/issues/13527)) ### New ENV variables + | Variable | Description | Parameters | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `INDEXER_DISABLE_HOT_SMART_CONTRACTS_FETCHER` | If `true`, `Indexer.Fetcher.Stats.HotSmartContracts` won't be started. Implemented in [#13471](https://github.com/blockscout/blockscout/pull/13471). | Version: v9.3.0\+
Default: `false`
Applications: Indexer | diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 0d9a6c8f8198..79a91d463832 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -431,9 +431,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do It renders the list of L2 transactions bound to the specified batch. """ @spec optimism_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def optimism_batch(conn, %{batch_number_param: batch_number_string} = params) do - {batch_number, ""} = Integer.parse(batch_number_string) - + def optimism_batch(conn, %{batch_number_param: batch_number} = params) do l2_block_number_from = OptimismTransactionBatch.edge_l2_block_number(batch_number, :min, @api_true) l2_block_number_to = OptimismTransactionBatch.edge_l2_block_number(batch_number, :max, @api_true) @@ -465,9 +463,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do It renders the list of L2 transactions bound to the specified batch. """ @spec scroll_batch(Plug.Conn.t(), map()) :: Plug.Conn.t() - def scroll_batch(conn, %{batch_number_param: batch_number_string} = params) do - {batch_number, ""} = Integer.parse(batch_number_string) - + def scroll_batch(conn, %{batch_number_param: batch_number} = params) do {l2_block_number_from, l2_block_number_to} = case ScrollReader.batch(batch_number, @api_true) do {:ok, batch} -> {batch.l2_block_range.from, batch.l2_block_range.to} From 373b793381028209639a66a47a65657a994dbb43 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 16 Dec 2025 11:27:32 +0300 Subject: [PATCH 0541/1006] perf: Improve performance of `api/v2/main-page/indexing-status` endpoint (#13730) --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain.ex | 45 ++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 738cca3a7cc9..eed4a3f523f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ ### ⚡ Performance +- Improve performance of api/v2/main-page/indexing-status endpoint ([#13730](https://github.com/blockscout/blockscout/pull/13730)) - Implement celo aggregated election rewards ([#13418](https://github.com/blockscout/blockscout/issues/13418)) ### ⚙️ Miscellaneous Tasks diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 1b9298dc8c1b..1d9749124764 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -707,6 +707,10 @@ defmodule Explorer.Chain do end end + @doc """ + Checks if indexing of blocks finished based on the ratio of indexed blocks to blockchain height + """ + @spec finished_indexing_from_ratio?(Decimal.t()) :: boolean() def finished_indexing_from_ratio?(ratio) do Decimal.compare(ratio, 1) !== :lt end @@ -1718,28 +1722,47 @@ defmodule Explorer.Chain do select_repo(options).one!(query) end - def indexer_running? do + @spec indexer_running?() :: boolean() + defp indexer_running? do Application.get_env(:indexer, Indexer.Supervisor)[:enabled] or match?({:ok, _, _}, HealthHelper.last_db_block_status()) end - def internal_transactions_fetcher_running? do + @spec internal_transactions_fetcher_running?() :: boolean() + defp internal_transactions_fetcher_running? do not Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor)[:disabled?] or match?({:ok, _, _}, last_db_internal_transaction_block_status()) end - def last_db_internal_transaction_block_status do - query = - from(it in InternalTransaction, - join: block in assoc(it, :block), - select: {block.number, block.timestamp}, - order_by: [desc: block.number], + @spec last_db_internal_transaction_block_status() :: + {:ok, Block.block_number(), DateTime.t()} + | {:stale, Block.block_number(), DateTime.t()} + | {:error, :no_blocks} + defp last_db_internal_transaction_block_status do + it_query = + from(internal_transaction in InternalTransaction, + select: internal_transaction.block_number, + order_by: [desc: internal_transaction.block_number], limit: 1 ) - query - |> Repo.one() - |> HealthHelper.block_status() + last_it_block_number = + it_query + |> Repo.one() + + if is_nil(last_it_block_number) do + {:error, :no_blocks} + else + block_query = + from(block in Block, + select: {block.number, block.timestamp}, + where: block.consensus == true and block.number == ^last_it_block_number + ) + + block_query + |> Repo.one() + |> HealthHelper.block_status() + end end def fetch_min_missing_block_cache(from \\ nil, to \\ nil) do From 1e25edd3dd74b54642e7c9ca8ac7548f2a99cf97 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 16 Dec 2025 18:43:33 +0400 Subject: [PATCH 0542/1006] fix: Fix tests for on-demand internal transaction fetcher (#13744) --- .../fetcher/on_demand/internal_transaction_test.exs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs index 56da961be67f..6146625ddaf1 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/internal_transaction_test.exs @@ -13,9 +13,15 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransactionTest do setup :verify_on_exit! setup do - initial_env = Application.get_all_env(:ethereum_jsonrpc) - on_exit(fn -> Application.put_all_env([{:ethereum_jsonrpc, initial_env}]) end) - EthereumJSONRPC.Case.Geth.Mox.setup() + initial_ethereum_jsonrpc_env = Application.get_all_env(:ethereum_jsonrpc) + initial_json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + %{json_rpc_named_arguments: json_rpc_named_arguments} = EthereumJSONRPC.Case.Geth.Mox.setup() + Application.put_env(:explorer, :json_rpc_named_arguments, json_rpc_named_arguments) + + on_exit(fn -> + Application.put_all_env([{:ethereum_jsonrpc, initial_ethereum_jsonrpc_env}]) + Application.put_env(:explorer, :json_rpc_named_arguments, initial_json_rpc_named_arguments) + end) end test "fetch_by_transaction/2" do From a9af70615c78c8808d1b32668138661ac0e3e289 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 16 Dec 2025 17:58:35 +0300 Subject: [PATCH 0543/1006] Add missing CHANGELOG entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index eed4a3f523f5..e95f2a19ed11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### 🐛 Bug Fixes +- Fix tests for on-demand internal transaction fetcher ([#13744](https://github.com/blockscout/blockscout/pull/13744)) - `batch_number` input param is now integer for OP and Scroll API endpoints ([#13727](https://github.com/blockscout/blockscout/pull/13727)) - Set timeout: :infinity for delete zero value migration ([#13708](https://github.com/blockscout/blockscout/pull/13708)) - Limit batch size for placeholders insertion ([#13699](https://github.com/blockscout/blockscout/pull/13699)) From 0ca9057353a9933ff6987ddbbe986b592ee9951c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Dec 2025 12:24:01 +0300 Subject: [PATCH 0544/1006] fix: Fix blob transactions list API endpoint (#13756) --- .../controllers/api/v2/block_controller.ex | 2 +- .../api/v2/transaction_controller.ex | 2 +- .../block_scout_web/schemas/api/v2/general.ex | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index eef82080d69a..4431eb307f4d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -377,7 +377,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do description: "Retrieves transactions included in a specific block, ordered by transaction index.", parameters: base_params() ++ - [block_hash_or_number_param(), transaction_type_param()] ++ + [block_hash_or_number_param(), block_transaction_type_param()] ++ define_paging_params(["block_number", "index", "items_count"]), responses: [ ok: diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 79a91d463832..4c9046679fec 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -219,7 +219,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do description: "Retrieves a paginated list of transactions with optional filtering by status, type, and method.", parameters: base_params() ++ - [transaction_filter_param()] ++ + [transaction_filter_param(), transaction_type_param()] ++ define_paging_params(["block_number", "index", "items_count", "hash", "inserted_at"]), responses: [ ok: diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 0500d997eb22..777cdfd5a483 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -384,6 +384,23 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do """ @spec transaction_type_param() :: Parameter.t() def transaction_type_param do + %Parameter{ + name: :type, + in: :query, + schema: %Schema{type: :string, enum: ["blob_transaction"]}, + required: false, + description: """ + Filter by transaction type. Comma-separated list of: + * blob_transaction - Only show blob transactions + """ + } + end + + @doc """ + Returns a parameter definition for filtering block transactions by type. + """ + @spec block_transaction_type_param() :: Parameter.t() + def block_transaction_type_param do %Parameter{ name: :type, in: :query, From 64de26e323097cb9bdd2126c229554266f766750 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 17 Dec 2025 12:46:47 +0300 Subject: [PATCH 0545/1006] 9.3.1 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .github/workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .github/workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .../publish-docker-image-for-optimism-exeperimental.yml | 2 +- .../publish-docker-image-for-optimism-worldchain.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 6 ++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 63 files changed, 68 insertions(+), 62 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index ddf27bfc7aa7..484e91d37d53 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 472082362460..75bfe8d87a82 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 0ffe1df8f82b..bed71a7473f9 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index a1c7cd93cedc..806378d1e38d 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 19b4c07e2278..00800bbc67d9 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 09548973a789..60086b3996c2 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index 1b0a50540bdf..fc6b81296404 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 8385bfe84d44..7586a34644a7 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index cd6df3268065..8020002344f4 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index e88fd3dff02e..3057a60ef469 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index ee1127c0c9a9..18762454354a 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 50a8d5ff6375..dbfabe6a1890 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 186aebda9806..0a38f3608f04 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 516f71a16f10..21b1b865e1d7 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 53276c2a3a19..28c7e5c9b4a2 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 057f2a1fb1ea..7b0675fd325a 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 7a337ba907f6..913c2bb6a7ef 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index fa5a88414b22..39bcd6c0675c 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 1e3e7615b39e..ffdbfb702eb1 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 936fc44b5e96..77fbc41db645 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index c7ac4d9b40b1..5b6a708aef58 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index 0b4242d041e0..c66a54432b74 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index a89d9faa83b9..54d6b3b1afe9 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 62e6842f52e5..72e1c3427441 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 9302e07650c1..965221bc48ae 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index f1b0a578a6f6..d55c5d169530 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml index a95e75e07362..e04ad1a1ee96 100644 --- a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml +++ b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index 98328d91e129..9f54378c78c3 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 442e2474036c..fba86d47fc7d 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index d1652d7d29d3..df11ace2dccc 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index d74844c8f0e3..c48aff7d7144 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index 4db8b8c0a83e..cbde2e237ea1 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 2b9af018a4e7..8348d643beff 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 1f4466402baf..2981d5d85bb5 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 09429c7dbb26..6884dc8095e9 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index c2cdfa3eacae..05f33caf8834 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index c572c31ced6b..99c79507d5e1 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index ad87f7dd24dd..fae001cf8ab0 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 6fa982fe95bd..2725c2280dbc 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index a960190fb244..c8483b0ffdd8 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index a897b863b120..befd5faebbfe 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 4b3bab79e258..3f019fc0b37a 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 0fb8c633362d..48e5328dd966 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 460cf5ebb974..0e9d4f579557 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index b1d7c7da6a24..230ffdcbc9cd 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 526b201593a2..cf9721371b64 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index 3a445a63b2f7..b7de98bd1e81 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 6200cecf0be2..0b0746c378e9 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index cb1e7f502fdc..218107564337 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index 3e8a52c61061..eb91ac5e5134 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index dfb2ec70f726..78f7de88a6f4 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index fcf32047be00..2dfa3e6f94d6 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.0 + RELEASE_VERSION: 9.3.1 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index e95f2a19ed11..4289a4c8a645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 9.3.1 + +### 🐛 Bug Fixes + +- Fix blob transactions list API endpoint ([#13756](https://github.com/blockscout/blockscout/issues/13756)) + ## 9.3.0 ### 🚀 Features diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 738851261884..4a360a468d06 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.0", + version: "9.3.1", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 857a1afa7939..751257911321 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -19,7 +19,7 @@ defmodule EthereumJSONRPC.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.0" + version: "9.3.1" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 431ccbe37b25..89041190c277 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.3.0"}] + @default_headers [{"User-Agent", "blockscout-9.3.1"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 21bb576dc31b..987f4dd7cc41 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -20,7 +20,7 @@ defmodule Explorer.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.0", + version: "9.3.1", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index b9d67bf8d207..7621baad42ca 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.0", + version: "9.3.1", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index a314adb137c8..1470518c4de1 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.3.0", + version: "9.3.1", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index addda8865dbb..f0ff75c8e34f 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.3.0", + version: "9.3.1", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index e51a7fc2ccf8..718c52e06e81 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.3.0' +RELEASE_VERSION ?= '9.3.1' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 255b2297c17f..b7d5ff2eb53a 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.3.0", + version: "9.3.1", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index eb883626e146..6e428d75e626 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.3.0" + set version: "9.3.1" set applications: [ :runtime_tools, block_scout_web: :permanent, From 4426504d424ac9ac556dba339ee05e838914b2a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:28:07 +0300 Subject: [PATCH 0546/1006] chore(deps): bump ex_aws_s3 from 2.5.8 to 2.5.9 (#13741) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index 025f706177de..d15817a3e970 100644 --- a/mix.lock +++ b/mix.lock @@ -16,7 +16,7 @@ "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, "cachex": {:hex, :cachex, "4.1.1", "574c5cd28473db313a0a76aac8c945fe44191659538ca6a1e8946ec300b1a19f", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:ex_hash_ring, "~> 6.0", [hex: :ex_hash_ring, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "d6b7449ff98d6bb92dda58bd4fc3189cae9f99e7042054d669596f56dc503cd8"}, "cafezinho": {:hex, :cafezinho, "0.4.4", "36c31fc5456b1284180d8a9d968c7eaaf474782df5af023a8f8b66937c5b2785", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "3ca334f2a2992ec081868c39ad0487eb97d213292702aae6b197fd5e6f04fd58"}, - "castore": {:hex, :castore, "1.0.16", "8a4f9a7c8b81cda88231a08fe69e3254f16833053b23fa63274b05cbc61d2a1e", [:mix], [], "hexpm", "33689203a0eaaf02fcd0e86eadfbcf1bd636100455350592e7e2628564022aaf"}, + "castore": {:hex, :castore, "1.0.17", "4f9770d2d45fbd91dcf6bd404cf64e7e58fed04fadda0923dc32acca0badffa2", [:mix], [], "hexpm", "12d24b9d80b910dd3953e165636d68f147a31db945d2dcb9365e441f8b5351e5"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, @@ -52,8 +52,8 @@ "evil_crc32c": {:hex, :evil_crc32c, "0.2.9", "9e4082418c4cc27c88869c1773f114f15a9257121ebb019037bea60c75c601d2", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "86ba14f8aad0fe55eda0b57e79e3d8dffe39ea326b753eac12463ae2bec757bf"}, "evision": {:hex, :evision, "0.2.14", "6005e44b38ea61a33109694c5b097d42e81c767723c55d0c230d9662d6c87987", [:make, :mix, :rebar3], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.11", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}, {:progress_bar, "~> 2.0 or ~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: true]}], "hexpm", "630714395f2c242e488944b4c9c75a5de96cbb95d3cbb69f0525bdb3e0a88775"}, "ex_abi": {:hex, :ex_abi, "0.8.3", "e01cbafd81008db9e8bc8ac0a4ae5bf08e86a2d82afdca242550269445b80e04", [:mix], [{:ex_keccak, "~> 0.7.6", [hex: :ex_keccak, repo: "hexpm", optional: true]}], "hexpm", "6349a10331c54a1e09865811f705913fa181bc75b4c9a2d326fd96e21bf60cd4"}, - "ex_aws": {:hex, :ex_aws, "2.6.0", "346e87e35e5df0b3c016a96fb30adf6001de102981a71648dfc3ce3ad04765af", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "30729ee9cbaacda674a4e4260d74206fa89bcd712267c4eaf42a0fc34592c0b3"}, - "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.8", "5ee7407bc8252121ad28fba936b3b293f4ecef93753962351feb95b8a66096fa", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "84e512ca2e0ae6a6c497036dff06d4493ffb422cfe476acc811d7c337c16691c"}, + "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, + "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, "ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, @@ -138,7 +138,7 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"}, + "plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.5", "261f21b67aea8162239b2d6d3b4c31efde4daa22a20d80b19c2c0f21b34b270e", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "20884bf58a90ff5a5663420f5d2c368e9e15ed1ad5e911daf0916ea3c57f77ac"}, "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, From 8505bfdf22cd46316cbffb97de53a4391bd99e3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:28:29 +0300 Subject: [PATCH 0547/1006] chore(deps): bump inet_cidr from 1.0.8 to 1.0.9 (#13738) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index d15817a3e970..d3dce0230d0c 100644 --- a/mix.lock +++ b/mix.lock @@ -90,7 +90,7 @@ "httpoison": {:hex, :httpoison, "2.3.0", "10eef046405bc44ba77dc5b48957944df8952cc4966364b3cf6aa71dce6de587", [:mix], [{:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "d388ee70be56d31a901e333dbcdab3682d356f651f93cf492ba9f06056436a2c"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, "image": {:hex, :image, "0.62.1", "1dd3d8d0d29d6562aa2141b5ef08c0f6a60e2a9f843fe475499b2f4f1ef60406", [:mix], [{:bumblebee, "~> 0.6", [hex: :bumblebee, repo: "hexpm", optional: true]}, {:evision, "~> 0.1.33 or ~> 0.2", [hex: :evision, repo: "hexpm", optional: true]}, {:exla, "~> 0.9", [hex: :exla, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:kino, "~> 0.13", [hex: :kino, repo: "hexpm", optional: true]}, {:nx, "~> 0.9", [hex: :nx, repo: "hexpm", optional: true]}, {:nx_image, "~> 0.1", [hex: :nx_image, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.1 or ~> 3.2 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:rustler, "> 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:scholar, "~> 0.3", [hex: :scholar, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}, {:vix, "~> 0.33", [hex: :vix, repo: "hexpm", optional: false]}], "hexpm", "5a5a7acaf68cfaed8932d478b95152cd7d84071442cac558c59f2d31427e91ab"}, - "inet_cidr": {:hex, :inet_cidr, "1.0.8", "d26bb7bdbdf21ae401ead2092bf2bb4bf57fe44a62f5eaa5025280720ace8a40", [:mix], [], "hexpm", "d5b26da66603bb56c933c65214c72152f0de9a6ea53618b56d63302a68f6a90e"}, + "inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, From a1d38dbc6c1d88470b7f0c581738dacbaf357d9c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:29:04 +0300 Subject: [PATCH 0548/1006] chore(deps): bump ecto_sql from 3.13.2 to 3.13.3 (#13737) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index d3dce0230d0c..73f38ffcd952 100644 --- a/mix.lock +++ b/mix.lock @@ -45,7 +45,7 @@ "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.2", "a07d2461d84107b3d037097c822ffdd36ed69d1cf7c0f70e12a3d1decf04e2e1", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "539274ab0ecf1a0078a6a72ef3465629e4d6018a3028095dc90f60a19c371717"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, From 986d6ef6788ae28dc7b9530f71c470388e6119c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:29:39 +0300 Subject: [PATCH 0549/1006] chore(deps): bump websockex from 0.5.0 to 0.5.1 (#13736) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 73f38ffcd952..54873aafd465 100644 --- a/mix.lock +++ b/mix.lock @@ -184,6 +184,6 @@ "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, - "websockex": {:hex, :websockex, "0.5.0", "3caac5f676a2c6ba2eab7bf2828991cd14f7281f936b03deccf041c8b948a5a2", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2f4b08ba6f9531df3debbd0a40a0c9a1336552eb10fabbdfa2d2e31b4e445896"}, + "websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"}, "ymlr": {:hex, :ymlr, "5.1.4", "b924d61e1fc1ec371cde6ab3ccd9311110b1e052fc5c2460fb322e8380e7712a", [:mix], [], "hexpm", "75f16cf0709fbd911b30311a0359a7aa4b5476346c01882addefd5f2b1cfaa51"}, } From a2ce11f28c024aebb462cf964d8a6f67a86a802a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:36:18 +0300 Subject: [PATCH 0550/1006] chore(deps): bump absinthe_phoenix from 2.0.3 to 2.0.4 (#13739) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 54873aafd465..99b1bc31fc76 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "absinthe": {:hex, :absinthe, "1.9.0", "28f11753d01c0e8b6cb6e764a23cf4081e0e6cae88f53f4c9e4320912aee9c07", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "db65993420944ad90e932827663d4ab704262b007d4e3900cd69615f14ccc8ce"}, - "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.3", "74e0862f280424b7bc290f6f69e133268bce0b4e7db0218c7e129c5c2b1d3fd4", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "caffaea03c17ea7419fe07e4bc04c2399c47f0d8736900623dbf4749a826fd2c"}, + "absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.4", "f36999412fbd6a2339abb5b7e24a4cc9492bbc7909d5806deeef83b06f55c508", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "66617ee63b725256ca16264364148b10b19e2ecb177488cd6353584f2e6c1cf3"}, "absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "90a8188e94e2650f13259fb16462075a87f98e18", [tag: "1.5.8"]}, "absinthe_relay": {:hex, :absinthe_relay, "1.6.0", "73590bdb0dcc192622f39fa11da6fca9df4e339b603c45ec7a93493eb94f1829", [:mix], [{:absinthe, ">= 1.7.10", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "32d6397a7af3fd02678ef9bc8e2f574487f14593cb3e4f9110fb1c695d4d2ac0"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, From 1e628000399c576947211328eb6e4eee70951730 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 16:36:39 +0300 Subject: [PATCH 0551/1006] chore(deps-dev): bump ex_doc from 0.39.1 to 0.39.3 (#13734) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 99b1bc31fc76..eb14833b3ef8 100644 --- a/mix.lock +++ b/mix.lock @@ -60,7 +60,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.36.0", "d0de502db2fa24eeae16749af1af0093f24fb823e8715c66076cd8c6a3a960e3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "17640b8daf2580a0a11317a720a26079e774d4c36f939d82f4e9f7075269897d"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.0", "7ade6ae1017a8f650f83f79a378ad9ad5bcc4eacfa2704349590c1f91f2eac19", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "eb93f99005f8d3f1b1b12c85a45323937350e11d3e79f8375ab51e2ff44392f2"}, - "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, From 29d05f28040c8c70315986c15e2fd2f61a90e086 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 17:24:43 +0300 Subject: [PATCH 0552/1006] chore(deps-dev): bump phoenix_live_reload from 1.6.1 to 1.6.2 (#13740) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index eb14833b3ef8..f7cc8b7e9dc5 100644 --- a/mix.lock +++ b/mix.lock @@ -133,7 +133,7 @@ "phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, - "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.1", "05df733a09887a005ed0d69a7fc619d376aea2730bf64ce52ac51ce716cc1ef0", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "74273843d5a6e4fef0bbc17599f33e3ec63f08e69215623a0cd91eea4288e5a0"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, From e0655b3e66ebde84d0eef25a7a92ad5c1db27cd2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 18:08:34 +0300 Subject: [PATCH 0553/1006] chore(deps): bump phoenix_live_view from 1.1.17 to 1.1.19 (#13733) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index f7cc8b7e9dc5..851b24ef89fc 100644 --- a/mix.lock +++ b/mix.lock @@ -134,7 +134,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.17", "1d782b5901cf13b137c6d8c56542ff6cb618359b2adca7e185b21df728fa0c6c", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fa82307dd9305657a8236d6b48e60ef2e8d9f742ee7ed832de4b8bcb7e0e5ed2"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, From 16ca2aceac03ce730948b6d630c19a9404c4cc17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 19:59:53 +0300 Subject: [PATCH 0554/1006] chore(deps-dev): bump credo from 1.7.13 to 1.7.14 (#13731) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Victor Baranov --- .../controllers/api/rpc/contract_controller.ex | 2 +- .../controllers/api/rpc/logs_controller.ex | 6 +++--- apps/explorer/lib/explorer/account/custom_abi.ex | 2 +- .../lib/explorer/arbitrum/claim_rollup_message.ex | 2 +- apps/explorer/lib/explorer/chain.ex | 4 +++- apps/explorer/lib/explorer/chain/blackfort/validator.ex | 2 +- apps/explorer/lib/explorer/chain/stability/validator.ex | 2 +- apps/explorer/lib/explorer/market/source/coin_gecko.ex | 2 +- apps/explorer/lib/explorer/market/source/dia.ex | 2 +- .../explorer/migrator/restore_omitted_weth_transfers.ex | 2 +- .../lib/explorer/third_party_integrations/auth0.ex | 2 +- apps/indexer/lib/indexer/fetcher/arbitrum/da/anytrust.ex | 8 ++++---- .../indexer/fetcher/arbitrum/workers/batches/events.ex | 2 +- .../fetcher/arbitrum/workers/confirmations/events.ex | 2 +- .../arbitrum/workers/confirmations/rollup_blocks.ex | 8 ++++---- .../indexer/fetcher/arbitrum/workers/l1_finalization.ex | 2 +- .../indexer/fetcher/arbitrum/workers/new_l1_executions.ex | 2 +- .../fetcher/arbitrum/workers/new_messages_to_l2.ex | 2 +- mix.lock | 2 +- 19 files changed, 29 insertions(+), 27 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index 5149417682b8..ce2bb90b0903 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -355,7 +355,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do files_array |> Enum.filter(fn file -> SmartContractHelper.sol_file?(file.filename) end) - if length(jsons) > 0 and length(sols) > 0 do + if not Enum.empty?(jsons) and not Enum.empty?(sols) do {:ok, files_array} else {:error, "You should attach at least one *.json and one *.sol files"} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/logs_controller.ex index 388b99df25a0..50c9906164e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/logs_controller.ex @@ -133,10 +133,10 @@ defmodule BlockScoutWeb.API.RPC.LogsController do fetched_params = Map.take(params, @required_params.one_of) found_keys = Map.keys(fetched_params) - if length(found_keys) > 0 do - {:ok, fetched_params} - else + if Enum.empty?(found_keys) do {:error, @required_params.one_of} + else + {:ok, fetched_params} end end diff --git a/apps/explorer/lib/explorer/account/custom_abi.ex b/apps/explorer/lib/explorer/account/custom_abi.ex index 3cc9eae51c93..66c69a2d16c9 100644 --- a/apps/explorer/lib/explorer/account/custom_abi.ex +++ b/apps/explorer/lib/explorer/account/custom_abi.ex @@ -103,7 +103,7 @@ defmodule Explorer.Account.CustomABI do defp check_is_abi_valid?(custom_abi, given_abi \\ nil) defp check_is_abi_valid?(%{abi: abi} = custom_abi, given_abi) when is_list(abi) do - with true <- length(abi) > 0, + with true <- not Enum.empty?(abi), filtered_abi <- filter_abi(abi), false <- Enum.empty?(filtered_abi) do Map.put(custom_abi, :abi, filtered_abi) diff --git a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex index d6185dc76879..39318ecf8590 100644 --- a/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex +++ b/apps/explorer/lib/explorer/arbitrum/claim_rollup_message.ex @@ -640,7 +640,7 @@ defmodule Explorer.Arbitrum.ClaimRollupMessage do [ArbitrumEvents.node_created()], json_l1_rpc_named_arguments ) do - {:ok, events} when is_list(events) and length(events) > 0 -> + {:ok, events} when is_list(events) and events !== [] -> node_created_event = List.last(events) # extract L2 block hash from the NodeCreated event l2_block_hash = l2_block_hash_from_node_created_event(node_created_event) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 1d9749124764..3cf7c7440231 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -3808,7 +3808,9 @@ defmodule Explorer.Chain do do: apply_filter_by_method_id_to_transactions(query, [filter]) defp map_name_or_method_id_to_method_id(string) when is_binary(string) do - if id = @method_name_to_id_map[string] do + id = @method_name_to_id_map[string] + + if id do decode_method_id(id) else trimmed = diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index abcffecdf0a8..4245451a00fe 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -73,7 +73,7 @@ defmodule Explorer.Chain.Blackfort.Validator do Delete validators by address hashes """ @spec delete_validators_by_address_hashes([binary() | HashAddress.t()]) :: {non_neg_integer(), nil | []} | :ignore - def delete_validators_by_address_hashes(list) when is_list(list) and length(list) > 0 do + def delete_validators_by_address_hashes(list) when is_list(list) and list !== [] do __MODULE__ |> where([vs], vs.address_hash in ^list) |> Repo.delete_all() diff --git a/apps/explorer/lib/explorer/chain/stability/validator.ex b/apps/explorer/lib/explorer/chain/stability/validator.ex index 1c4282eb8b67..4814d3eabac4 100644 --- a/apps/explorer/lib/explorer/chain/stability/validator.ex +++ b/apps/explorer/lib/explorer/chain/stability/validator.ex @@ -199,7 +199,7 @@ defmodule Explorer.Chain.Stability.Validator do Delete validators by address hashes """ @spec delete_validators_by_address_hashes([binary() | HashAddress.t()]) :: {non_neg_integer(), nil | []} | :ignore - def delete_validators_by_address_hashes(list) when is_list(list) and length(list) > 0 do + def delete_validators_by_address_hashes(list) when is_list(list) and list !== [] do __MODULE__ |> where([vs], vs.address_hash in ^list) |> Repo.delete_all() diff --git a/apps/explorer/lib/explorer/market/source/coin_gecko.ex b/apps/explorer/lib/explorer/market/source/coin_gecko.ex index b2bbd881bf62..fbbfeedf9acc 100644 --- a/apps/explorer/lib/explorer/market/source/coin_gecko.ex +++ b/apps/explorer/lib/explorer/market/source/coin_gecko.ex @@ -30,7 +30,7 @@ defmodule Explorer.Market.Source.CoinGecko do {:error, _reason} = error -> error - tokens_to_fetch when is_list(tokens_to_fetch) and length(tokens_to_fetch) > 0 -> + tokens_to_fetch when is_list(tokens_to_fetch) and tokens_to_fetch !== [] -> fetch_tokens(tokens_to_fetch, batch_size) _ -> diff --git a/apps/explorer/lib/explorer/market/source/dia.ex b/apps/explorer/lib/explorer/market/source/dia.ex index 78720534a90d..52cba97900f2 100644 --- a/apps/explorer/lib/explorer/market/source/dia.ex +++ b/apps/explorer/lib/explorer/market/source/dia.ex @@ -32,7 +32,7 @@ defmodule Explorer.Market.Source.DIA do {:error, _reason} = error -> error - tokens_to_fetch when is_list(tokens_to_fetch) and length(tokens_to_fetch) > 0 -> + tokens_to_fetch when is_list(tokens_to_fetch) and tokens_to_fetch !== [] -> fetch_tokens(tokens_to_fetch, batch_size) _ -> diff --git a/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex b/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex index 8894e5c0f15a..13d73c42aa61 100644 --- a/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex +++ b/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex @@ -118,7 +118,7 @@ defmodule Explorer.Migrator.RestoreOmittedWETHTransfers do # fetch token balances @impl true def handle_info(:migrate, %{queue: queue, current_concurrency: current_concurrency} = state) do - if Enum.count(queue) > 0 and current_concurrency < concurrency() do + if not Enum.empty?(queue) and current_concurrency < concurrency() do to_take = batch_size() * (concurrency() - current_concurrency) {to_process, remainder} = Enum.split(queue, to_take) diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex index b703ca4917af..ab6561681104 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex @@ -106,7 +106,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do {:ok, []} -> Internal.send_otp(email, ip) - {:ok, users} when is_list(users) and length(users) > 0 -> + {:ok, users} when is_list(users) and users !== [] -> {:error, "Account with this email already exists"} error -> diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/da/anytrust.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/da/anytrust.ex index f42de7202719..6566cab480e1 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/da/anytrust.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/da/anytrust.ex @@ -362,13 +362,13 @@ defmodule Indexer.Fetcher.Arbitrum.DA.Anytrust do json_rpc_named_arguments ) - if length(logs) > 0 do + if Enum.empty?(logs) do + log_error("No SetValidKeyset logs found in the block #{block_number}") + nil + else log_info("Found #{length(logs)} SetValidKeyset logs") set_valid_keyset_event_parse(List.first(logs)) - else - log_error("No SetValidKeyset logs found in the block #{block_number}") - nil end end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/events.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/events.ex index ecd62667fe55..56c6e7572142 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/events.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/events.ex @@ -38,7 +38,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Events do json_rpc_named_arguments ) - if length(logs) > 0 do + unless Enum.empty?(logs) do log_debug("Found #{length(logs)} SequencerBatchDelivered logs") end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex index cf47ca219b3b..9ea7ef973c97 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/events.ex @@ -70,7 +70,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.Events do {cached_logs, cache} end - if length(logs) > 0 do + unless Enum.empty?(logs) do log_debug("Found #{length(logs)} SendRootUpdated logs") end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex index 36a87e9840cd..05ef97e7f70d 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/confirmations/rollup_blocks.ex @@ -100,14 +100,14 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Confirmations.RollupBlocks do ) # credo:disable-for-next-line Credo.Check.Refactor.Nesting - if length(confirmed_blocks) > 0 do + if Enum.empty?(confirmed_blocks) do + log_info("Either no unconfirmed blocks found or DB inconsistency error discovered") + [] + else log_info("Found #{length(confirmed_blocks)} confirmed blocks") add_confirmation_transaction(confirmed_blocks, block_to_l1_transactions[block_number].l1_transaction_hash) ++ updated_rollup_blocks - else - log_info("Either no unconfirmed blocks found or DB inconsistency error discovered") - [] end end) end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/l1_finalization.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/l1_finalization.ex index 86d3cdadbf7e..7e99868ebd38 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/l1_finalization.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/l1_finalization.ex @@ -80,7 +80,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.L1Finalization do lifecycle_transactions = Db.lifecycle_unfinalized_transactions(safe_block) - if length(lifecycle_transactions) > 0 do + unless Enum.empty?(lifecycle_transactions) do log_info("Discovered #{length(lifecycle_transactions)} lifecycle transaction to be finalized") updated_lifecycle_transactions = diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex index f6f3f7be8684..9232c4b3b9d3 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_l1_executions.ex @@ -286,7 +286,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewL1Executions do json_rpc_named_arguments ) - if length(logs) > 0 do + unless Enum.empty?(logs) do log_debug("Found #{length(logs)} OutBoxTransactionExecuted logs") end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex index f0cbc2fc3b9f..1c97ee02c174 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex @@ -236,7 +236,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do json_rpc_named_arguments ) - if length(logs) > 0 do + unless Enum.empty?(logs) do log_debug("Found #{length(logs)} MessageDelivered logs") end diff --git a/mix.lock b/mix.lock index 851b24ef89fc..2bd969a791f1 100644 --- a/mix.lock +++ b/mix.lock @@ -32,7 +32,7 @@ "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, - "credo": {:hex, :credo, "1.7.13", "126a0697df6b7b71cd18c81bc92335297839a806b6f62b61d417500d1070ff4e", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "47641e6d2bbff1e241e87695b29f617f1a8f912adea34296fb10ecc3d7e9e84f"}, + "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, From 514be680d88c457cda9fcd91ea13f9da90d3619d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 19 Dec 2025 12:36:00 +0300 Subject: [PATCH 0555/1006] fix: Resolve TLS version issue on application startup (#13767) --- config/runtime/dev.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index 007996057cb0..2ca2524d4dde 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -21,7 +21,7 @@ config :block_scout_web, BlockScoutWeb.Endpoint, ], https: [ port: port + 1, - cipher_suite: :strong, + cipher_suite: :compatible, certfile: System.get_env("CERTFILE") || "priv/cert/selfsigned.pem", keyfile: System.get_env("KEYFILE") || "priv/cert/selfsigned_key.pem" ] @@ -38,7 +38,7 @@ config :block_scout_web, BlockScoutWeb.HealthEndpoint, ], https: [ port: port + 1, - cipher_suite: :strong, + cipher_suite: :compatible, certfile: System.get_env("CERTFILE") || "priv/cert/selfsigned.pem", keyfile: System.get_env("KEYFILE") || "priv/cert/selfsigned_key.pem" ] From 90e3cc5c66dfd1ed1f6e77a2421633cbaab10586 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:01:19 +0300 Subject: [PATCH 0556/1006] fix: make `find_history_and_token_fetchers` public (#13768) --- apps/explorer/lib/explorer/market/market.ex | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index 1934b5aee2dd..f9903f187844 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -144,5 +144,22 @@ defmodule Explorer.Market do :persistent_term.get(@history_key, false) end - defp find_history_and_token_fetchers, do: {GenServer.whereis(HistoryFetcher), GenServer.whereis(TokenFetcher)} + @doc """ + Locates the running processes for the market history and token fiat value fetchers. + + This function checks whether the `Explorer.Market.Fetcher.History` and + `Explorer.Market.Fetcher.Token` GenServer processes are currently running + and returns their process identifiers. + + ## Parameters + None. + + ## Returns + - A tuple `{history_fetcher, token_fetcher}` where each element is: + - `nil` if the corresponding fetcher process is not running + - A `pid()` if the process is registered locally + - A `{atom(), atom()}` tuple if registered via `:global` or `:via` + """ + @spec find_history_and_token_fetchers() :: {nil | pid() | {atom(), atom()}, nil | pid() | {atom(), atom()}} + def find_history_and_token_fetchers, do: {GenServer.whereis(HistoryFetcher), GenServer.whereis(TokenFetcher)} end From caeb1cf84b3cbc529df8d6dbed9cc30214991571 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 19 Dec 2025 13:28:33 +0300 Subject: [PATCH 0557/1006] fix: handle_continue bad return value (#13769) --- apps/explorer/lib/explorer/market/market.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index f9903f187844..f2bb99cc08ba 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -54,7 +54,7 @@ defmodule Explorer.Market do :persistent_term.put(@history_key, !is_nil(history_pid)) :persistent_term.put(@tokens_key, !is_nil(token_pid)) - {:stop, :normal} + {:stop, :normal, nil} multiple_indexers -> if attempt < 5 do From 7a860f2de5b10ac0e57d5bdb30a6abdb49341e25 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 19 Dec 2025 13:37:40 +0300 Subject: [PATCH 0558/1006] 9.3.2 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .github/workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .github/workflows/publish-docker-image-custom-build.yml | 2 +- .github/workflows/publish-docker-image-every-push.yml | 2 +- .github/workflows/publish-docker-image-for-arbitrum.yml | 2 +- .github/workflows/publish-docker-image-for-celo.yml | 2 +- .github/workflows/publish-docker-image-for-core.yml | 2 +- .../workflows/publish-docker-image-for-eth-sepolia.yml | 2 +- .github/workflows/publish-docker-image-for-eth.yml | 2 +- .github/workflows/publish-docker-image-for-filecoin.yml | 2 +- .github/workflows/publish-docker-image-for-fuse.yml | 2 +- .../workflows/publish-docker-image-for-gnosis-chain.yml | 2 +- .github/workflows/publish-docker-image-for-l2-staging.yml | 2 +- .github/workflows/publish-docker-image-for-lukso.yml | 2 +- .../publish-docker-image-for-optimism-exeperimental.yml | 2 +- .../publish-docker-image-for-optimism-worldchain.yml | 2 +- .github/workflows/publish-docker-image-for-optimism.yml | 2 +- .github/workflows/publish-docker-image-for-rootstock.yml | 2 +- .github/workflows/publish-docker-image-for-scroll.yml | 2 +- .github/workflows/publish-docker-image-for-zetachain.yml | 2 +- .github/workflows/publish-docker-image-for-zilliqa.yml | 2 +- .github/workflows/publish-docker-image-for-zkevm.yml | 2 +- .github/workflows/publish-docker-image-for-zksync.yml | 2 +- .github/workflows/publish-docker-image-old-ui.yml | 2 +- .../workflows/publish-docker-image-staging-on-demand.yml | 2 +- .../workflows/publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 8 ++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 63 files changed, 70 insertions(+), 62 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 484e91d37d53..5b6b1b07262b 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 75bfe8d87a82..713fe658c7c5 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index bed71a7473f9..53075f3776f3 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 806378d1e38d..0a6a7a4a0eae 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 00800bbc67d9..0335fe5d053a 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 60086b3996c2..28e9ac3bf9f7 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index fc6b81296404..f3a710fd1510 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index 7586a34644a7..d78647276f5e 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 8020002344f4..296fced45414 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 3057a60ef469..688cd54b104f 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index 18762454354a..b964d90c05d0 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index dbfabe6a1890..2fcadd539f2e 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 0a38f3608f04..4f138a0fbaa4 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 21b1b865e1d7..ae0178cbb178 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 28c7e5c9b4a2..4521a13e8c38 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 7b0675fd325a..9176da5873d9 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 913c2bb6a7ef..7d0a5fb2af14 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index 39bcd6c0675c..da1cb23f62b5 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index ffdbfb702eb1..03430db8b3aa 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 77fbc41db645..74ef28730787 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index 5b6a708aef58..cf88648ff32c 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index c66a54432b74..ae2008a34535 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index 54d6b3b1afe9..fb70d1044c16 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 72e1c3427441..07deba7959d8 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 965221bc48ae..3d9efa6d6462 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index d55c5d169530..87dc20188bb4 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml index e04ad1a1ee96..415d0c6f1ac1 100644 --- a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml +++ b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index 9f54378c78c3..5f8037ded372 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index fba86d47fc7d..4c0b3016e50b 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index df11ace2dccc..9190bcd76c12 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index c48aff7d7144..cb743d9727fc 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index cbde2e237ea1..bd5a29eee3c2 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index 8348d643beff..a0342c8c3e4e 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index 2981d5d85bb5..d3ed70acb574 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index 6884dc8095e9..e65e84d4cddc 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index 05f33caf8834..f01f7cdd2967 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 99c79507d5e1..6b4f6d4c2240 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index fae001cf8ab0..a09d89e07e49 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index 2725c2280dbc..bf64e5635610 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index c8483b0ffdd8..c17dd8151ac2 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index befd5faebbfe..5c51640eab4f 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index 3f019fc0b37a..a55c70df58e0 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 48e5328dd966..3a403aa0ed02 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 0e9d4f579557..26cf0eaba4c7 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 230ffdcbc9cd..886d69032fed 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index cf9721371b64..1de8b2536a2b 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index b7de98bd1e81..b6d5b5774cc0 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 0b0746c378e9..8491b46952df 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 218107564337..77662dc88a53 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index eb91ac5e5134..cce8e7bcb54b 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 78f7de88a6f4..5de2529bfe61 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index 2dfa3e6f94d6..fc39e7ac8d8d 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.1 + RELEASE_VERSION: 9.3.2 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index 4289a4c8a645..c9bfffd57937 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 9.3.2 + +### 🐛 Bug Fixes + +- Handle_continue bad return value ([#13769](https://github.com/blockscout/blockscout/issues/13769)) +- Make `find_history_and_token_fetchers` public ([#13768](https://github.com/blockscout/blockscout/issues/13768)) +- Resolve TLS version issue on application startup ([#13767](https://github.com/blockscout/blockscout/issues/13767)) + ## 9.3.1 ### 🐛 Bug Fixes diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 4a360a468d06..6266e5513330 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.1", + version: "9.3.2", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 751257911321..aa4c63546b44 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -19,7 +19,7 @@ defmodule EthereumJSONRPC.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.1" + version: "9.3.2" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 89041190c277..7f3b43fb73e7 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.3.1"}] + @default_headers [{"User-Agent", "blockscout-9.3.2"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 987f4dd7cc41..d17ec2e1cb81 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -20,7 +20,7 @@ defmodule Explorer.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.1", + version: "9.3.2", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 7621baad42ca..0ef7e743e798 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.1", + version: "9.3.2", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 1470518c4de1..79c2f1991153 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.3.1", + version: "9.3.2", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index f0ff75c8e34f..b0412655e2b2 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.3.1", + version: "9.3.2", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 718c52e06e81..4fe5727f6f2e 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.3.1' +RELEASE_VERSION ?= '9.3.2' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index b7d5ff2eb53a..eee9d0f914d9 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.3.1", + version: "9.3.2", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 6e428d75e626..7906ed07e184 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.3.1" + set version: "9.3.2" set applications: [ :runtime_tools, block_scout_web: :permanent, From 07bda78202ebe8b0250ec2b86aa59702386a95d8 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Mon, 22 Dec 2025 16:13:02 +0300 Subject: [PATCH 0559/1006] fix: improve Arbitrum L1->L2 message discovery for reorg and RPC consistency (#13770) --- .../lib/explorer/chain/arbitrum/message.ex | 8 ++ .../chain/import/runner/arbitrum/messages.ex | 2 +- .../lib/indexer/fetcher/arbitrum/utils/rpc.ex | 51 +++++++++++++ .../fetcher/arbitrum/workers/batches/tasks.ex | 25 +++---- .../arbitrum/workers/new_messages_to_l2.ex | 73 ++++++++++++------- 5 files changed, 118 insertions(+), 41 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/arbitrum/message.ex b/apps/explorer/lib/explorer/chain/arbitrum/message.ex index 4387096e6c8e..81d7a8b36339 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/message.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/message.ex @@ -13,6 +13,8 @@ defmodule Explorer.Chain.Arbitrum.Message do alias Explorer.Chain.Hash + @insert_result_key :insert_arbitrum_messages + @optional_attrs ~w(originator_address originating_transaction_hash origination_timestamp originating_transaction_block_number completion_transaction_hash)a @required_attrs ~w(direction message_id status)a @@ -83,4 +85,10 @@ defmodule Explorer.Chain.Arbitrum.Message do |> validate_required(@required_attrs) |> unique_constraint([:direction, :message_id]) end + + @doc """ + Shared result key used by import runners to return inserted Arbitrum messages. + """ + @spec insert_result_key() :: atom() + def insert_result_key, do: @insert_result_key end diff --git a/apps/explorer/lib/explorer/chain/import/runner/arbitrum/messages.ex b/apps/explorer/lib/explorer/chain/import/runner/arbitrum/messages.ex index 9aef34064ffa..e7a67e3b5f33 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/arbitrum/messages.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/arbitrum/messages.ex @@ -42,7 +42,7 @@ defmodule Explorer.Chain.Import.Runner.Arbitrum.Messages do |> Map.put_new(:timeout, @timeout) |> Map.put(:timestamps, timestamps) - Multi.run(multi, :insert_arbitrum_messages, fn repo, _ -> + Multi.run(multi, CrosslevelMessage.insert_result_key(), fn repo, _ -> Instrumenter.block_import_stage_runner( fn -> insert(repo, changes_list, insert_options) end, :block_referencing, diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex index f18c5e90192d..f9dfca0fd801 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/rpc.ex @@ -324,6 +324,57 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Rpc do {safe_block, latest_chain_block} end + @doc """ + Computes the safe start and end L1 blocks for discovery, respecting overlap safeguards. + + The returned `safe_start_block` is constrained to avoid revisiting blocks already + covered by historical entities discovery (`historical_entities_end_block`) while + still overlapping the safe block window derived from the current chain state. + The `end_block` is bounded by both the configured block range and the latest block + number. + + ## Parameters + - `new_entities_start_block`: Proposed start block for new-entity discovery. + - `historical_entities_end_block`: Last block already covered by historical discovery. + - `json_rpc_named_arguments`: RPC arguments used to query safe/latest blocks. + - `rpc_logs_block_range`: Max range to cover in a single discovery iteration. + + ## Returns + - `{safe_start_block, end_block}` tuple delimiting the adjusted discovery range. + """ + @spec safe_start_and_end_blocks( + non_neg_integer(), + non_neg_integer(), + EthereumJSONRPC.json_rpc_named_arguments(), + non_neg_integer() + ) :: {non_neg_integer(), non_neg_integer()} + def safe_start_and_end_blocks( + new_entities_start_block, + historical_entities_end_block, + json_rpc_named_arguments, + rpc_logs_block_range + ) + when is_integer(new_entities_start_block) and new_entities_start_block >= 0 and + is_integer(historical_entities_end_block) and + historical_entities_end_block >= 0 and is_integer(rpc_logs_block_range) and rpc_logs_block_range > 0 do + # It is necessary to revisit some of the previous blocks to ensure that + # no information is missed due to reorgs or RPC node inconsistency behind + # a load balancer. The number of blocks to revisit depends on the current safe + # block or the block which is considered as safest in case of L3 (where the + # safe block could be too far behind the latest block) or if RPC does not + # support "safe" block. + {safe_block, latest_block} = get_safe_and_latest_l1_blocks(json_rpc_named_arguments, rpc_logs_block_range) + + # At the same time it does not make sense to revisit blocks that will be + # revisited by the historical entities discovery process. + # If the new entities discovery process does not reach the chain head + # previously, there is no need to revisit the blocks. + safe_start_block = max(min(new_entities_start_block, safe_block), historical_entities_end_block + 1) + end_block = min(new_entities_start_block + rpc_logs_block_range - 1, latest_block) + + {safe_start_block, end_block} + end + @doc """ Identifies the block range for a batch by using the block number located on one end of the range. diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex index c7552ccd9afe..1eee3223434b 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/batches/tasks.ex @@ -141,21 +141,16 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.Batches.Tasks do # Requesting the "latest" block instead of "safe" allows to catch new batches # without latency. - # It is necessary to re-visit some amount of the previous blocks to ensure that - # no batches are missed due to reorgs. The amount of blocks to re-visit depends - # on the current safe block or the block which is considered as safest in case - # of L3 (where the safe block could be too far behind the latest block) or if - # RPC does not support "safe" block. - {safe_block, latest_block} = - Rpc.get_safe_and_latest_l1_blocks(l1_rpc_config.json_rpc_named_arguments, l1_rpc_config.logs_block_range) - - # At the same time it does not make sense to re-visit blocks that will be - # re-visited by the historical batches discovery process. - # If the new batches discovery process does not reach the chain head previously - # no need to re-visit the blocks. - safe_start_block = max(min(start_block, safe_block), historical_batches_end_block + 1) - - end_block = min(start_block + l1_rpc_config.logs_block_range - 1, latest_block) + # It is necessary to revisit some of the previous blocks to ensure that + # no information is missed due to reorgs or RPC node inconsistency behind + # a load balancer. + {safe_start_block, end_block} = + Rpc.safe_start_and_end_blocks( + start_block, + historical_batches_end_block, + l1_rpc_config.json_rpc_named_arguments, + l1_rpc_config.logs_block_range + ) if safe_start_block <= end_block do log_info("Block range for new batches discovery: #{safe_start_block}..#{end_block}") diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex index 1c97ee02c174..cafeda8fc507 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex @@ -21,11 +21,13 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] + alias Indexer.Fetcher.Arbitrum.Utils.Helper, as: ArbitrumHelper alias Indexer.Fetcher.Arbitrum.Utils.Rpc alias Indexer.Helper, as: IndexerHelper alias Explorer.Chain alias Explorer.Chain.Arbitrum + alias Explorer.Chain.Arbitrum.Message, as: ArbitrumMessage alias Explorer.Chain.Events.Publisher require Logger @@ -65,6 +67,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do }, :data => %{ :new_msg_to_l2_start_block => non_neg_integer(), + :historical_msg_to_l2_end_block => non_neg_integer(), optional(any()) => any() }, optional(any()) => any() @@ -77,31 +80,43 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do l1_rpc_block_range: rpc_block_range, l1_bridge_address: bridge_address }, - data: %{new_msg_to_l2_start_block: start_block} + data: %{ + new_msg_to_l2_start_block: start_block, + historical_msg_to_l2_end_block: historical_msg_to_l2_end_block + } } = _state ) do - # Requesting the "latest" block instead of "safe" allows to get messages originated to L2 - # much earlier than they will be seen by the Arbitrum Sequencer. - {:ok, latest_block} = - IndexerHelper.get_block_number_by_tag( - "latest", + # It is necessary to revisit some of the previous blocks to ensure that + # no information is missed due to reorgs or RPC node inconsistency behind + # a load balancer. + {safe_start_block, end_block} = + Rpc.safe_start_and_end_blocks( + start_block, + historical_msg_to_l2_end_block, json_rpc_named_arguments, - Rpc.get_resend_attempts() + rpc_block_range ) - end_block = min(start_block + rpc_block_range - 1, latest_block) - - if start_block <= end_block do - log_info("Block range for discovery new messages from L1: #{start_block}..#{end_block}") + if safe_start_block <= end_block do + log_info("Block range for discovery new messages from L1: #{safe_start_block}..#{end_block}") + # credo:disable-for-lines:16 Credo.Check.Refactor.PipeChainStart new_messages_amount = - discover( - bridge_address, - start_block, + ArbitrumHelper.execute_for_block_range_in_chunks( + safe_start_block, end_block, - json_rpc_named_arguments, - chunk_size + rpc_block_range, + fn chunk_start, chunk_end -> + discover( + bridge_address, + chunk_start, + chunk_end, + json_rpc_named_arguments, + chunk_size + ) + end ) + |> Enum.reduce(0, fn {_range, amount}, acc -> acc + amount end) if new_messages_amount > 0 do Publisher.broadcast(%{new_messages_to_arbitrum_amount: new_messages_amount}, :realtime) @@ -211,17 +226,25 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do messages = get_messages_from_logs(logs, json_rpc_named_argument, chunk_size) - if messages != [] do - log_info("Origins of #{length(messages)} L1-to-L2 messages will be imported") - end + case messages do + [] -> + 0 - {:ok, _} = - Chain.import(%{ - arbitrum_messages: %{params: messages}, - timeout: :infinity - }) + _ -> + log_info("Origins of #{length(messages)} L1-to-L2 messages will be imported") - length(messages) + {:ok, import_result} = + Chain.import(%{ + arbitrum_messages: %{params: messages}, + timeout: :infinity + }) + + # Count the actual imported records returned by the runner; the input `messages` + # length can overstate inserts when conflicts/updates happen during import. + import_result + |> Map.get(ArbitrumMessage.insert_result_key(), []) + |> length() + end end # Retrieves logs representing the `MessageDelivered` events. From 44a558d78241879f00c86377436ec829ffc630da Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:55:13 +0400 Subject: [PATCH 0560/1006] chore: Make "jsonrpc" field in response optional (#13724) --- apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex index 92fc0753c3f4..12ec8e486fce 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex @@ -191,11 +191,11 @@ defmodule EthereumJSONRPC.HTTP do """ @spec standardize_response(map()) :: %{ :id => nil | non_neg_integer(), - :jsonrpc => binary(), + optional(:jsonrpc) => binary(), optional(:error) => %{:code => integer(), :message => binary(), optional(:data) => any()}, optional(:result) => any() } - def standardize_response(%{"jsonrpc" => "2.0" = jsonrpc} = unstandardized) do + def standardize_response(unstandardized) do # Avoid extracting `id` directly in the function declaration. Some endpoints # do not adhere to standards and may omit the `id` in responses related to # error scenarios. Consequently, the function call would fail during input @@ -204,7 +204,7 @@ defmodule EthereumJSONRPC.HTTP do # Nethermind return string ids id = sanitize_id(unstandardized["id"]) - standardized = %{jsonrpc: jsonrpc, id: id} + standardized = %{jsonrpc: unstandardized["jsonrpc"], id: id} case {id, unstandardized} do {_id, %{"result" => _, "error" => _}} -> From 623f49f71c9926a563a0060e0d5a49c35305d7e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Dec 2025 13:43:16 +0300 Subject: [PATCH 0561/1006] chore(deps): bump ex_json_schema from 0.11.1 to 0.11.2 (#13781) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 2bd969a791f1..5267898f031d 100644 --- a/mix.lock +++ b/mix.lock @@ -63,7 +63,7 @@ "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, - "ex_json_schema": {:hex, :ex_json_schema, "0.11.1", "b593f92937a095f66054bb318681397dfe7304e7d2b6b1a7534ea3aa40024f8c", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "32d651a575a6ce2fd613f140b0fef8dd0acc7cf8e8bcd29a3a1be5c945700dd5"}, + "ex_json_schema": {:hex, :ex_json_schema, "0.11.2", "8f8200e6afa5473f37dbebd1e72bf97d8d5dd0128125123ed1532611267f0138", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "395f4aaf32ea0a14d861b16695e7bc8a1b5d841e0fd374d25aef9701bf8da825"}, "ex_keccak": {:hex, :ex_keccak, "0.7.8", "be1cf194d3158f0a305eaed0334e478d0d0f2c827e7c1f8f0e1e2a667da5a8ac", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "52de5b42b718df2534fb9a55780d8a05bbaea539f867c3e7c0a8e7e1d5f149d9"}, "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.5", "583733a1134a0c8b4df70c6b3e874d1896bf48f2aaafdf332ec5480c7b5117bf", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f138b2acdc5a4d87880e42742e9bead4d1235fd9b1b2b29be0d1da1e01c3a312"}, From 9a3bb51ca6dfc795467cea1a354f6d76fe43d21c Mon Sep 17 00:00:00 2001 From: GimluCom <79271880+GimluCom@users.noreply.github.com> Date: Mon, 5 Jan 2026 10:15:51 +0100 Subject: [PATCH 0562/1006] fix: Fix methodId detection (#13811) --- apps/explorer/lib/explorer/chain/transaction.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index becd1f61c690..73df78681e65 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -2412,7 +2412,7 @@ defmodule Explorer.Chain.Transaction do @doc """ Return method id used in transaction """ - def method_id(%__MODULE__{ + def method_id(%{ created_contract_address_hash: nil, input: %{bytes: <>} }), From a34d9e3759da5bd587c8da1d07f64dd1a4e3a963 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:06:42 +0300 Subject: [PATCH 0563/1006] feat: Mark contract addresses in search results (#13636) --- .../lib/block_scout_web/views/api/v2/search_view.ex | 12 ++++++++---- .../controllers/api/v2/search_controller_test.exs | 3 ++- apps/explorer/lib/explorer/chain/search.ex | 12 +++++++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index f25482920faf..27f50b7fa213 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -44,7 +44,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "is_verified_via_admin_panel" => search_result.is_verified_via_admin_panel, "certified" => search_result.certified || false, "priority" => search_result.priority, - "reputation" => search_result.reputation + "reputation" => search_result.reputation, + "is_smart_contract_address" => search_result.is_smart_contract_address } end @@ -58,7 +59,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), "priority" => search_result.priority, - "reputation" => search_result.reputation + "reputation" => search_result.reputation, + "is_smart_contract_address" => search_result.is_smart_contract_address } end @@ -73,7 +75,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), "priority" => search_result.priority, - "reputation" => search_result.reputation + "reputation" => search_result.reputation, + "is_smart_contract_address" => search_result.is_smart_contract_address } end @@ -88,7 +91,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do "certified" => if(search_result.certified, do: search_result.certified, else: false), "priority" => search_result.priority, "metadata" => search_result.metadata, - "reputation" => search_result.reputation + "reputation" => search_result.reputation, + "is_smart_contract_address" => search_result.is_smart_contract_address } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs index 06fb2ca16151..4680c2146791 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs @@ -2470,7 +2470,8 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do "priority" => 0, "type" => "address", "url" => "/address/#{address_hash}", - "reputation" => "ok" + "reputation" => "ok", + "is_smart_contract_address" => false } | for( i <- 0..48, diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 5142c77e4a55..02630fe53952 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -537,6 +537,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:name, dynamic([address_tag: at], at.display_name)) |> Map.put(:inserted_at, dynamic([address_to_tag: att], att.inserted_at)) |> Map.put(:verified, dynamic([smart_contract: smart_contract], not is_nil(smart_contract))) + |> Map.put(:is_smart_contract_address, dynamic([address: address], not is_nil(address.contract_code))) |> Map.put(:priority, 3) inner_query = @@ -554,6 +555,9 @@ defmodule Explorer.Chain.Search do left_join: smart_contract in SmartContract, as: :smart_contract, on: att.address_hash == smart_contract.address_hash, + left_join: address in Address, + as: :address, + on: att.address_hash == address.hash, select: ^label_search_fields ) @@ -615,6 +619,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:inserted_at, dynamic([smart_contract: smart_contract], smart_contract.inserted_at)) |> Map.put(:certified, dynamic([smart_contract: smart_contract], smart_contract.certified)) |> Map.put(:verified, true) + |> Map.put(:is_smart_contract_address, true) |> Map.put(:priority, 0) |> add_reputation_to_search_fields(options) @@ -640,6 +645,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:inserted_at, dynamic([address: address], address.inserted_at)) |> Map.put(:verified, dynamic([address: address], address.verified)) |> Map.put(:certified, dynamic([smart_contract: smart_contract], smart_contract.certified)) + |> Map.put(:is_smart_contract_address, dynamic([address: address], not is_nil(address.contract_code))) base_address_query() |> where([address: address], address.hash == ^address_hash) @@ -1074,7 +1080,8 @@ defmodule Explorer.Chain.Search do order: 0, metadata: dynamic(type(^nil, :map)), addresses_index: 0, - reputation: "ok" + reputation: "ok", + is_smart_contract_address: dynamic(type(^nil, :boolean)) } end @@ -1094,6 +1101,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:is_verified_via_admin_panel, dynamic([token: token], token.is_verified_via_admin_panel)) |> Map.put(:verified, dynamic([smart_contract: smart_contract], not is_nil(smart_contract))) |> Map.put(:certified, dynamic([smart_contract: smart_contract], smart_contract.certified)) + |> Map.put(:is_smart_contract_address, true) |> Map.put(:priority, 2) end @@ -1106,6 +1114,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:order, dynamic([metadata_tag: tag], tag.id)) |> Map.put(:addresses_index, dynamic([metadata_tag: tag], tag.addresses_index)) |> Map.put(:verified, dynamic([address: address], address.verified)) + |> Map.put(:is_smart_contract_address, dynamic([address: address], not is_nil(address.contract_code))) |> Map.put(:priority, 1) end @@ -1390,6 +1399,7 @@ defmodule Explorer.Chain.Search do |> Map.put(:inserted_at, dynamic([address: address], address.inserted_at)) |> Map.put(:verified, dynamic([address: address], address.verified)) |> Map.put(:certified, dynamic([smart_contract: smart_contract], smart_contract.certified)) + |> Map.put(:is_smart_contract_address, dynamic([address: address], not is_nil(address.contract_code))) base_address_query() |> join( From e3462de7c579b88b27603f532b9fa7b4390a6e29 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 5 Jan 2026 14:53:15 +0300 Subject: [PATCH 0564/1006] feat: Show ENS domains without resolved address in search (#13638) --- .../views/api/v2/search_view.ex | 8 +++++-- apps/explorer/lib/explorer/chain/search.ex | 22 +++++++++++++++---- .../explorer/microservice_interfaces/bens.ex | 10 +++++---- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index 27f50b7fa213..826450e73551 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -70,7 +70,7 @@ defmodule BlockScoutWeb.API.V2.SearchView do "type" => search_result.type, "name" => search_result.name, "address_hash" => search_result.address_hash, - "url" => address_path(Endpoint, :show, search_result.address_hash), + "url" => search_result.address_hash && address_path(Endpoint, :show, search_result.address_hash), "is_smart_contract_verified" => search_result.verified, "ens_info" => search_result[:ens_info], "certified" => if(search_result.certified, do: search_result.certified, else: false), @@ -173,10 +173,14 @@ defmodule BlockScoutWeb.API.V2.SearchView do %{"type" => "address", "parameter" => Address.checksum(item.hash)} end - defp redirect_search_results(%{address_hash: address_hash}) do + defp redirect_search_results(%{address_hash: address_hash}) when not is_nil(address_hash) do %{"type" => "address", "parameter" => address_hash} end + defp redirect_search_results(%{name: name, protocol: _protocol}) do + %{"type" => "ens_domain", "parameter" => name} + end + defp redirect_search_results(%Block{} = item) do %{"type" => "block", "parameter" => to_string(item.hash)} end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 02630fe53952..b967775c63f6 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -968,6 +968,11 @@ defmodule Explorer.Chain.Search do defp search_ens_name(search_query, options) do case search_ens_name_in_bens(search_query) do + {ens_result, nil} -> + [ + merge_address_search_result_with_ens_info(nil, ens_result) + ] + {ens_result, address_hash} -> [ address_hash @@ -1018,18 +1023,18 @@ defmodule Explorer.Chain.Search do @spec search_ens_name_in_bens(binary()) :: nil | {%{ - address_hash: binary(), + address_hash: binary() | nil, expiry_date: any(), name: any(), names_count: non_neg_integer(), protocol: any() - }, Hash.Address.t()} + }, Hash.Address.t() | nil} def search_ens_name_in_bens(search_query) do trimmed_query = String.trim(search_query) with true <- Regex.match?(~r/\w+\.\w+/, trimmed_query), - %{address_hash: address_hash_string} = result <- ens_domain_name_lookup(search_query), - {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do + %{address_hash: address_hash_string_or_nil} = result <- ens_domain_name_lookup(search_query), + address_hash <- address_hash_string_or_nil && Chain.string_to_address_hash_or_nil(address_hash_string_or_nil) do {result, address_hash} else _ -> @@ -1037,6 +1042,15 @@ defmodule Explorer.Chain.Search do end end + defp merge_address_search_result_with_ens_info(nil, ens_info) do + search_fields() + |> Map.put(:address_hash, nil) + |> Map.put(:type, "ens_domain") + |> Map.put(:ens_info, ens_info) + |> Map.put(:timestamp, nil) + |> Map.put(:priority, 4) + end + defp merge_address_search_result_with_ens_info([], ens_info) do search_fields() |> Map.put(:address_hash, ens_info[:address_hash]) diff --git a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex index 14dbc8829e57..09a76d6c68c1 100644 --- a/apps/explorer/lib/explorer/microservice_interfaces/bens.ex +++ b/apps/explorer/lib/explorer/microservice_interfaces/bens.ex @@ -83,7 +83,8 @@ defmodule Explorer.MicroserviceInterfaces.BENS do Request for ENS name via GET {{baseUrl}}/api/v1/:chainId/domains:lookup """ @spec ens_domain_name_lookup(binary()) :: - nil | %{address_hash: binary(), expiry_date: any(), name: any(), names_count: integer(), protocol: any()} + nil + | %{address_hash: binary() | nil, expiry_date: any(), name: any(), names_count: integer(), protocol: any()} def ens_domain_name_lookup(domain) do domain |> ens_domain_lookup() |> parse_lookup_response() end @@ -177,20 +178,21 @@ defmodule Explorer.MicroserviceInterfaces.BENS do %{ "name" => name, "expiry_date" => expiry_date, - "resolved_address" => %{"hash" => address_hash_string}, + "resolved_address" => resolved_address, "protocol" => protocol } | _other ] = items }} ) do - {:ok, hash} = Chain.string_to_address_hash(address_hash_string) + hash_or_nil = resolved_address["hash"] && Chain.string_to_address_hash_or_nil(resolved_address["hash"]) + address_hash = hash_or_nil && Address.checksum(hash_or_nil) %{ name: name, expiry_date: expiry_date, names_count: Enum.count(items), - address_hash: Address.checksum(hash), + address_hash: address_hash, protocol: protocol } end From a1c747dbf9106a299c4233ea4e28bb6c0e6eb091 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:09:26 +0400 Subject: [PATCH 0565/1006] chore: Improve replica usage (#13344) --- apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex | 2 +- apps/explorer/lib/explorer/chain.ex | 2 +- apps/explorer/lib/explorer/chain/address/reputation.ex | 2 +- apps/explorer/lib/explorer/chain/cache/counters/helper.ex | 2 +- apps/explorer/lib/explorer/chain/import/stage/addresses.ex | 2 +- .../chain/smart_contract/proxy/models/implementation.ex | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 026bcbc93adf..030d42d05231 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -221,7 +221,7 @@ defmodule BlockScoutWeb.API.V2.Helper do def get_transaction_stats do stats_scale = date_range(1) - transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest) + transaction_stats = TransactionStats.by_date_range(stats_scale.earliest, stats_scale.latest, api?: true) # Need datapoint for legend if none currently available. if Enum.empty?(transaction_stats) do diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 3cf7c7440231..860361ddd570 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1705,7 +1705,7 @@ defmodule Explorer.Chain do """ @spec max_consensus_block_number(Keyword.t()) :: {:ok, Block.block_number()} | {:error, :not_found} - def max_consensus_block_number(options \\ []) do + def max_consensus_block_number(options \\ [api?: true]) do Block |> where(consensus: true) |> select_repo(options).aggregate(:max, :number) diff --git a/apps/explorer/lib/explorer/chain/address/reputation.ex b/apps/explorer/lib/explorer/chain/address/reputation.ex index 30fa4247d0e0..29328ecd6f17 100644 --- a/apps/explorer/lib/explorer/chain/address/reputation.ex +++ b/apps/explorer/lib/explorer/chain/address/reputation.ex @@ -22,7 +22,7 @@ defmodule Explorer.Chain.Address.Reputation do if Application.get_env(:block_scout_web, :hide_scam_addresses) do ScamBadgeToAddress |> where([sb], sb.address_hash in ^address_hashes) - |> Repo.all() + |> Repo.replica().all() |> Map.new(&{&1.address_hash, &1}) else %{} diff --git a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex index 846b7714c140..68010009b2e9 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex @@ -119,7 +119,7 @@ defmodule Explorer.Chain.Cache.Counters.Helper do def evaluate_count(cache_key, nil, estimated_count_fun) do cached_value_from_db = cache_key - |> LastFetchedCounter.get() + |> LastFetchedCounter.get(api?: true) |> case do nil -> 0 value -> Decimal.to_integer(value) diff --git a/apps/explorer/lib/explorer/chain/import/stage/addresses.ex b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex index 915de483d0a0..48f7ff82b454 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/addresses.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/addresses.ex @@ -17,7 +17,7 @@ defmodule Explorer.Chain.Import.Stage.Addresses do @impl Stage def all_runners, do: runners() - @addresses_chunk_size 50 + @addresses_chunk_size 200 @impl Stage def multis(runner_to_changes_list, options) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex index f94085581b7e..5373edb1cf6c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex @@ -94,7 +94,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do Returns all implementations for the given smart-contract address hash """ @spec get_proxy_implementations(Hash.Address.t() | nil, Keyword.t()) :: __MODULE__.t() | nil - def get_proxy_implementations(proxy_address_hash, options \\ []) do + def get_proxy_implementations(proxy_address_hash, options \\ [api?: true]) do proxy_address_hash |> get_proxy_implementations_query() |> Chain.select_repo(options).one() From 84ae54299d69b22fe3c04699391a07ba97eed2f0 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 5 Jan 2026 17:14:41 +0400 Subject: [PATCH 0566/1006] chore: Disable MissingRangesManipulator (#13359) --- apps/explorer/lib/explorer/application.ex | 1 - apps/explorer/lib/explorer/chain/block.ex | 6 +- .../explorer/chain/import/runner/blocks.ex | 4 +- .../import/runner/internal_transactions.ex | 4 +- .../chain/import/runner/transactions.ex | 4 +- .../explorer/utility/missing_block_range.ex | 194 ++++++++++-------- .../utility/missing_ranges_manipulator.ex | 61 ------ .../utility/missing_block_range_test.exs | 40 ++++ .../lib/indexer/block/catchup/fetcher.ex | 6 +- .../block/catchup/missing_ranges_collector.ex | 10 +- .../lib/indexer/block/realtime/fetcher.ex | 4 +- .../fetcher/on_demand/contract_creator.ex | 6 +- .../temporary/uncataloged_token_transfers.ex | 4 +- .../indexer/block/catchup/fetcher_test.exs | 7 - 14 files changed, 177 insertions(+), 174 deletions(-) delete mode 100644 apps/explorer/lib/explorer/utility/missing_ranges_manipulator.ex diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index dd2b75d51234..79ab9eac9a82 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -93,7 +93,6 @@ defmodule Explorer.Application do con_cache_child_spec(MarketHistoryCache.cache_name()), con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)), {Redix, redix_opts()}, - {Explorer.Utility.MissingRangesManipulator, []}, {Explorer.Utility.ReplicaAccessibilityManager, []}, :hackney_pool.child_spec(:default, recv_timeout: 60_000, diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 2867a95579e3..b301ea26350c 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -220,7 +220,7 @@ defmodule Explorer.Chain.Block do alias Explorer.Chain.{Block, Hash, Transaction, Wei} alias Explorer.Chain.Block.{EmissionReward, Reward} alias Explorer.{Helper, Repo} - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange @optional_attrs ~w(size refetch_needed total_difficulty difficulty base_fee_per_gas)a @@ -618,7 +618,9 @@ defmodule Explorer.Chain.Block do set: [refetch_needed: true, updated_at: Timex.now()] ) - MissingRangesManipulator.add_ranges_by_block_numbers(updated_numbers) + MissingBlockRange.add_ranges_by_block_numbers(updated_numbers) + + :ok end def set_refetch_needed(block_number), do: set_refetch_needed([block_number]) diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index c9f9fdf2984f..49172a52e75c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -36,7 +36,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do alias Explorer.Chain.Zilliqa.Zrc2.TokenTransfer, as: Zrc2TokenTransfer alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange alias Explorer.Chain.Celo.AggregatedElectionReward, as: CeloAggregatedElectionReward alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward @@ -627,7 +627,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do removed_consensus_block_numbers |> Enum.reject(&Enum.member?(consensus_block_numbers, &1)) - |> MissingRangesManipulator.add_ranges_by_block_numbers() + |> MissingBlockRange.add_ranges_by_block_numbers() {:ok, removed_consensus_blocks} rescue diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index ca089d70796d..cae96e5c503e 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -26,7 +26,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias Explorer.Migrator.DeleteZeroValueInternalTransactions alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange import Ecto.Query @@ -823,7 +823,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do try do {_num, result} = repo.update_all(update_block_query, []) - MissingRangesManipulator.add_ranges_by_block_numbers(invalid_block_numbers) + MissingBlockRange.add_ranges_by_block_numbers(invalid_block_numbers) Logger.debug(fn -> [ diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 27e13af507b9..71d80308394c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -13,7 +13,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do alias Explorer.Chain.{Block, Hash, Import, PendingOperationsHelper, PendingTransactionOperation, Transaction} alias Explorer.Chain.Import.Runner.TokenTransfers alias Explorer.Prometheus.Instrumenter - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange @behaviour Import.Runner @@ -726,7 +726,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do PendingOperationsHelper.delete_related_transaction_operations(transaction_hashes) - MissingRangesManipulator.add_ranges_by_block_numbers(result) + MissingBlockRange.add_ranges_by_block_numbers(result) {:ok, result} rescue diff --git a/apps/explorer/lib/explorer/utility/missing_block_range.ex b/apps/explorer/lib/explorer/utility/missing_block_range.ex index 42af949565e9..d66901feddc4 100644 --- a/apps/explorer/lib/explorer/utility/missing_block_range.ex +++ b/apps/explorer/lib/explorer/utility/missing_block_range.ex @@ -1,3 +1,4 @@ +# credo:disable-for-this-file defmodule Explorer.Utility.MissingBlockRange do @moduledoc """ Module is responsible for keeping the ranges of blocks that need to be (re)fetched. @@ -106,11 +107,13 @@ defmodule Explorer.Utility.MissingBlockRange do This function first converts the list of block numbers into ranges using `numbers_to_ranges/1` and then saves the resulting ranges in a batch with the specified priority. """ - @spec add_ranges_by_block_numbers([Block.block_number()], integer() | nil) :: [__MODULE__.t()] - def add_ranges_by_block_numbers(numbers, priority) do + @spec add_ranges_by_block_numbers([Block.block_number()], integer() | nil) :: :ok + def add_ranges_by_block_numbers(numbers, priority \\ nil) do numbers |> numbers_to_ranges() |> save_batch(priority) + + :ok end # Saves a range of block numbers with an optional priority. @@ -154,43 +157,44 @@ defmodule Explorer.Utility.MissingBlockRange do min_number = min(from, to) max_number = max(from, to) - lower_range = get_range_by_block_number(min_number) - higher_range = get_range_by_block_number(max_number) + Repo.transaction(fn -> + {all_ranges, lower_range, higher_range} = lock_related_ranges(max_number, min_number) - case {lower_range, higher_range} do - {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> - if is_nil(same_range.priority) && not is_nil(priority) do - Repo.delete(same_range) + case {lower_range, higher_range} do + {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> + if is_nil(same_range.priority) && not is_nil(priority) do + Repo.delete(same_range) - inside_range_params = %{from_number: max_number, to_number: min_number, priority: priority} - insert_range(inside_range_params) + inside_range_params = %{from_number: max_number, to_number: min_number, priority: priority} + insert_range(inside_range_params) - insert_outside_right_range_params(same_range, min_number) - insert_outside_left_range_params(same_range, max_number) - end + insert_outside_right_range_params(same_range, min_number) + insert_outside_left_range_params(same_range, max_number) + end - {%__MODULE__{} = range, nil} -> - delete_less_priority_range(range, priority) - split_right_range_priorities(range, priority, min_number) - fill_ranges_between(max_number, range.from_number + 1, priority) + {%__MODULE__{} = range, nil} -> + delete_less_priority_range(range, priority) + split_right_range_priorities(range, priority, min_number) + fill_ranges_between(all_ranges, max_number, range.from_number + 1, priority) - {nil, %__MODULE__{} = range} -> - delete_less_priority_range(range, priority) - split_left_range_priorities(range, priority, max_number) - fill_ranges_between(range.to_number - 1, min_number, priority) + {nil, %__MODULE__{} = range} -> + delete_less_priority_range(range, priority) + split_left_range_priorities(range, priority, max_number) + fill_ranges_between(all_ranges, range.to_number - 1, min_number, priority) - {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> - delete_less_priority_range(range_2, priority) - delete_less_priority_range(range_1, priority) + {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> + delete_less_priority_range(range_2, priority) + delete_less_priority_range(range_1, priority) - split_left_range_priorities(range_2, priority, max_number) - split_right_range_priorities(range_1, priority, min_number) + split_left_range_priorities(range_2, priority, max_number) + split_right_range_priorities(range_1, priority, min_number) - fill_ranges_between(range_2.to_number - 1, range_1.from_number + 1, priority) + fill_ranges_between(all_ranges, range_2.to_number - 1, range_1.from_number + 1, priority) - {nil, nil} -> - fill_ranges_between(max_number, min_number, priority) - end + {nil, nil} -> + fill_ranges_between(all_ranges, max_number, min_number, priority) + end + end) end @spec insert_inside_left_range_params(__MODULE__.t(), Block.block_number(), integer() | nil) :: @@ -264,43 +268,60 @@ defmodule Explorer.Utility.MissingBlockRange do min_number = min(from, to) max_number = max(from, to) - lower_range = get_range_by_block_number(min_number) - higher_range = get_range_by_block_number(max_number) + Repo.transaction(fn -> + {all_ranges, lower_range, higher_range} = lock_related_ranges(max_number, min_number) - case {lower_range, higher_range} do - {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> - Repo.delete(same_range) + case {lower_range, higher_range} do + {%__MODULE__{} = same_range, %__MODULE__{} = same_range} -> + Repo.delete(same_range) - if same_range.from_number > max_number do - insert_range(%{ - from_number: same_range.from_number, - to_number: BlockNumberHelper.next_block_number(max_number) - }) - end + if same_range.from_number > max_number do + insert_range(%{ + from_number: same_range.from_number, + to_number: BlockNumberHelper.next_block_number(max_number), + priority: same_range.priority + }) + end - if same_range.to_number < min_number do - insert_range(%{ - from_number: BlockNumberHelper.previous_block_number(min_number), - to_number: same_range.to_number - }) - end + if same_range.to_number < min_number do + insert_range(%{ + from_number: BlockNumberHelper.previous_block_number(min_number), + to_number: same_range.to_number, + priority: same_range.priority + }) + end - {%__MODULE__{} = range, nil} -> - delete_ranges_between(max_number, range.from_number) - update_from_number_or_delete_range(range, min_number) + {%__MODULE__{} = range, nil} -> + delete_ranges_between(all_ranges, max_number, range.from_number) + update_from_number_or_delete_range(range, min_number) - {nil, %__MODULE__{} = range} -> - delete_ranges_between(range.to_number, min_number) - update_to_number_or_delete_range(range, max_number) + {nil, %__MODULE__{} = range} -> + delete_ranges_between(all_ranges, range.to_number, min_number) + update_to_number_or_delete_range(range, max_number) - {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> - delete_ranges_between(range_2.to_number, range_1.from_number) - update_from_number_or_delete_range(range_1, min_number) - update_to_number_or_delete_range(range_2, max_number) + {%__MODULE__{} = range_1, %__MODULE__{} = range_2} -> + delete_ranges_between(all_ranges, range_2.to_number, range_1.from_number) + update_from_number_or_delete_range(range_1, min_number) + update_to_number_or_delete_range(range_2, max_number) - _ -> - delete_ranges_between(max_number, min_number) - end + _ -> + delete_ranges_between(all_ranges, max_number, min_number) + end + end) + end + + defp lock_related_ranges(from, to) do + all_ranges = + __MODULE__ + |> where([m], fragment("int4range(?, ?, '[]') && int4range(?, ?, '[]')", ^to, ^from, m.to_number, m.from_number)) + |> order_by([m], desc: m.from_number) + |> lock("FOR UPDATE") + |> Repo.all() + + lower_range = Enum.find(all_ranges, &(&1.from_number >= to and &1.to_number <= to)) + higher_range = Enum.find(all_ranges, &(&1.from_number >= from and &1.to_number <= from)) + + {all_ranges, lower_range, higher_range} end def clear_batch(batch) do @@ -379,10 +400,10 @@ defmodule Explorer.Utility.MissingBlockRange do end defp update_from_number_or_delete_range(%{to_number: to} = range, from) when from <= to, do: Repo.delete(range) - defp update_from_number_or_delete_range(range, from), do: update_range(range, %{from_number: from}) + defp update_from_number_or_delete_range(range, from), do: update_range(range, %{from_number: from - 1}) defp update_to_number_or_delete_range(%{from_number: from} = range, to) when to >= from, do: Repo.delete(range) - defp update_to_number_or_delete_range(range, to), do: update_range(range, %{to_number: to}) + defp update_to_number_or_delete_range(range, to), do: update_range(range, %{to_number: to + 1}) @doc """ Fetches the range of blocks that includes the given block number if it falls @@ -404,15 +425,23 @@ defmodule Explorer.Utility.MissingBlockRange do end # Fills all missing block ranges that overlap with the interval [from, to] - @spec fill_ranges_between(Block.block_number(), Block.block_number(), integer() | nil) :: :ok - defp fill_ranges_between(from, to, priority) when from >= to do + @spec fill_ranges_between([__MODULE__.t()], Block.block_number(), Block.block_number(), integer() | nil) :: :ok + defp fill_ranges_between(all_ranges, from, to, priority) when from >= to do + ids_to_delete = + all_ranges + |> Enum.filter(fn range -> + range.to_number >= to and range.from_number <= from and priority_filter(range, priority) + end) + |> Enum.map(& &1.id) + __MODULE__ - |> where([r], fragment("int4range(?, ?, '[]') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) - |> priority_filter(priority) + |> where([r], r.id in ^ids_to_delete) |> Repo.delete_all() + filtered_ranges = Enum.filter(all_ranges, &(&1.id not in ids_to_delete)) + # select all left priority ranges - priority_ranges = select_all_ranges_within_the_range(from, to) + priority_ranges = select_all_ranges_within_the_range(filtered_ranges, from, to) if Enum.empty?(priority_ranges) do # if no priority ranges inside the requested interval, fill the full range @@ -480,7 +509,7 @@ defmodule Explorer.Utility.MissingBlockRange do :ok end - defp fill_ranges_between(_from, _to, _priority), do: :ok + defp fill_ranges_between(_all_ranges, _from, _to, _priority), do: :ok defp insert_or_update_adjacent_ranges(from, to, priority, :both) do upper_range = get_range_by_block_number(from + 1, priority) @@ -516,26 +545,25 @@ defmodule Explorer.Utility.MissingBlockRange do end end - defp select_all_ranges_within_the_range(from, to) do - __MODULE__ - |> where([r], fragment("int4range(?, ?, '[]') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) - |> order_by([r], desc: r.from_number) - |> Repo.all() - end - - defp priority_filter(query, nil) do - where(query, [r], is_nil(r.priority)) + defp select_all_ranges_within_the_range(all_ranges, from, to) do + all_ranges + |> Enum.filter(fn range -> range.to_number >= to and range.from_number <= from end) + |> Enum.sort_by(& &1.from_number, &>=/2) end - defp priority_filter(query, _priority) do - query - end + defp priority_filter(range, nil), do: is_nil(range.priority) + defp priority_filter(_range, _priority), do: true # Deletes all missing block ranges that overlap with the interval [from, to] - @spec delete_ranges_between(Block.block_number(), Block.block_number()) :: :ok - defp delete_ranges_between(from, to) do + @spec delete_ranges_between([__MODULE__.t()], Block.block_number(), Block.block_number()) :: :ok + defp delete_ranges_between(all_ranges, from, to) do + ids_to_delete = + all_ranges + |> Enum.filter(fn range -> range.to_number > to and range.from_number < from end) + |> Enum.map(& &1.id) + __MODULE__ - |> where([r], fragment("int4range(?, ?, '()') @> int4range(?, ?, '[]')", ^to, ^from, r.to_number, r.from_number)) + |> where([r], r.id in ^ids_to_delete) |> Repo.delete_all() end diff --git a/apps/explorer/lib/explorer/utility/missing_ranges_manipulator.ex b/apps/explorer/lib/explorer/utility/missing_ranges_manipulator.ex deleted file mode 100644 index e66aaf4fa2ac..000000000000 --- a/apps/explorer/lib/explorer/utility/missing_ranges_manipulator.ex +++ /dev/null @@ -1,61 +0,0 @@ -defmodule Explorer.Utility.MissingRangesManipulator do - @moduledoc """ - Performs concurrent-safe actions on missing block ranges. - """ - - use GenServer - - alias Explorer.Utility.MissingBlockRange - - @spec start_link(term()) :: GenServer.on_start() - def start_link(_) do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) - end - - def get_latest_batch(size) do - GenServer.call(__MODULE__, {:get_latest_batch, size}) - end - - def clear_batch(batch) do - GenServer.call(__MODULE__, {:clear_batch, batch}, timeout(batch)) - end - - def save_batch(batch, priority \\ nil) do - GenServer.call(__MODULE__, {:save_batch, batch, priority}, timeout(batch)) - end - - def add_ranges_by_block_numbers(numbers, priority \\ nil) do - GenServer.cast(__MODULE__, {:add_ranges_by_block_numbers, numbers, priority}) - end - - @impl true - def init(_) do - {:ok, %{}} - end - - @impl true - def handle_call({:get_latest_batch, size}, _from, state) do - {:reply, MissingBlockRange.get_latest_batch(size), state} - end - - def handle_call({:clear_batch, batch}, _from, state) do - {:reply, MissingBlockRange.clear_batch(batch), state} - end - - def handle_call({:save_batch, batch, priority}, _from, state) do - {:reply, MissingBlockRange.save_batch(batch, priority), state} - end - - @impl true - def handle_cast({:add_ranges_by_block_numbers, numbers, priority}, state) do - MissingBlockRange.add_ranges_by_block_numbers(numbers, priority) - - {:noreply, state} - end - - @default_timeout 5000 - @timeout_by_range 2000 - defp timeout(batch) do - @default_timeout + @timeout_by_range * Enum.count(batch) - end -end diff --git a/apps/explorer/test/explorer/utility/missing_block_range_test.exs b/apps/explorer/test/explorer/utility/missing_block_range_test.exs index e2dbaee77d2c..433b30de4587 100644 --- a/apps/explorer/test/explorer/utility/missing_block_range_test.exs +++ b/apps/explorer/test/explorer/utility/missing_block_range_test.exs @@ -441,4 +441,44 @@ defmodule Explorer.Utility.MissingBlockRangeTest do end) end end + + describe "clear_batch/1" do + setup do + # Ensure the database is clean before each test + Repo.delete_all(MissingBlockRange) + + on_exit(fn -> + # Clean up the database after each test + Repo.delete_all(MissingBlockRange) + end) + + :ok + end + + test "correctly clears the batch" do + Repo.insert!(%MissingBlockRange{from_number: 112, to_number: 86, priority: 1}) + Repo.insert!(%MissingBlockRange{from_number: 45, to_number: 30, priority: 1}) + Repo.insert!(%MissingBlockRange{from_number: 25, to_number: 20, priority: nil}) + + batch = [95..80//-1, 60..58//-1, 42..35//-1, 30..19//-1] + + MissingBlockRange.clear_batch(batch) + + ranges = Repo.all(MissingBlockRange) + + assert length(ranges) == 3 + + assert Enum.any?(ranges, fn range -> + range.from_number == 112 and range.to_number == 96 and range.priority == 1 + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 45 and range.to_number == 43 and range.priority == 1 + end) + + assert Enum.any?(ranges, fn range -> + range.from_number == 34 and range.to_number == 31 and range.priority == 1 + end) + end + end end diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 3f113874bd50..34115b9b39b4 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -30,7 +30,7 @@ defmodule Indexer.Block.Catchup.Fetcher do alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain alias Explorer.Chain.NullRoundHeight - alias Explorer.Utility.{MassiveBlock, MissingBlockRange, MissingRangesManipulator} + alias Explorer.Utility.{MassiveBlock, MissingBlockRange} alias Indexer.{Block, Tracer} alias Indexer.Block.Catchup.TaskSupervisor alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @@ -238,7 +238,7 @@ defmodule Indexer.Block.Catchup.Fetcher do acc end) |> numbers_to_ranges() - |> MissingRangesManipulator.clear_batch() + |> MissingBlockRange.clear_batch() end defp handle_null_rounds(errors) do @@ -282,7 +282,7 @@ defmodule Indexer.Block.Catchup.Fetcher do success_numbers |> numbers_to_ranges() - |> MissingRangesManipulator.clear_batch() + |> MissingBlockRange.clear_batch() end defp block_error_to_number(%{data: %{number: number}}) when is_integer(number), do: number diff --git a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex index 4888a04c2bc5..1a1f87439752 100644 --- a/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex +++ b/apps/indexer/lib/indexer/block/catchup/missing_ranges_collector.ex @@ -64,7 +64,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.{Chain, Repo} alias Explorer.Chain.Cache.Counters.LastFetchedCounter - alias Explorer.Utility.{MissingBlockRange, MissingRangesManipulator} + alias Explorer.Utility.MissingBlockRange @default_missing_ranges_batch_size 100_000 @past_check_interval 10 @@ -187,7 +187,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do ranges |> Enum.reverse() |> Enum.flat_map(fn f..l//_ -> Chain.missing_block_number_ranges(l..f) end) - |> MissingRangesManipulator.save_batch() + |> MissingBlockRange.save_batch() if not is_nil(max_fetched_block_number) do schedule_future_check() @@ -275,7 +275,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do %{min: nil, max: nil} -> max_number = last_block() {min_number, first_batch} = fetch_missing_ranges_batch(max_number, false) - MissingRangesManipulator.save_batch(first_batch) + MissingBlockRange.save_batch(first_batch) {min_number, max_number} %{min: min, max: max} -> @@ -297,7 +297,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do def handle_info(:update_future, %{max_fetched_block_number: max_number} = state) do if continue_future_updating?(max_number) do {new_max_number, batch} = fetch_missing_ranges_batch(max_number, true) - MissingRangesManipulator.save_batch(batch) + MissingBlockRange.save_batch(batch) schedule_future_check() {:noreply, %{state | max_fetched_block_number: new_max_number}} else @@ -321,7 +321,7 @@ defmodule Indexer.Block.Catchup.MissingRangesCollector do def handle_info(:update_past, %{min_fetched_block_number: min_number} = state) do if min_number > first_block() do {new_min_number, batch} = fetch_missing_ranges_batch(min_number, false) - MissingRangesManipulator.save_batch(batch) + MissingBlockRange.save_batch(batch) schedule_past_check(state.first_check_completed?) {:noreply, %{state | min_fetched_block_number: new_min_number}} else diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 61b0d465f383..37ecc87c08a0 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -36,7 +36,7 @@ defmodule Indexer.Block.Realtime.Fetcher do alias Explorer.Chain alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Explorer.Chain.Events.Publisher - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange alias Indexer.{Block, Tracer} alias Indexer.Block.Realtime.TaskSupervisor alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand @@ -435,7 +435,7 @@ defmodule Indexer.Block.Realtime.Fetcher do case result do {:ok, %{inserted: inserted, errors: []}} -> log_import_timings(inserted, fetch_duration, time_before) - MissingRangesManipulator.clear_batch([block_number_to_fetch..block_number_to_fetch]) + MissingBlockRange.clear_batch([block_number_to_fetch..block_number_to_fetch]) Logger.debug("Fetched and imported.") {:ok, %{inserted: _, errors: [_ | _] = errors}} -> diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex index 01bd5820239f..7edb1268f2ef 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex @@ -13,7 +13,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do alias EthereumJSONRPC.Nonce alias Explorer.Chain.Address alias Explorer.Chain.Cache.BlockNumber - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange import Indexer.Block.Fetcher, only: [ @@ -98,7 +98,9 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do :ets.insert(@table_name, {@pending_blocks_cache_key, updated_pending_blocks}) # Change `1` to specific label when `priority` field becomes `Ecto.Enum`. - MissingRangesManipulator.add_ranges_by_block_numbers([contract_creation_block_number], 1) + MissingBlockRange.add_ranges_by_block_numbers([contract_creation_block_number], 1) + + :ok end defp find_contract_creation_block_number(block_ranges, address_hash) do diff --git a/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex b/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex index 53664810aa77..bcd5b3839dd9 100644 --- a/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex +++ b/apps/indexer/lib/indexer/temporary/uncataloged_token_transfers.ex @@ -13,7 +13,7 @@ defmodule Indexer.Temporary.UncatalogedTokenTransfers do require Logger alias Explorer.Chain.TokenTransfer - alias Explorer.Utility.MissingRangesManipulator + alias Explorer.Utility.MissingBlockRange alias Indexer.Temporary.UncatalogedTokenTransfers def child_spec([init_arguments]) do @@ -96,7 +96,7 @@ defmodule Indexer.Temporary.UncatalogedTokenTransfers do defp async_push_front(block_numbers) do Task.Supervisor.async_nolink( UncatalogedTokenTransfers.TaskSupervisor, - MissingRangesManipulator, + MissingBlockRange, :add_ranges_by_block_numbers, [block_numbers] ) diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs index 0be013f9c980..9cfb13b2ede7 100644 --- a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -8,7 +8,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do alias Explorer.Chain alias Explorer.Chain.Block.Reward alias Explorer.Chain.Hash - alias Explorer.Utility.MissingRangesManipulator alias Explorer.Utility.MissingBlockRange alias Indexer.Block alias Indexer.Block.Catchup.Fetcher @@ -61,7 +60,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do ) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) parent = self() @@ -186,7 +184,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) latest_block_number = 2 latest_block_quantity = integer_to_quantity(latest_block_number) @@ -346,7 +343,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) latest_block_number = 2 latest_block_quantity = integer_to_quantity(latest_block_number) @@ -503,7 +499,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) latest_block_number = 2 latest_block_quantity = integer_to_quantity(latest_block_number) @@ -646,7 +641,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do Application.put_env(:indexer, :block_ranges, "0..1") start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) EthereumJSONRPC.Mox |> expect(:json_rpc, 1, fn @@ -707,7 +701,6 @@ defmodule Indexer.Block.Catchup.FetcherTest do Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) - MissingRangesManipulator.start_link([]) latest_block_number = 3 latest_block_quantity = integer_to_quantity(latest_block_number) From fd03433e020231e2b5be7a009360d23b144a2f2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:59:23 +0300 Subject: [PATCH 0567/1006] chore(deps-dev): bump credo from 1.7.14 to 1.7.15 (#13822) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 5267898f031d..3c09e8c02935 100644 --- a/mix.lock +++ b/mix.lock @@ -32,7 +32,7 @@ "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, - "credo": {:hex, :credo, "1.7.14", "c7e75216cea8d978ba8c60ed9dede4cc79a1c99a266c34b3600dd2c33b96bc92", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "12a97d6bb98c277e4fb1dff45aaf5c137287416009d214fb46e68147bd9e0203"}, + "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, From 54a31424b24716d504f4143ac0cbbe6e75bfb5a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 22:59:52 +0300 Subject: [PATCH 0568/1006] chore(deps): bump ecto_sql from 3.13.3 to 3.13.4 (#13823) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 3c09e8c02935..dc5a5335342d 100644 --- a/mix.lock +++ b/mix.lock @@ -45,7 +45,7 @@ "dns": {:hex, :dns, "2.4.0", "44790a0375b28bdc7b59fc894460bfcb03ffeec4c5984e2c3e8b0797b1518327", [:mix], [], "hexpm", "e178e353c469820d02ba889d6a80d01c8c27b47dfcda4016a9cbc6218e3eed64"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, - "ecto_sql": {:hex, :ecto_sql, "3.13.3", "81f7067dd1951081888529002dbc71f54e5e891b69c60195040ea44697e1104a", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5751caea36c8f5dd0d1de6f37eceffea19d10bd53f20e5bbe31c45f2efc8944a"}, + "ecto_sql": {:hex, :ecto_sql, "3.13.4", "b6e9d07557ddba62508a9ce4a484989a5bb5e9a048ae0e695f6d93f095c25d60", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2b38cf0749ca4d1c5a8bcbff79bbe15446861ca12a61f9fba604486cb6b62a14"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"}, "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"}, From 8fe168634d3c5749ae9929aa8d73a0f6c88e4746 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Jan 2026 23:50:49 +0300 Subject: [PATCH 0569/1006] chore(deps): bump ex_cldr_numbers from 2.36.0 to 2.37.0 (#13821) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index dc5a5335342d..b642edb92fa2 100644 --- a/mix.lock +++ b/mix.lock @@ -58,7 +58,7 @@ "ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.36.0", "d0de502db2fa24eeae16749af1af0093f24fb823e8715c66076cd8c6a3a960e3", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "17640b8daf2580a0a11317a720a26079e774d4c36f939d82f4e9f7075269897d"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.37.0", "701922f371d5569f4aef4dd81958231fc15dd9976564251871977f21215f4bc7", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "adc011aad34ab545e1d53ae248891479efcd25ba51f662822ec7c5083d0122f8"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.0", "7ade6ae1017a8f650f83f79a378ad9ad5bcc4eacfa2704349590c1f91f2eac19", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "eb93f99005f8d3f1b1b12c85a45323937350e11d3e79f8375ab51e2ff44392f2"}, "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, From 55f01230e64a02a6eb1ca30731ecfa1573aa1886 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 16:23:52 +0300 Subject: [PATCH 0570/1006] chore(deps): bump prometheus from 6.1.1 to 6.1.2 (#13824) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index b642edb92fa2..f0811603639a 100644 --- a/mix.lock +++ b/mix.lock @@ -36,7 +36,7 @@ "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, - "ddskerl": {:hex, :ddskerl, "0.4.2", "a51a90be9ac9b36a94017670bed479c623b10ca9d4bda1edf3a0e48caeeada2a", [:rebar3], [], "hexpm", "63f907373d7e548151d584d4da8a38928fd26ec9477b94c0ffaad87d7cb69fe7"}, + "ddskerl": {:hex, :ddskerl, "0.4.3", "bb97b90eef1b5906520cebdd38da29b18a770b5567f6297927f53566eae9bebb", [:rebar3], [], "hexpm", "4e0f6047c6a002ce38f6ea155276dd918cd635fd0a5edb9e0b46aeb6f7aff2c2"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, @@ -144,7 +144,7 @@ "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, - "prometheus": {:hex, :prometheus, "6.1.1", "3c9d8e2d4fcf948450550693f6a82fae013d3665ba10aa55955b64c2875fadb3", [:rebar3], [{:ddskerl, "0.4.2", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "c3ecc7a35676948089e28ff4383ccb85dcef447f0982c01e5630a084008c456a"}, + "prometheus": {:hex, :prometheus, "6.1.2", "e5c3c567cd8b0994425920763405635211183c15bdc47d0cd524807dde20ca1d", [:rebar3], [{:ddskerl, "0.4.3", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "109662001328112b860de112d49b575310be8ff33fe04bf0482eec4b1e5a1278"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:hex, :prometheus_ex, "5.0.0", "8e5514e385d5408b559792698dee2cfa60faed3b354d2a1046622923ecad4a68", [:mix], [{:prometheus, "~> 6.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "148bf80b110799362152e4bb154b508cba233456a171b5de6fe97e652bc90019"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.3.0", "c4b527e0b3a9ef1af26bdcfbfad3998f37795b9185d475ca610fe4388fdd3bb5", [:mix], [{:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "c4d1404ac4e9d3d963da601db2a7d8ea31194f0017057fabf0cfb9bf5a6c8c75"}, From 803c40fd8611079c210d729a79fcf18e9653da33 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 7 Jan 2026 15:14:05 +0400 Subject: [PATCH 0571/1006] fix: Fix block reindex condition in ContractCreator on-demand (#13831) Co-authored-by: Victor Baranov --- apps/explorer/lib/explorer/chain/block.ex | 16 ++++++++++++++++ .../fetcher/on_demand/contract_creator.ex | 8 +++++--- .../fetcher/on_demand/contract_creator_test.exs | 6 +++--- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index b301ea26350c..812e9b28ace0 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -686,6 +686,22 @@ defmodule Explorer.Chain.Block do def aggregate_transactions(block), do: block + @doc """ + Checks if block with consensus and not marked to re-fetch block is present in the DB with the given number + """ + @spec indexed?(non_neg_integer()) :: boolean() + def indexed?(block_number) do + query = + from( + block in __MODULE__, + where: block.number == ^block_number, + where: block.consensus == true, + where: block.refetch_needed == false + ) + + Repo.exists?(query) + end + defp transaction_aggregator(transaction, acc, block_base_fee_per_gas) do gas_used = Helper.number_to_decimal(transaction.gas_used) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex index 7edb1268f2ef..7f82633abf8d 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_creator.ex @@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do import EthereumJSONRPC, only: [id_to_params: 1, integer_to_quantity: 1, json_rpc: 2] alias EthereumJSONRPC.Nonce - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Block} alias Explorer.Chain.Cache.BlockNumber alias Explorer.Utility.MissingBlockRange @@ -97,8 +97,10 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreator do :ets.insert(@table_name, {@pending_blocks_cache_key, updated_pending_blocks}) - # Change `1` to specific label when `priority` field becomes `Ecto.Enum`. - MissingBlockRange.add_ranges_by_block_numbers([contract_creation_block_number], 1) + unless Block.indexed?(contract_creation_block_number) do + # Change `1` to specific label when `priority` field becomes `Ecto.Enum`. + MissingBlockRange.add_ranges_by_block_numbers([contract_creation_block_number], 1) + end :ok end diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs index eea538e39961..26ee35968e33 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs @@ -123,7 +123,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreatorTest do now = Timex.now() Enum.each(0..4, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, minutes: -i)) + insert(:block, number: i, refetch_needed: i == 3, timestamp: Timex.shift(now, minutes: -i)) end) Explorer.Chain.Cache.BlockNumber.get_max() @@ -161,7 +161,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreatorTest do now = Timex.now() Enum.each(0..4, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, minutes: -i)) + insert(:block, number: i, refetch_needed: i == 2, timestamp: Timex.shift(now, minutes: -i)) end) Explorer.Chain.Cache.BlockNumber.get_max() @@ -198,7 +198,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreatorTest do now = Timex.now() Enum.each(0..4, fn i -> - insert(:block, number: i, timestamp: Timex.shift(now, minutes: -i)) + insert(:block, number: i, refetch_needed: i == 1, timestamp: Timex.shift(now, minutes: -i)) end) Explorer.Chain.Cache.BlockNumber.get_max() From 4d63cb1c7e77b4a2ac3d6251e52e9e58db231940 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 8 Jan 2026 17:58:38 +0300 Subject: [PATCH 0572/1006] chore: Disband 37% of Explorer.Chain module (#13755) --- .../address_contract_controller.ex | 2 +- .../address_read_contract_controller.ex | 4 +- .../address_read_proxy_controller.ex | 2 +- .../address_validation_controller.ex | 3 +- .../address_write_contract_controller.ex | 4 +- .../address_write_proxy_controller.ex | 2 +- .../api/rpc/transaction_controller.ex | 2 +- .../controllers/api/v2/address_controller.ex | 6 +- .../controllers/api/v2/block_controller.ex | 4 +- .../api/v2/main_page_controller.ex | 7 +- .../api/v2/smart_contract_controller.ex | 2 +- .../controllers/api/v2/token_controller.ex | 2 +- .../api/v2/transaction_controller.ex | 4 +- .../api/v2/withdrawal_controller.ex | 5 +- .../pending_transaction_controller.ex | 3 +- .../recent_transactions_controller.ex | 4 +- .../controllers/smart_contract_controller.ex | 4 +- .../controllers/tokens/token_controller.ex | 3 +- .../controllers/transaction_controller.ex | 6 +- ...saction_internal_transaction_controller.ex | 4 +- .../transaction_token_transfer_controller.ex | 3 +- .../visualize_sol2uml_controller.ex | 5 +- .../controllers/withdrawal_controller.ex | 6 +- .../templates/transaction/index.html.eex | 2 +- .../views/api/v2/address_view.ex | 3 +- .../lib/block_scout_web/views/block_view.ex | 4 +- apps/explorer/README.md | 6 +- .../recent_collated_transactions.benchee | Bin .../recent_collated_transactions.exs | 8 +- .../lib/explorer/account/watchlist_address.ex | 91 +- apps/explorer/lib/explorer/chain.ex | 1565 +---------------- apps/explorer/lib/explorer/chain/address.ex | 108 +- .../explorer/chain/address/token_balance.ex | 34 +- apps/explorer/lib/explorer/chain/block.ex | 335 +++- .../lib/explorer/chain/cache/block_number.ex | 6 +- .../chain/cache/counters/contracts_count.ex | 18 +- .../explorer/chain/cache/counters/helper.ex | 3 +- .../cache/counters/new_contracts_count.ex | 23 +- .../counters/new_verified_contracts_count.ex | 17 +- .../counters/verified_contracts_count.ex | 9 +- .../chain/cache/counters/withdrawals_sum.ex | 10 +- .../explorer/chain/internal_transaction.ex | 150 ++ apps/explorer/lib/explorer/chain/token.ex | 167 +- .../lib/explorer/chain/transaction.ex | 767 +++++++- .../explorer/lib/explorer/chain/withdrawal.ex | 28 + apps/explorer/lib/explorer/etherscan.ex | 2 +- .../migrator/backfill_metadata_url.ex | 5 +- ...l_transactions_with_incompatible_status.ex | 4 +- .../restore_omitted_weth_transfers.ex | 2 +- .../20180117221923_create_transactions.exs | 2 +- .../chain/address/token_balance_test.exs | 11 + .../test/explorer/chain/address_test.exs | 45 +- .../explorer/chain/beacon/deposit_test.exs | 2 +- .../test/explorer/chain/block_test.exs | 258 ++- .../chain/internal_transaction_test.exs | 115 +- .../explorer/test/explorer/chain/log_test.exs | 1 - .../test/explorer/chain/transaction_test.exs | 150 ++ apps/explorer/test/explorer/chain_test.exs | 573 +----- .../explorer/market/fetcher/coin_test.exs | 1 - .../explorer/market/fetcher/history_test.exs | 5 +- .../market/source/coin_gecko_test.exs | 1 - ...sactions_denormalization_migrator_test.exs | 1 - apps/indexer/lib/indexer/block/fetcher.ex | 4 +- .../lib/indexer/fetcher/block_reward.ex | 8 +- .../indexer/fetcher/internal_transaction.ex | 6 +- .../internal_transaction/delete_queue.ex | 7 +- .../indexer/fetcher/on_demand/first_trace.ex | 3 +- .../fetcher/on_demand/internal_transaction.ex | 2 +- .../fetcher/optimism/eip1559_config_update.ex | 4 +- .../pending_block_operations_sanitizer.ex | 6 +- .../indexer/fetcher/replaced_transaction.ex | 9 +- .../fetcher/signed_authorization_status.ex | 2 +- .../lib/indexer/fetcher/token_balance.ex | 6 +- .../indexer/fetcher/token_counters_updater.ex | 3 +- .../indexer/fetcher/token_instance/helper.ex | 4 +- .../lib/indexer/fetcher/transaction_action.ex | 2 +- .../lib/indexer/fetcher/uncle_block.ex | 3 +- .../migrator/recovery_weth_token_transfers.ex | 6 +- .../lib/indexer/pending_ops_cleaner.ex | 4 +- .../indexer/pending_transactions_sanitizer.ex | 6 +- 80 files changed, 2381 insertions(+), 2323 deletions(-) rename apps/explorer/benchmarks/explorer/chain/{ => transaction}/recent_collated_transactions.benchee (100%) rename apps/explorer/benchmarks/explorer/chain/{ => transaction}/recent_collated_transactions.exs (92%) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex index 79460fa4017f..329b33b9f523 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex @@ -27,7 +27,7 @@ defmodule BlockScoutWeb.AddressContractController do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), _ <- PublishHelper.sourcify_check(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options) do + {:ok, address} <- Address.find_contract_address(address_hash, address_options) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex index 1211cf9b6682..d9e80a100d76 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex @@ -45,7 +45,7 @@ defmodule BlockScoutWeb.AddressReadContractController do ] with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), false <- is_nil(address.smart_contract), need_wallet? <- Reader.read_functions_required_wallet_from_abi(address.smart_contract.abi) != [], {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do @@ -66,7 +66,7 @@ defmodule BlockScoutWeb.AddressReadContractController do _ -> if custom_abi? do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do render( conn, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex index 81381240eefd..f7c7f57ad139 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_proxy_controller.ex @@ -24,7 +24,7 @@ defmodule BlockScoutWeb.AddressReadProxyController do ] with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), false <- is_nil(address.smart_contract), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do render( diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex index 92ac7af242c6..f3fc4ed8f580 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex @@ -13,6 +13,7 @@ defmodule BlockScoutWeb.AddressValidationController do alias BlockScoutWeb.{AccessHelper, BlockView, Controller} alias Explorer.{Chain, Market} + alias Explorer.Chain.Block alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand alias Phoenix.View @@ -33,7 +34,7 @@ defmodule BlockScoutWeb.AddressValidationController do paging_options(params) ) - blocks_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) + blocks_plus_one = Block.get_blocks_validated_by_address(full_options, address_hash) {blocks, next_page} = split_list_by_page(blocks_plus_one) next_page_path = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex index 4b0124ce1f4d..b0dee0db3054 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_contract_controller.ex @@ -41,7 +41,7 @@ defmodule BlockScoutWeb.AddressWriteContractController do with false <- AddressView.contract_interaction_disabled?(), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), false <- is_nil(address.smart_contract), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do render( @@ -60,7 +60,7 @@ defmodule BlockScoutWeb.AddressWriteContractController do _ -> if custom_abi? do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do render( conn, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex index 9643660d814e..868547c46f32 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_write_proxy_controller.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.AddressWriteProxyController do with false <- AddressView.contract_interaction_disabled?(), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), false <- is_nil(address.smart_contract), {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do render( diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex index 2042fa6d7740..6313a23aa959 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do transaction_updated = if (error == "Reverted" || error == "execution reverted") && !revert_reason do - %Transaction{transaction | revert_reason: Chain.fetch_transaction_revert_reason(transaction)} + %Transaction{transaction | revert_reason: Transaction.fetch_transaction_revert_reason(transaction)} else transaction end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index cf7f2c8f7549..ef4dac14d6d6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -43,7 +43,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do alias BlockScoutWeb.Schemas.Helper, as: SchemasHelper alias Explorer.{Chain, Market, PagingOptions} - alias Explorer.Chain.{Address, Beacon.Deposit, Hash, Transaction} + alias Explorer.Chain.{Address, Beacon.Deposit, Block, Hash, Token, Transaction} alias Explorer.Chain.Address.{CoinBalance, Counters} alias Explorer.Chain.Token.Instance @@ -497,7 +497,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do def token_transfers(conn, %{address_hash_param: address_hash_string} = params) do with {:ok, address_hash} <- validate_address_hash(address_hash_string, params), {:ok, token_address_hash} <- validate_optional_address_hash(params[:token], params), - token_address_exists <- (token_address_hash && Chain.check_token_exists(token_address_hash)) || :ok do + token_address_exists <- (token_address_hash && Token.check_token_exists(token_address_hash)) || :ok do case {Chain.hash_to_address(address_hash, @address_options), token_address_exists} do {{:ok, _address}, :ok} -> paging_options = paging_options(params) @@ -748,7 +748,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> Keyword.merge(paging_options(params)) |> Keyword.merge(@api_true) - results_plus_one = Chain.get_blocks_validated_by_address(full_options, address_hash) + results_plus_one = Block.get_blocks_validated_by_address(full_options, address_hash) {blocks, next_page} = split_list_by_page(results_plus_one) next_page_params = next_page |> next_page_params(blocks, params) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 4431eb307f4d..f7485da39035 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -42,8 +42,8 @@ defmodule BlockScoutWeb.API.V2.BlockController do alias Explorer.Chain alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader alias Explorer.Chain.Beacon.Deposit + alias Explorer.Chain.{Block, InternalTransaction} alias Explorer.Chain.Cache.{BlockNumber, Counters.AverageBlockTime} - alias Explorer.Chain.InternalTransaction alias Explorer.Chain.Optimism.TransactionBatch, as: OptimismTransactionBatch alias Explorer.Chain.Scroll.Reader, as: ScrollReader alias Timex.Duration @@ -181,7 +181,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do ok_response _ -> - {:lost_consensus, Chain.nonconsensus_block_by_number(number, @api_true)} + {:lost_consensus, Block.nonconsensus_block_by_number(number, @api_true)} end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index a206eb58ca74..e4aa7618233c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -7,7 +7,9 @@ defmodule BlockScoutWeb.API.V2.MainPageController do use OpenApiSpex.ControllerSpecs alias BlockScoutWeb.API.V2.{BlockView, TransactionView} + alias Explorer.Account.WatchlistAddress alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.Transaction import BlockScoutWeb.Account.AuthController, only: [current_user: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] @@ -97,7 +99,7 @@ defmodule BlockScoutWeb.API.V2.MainPageController do """ @spec transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() def transactions(conn, _params) do - recent_transactions = Chain.recent_collated_transactions(false, @transactions_options) + recent_transactions = Transaction.recent_collated_transactions(false, @transactions_options) conn |> put_status(200) @@ -126,7 +128,8 @@ defmodule BlockScoutWeb.API.V2.MainPageController do @spec watchlist_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() def watchlist_transactions(conn, _params) do with {:auth, %{watchlist_id: watchlist_id}} <- {:auth, current_user(conn)} do - {watchlist_names, transactions} = Chain.fetch_watchlist_transactions(watchlist_id, @transactions_options) + {watchlist_names, transactions} = + WatchlistAddress.fetch_watchlist_transactions(watchlist_id, @transactions_options) conn |> put_status(200) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index a988f008466b..e3b20526873a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -63,7 +63,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), _ <- PublishHelper.sourcify_check(address_hash_string), {:not_found, {:ok, %Address{} = address}} <- - {:not_found, Chain.find_contract_address(address_hash, smart_contract_address_options())} do + {:not_found, Address.find_contract_address(address_hash, smart_contract_address_options())} do implementations = SmartContractHelper.pre_fetch_implementations(address) conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index d972a6f05772..3c105f042587 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -116,7 +116,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, true} <- {:not_found, Token.by_contract_address_hash_exists?(address_hash, @api_true)} do - {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 5_000) + {transfers_count, holders_count} = Token.fetch_token_counters(address_hash, 5_000) json(conn, %{transfers_count: to_string(transfers_count), token_holders_count: to_string(holders_count)}) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 4c9046679fec..5165584f5aa1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -49,6 +49,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.{ForbiddenResponse, NotFoundResponse} + alias Explorer.Account.WatchlistAddress alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.Arbitrum.Reader.API.Settlement, as: ArbitrumSettlementReader alias Explorer.Chain.Beacon.Deposit, as: BeaconDeposit @@ -904,7 +905,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do |> Keyword.merge(paging_options(params, [:validated])) |> Keyword.merge(@api_true) - {watchlist_names, transactions_plus_one} = Chain.fetch_watchlist_transactions(watchlist_id, full_options) + {watchlist_names, transactions_plus_one} = + WatchlistAddress.fetch_watchlist_transactions(watchlist_id, full_options) {transactions, next_page} = split_list_by_page(transactions_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex index 33f21b1b350f..6d20c907de29 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] alias Explorer.Chain + alias Explorer.Chain.Withdrawal tags(["withdrawals"]) @@ -68,8 +69,8 @@ defmodule BlockScoutWeb.API.V2.WithdrawalController do def withdrawals_counters(conn, _params) do conn |> json(%{ - withdrawals_count: Chain.count_withdrawals_from_cache(api?: true), - withdrawals_sum: Chain.sum_withdrawals_from_cache(api?: true) + withdrawals_count: Withdrawal.count_withdrawals_from_cache(api?: true), + withdrawals_sum: Withdrawal.sum_withdrawals_from_cache(api?: true) }) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex index f709855b000a..4704d652528c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex @@ -6,6 +6,7 @@ defmodule BlockScoutWeb.PendingTransactionController do alias BlockScoutWeb.{Controller, TransactionView} alias Explorer.Chain + alias Explorer.Chain.Transaction alias Phoenix.View {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @@ -64,7 +65,7 @@ defmodule BlockScoutWeb.PendingTransactionController do end defp get_pending_transactions_and_next_page(options) do - transactions_plus_one = Chain.recent_pending_transactions(options, true) + transactions_plus_one = Transaction.recent_pending_transactions(options, true) split_list_by_page(transactions_plus_one) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex index 0863579eab95..44a8db384387 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{DenormalizationHelper, Hash} + alias Explorer.Chain.{DenormalizationHelper, Hash, Transaction} alias Phoenix.View {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @@ -13,7 +13,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do def index(conn, _params) do if ajax?(conn) do recent_transactions = - Chain.recent_collated_transactions( + Transaction.recent_collated_transactions( true, DenormalizationHelper.extend_block_necessity( [ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index 9a53ddd1e952..50efc65576e1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.SmartContractController do true <- ajax?(conn), {:custom_abi, false} <- {:custom_abi, is_custom_abi}, {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options) do + {:ok, address} <- Address.find_contract_address(address_hash, address_options) do implementation_address_hash_string = implementation_address_hash(contract_type, address) functions = @@ -169,7 +169,7 @@ defmodule BlockScoutWeb.SmartContractController do with true <- ajax?(conn), {:ok, address_hash} <- Chain.string_to_address_hash(params["id"]), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options) do + {:ok, address} <- Address.find_contract_address(address_hash, address_options) do contract_type = if params["type"] == "proxy", do: :proxy, else: :regular args = diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex index 2c728e9b47e9..363cf137cf1d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex @@ -5,6 +5,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do alias BlockScoutWeb.AccessHelper alias Explorer.Chain + alias Explorer.Chain.Token def show(conn, %{"id" => address_hash_string}) do redirect(conn, to: AccessHelper.get_path(conn, :token_transfer_path, :index, address_hash_string)) @@ -13,7 +14,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do def token_counters(conn, %{"id" => address_hash_string}) do case Chain.string_to_address_hash(address_hash_string) do {:ok, address_hash} -> - {transfers_count, holders_count} = Chain.fetch_token_counters(address_hash, 30_000) + {transfers_count, holders_count} = Token.fetch_token_counters(address_hash, 30_000) json(conn, %{transfer_count: transfers_count, token_holder_count: holders_count}) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index ed4332c46236..6fa1798d7136 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -26,7 +26,7 @@ defmodule BlockScoutWeb.TransactionController do alias Explorer.{Chain, Market} alias Explorer.Chain.Cache.Counters.TransactionsCount - alias Explorer.Chain.DenormalizationHelper + alias Explorer.Chain.{DenormalizationHelper, Transaction} alias Phoenix.View @necessity_by_association %{ @@ -68,7 +68,7 @@ defmodule BlockScoutWeb.TransactionController do ) %{total_transactions_count: transactions_count, transactions: transactions_plus_one} = - Chain.recent_collated_transactions_for_rap(full_options) + Transaction.recent_collated_transactions_for_rap(full_options) {transactions, next_page} = if fetch_page_number(params) == 1 do @@ -150,7 +150,7 @@ defmodule BlockScoutWeb.TransactionController do def show(conn, %{"id" => id} = params) do with {:ok, transaction_hash} <- Chain.string_to_full_hash(id), - :ok <- Chain.check_transaction_exists(transaction_hash) do + :ok <- Transaction.check_transaction_exists(transaction_hash) do if Chain.transaction_has_token_transfers?(transaction_hash) do with {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash, necessity_by_association: @necessity_by_association), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex index ae16db75e0bb..086ebafec71e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex @@ -11,12 +11,12 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do alias BlockScoutWeb.{AccessHelper, Controller, InternalTransactionView, TransactionController} alias Explorer.{Chain, Market} - alias Explorer.Chain.DenormalizationHelper + alias Explorer.Chain.{DenormalizationHelper, Transaction} alias Phoenix.View def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), - :ok <- Chain.check_transaction_exists(transaction_hash), + :ok <- Transaction.check_transaction_exists(transaction_hash), {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash, []), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex index 1ca50b21ca9d..c1011b6e73b9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex @@ -9,6 +9,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do alias BlockScoutWeb.{AccessHelper, Controller, TransactionController, TransactionTokenTransferView} alias Explorer.{Chain, Market} + alias Explorer.Chain.Transaction alias Phoenix.View {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) @@ -16,7 +17,7 @@ defmodule BlockScoutWeb.TransactionTokenTransferController do def index(conn, %{"transaction_id" => transaction_hash_string, "type" => "JSON"} = params) do with {:ok, transaction_hash} <- Chain.string_to_full_hash(transaction_hash_string), - :ok <- Chain.check_transaction_exists(transaction_hash), + :ok <- Transaction.check_transaction_exists(transaction_hash), {:ok, transaction} <- Chain.hash_to_transaction( transaction_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex index 025706a2c71c..be136a82f5ed 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.VisualizeSol2umlController do use BlockScoutWeb, :controller alias BlockScoutWeb.AccessHelper alias Explorer.Chain + alias Explorer.Chain.Address alias Explorer.Visualize.Sol2uml def index(conn, %{"type" => "JSON", "address" => address_hash_string}) do @@ -14,7 +15,7 @@ defmodule BlockScoutWeb.VisualizeSol2umlController do if Sol2uml.enabled?() do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options), + {:ok, address} <- Address.find_contract_address(address_hash, address_options), # check that contract is verified. partial and bytecode twin verification is ok for this case false <- is_nil(address.smart_contract) do sources = @@ -51,7 +52,7 @@ defmodule BlockScoutWeb.VisualizeSol2umlController do with true <- Sol2uml.enabled?(), {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_contract_address(address_hash, address_options) do + {:ok, address} <- Address.find_contract_address(address_hash, address_options) do render(conn, "index.html", address: address, get_svg_path: visualize_sol2uml_path(conn, :index, %{"type" => "JSON", "address" => address_hash_string}) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex index 4df6d86f8448..8d5c56fc0270 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.WithdrawalController do alias BlockScoutWeb.{Controller, WithdrawalView} alias Explorer.Chain - alias Explorer.Chain.Wei + alias Explorer.Chain.{Wei, Withdrawal} alias Phoenix.View def index(conn, %{"type" => "JSON"} = params) do @@ -35,8 +35,8 @@ defmodule BlockScoutWeb.WithdrawalController do render(conn, "index.html", current_path: Controller.current_full_path(conn), page_number: params |> fetch_page_number() |> Integer.to_string(), - withdrawals_count: Chain.count_withdrawals_from_cache(), - withdrawals_sum: Chain.sum_withdrawals_from_cache() |> Wei.from(:wei) + withdrawals_count: Withdrawal.count_withdrawals_from_cache(), + withdrawals_sum: Withdrawal.sum_withdrawals_from_cache() |> Wei.from(:wei) ) end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex index dad9e730ce7d..e6b709a1060c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex @@ -11,7 +11,7 @@

<%= gettext "Validated Transactions" %>

- <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "top", showing_limit: if(Chain.transactions_available_count() == Chain.limit_showing_transactions(), do: Chain.limit_showing_transactions(), else: nil) %> + <%= render BlockScoutWeb.CommonComponentsView, "_rap_pagination_container.html", position: "top", showing_limit: if(Transaction.transactions_available_count() == Transaction.limit_showing_transactions(), do: Transaction.limit_showing_transactions(), else: nil) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index da77ea408c58..aa92a47938dc 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -6,6 +6,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do alias BlockScoutWeb.{AddressContractView, AddressView} alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} + alias Explorer.Account.WatchlistAddress alias Explorer.{Chain, Market} alias Explorer.Chain.Address alias Explorer.Chain.Address.Counters @@ -120,7 +121,7 @@ defmodule BlockScoutWeb.API.V2.AddressView do "has_logs" => Counters.check_if_logs_at_address(address.hash, @api_true), "has_tokens" => Counters.check_if_tokens_at_address(address.hash, @api_true), "has_token_transfers" => Counters.check_if_token_transfers_at_address(address.hash, @api_true), - "watchlist_address_id" => Chain.select_watchlist_address_id(get_watchlist_id(conn), address.hash), + "watchlist_address_id" => WatchlistAddress.select_watchlist_address_id(get_watchlist_id(conn), address.hash), "has_beacon_chain_withdrawals" => Counters.check_if_withdrawals_at_address(address.hash, @api_true) }) diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex index 65f9ee9b58dd..e2ca31b82dd3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/block_view.ex @@ -56,7 +56,7 @@ defmodule BlockScoutWeb.BlockView do def show_reward?(_), do: true def block_reward_text(%Reward{address_hash: beneficiary_address, address_type: :validator}, block_miner_address) do - if Application.get_env(:explorer, Explorer.Chain.Block.Reward, %{})[:keys_manager_contract_address] do + if Application.get_env(:explorer, Reward, %{})[:keys_manager_contract_address] do %{payout_key: block_miner_payout_address} = Reward.get_validator_payout_key_by_mining_from_db(block_miner_address) if beneficiary_address == block_miner_payout_address do @@ -79,7 +79,7 @@ defmodule BlockScoutWeb.BlockView do def combined_rewards_value(block) do block - |> Chain.block_combined_rewards() + |> Block.block_combined_rewards() |> format_wei_value(:ether) end end diff --git a/apps/explorer/README.md b/apps/explorer/README.md index c283c1c5214d..5ae360e139c6 100644 --- a/apps/explorer/README.md +++ b/apps/explorer/README.md @@ -31,8 +31,8 @@ To get BlockScout up and running locally: ### Benchmarking -#### `Explorer.Chain.recent_collated_transactions/0` +#### `Explorer.Chain.Transaction.recent_collated_transactions/0` * Reset the test database: `MIX_ENV=test mix do ecto.drop, ecto.create, ecto.migrate` -* Change `tag` in `benchmarks/explorer/chain/recent_collated_transactions.exs` to a new value, so that it will compare against the old values saved in `benchmarks/explorer/chain/recent_collated_transactions.benchee` -* Run the benchmark: `MIX_ENV=test mix run benchmarks/explorer/chain/recent_collated_transactions.exs` +* Change `tag` in `benchmarks/explorer/chain/transaction/recent_collated_transactions.exs` to a new value, so that it will compare against the old values saved in `benchmarks/explorer/chain/transaction/recent_collated_transactions.benchee` +* Run the benchmark: `MIX_ENV=test mix run benchmarks/explorer/chain/transaction/recent_collated_transactions.exs` diff --git a/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.benchee b/apps/explorer/benchmarks/explorer/chain/transaction/recent_collated_transactions.benchee similarity index 100% rename from apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.benchee rename to apps/explorer/benchmarks/explorer/chain/transaction/recent_collated_transactions.benchee diff --git a/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs b/apps/explorer/benchmarks/explorer/chain/transaction/recent_collated_transactions.exs similarity index 92% rename from apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs rename to apps/explorer/benchmarks/explorer/chain/transaction/recent_collated_transactions.exs index 335844236ea0..573be65b2241 100644 --- a/apps/explorer/benchmarks/explorer/chain/recent_collated_transactions.exs +++ b/apps/explorer/benchmarks/explorer/chain/transaction/recent_collated_transactions.exs @@ -2,13 +2,13 @@ path = "benchmarks/explorer/chain/recent_collated_transactions.benchee" import Explorer.Factory -alias Explorer.{Chain, Repo} -alias Explorer.Chain.Block +alias Explorer.Repo +alias Explorer.Chain.{Block, Transaction} Benchee.run( %{ - "Explorer.Chain.recent_collated_transactions" => fn _ -> - Chain.recent_collated_transactions(true) + "Explorer.Chain.Transaction.recent_collated_transactions" => fn _ -> + Transaction.recent_collated_transactions(true) end }, inputs: %{ diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex index 58800975abaa..715213ce6bee 100644 --- a/apps/explorer/lib/explorer/account/watchlist_address.ex +++ b/apps/explorer/lib/explorer/account/watchlist_address.ex @@ -84,10 +84,13 @@ defmodule Explorer.Account.WatchlistAddress do alias Explorer.Account.Notifier.ForbiddenAddress alias Explorer.Account.Watchlist alias Explorer.{Chain, PagingOptions, Repo} - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, Hash, Transaction} import Explorer.Chain, only: [hash_to_lower_case_string: 1] + @default_page_size 50 + @default_paging_options %PagingOptions{page_size: @default_page_size} + @watchlist_not_found "Watchlist not found" @attrs_base ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output watch_erc_404_input watch_erc_404_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a @@ -215,12 +218,12 @@ defmodule Explorer.Account.WatchlistAddress do end end - def watchlist_addresses_by_watchlist_id_query(watchlist_id) when not is_nil(watchlist_id) do + defp watchlist_addresses_by_watchlist_id_query(watchlist_id) when not is_nil(watchlist_id) do __MODULE__ |> where([wl_address], wl_address.watchlist_id == ^watchlist_id) end - def watchlist_addresses_by_watchlist_id_query(_), do: nil + defp watchlist_addresses_by_watchlist_id_query(_), do: nil @doc """ Query paginated watchlist addresses by watchlist id @@ -359,4 +362,86 @@ defmodule Explorer.Account.WatchlistAddress do )} end) end + + @doc """ + Fetches the watchlist transactions for the given watchlist ID and options. + """ + @spec fetch_watchlist_transactions(integer(), keyword()) :: + {%{Hash.Address.t() => %{label: String.t(), display_name: String.t()}}, [Transaction.t()]} + def fetch_watchlist_transactions(watchlist_id, options) do + watchlist_addresses = + watchlist_id + |> watchlist_addresses_by_watchlist_id_query() + |> Repo.account_repo().all() + + address_hashes = Enum.map(watchlist_addresses, fn wa -> wa.address_hash end) + + watchlist_names = + Enum.reduce(watchlist_addresses, %{}, fn wa, acc -> + Map.put(acc, wa.address_hash, %{label: wa.name, display_name: wa.name}) + end) + + {watchlist_names, address_hashes_to_mined_transactions_without_rewards(address_hashes, options)} + end + + defp address_hashes_to_mined_transactions_without_rewards(address_hashes, options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + address_hashes + |> address_hashes_to_mined_transactions_tasks(options) + |> Transaction.wait_for_address_transactions() + |> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2) + |> Enum.dedup_by(& &1.hash) + |> Enum.take(paging_options.page_size) + |> Chain.select_repo(options).preload(Map.keys(necessity_by_association)) + end + + defp address_hashes_to_mined_transactions_tasks(address_hashes, options) do + direction = Keyword.get(options, :direction) + + options + |> Transaction.address_to_transactions_tasks_query(true) + |> Transaction.not_pending_transactions() + |> Transaction.matching_address_queries_list(direction, address_hashes) + |> Enum.map(fn query -> + Task.async(fn -> + query + |> Transaction.put_has_token_transfers_to_transaction(false, aliased?: true) + |> Chain.select_repo(options).all() + end) + end) + end + + @doc """ + Retrieves the ID of a WatchlistAddress entry for a given watchlist and address. + + This function queries the WatchlistAddress table to find an entry that matches + both the provided watchlist ID and address hash. It returns the ID of the first + matching entry, if found. + + ## Parameters + - `watchlist_id`: The ID of the watchlist to search within. + - `address_hash`: The address hash to look for, as a `Hash.Address.t()` struct. + + ## Returns + - An integer representing the ID of the matching WatchlistAddress entry, if found. + - `nil` if no matching entry is found or if either input is `nil`. + """ + @spec select_watchlist_address_id(integer() | nil, Hash.Address.t() | nil) :: integer() | nil + def select_watchlist_address_id(watchlist_id, address_hash) + when not is_nil(watchlist_id) and not is_nil(address_hash) do + wa_ids = + __MODULE__ + |> where([wa], wa.watchlist_id == ^watchlist_id and wa.address_hash_hash == ^address_hash) + |> select([wa], wa.id) + |> Repo.account_repo().all() + + case wa_ids do + [wa_id | _] -> wa_id + _ -> nil + end + end + + def select_watchlist_address_id(_watchlist_id, _address_hash), do: nil end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 860361ddd570..7d61f0081553 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -5,37 +5,27 @@ defmodule Explorer.Chain do import Ecto.Query, only: [ - dynamic: 1, dynamic: 2, from: 2, join: 4, join: 5, limit: 2, - lock: 2, - offset: 2, order_by: 2, order_by: 3, preload: 2, preload: 3, - select: 2, select: 3, subquery: 1, where: 2, where: 3 ] - import EthereumJSONRPC, only: [fetch_block_internal_transactions: 2] - require Logger alias ABI.TypeDecoder - alias Ecto.Changeset - alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction alias EthereumJSONRPC.Utility.RangesHelper - alias Explorer.Account.WatchlistAddress - alias Explorer.Chain alias Explorer.Chain.{ @@ -48,14 +38,11 @@ defmodule Explorer.Chain do BlockNumberHelper, CurrencyHelper, Data, - DenormalizationHelper, Hash, Import, InternalTransaction, Log, - PendingBlockOperation, PendingOperationsHelper, - PendingTransactionOperation, SmartContract, Token, TokenTransfer, @@ -69,25 +56,19 @@ defmodule Explorer.Chain do alias Explorer.Chain.Cache.{ BlockNumber, Blocks, - Transactions, Uncles } alias Explorer.Chain.Cache.Counters.{ BlocksCount, ContractsCount, - LastFetchedCounter, NewContractsCount, NewVerifiedContractsCount, - TokenHoldersCount, - TokenTransfersCount, - VerifiedContractsCount, - WithdrawalsSum + VerifiedContractsCount } alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.Health.Helper, as: HealthHelper - alias Explorer.Chain.InternalTransaction.{CallType, Type} alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.Helper, as: ExplorerHelper @@ -102,14 +83,6 @@ defmodule Explorer.Chain do @token_transfers_per_transaction_preview 10 - @method_name_to_id_map %{ - "approve" => "095ea7b3", - "transfer" => "a9059cbb", - "multicall" => "5ae401dc", - "mint" => "40c10f19", - "commit" => "f14fcbc8" - } - @revert_msg_prefix_1 "Revert: " @revert_msg_prefix_2 "revert: " @revert_msg_prefix_3 "reverted " @@ -118,8 +91,6 @@ defmodule Explorer.Chain do @revert_msg_prefix_5 "execution reverted: " @revert_msg_prefix_6_empty "execution reverted" - @limit_showing_transactions 10_000 - @typedoc """ The name of an association on the `t:Ecto.Schema.t/0` """ @@ -172,35 +143,6 @@ defmodule Explorer.Chain do ) end - def address_hashes_to_mined_transactions_without_rewards(address_hashes, options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - - address_hashes - |> address_hashes_to_mined_transactions_tasks(options) - |> Transaction.wait_for_address_transactions() - |> Enum.sort_by(&{&1.block_number, &1.index}, &>=/2) - |> Enum.dedup_by(& &1.hash) - |> Enum.take(paging_options.page_size) - |> select_repo(options).preload(Map.keys(necessity_by_association)) - end - - defp address_hashes_to_mined_transactions_tasks(address_hashes, options) do - direction = Keyword.get(options, :direction) - - options - |> Transaction.address_to_transactions_tasks_query(true) - |> Transaction.not_pending_transactions() - |> Transaction.matching_address_queries_list(direction, address_hashes) - |> Enum.map(fn query -> - Task.async(fn -> - query - |> Transaction.put_has_token_transfers_to_transaction(false, aliased?: true) - |> select_repo(options).all() - end) - end) - end - @spec address_hash_to_token_transfers(Hash.Address.t(), Keyword.t()) :: [Transaction.t()] def address_hash_to_token_transfers(address_hash, options \\ []) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -363,58 +305,6 @@ defmodule Explorer.Chain do Repo.aggregate(Block, :count, :hash) end - @doc """ - The `t:Explorer.Chain.Wei.t/0` paid to the miners of the `t:Explorer.Chain.Block.t/0`s with `hash` - `Explorer.Chain.Hash.Full.t/0` by the signers of the transactions in those blocks to cover the gas fee - (`gas_used * gas_price`). - """ - @spec gas_payment_by_block_hash([Hash.Full.t()]) :: %{Hash.Full.t() => Wei.t()} - def gas_payment_by_block_hash(block_hashes) when is_list(block_hashes) do - query = - if DenormalizationHelper.transactions_denormalization_finished?() do - from( - transaction in Transaction, - where: transaction.block_hash in ^block_hashes and transaction.block_consensus == true, - group_by: transaction.block_hash, - select: {transaction.block_hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} - ) - else - from( - block in Block, - left_join: transaction in assoc(block, :transactions), - where: block.hash in ^block_hashes and block.consensus == true, - group_by: block.hash, - select: {block.hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} - ) - end - - initial_gas_payments = - block_hashes - |> Enum.map(&{&1, %Wei{value: Decimal.new(0)}}) - |> Enum.into(%{}) - - existing_data = - query - |> Repo.all() - |> Enum.into(%{}) - - Map.merge(initial_gas_payments, existing_data) - end - - def timestamp_by_block_hash(block_hashes) when is_list(block_hashes) do - query = - from( - block in Block, - where: block.hash in ^block_hashes and block.consensus == true, - group_by: block.hash, - select: {block.hash, block.timestamp} - ) - - query - |> Repo.all() - |> Enum.into(%{}) - end - @doc """ Finds all `t:Explorer.Chain.Transaction.t/0`s in the `t:Explorer.Chain.Block.t/0`. @@ -440,7 +330,7 @@ defmodule Explorer.Chain do |> fetch_transactions_in_ascending_order_by_index() |> join(:inner, [transaction], block in assoc(transaction, :block)) |> where([_, block], block.hash == ^block_hash) - |> apply_filter_by_type_to_transactions(type_filter) + |> Transaction.apply_filter_by_type_to_transactions(type_filter) |> join_associations(necessity_by_association) |> Transaction.put_has_token_transfers_to_transaction(old_ui?) |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() @@ -885,41 +775,6 @@ defmodule Explorer.Chain do ) end - @doc """ - Finds an `t:Explorer.Chain.Address.t/0` that has the provided `t:Explorer.Chain.Address.t/0` `hash` and a contract. - - ## Options - - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association, - then the `t:Explorer.Chain.Address.t/0` will not be included in the list. - - """ - @spec find_contract_address(Hash.Address.t(), [necessity_by_association_option]) :: - {:ok, Address.t()} | {:error, :not_found} - def find_contract_address( - %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, - options \\ [] - ) do - necessity_by_association = - options - |> Keyword.get(:necessity_by_association, %{}) - |> Map.merge(%{ - [smart_contract: :smart_contract_additional_sources] => :optional, - Implementation.proxy_implementations_association() => :optional - }) - - hash - |> Address.address_with_bytecode_query() - |> join_associations(necessity_by_association) - |> select_repo(options).one() - |> Address.update_address_result(options, false) - |> case do - nil -> {:error, :not_found} - address -> {:ok, address} - end - end - @doc """ Converts `t:Explorer.Chain.Block.t/0` `hash` to the `t:Explorer.Chain.Block.t/0` with that `hash`. @@ -1253,73 +1108,6 @@ defmodule Explorer.Chain do |> Decimal.min(Decimal.new(1)) end - @doc """ - Fetches the lowest block number available in the database. - - Queries the database for the minimum block number among blocks marked as consensus - blocks. Returns 0 if no consensus blocks exist or if the query fails. - - ## Returns - - `non_neg_integer`: The lowest block number from consensus blocks, or 0 if none found - """ - @spec fetch_min_block_number() :: non_neg_integer - def fetch_min_block_number do - query = - from(block in Block, - select: block.number, - where: block.consensus == true, - order_by: [asc: block.number], - limit: 1 - ) - - Repo.one(query) || 0 - rescue - _ -> - 0 - end - - @doc """ - Fetches the highest block number available in the database. - - Queries the database for the maximum block number among blocks marked as consensus - blocks. Returns 0 if no consensus blocks exist or if the query fails. - - ## Returns - - `non_neg_integer`: The highest block number from consensus blocks, or 0 if none found - """ - @spec fetch_max_block_number() :: non_neg_integer - def fetch_max_block_number do - query = - from(block in Block, - select: block.number, - where: block.consensus == true, - order_by: [desc: block.number], - limit: 1 - ) - - Repo.one(query) || 0 - rescue - _ -> - 0 - end - - def fetch_block_by_hash(block_hash) do - Repo.get(Block, block_hash) - end - - def filter_non_refetch_needed_block_numbers(block_numbers) do - query = - from( - block in Block, - where: block.number in ^block_numbers, - where: block.consensus == true, - where: block.refetch_needed == false, - select: block.number - ) - - Repo.all(query) - end - @doc """ Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`. @@ -1366,7 +1154,7 @@ defmodule Explorer.Chain do end end - def uncles_from_cache(block_type, paging_options, necessity_by_association, options) do + defp uncles_from_cache(block_type, paging_options, necessity_by_association, options) do case Uncles.atomic_take_enough(paging_options.page_size) do nil -> elements = fetch_blocks(block_type, paging_options, necessity_by_association, options) @@ -1396,29 +1184,6 @@ defmodule Explorer.Chain do end end - @doc """ - Map `block_number`s to their `t:Explorer.Chain.Block.t/0` `hash` `t:Explorer.Chain.Hash.Full.t/0`. - - Does not include non-consensus blocks. - - iex> block = insert(:block, consensus: false) - iex> Explorer.Chain.block_hash_by_number([block.number]) - %{} - - """ - @spec block_hash_by_number([Block.block_number()]) :: %{Block.block_number() => Hash.Full.t()} - def block_hash_by_number(block_numbers) when is_list(block_numbers) do - query = - from(block in Block, - where: block.consensus == true and block.number in ^block_numbers, - select: {block.number, block.hash} - ) - - query - |> Repo.all() - |> Enum.into(%{}) - end - @doc """ Retrieves the total row count for a given table. @@ -1445,79 +1210,6 @@ defmodule Explorer.Chain do end end - @doc """ - Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`. - """ - def stream_blocks_without_rewards(initial, reducer, limited? \\ false) when is_function(reducer, 2) do - Block.blocks_without_reward_query() - |> add_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - - @doc """ - Finds all transactions of a certain block number - """ - def get_transactions_of_block_number(block_number) do - block_number - |> Transaction.transactions_with_block_number() - |> Repo.all() - end - - @doc """ - Finds all transactions of a certain block numbers - """ - def get_transactions_of_block_numbers(block_numbers) do - block_numbers - |> Transaction.transactions_for_block_numbers() - |> Repo.all() - end - - @doc """ - Finds transactions by hashes - """ - @spec get_transactions_by_hashes([Hash.t()]) :: [Transaction.t()] - def get_transactions_by_hashes(transaction_hashes) do - transaction_hashes - |> Transaction.by_hashes_query() - |> Repo.all() - end - - @doc """ - Finds all Blocks validated by the address with the given hash. - - ## Options - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the - `t:Explorer.Chain.Block.t/0` will not be included in the page `entries`. - * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and - `:key` (a tuple of the lowest/oldest `{block_number}`) and. Results will be the internal - transactions older than the `block_number` that are passed. - - Returns all blocks validated by the address given. - """ - @spec get_blocks_validated_by_address( - [paging_options | necessity_by_association_option], - Hash.Address.t() - ) :: [Block.t()] - def get_blocks_validated_by_address(options \\ [], address_hash) when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - case paging_options do - %PagingOptions{key: {0}} -> - [] - - _ -> - Block - |> join_associations(necessity_by_association) - |> where(miner_hash: ^address_hash) - |> page_blocks(paging_options) - |> limit(^paging_options.page_size) - |> order_by(desc: :number) - |> select_repo(options).all() - end - end - @doc """ Return the balance in usd corresponding to this token. Return nil if the fiat_value of the token is not present. """ @@ -1534,153 +1226,6 @@ defmodule Explorer.Chain do Decimal.mult(tokens, fiat_value) end - @doc """ - Returns a stream of all token balances that weren't fetched values. - """ - @spec stream_unfetched_token_balances( - initial :: accumulator, - reducer :: (entry :: TokenBalance.t(), accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_unfetched_token_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do - TokenBalance.unfetched_token_balances() - |> add_token_balances_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - - def remove_nonconsensus_blocks_from_pending_ops(block_hashes) do - query = - case PendingOperationsHelper.pending_operations_type() do - "blocks" -> - PendingOperationsHelper.block_hash_in_query(block_hashes) - - "transactions" -> - from( - pto in PendingTransactionOperation, - join: t in assoc(pto, :transaction), - where: t.block_hash in ^block_hashes - ) - end - - {_, _} = Repo.delete_all(query) - - :ok - end - - def remove_nonconsensus_blocks_from_pending_ops do - query = - case PendingOperationsHelper.pending_operations_type() do - "blocks" -> - from( - pbo in PendingBlockOperation, - inner_join: block in Block, - on: block.hash == pbo.block_hash, - where: block.consensus == false - ) - - "transactions" -> - from( - pto in PendingTransactionOperation, - join: t in assoc(pto, :transaction), - where: t.block_consensus == false - ) - end - - {_, _} = Repo.delete_all(query) - - :ok - end - - @spec stream_mined_transactions( - fields :: [ - :block_hash - | :created_contract_code_indexed_at - | :from_address_hash - | :gas - | :gas_price - | :hash - | :index - | :input - | :nonce - | :r - | :s - | :to_address_hash - | :v - | :value - ], - initial :: accumulator, - reducer :: (entry :: term(), accumulator -> accumulator) - ) :: {:ok, accumulator} - when accumulator: term() - def stream_mined_transactions(fields, initial, reducer) when is_function(reducer, 2) do - query = - from(t in Transaction, - where: not is_nil(t.block_hash) and not is_nil(t.nonce) and not is_nil(t.from_address_hash), - select: ^fields - ) - - Repo.stream_reduce(query, initial, reducer) - end - - @spec stream_pending_transactions( - fields :: [ - :block_hash - | :created_contract_code_indexed_at - | :from_address_hash - | :gas - | :gas_price - | :hash - | :index - | :input - | :nonce - | :r - | :s - | :to_address_hash - | :v - | :value - ], - initial :: accumulator, - reducer :: (entry :: term(), accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_pending_transactions(fields, initial, reducer, limited? \\ false) when is_function(reducer, 2) do - query = - Transaction - |> pending_transactions_query() - |> select(^fields) - |> add_fetcher_limit(limited?) - - Repo.stream_reduce(query, initial, reducer) - end - - @doc """ - Returns a stream of all blocks that are marked as unfetched in `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`. - For each uncle block a `hash` of nephew block and an `index` of the block in it are returned. - - When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be - returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` - `uncle_fetched_at` will be set and it won't be returned anymore. - """ - @spec stream_unfetched_uncles( - initial :: accumulator, - reducer :: (entry :: term(), accumulator -> accumulator), - limited? :: boolean() - ) :: {:ok, accumulator} - when accumulator: term() - def stream_unfetched_uncles(initial, reducer, limited? \\ false) when is_function(reducer, 2) do - query = - from(bsdr in Block.SecondDegreeRelation, - where: is_nil(bsdr.uncle_fetched_at) and not is_nil(bsdr.index), - select: [:nephew_hash, :index] - ) - - query - |> add_fetcher_limit(limited?) - |> Repo.stream_reduce(initial, reducer) - end - @doc """ Max consensus block numbers. @@ -1982,255 +1527,38 @@ defmodule Explorer.Chain do end end - @spec nonconsensus_block_by_number(Block.block_number(), [api?]) :: {:ok, Block.t()} | {:error, :not_found} - def nonconsensus_block_by_number(number, options) do - Block - |> where(consensus: false, number: ^number) - |> select_repo(options).one() - |> case do - nil -> {:error, :not_found} - block -> {:ok, block} - end - end + def default_page_size, do: @default_page_size @doc """ - Count of pending `t:Explorer.Chain.Transaction.t/0`. - - A count of all pending transactions. + The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.Address.t/0`. - iex> insert(:transaction) - iex> :transaction |> insert() |> with_block() - iex> Explorer.Chain.pending_transaction_count() - 1 + iex> Explorer.Chain.string_to_address_hash("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, + 231, 239, 27, 234, 237>> + } + } - """ - @spec pending_transaction_count() :: non_neg_integer() - def pending_transaction_count do - Transaction - |> pending_transactions_query() - |> Repo.aggregate(:count, :hash) - end + iex> Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + { + :ok, + %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, + 231, 239, 27, 234, 237>> + } + } - @doc """ - Returns the paged list of collated transactions that occurred recently from newest to oldest using `block_number` - and `index`. - - iex> newest_first_transactions = 50 |> insert_list(:transaction) |> with_block() |> Enum.reverse() - iex> oldest_seen = Enum.at(newest_first_transactions, 9) - iex> paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} - iex> recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options) - iex> length(recent_collated_transactions) - 10 - iex> hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash - true + iex> Base.encode16(<<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, 231, 239, 27, 234, 237>>, case: :lower) + "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" - ## Options + `String.t` format must always have 40 hexadecimal digits after the `0x` base prefix. - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, - then the `t:Explorer.Chain.Transaction.t/0` will not be included in the list. - * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and - `:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than - the `block_number` and `index` that are passed. - - """ - @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option | api?]) :: [ - Transaction.t() - ] - def recent_collated_transactions(old_ui?, options \\ []) - when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - method_id_filter = Keyword.get(options, :method) - type_filter = Keyword.get(options, :type) - - fetch_recent_collated_transactions( - old_ui?, - paging_options, - necessity_by_association, - method_id_filter, - type_filter, - options - ) - end - - # RAP - random access pagination - @spec recent_collated_transactions_for_rap([paging_options | necessity_by_association_option]) :: %{ - :total_transactions_count => non_neg_integer(), - :transactions => [Transaction.t()] - } - def recent_collated_transactions_for_rap(options \\ []) when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - total_transactions_count = transactions_available_count() - - fetched_transactions = - if is_nil(paging_options.key) or paging_options.page_number == 1 do - paging_options.page_size - |> Kernel.+(1) - |> Transactions.atomic_take_enough() - |> case do - nil -> - transactions = fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) - Transactions.update(transactions) - transactions - - transactions -> - transactions - end - else - fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) - end - - %{total_transactions_count: total_transactions_count, transactions: fetched_transactions} - end - - def default_page_size, do: @default_page_size - - def fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) do - fetch_transactions_for_rap() - |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) - |> handle_random_access_paging_options(paging_options) - |> join_associations(necessity_by_association) - |> preload([{:token_transfers, [:token, :from_address, :to_address]}]) - |> Repo.all() - end - - defp fetch_transactions_for_rap do - Transaction - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) - end - - def transactions_available_count do - Transaction - |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) - |> limit(^@limit_showing_transactions) - |> Repo.aggregate(:count, :hash) - end - - def fetch_recent_collated_transactions( - old_ui?, - paging_options, - necessity_by_association, - method_id_filter, - type_filter, - options - ) do - case paging_options do - %PagingOptions{key: {0, 0}, is_index_in_asc_order: false} -> - [] - - _ -> - paging_options - |> Transaction.fetch_transactions() - |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) - |> apply_filter_by_method_id_to_transactions(method_id_filter) - |> apply_filter_by_type_to_transactions(type_filter) - |> join_associations(necessity_by_association) - |> Transaction.put_has_token_transfers_to_transaction(old_ui?) - |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> select_repo(options).all() - end - end - - @doc """ - Return the list of pending transactions that occurred recently. - - iex> 2 |> insert_list(:transaction) - iex> :transaction |> insert() |> with_block() - iex> 8 |> insert_list(:transaction) - iex> recent_pending_transactions = Explorer.Chain.recent_pending_transactions() - iex> length(recent_pending_transactions) - 10 - iex> Enum.all?(recent_pending_transactions, fn %Explorer.Chain.Transaction{block_hash: block_hash} -> - ...> is_nil(block_hash) - ...> end) - true - - ## Options - - * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is - `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, - then the `t:Explorer.Chain.Transaction.t/0` will not be included in the list. - * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` (defaults to - `#{@default_paging_options.page_size}`) and `:key` (a tuple of the lowest/oldest `{inserted_at, hash}`) and. - Results will be the transactions older than the `inserted_at` and `hash` that are passed. - - """ - @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [ - Transaction.t() - ] - def recent_pending_transactions(options \\ [], old_ui? \\ true) - when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - method_id_filter = Keyword.get(options, :method) - type_filter = Keyword.get(options, :type) - - Transaction - |> Transaction.page_pending_transaction(paging_options) - |> limit(^paging_options.page_size) - |> pending_transactions_query() - |> apply_filter_by_method_id_to_transactions(method_id_filter) - |> apply_filter_by_type_to_transactions(type_filter) - |> order_by([transaction], desc: transaction.inserted_at, asc: transaction.hash) - |> join_associations(necessity_by_association) - |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() - |> select_repo(options).all() - end - - @doc """ - Query to return all pending transactions - """ - @spec pending_transactions_query(Ecto.Queryable.t()) :: Ecto.Queryable.t() - def pending_transactions_query(query) do - from(transaction in query, - where: is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced") - ) - end - - @doc """ - Returns pending transactions list from the DB - """ - @spec pending_transactions_list() :: Ecto.Schema.t() | term() - def pending_transactions_list do - Transaction - |> pending_transactions_query() - |> where([t], t.inserted_at < ago(1, "day")) - |> Repo.all(timeout: :infinity) - end - - @doc """ - The `string` must start with `0x`, then is converted to an integer and then to `t:Explorer.Chain.Hash.Address.t/0`. - - iex> Explorer.Chain.string_to_address_hash("0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed") - { - :ok, - %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, - 231, 239, 27, 234, 237>> - } - } - - iex> Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - { - :ok, - %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, - 231, 239, 27, 234, 237>> - } - } - - iex> Base.encode16(<<90, 174, 182, 5, 63, 62, 148, 201, 185, 160, 159, 51, 102, 148, 53, 231, 239, 27, 234, 237>>, case: :lower) - "5aaeb6053f3e94c9b9a09f33669435e7ef1beaed" - - `String.t` format must always have 40 hexadecimal digits after the `0x` base prefix. - - iex> Explorer.Chain.string_to_address_hash("0x0") - :error + iex> Explorer.Chain.string_to_address_hash("0x0") + :error """ @spec string_to_address_hash(String.t()) :: {:ok, Hash.Address.t()} | :error @@ -2402,87 +1730,12 @@ defmodule Explorer.Chain do %Transaction{revert_reason: revert_reason} = transaction if revert_reason == nil do - fetch_transaction_revert_reason(transaction) + Transaction.fetch_transaction_revert_reason(transaction) else revert_reason end end - def fetch_transaction_revert_reason(transaction) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - hash_string = to_string(transaction.hash) - - response = - fetch_first_trace( - [ - %{ - block_hash: transaction.block_hash, - block_number: transaction.block_number, - hash_data: hash_string, - transaction_index: transaction.index - } - ], - json_rpc_named_arguments - ) - - revert_reason = - case response do - {:ok, first_trace_params} -> - first_trace_params |> Enum.at(0) |> Map.get(:output, %Data{bytes: <<>>}) |> to_string() - - {:error, reason} -> - Logger.error(fn -> - ["Error while fetching first trace for transaction: #{hash_string} error reason: ", inspect(reason)] - end) - - fetch_transaction_revert_reason_using_call(transaction) - - :ignore -> - fetch_transaction_revert_reason_using_call(transaction) - end - - if !is_nil(revert_reason) do - transaction - |> Changeset.change(%{revert_reason: revert_reason}) - |> Repo.update() - end - - revert_reason - end - - defp fetch_transaction_revert_reason_using_call(%Transaction{ - block_number: block_number, - to_address_hash: to_address_hash, - from_address_hash: from_address_hash, - input: data, - gas: gas, - gas_price: gas_price, - value: value - }) do - json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) - - req = - EthereumJSONRPCTransaction.eth_call_request( - 0, - block_number, - data, - to_address_hash, - from_address_hash, - Wei.hex_format(gas), - Wei.hex_format(gas_price), - Wei.hex_format(value) - ) - - case EthereumJSONRPC.json_rpc(req, json_rpc_named_arguments) do - {:error, error} -> - parse_revert_reason_from_error(error) - - _ -> - nil - end - end - @doc """ Fetches the raw traces of transaction. """ @@ -2669,47 +1922,6 @@ defmodule Explorer.Chain do |> limit(^paging_options.page_size) end - defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}], - do: limit(query, ^(@default_page_size + 1)) - - defp handle_random_access_paging_options(query, paging_options) do - query - |> (&if(paging_options |> Map.get(:page_number, 1) |> process_page_number() == 1, - do: &1, - else: Transaction.page_transaction(&1, paging_options) - )).() - |> handle_page(paging_options) - end - - defp handle_page(query, paging_options) do - page_number = paging_options |> Map.get(:page_number, 1) |> process_page_number() - page_size = Map.get(paging_options, :page_size, @default_page_size) - - cond do - page_in_bounds?(page_number, page_size) && page_number == 1 -> - query - |> limit(^(page_size + 1)) - - page_in_bounds?(page_number, page_size) -> - query - |> limit(^page_size) - |> offset(^((page_number - 2) * page_size)) - - true -> - query - |> limit(^(@default_page_size + 1)) - end - end - - defp process_page_number(number) when number < 1, do: 1 - - defp process_page_number(number), do: number - - defp page_in_bounds?(page_number, page_size), - do: page_size <= @limit_showing_transactions && @limit_showing_transactions - page_number * page_size >= 0 - - def limit_showing_transactions, do: @limit_showing_transactions - @doc """ Dynamically joins and preloads associations in a query based on necessity. @@ -2777,9 +1989,9 @@ defmodule Explorer.Chain do end) end - defp page_blocks(query, %PagingOptions{key: nil}), do: query + def page_blocks(query, %PagingOptions{key: nil}), do: query - defp page_blocks(query, %PagingOptions{key: {block_number}}) do + def page_blocks(query, %PagingOptions{key: {block_number}}) do where(query, [block], block.number < ^block_number) end @@ -2994,87 +2206,6 @@ defmodule Explorer.Chain do Repo.exists?(query) end - @spec find_and_update_replaced_transactions([ - %{ - required(:nonce) => non_neg_integer, - required(:from_address_hash) => Hash.Address.t(), - required(:hash) => Hash.t() - } - ]) :: {integer(), nil | [term()]} - def find_and_update_replaced_transactions(transactions, timeout \\ :infinity) do - query = - transactions - |> Enum.reduce( - Transaction, - fn %{hash: hash, nonce: nonce, from_address_hash: from_address_hash}, query -> - from(t in query, - or_where: - t.nonce == ^nonce and t.from_address_hash == ^from_address_hash and t.hash != ^hash and - not is_nil(t.block_number) - ) - end - ) - # Enforce Transaction ShareLocks order (see docs: sharelocks.md) - |> order_by(asc: :hash) - |> lock("FOR NO KEY UPDATE") - - hashes = Enum.map(transactions, & &1.hash) - - transactions_to_update = - from(pending in Transaction, - join: duplicate in subquery(query), - on: duplicate.nonce == pending.nonce, - on: duplicate.from_address_hash == pending.from_address_hash, - where: pending.hash in ^hashes and is_nil(pending.block_hash) - ) - - Repo.update_all(transactions_to_update, [set: [error: "dropped/replaced", status: :error]], timeout: timeout) - end - - @spec update_replaced_transactions([ - %{ - required(:nonce) => non_neg_integer, - required(:from_address_hash) => Hash.Address.t(), - required(:block_hash) => Hash.Full.t() - } - ]) :: {integer(), nil | [term()]} - def update_replaced_transactions(transactions, timeout \\ :infinity) do - filters = - transactions - |> Enum.filter(fn transaction -> - transaction.block_hash && transaction.nonce && transaction.from_address_hash - end) - |> Enum.map(fn transaction -> - {transaction.nonce, transaction.from_address_hash} - end) - |> Enum.uniq() - - if Enum.empty?(filters) do - {0, []} - else - query = - filters - |> Enum.reduce(Transaction, fn {nonce, from_address}, query -> - from(t in query, - or_where: - t.nonce == ^nonce and - t.from_address_hash == ^from_address and - is_nil(t.block_hash) and - (is_nil(t.error) or t.error != "dropped/replaced") - ) - end) - # Enforce Transaction ShareLocks order (see docs: sharelocks.md) - |> order_by(asc: :hash) - |> lock("FOR NO KEY UPDATE") - - Repo.update_all( - from(t in Transaction, join: s in subquery(query), on: t.hash == s.hash), - [set: [error: "dropped/replaced", status: :error]], - timeout: timeout - ) - end - end - @spec fetch_last_token_balances_include_unfetched([Hash.Address.t()], [api?]) :: [] def fetch_last_token_balances_include_unfetched(address_hashes, options \\ []) do address_hashes @@ -3114,26 +2245,6 @@ defmodule Explorer.Chain do end end - @spec fetch_last_token_balance(Hash.Address.t(), Hash.Address.t()) :: Decimal.t() - def fetch_last_token_balance(address_hash, token_contract_address_hash) do - if address_hash !== %{} do - address_hash - |> CurrentTokenBalance.last_token_balance(token_contract_address_hash) || Decimal.new(0) - else - Decimal.new(0) - end - end - - # @spec fetch_last_token_balance_1155(Hash.Address.t(), Hash.Address.t()) :: Decimal.t() - def fetch_last_token_balance_1155(address_hash, token_contract_address_hash, token_id) do - if address_hash !== %{} do - address_hash - |> CurrentTokenBalance.last_token_balance_1155(token_contract_address_hash, token_id) || Decimal.new(0) - else - Decimal.new(0) - end - end - def get_token_balance(address_hash, token_contract_address_hash, block_number, token_id \\ nil, options \\ []) do query = TokenBalance.fetch_token_balance(address_hash, token_contract_address_hash, block_number, token_id) @@ -3379,56 +2490,12 @@ defmodule Explorer.Chain do end end - @doc """ - Combined block reward from all the fees. - """ - @spec block_combined_rewards(Block.t()) :: Wei.t() - def block_combined_rewards(block) do - {:ok, value} = - block.rewards - |> Enum.reduce( - 0, - fn block_reward, acc -> - {:ok, decimal} = Wei.dump(block_reward.reward) - - Decimal.add(decimal, acc) - end - ) - |> Wei.cast() - - value - end - defp decode_params(params, types) do params |> Base.decode16!(case: :mixed) |> TypeDecoder.decode_raw(types) end - @spec get_token_types([String.t()]) :: [{Hash.Address.t(), String.t()}] - def get_token_types(hashes) do - query = - from( - token in Token, - where: token.contract_address_hash in ^hashes, - select: {token.contract_address_hash, token.type} - ) - - Repo.all(query) - end - - @spec get_token_type(Hash.Address.t()) :: String.t() | nil - def get_token_type(hash) do - query = - from( - token in Token, - where: token.contract_address_hash == ^hash, - select: token.type - ) - - Repo.one(query) - end - @spec erc_20_token?(Token.t()) :: bool def erc_20_token?(token) do erc_20_token_type?(token.type) @@ -3441,273 +2508,10 @@ defmodule Explorer.Chain do end end - @doc """ - Checks if it exists an `t:Explorer.Chain.Address.t/0` that has the provided - `t:Explorer.Chain.Address.t/0` `hash` and a contract. - - Returns `:ok` if found and `:not_found` otherwise. - """ - @spec check_contract_address_exists(Hash.Address.t()) :: :ok | :not_found - def check_contract_address_exists(address_hash) do - address_hash - |> contract_address_exists?() - |> boolean_to_check_result() - end - - @doc """ - Checks if it exists an `t:Explorer.Chain.Address.t/0` that has the provided - `t:Explorer.Chain.Address.t/0` `hash` and a contract. - - Returns `true` if found and `false` otherwise. - """ - @spec contract_address_exists?(Hash.Address.t()) :: boolean() - def contract_address_exists?(address_hash) do - query = - from( - address in Address, - where: address.hash == ^address_hash and not is_nil(address.contract_code) - ) - - Repo.exists?(query) - end - - @doc """ - Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists. - - Returns `:ok` if found - - iex> %Transaction{hash: hash} = insert(:transaction) - iex> Explorer.Chain.check_transaction_exists(hash) - :ok - - Returns `:not_found` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( - ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" - ...> ) - iex> Explorer.Chain.check_transaction_exists(hash) - :not_found - """ - @spec check_transaction_exists(Hash.Full.t()) :: :ok | :not_found - def check_transaction_exists(hash) do - hash - |> transaction_exists?() - |> boolean_to_check_result() - end - - @doc """ - Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists. - - Returns `true` if found - - iex> %Transaction{hash: hash} = insert(:transaction) - iex> Explorer.Chain.transaction_exists?(hash) - true - - Returns `false` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( - ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" - ...> ) - iex> Explorer.Chain.transaction_exists?(hash) - false - """ - @spec transaction_exists?(Hash.Full.t()) :: boolean() - def transaction_exists?(hash) do - query = - from( - transaction in Transaction, - where: transaction.hash == ^hash - ) - - Repo.exists?(query) - end - - @doc """ - Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists. - - Returns `:ok` if found - - iex> address = insert(:address) - iex> insert(:token, contract_address: address) - iex> Explorer.Chain.check_token_exists(address.hash) - :ok - - Returns `:not_found` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.check_token_exists(hash) - :not_found - """ - @spec check_token_exists(Hash.Address.t()) :: :ok | :not_found - def check_token_exists(hash) do - hash - |> token_exists?() - |> boolean_to_check_result() - end - - @doc """ - Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists. - - Returns `true` if found - - iex> address = insert(:address) - iex> insert(:token, contract_address: address) - iex> Explorer.Chain.token_exists?(address.hash) - true - - Returns `false` if not found - - iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") - iex> Explorer.Chain.token_exists?(hash) - false - """ - @spec token_exists?(Hash.Address.t()) :: boolean() - def token_exists?(hash) do - query = - from( - token in Token, - where: token.contract_address_hash == ^hash - ) - - Repo.exists?(query) - end - def boolean_to_check_result(true), do: :ok def boolean_to_check_result(false), do: :not_found - @doc """ - Fetches the first trace from the Nethermind trace URL. - """ - def fetch_first_trace(transactions_params, json_rpc_named_arguments) do - case EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) do - {:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} -> - format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) - - {:error, error} -> - {:error, error} - - :ignore -> - :ignore - end - end - - defp format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) do - {:ok, to_address_hash} = - if Map.has_key?(first_trace, :to_address_hash) do - Chain.string_to_address_hash(first_trace.to_address_hash) - else - {:ok, nil} - end - - {:ok, from_address_hash} = Chain.string_to_address_hash(first_trace.from_address_hash) - - {:ok, created_contract_address_hash} = - if Map.has_key?(first_trace, :created_contract_address_hash) do - Chain.string_to_address_hash(first_trace.created_contract_address_hash) - else - {:ok, nil} - end - - {:ok, transaction_hash} = Chain.string_to_full_hash(first_trace.transaction_hash) - - {:ok, call_type} = - if Map.has_key?(first_trace, :call_type) do - CallType.load(first_trace.call_type) - else - {:ok, nil} - end - - {:ok, type} = Type.load(first_trace.type) - - {:ok, input} = - if Map.has_key?(first_trace, :input) do - Data.cast(first_trace.input) - else - {:ok, nil} - end - - {:ok, output} = - if Map.has_key?(first_trace, :output) do - Data.cast(first_trace.output) - else - {:ok, nil} - end - - {:ok, created_contract_code} = - if Map.has_key?(first_trace, :created_contract_code) do - Data.cast(first_trace.created_contract_code) - else - {:ok, nil} - end - - {:ok, init} = - if Map.has_key?(first_trace, :init) do - Data.cast(first_trace.init) - else - {:ok, nil} - end - - block_index = - get_block_index(%{ - transaction_index: first_trace.transaction_index, - transaction_hash: first_trace.transaction_hash, - block_number: first_trace.block_number, - json_rpc_named_arguments: json_rpc_named_arguments - }) - - value = %Wei{value: Decimal.new(first_trace.value)} - - first_trace_formatted = - first_trace - |> Map.merge(%{ - block_index: block_index, - block_hash: block_hash, - call_type: call_type, - to_address_hash: to_address_hash, - created_contract_address_hash: created_contract_address_hash, - from_address_hash: from_address_hash, - input: input, - output: output, - created_contract_code: created_contract_code, - init: init, - transaction_hash: transaction_hash, - type: type, - value: value - }) - - {:ok, [first_trace_formatted]} - end - - defp get_block_index(%{ - transaction_index: transaction_index, - transaction_hash: transaction_hash, - block_number: block_number, - json_rpc_named_arguments: json_rpc_named_arguments - }) do - if transaction_index == 0 do - 0 - else - filtered_block_numbers = RangesHelper.filter_traceable_block_numbers([block_number]) - {:ok, traces} = fetch_block_internal_transactions(filtered_block_numbers, json_rpc_named_arguments) - - sorted_traces = - traces - |> Enum.sort_by(&{&1.transaction_index, &1.index}) - |> Enum.with_index() - - {_, block_index} = - sorted_traces - |> Enum.find({nil, -1}, fn {trace, _} -> - trace.transaction_index == transaction_index && - trace.transaction_hash == transaction_hash - end) - - block_index - end - end - @spec get_token_transfer_type(TokenTransfer.t()) :: :token_burning | :token_minting | :token_spawning | :token_transfer def get_token_transfer_type(transfer) do @@ -3784,182 +2588,11 @@ defmodule Explorer.Chain do end def recent_transactions(options, [:pending | _]) do - recent_pending_transactions(options, false) + Transaction.recent_pending_transactions(options, false) end def recent_transactions(options, _) do - recent_collated_transactions(false, options) - end - - def apply_filter_by_method_id_to_transactions(query, nil), do: query - - def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do - method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1) - - if method_ids != [] do - query - |> where([transaction], fragment("SUBSTRING(? FOR 4)", transaction.input) in ^method_ids) - else - query - end - end - - def apply_filter_by_method_id_to_transactions(query, filter), - do: apply_filter_by_method_id_to_transactions(query, [filter]) - - defp map_name_or_method_id_to_method_id(string) when is_binary(string) do - id = @method_name_to_id_map[string] - - if id do - decode_method_id(id) - else - trimmed = - string - |> String.replace("0x", "", global: false) - - decode_method_id(trimmed) - end - end - - defp decode_method_id(method_id) when is_binary(method_id) do - case String.length(method_id) == 8 && Base.decode16(method_id, case: :mixed) do - {:ok, bytes} -> - [bytes] - - _ -> - [] - end - end - - def apply_filter_by_type_to_transactions(query, [_ | _] = filter) do - {dynamic, modified_query} = apply_filter_by_type_to_transactions_inner(filter, query) - - modified_query - |> where(^dynamic) - end - - def apply_filter_by_type_to_transactions(query, _filter), do: query - - def apply_filter_by_type_to_transactions_inner(dynamic \\ dynamic(false), filter, query) - - def apply_filter_by_type_to_transactions_inner(dynamic, [type | remain], query) do - case type do - :contract_call -> - dynamic - |> filter_contract_call_dynamic() - |> apply_filter_by_type_to_transactions_inner( - remain, - join(query, :inner, [transaction], address in assoc(transaction, :to_address), as: :to_address) - ) - - :contract_creation -> - dynamic - |> filter_contract_creation_dynamic() - |> apply_filter_by_type_to_transactions_inner(remain, query) - - :coin_transfer -> - dynamic - |> filter_transaction_dynamic() - |> apply_filter_by_type_to_transactions_inner(remain, query) - - :token_transfer -> - dynamic - |> filter_token_transfer_dynamic() - |> apply_filter_by_type_to_transactions_inner(remain, query) - - :token_creation -> - dynamic - |> filter_token_creation_dynamic() - |> apply_filter_by_type_to_transactions_inner( - remain, - join(query, :inner, [transaction], token in Token, - on: token.contract_address_hash == transaction.created_contract_address_hash, - as: :created_token - ) - ) - - :blob_transaction -> - dynamic - |> filter_blob_transaction_dynamic() - |> apply_filter_by_type_to_transactions_inner(remain, query) - end - end - - def apply_filter_by_type_to_transactions_inner(dynamic_query, _, query), do: {dynamic_query, query} - - def filter_contract_creation_dynamic(dynamic) do - dynamic([transaction], ^dynamic or is_nil(transaction.to_address_hash)) - end - - def filter_transaction_dynamic(dynamic) do - dynamic([transaction], ^dynamic or transaction.value > ^0) - end - - def filter_contract_call_dynamic(dynamic) do - dynamic([transaction, to_address: to_address], ^dynamic or not is_nil(to_address.contract_code)) - end - - def filter_token_transfer_dynamic(dynamic) do - # TokenTransfer.__struct__.__meta__.source - dynamic( - [transaction], - ^dynamic or - fragment( - "NOT (SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NULL", - transaction.hash - ) - ) - end - - def filter_token_creation_dynamic(dynamic) do - dynamic([transaction, created_token: created_token], ^dynamic or not is_nil(created_token)) - end - - def filter_blob_transaction_dynamic(dynamic) do - # EIP-2718 blob transaction type - dynamic([transaction], ^dynamic or transaction.type == 3) - end - - def count_verified_contracts do - Repo.aggregate(SmartContract, :count, timeout: :infinity) - end - - def count_new_verified_contracts do - query = - from(contract in SmartContract, - select: contract.inserted_at, - where: fragment("NOW() - ? at time zone 'UTC' <= interval '24 hours'", contract.inserted_at) - ) - - query - |> Repo.aggregate(:count, timeout: :infinity) - end - - def count_contracts do - query = - from(address in Address, - select: address, - where: not is_nil(address.contract_code) - ) - - query - |> Repo.aggregate(:count, timeout: :infinity) - end - - def count_new_contracts do - query = - from(transaction in Transaction, - select: transaction, - where: - transaction.status == ^:ok and - fragment( - "NOW() - ? at time zone 'UTC' <= interval '24 hours'", - transaction.created_contract_code_indexed_at - ) - ) - - query - |> Repo.aggregate(:count, timeout: :infinity) + Transaction.recent_collated_transactions(false, options) end def count_verified_contracts_from_cache(options \\ []) do @@ -3978,63 +2611,6 @@ defmodule Explorer.Chain do NewContractsCount.fetch(options) end - def fetch_token_counters(address_hash, timeout) do - total_token_transfers_task = - Task.async(fn -> - TokenTransfersCount.fetch(address_hash) - end) - - total_token_holders_task = - Task.async(fn -> - TokenHoldersCount.fetch(address_hash) - end) - - [total_token_transfers_task, total_token_holders_task] - |> Task.yield_many(timeout) - |> Enum.map(fn {task, res} -> - case res do - {:ok, result} -> - result - - {:exit, reason} -> - Logger.warning("Query fetching token counters terminated: #{inspect(reason)}") - - fallback_cached_value_based_on_async_task_pid( - task.pid, - total_token_transfers_task.pid, - total_token_holders_task.pid, - address_hash - ) - - nil -> - Logger.warning("Query fetching token counters timed out.") - - fallback_cached_value_based_on_async_task_pid( - task.pid, - total_token_transfers_task.pid, - total_token_holders_task.pid, - address_hash - ) - end - end) - |> List.to_tuple() - end - - defp fallback_cached_value_based_on_async_task_pid( - task_pid, - total_token_transfers_task_pid, - total_token_holders_task_pid, - address_hash - ) do - case task_pid do - ^total_token_transfers_task_pid -> - TokenTransfersCount.fetch_count_from_cache(address_hash) - - ^total_token_holders_task_pid -> - TokenHoldersCount.fetch_count_from_cache(address_hash) - end - end - @spec flat_1155_batch_token_transfers([TokenTransfer.t()], Decimal.t() | nil) :: [TokenTransfer.t()] def flat_1155_batch_token_transfers(token_transfers, token_id \\ nil) when is_list(token_transfers) do token_transfers @@ -4117,54 +2693,6 @@ defmodule Explorer.Chain do end end - @doc """ - Retrieves the ID of a WatchlistAddress entry for a given watchlist and address. - - This function queries the WatchlistAddress table to find an entry that matches - both the provided watchlist ID and address hash. It returns the ID of the first - matching entry, if found. - - ## Parameters - - `watchlist_id`: The ID of the watchlist to search within. - - `address_hash`: The address hash to look for, as a `Hash.Address.t()` struct. - - ## Returns - - An integer representing the ID of the matching WatchlistAddress entry, if found. - - `nil` if no matching entry is found or if either input is `nil`. - """ - @spec select_watchlist_address_id(integer() | nil, Hash.Address.t() | nil) :: integer() | nil - def select_watchlist_address_id(watchlist_id, address_hash) - when not is_nil(watchlist_id) and not is_nil(address_hash) do - wa_ids = - WatchlistAddress - |> where([wa], wa.watchlist_id == ^watchlist_id and wa.address_hash_hash == ^address_hash) - |> select([wa], wa.id) - |> Repo.account_repo().all() - - case wa_ids do - [wa_id | _] -> wa_id - _ -> nil - end - end - - def select_watchlist_address_id(_watchlist_id, _address_hash), do: nil - - def fetch_watchlist_transactions(watchlist_id, options) do - watchlist_addresses = - watchlist_id - |> WatchlistAddress.watchlist_addresses_by_watchlist_id_query() - |> Repo.account_repo().all() - - address_hashes = Enum.map(watchlist_addresses, fn wa -> wa.address_hash end) - - watchlist_names = - Enum.reduce(watchlist_addresses, %{}, fn wa, acc -> - Map.put(acc, wa.address_hash, %{label: wa.name, display_name: wa.name}) - end) - - {watchlist_names, address_hashes_to_mined_transactions_without_rewards(address_hashes, options)} - end - def list_withdrawals(options \\ []) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) @@ -4182,25 +2710,6 @@ defmodule Explorer.Chain do end end - def sum_withdrawals do - Repo.aggregate(Withdrawal, :sum, :amount, timeout: :infinity) - end - - def upsert_count_withdrawals(index) do - LastFetchedCounter.upsert(%{ - counter_type: "withdrawals_count", - value: index - }) - end - - def sum_withdrawals_from_cache(options \\ []) do - WithdrawalsSum.fetch(options) - end - - def count_withdrawals_from_cache(options \\ []) do - "withdrawals_count" |> LastFetchedCounter.get(options) |> Decimal.add(1) - end - def add_fetcher_limit(query, false), do: query def add_fetcher_limit(query, true) do @@ -4209,14 +2718,6 @@ defmodule Explorer.Chain do limit(query, ^fetcher_limit) end - defp add_token_balances_fetcher_limit(query, false), do: query - - defp add_token_balances_fetcher_limit(query, true) do - token_balances_fetcher_limit = Application.get_env(:indexer, :token_balances_fetcher_init_limit) - - limit(query, ^token_balances_fetcher_limit) - end - @spec default_paging_options() :: map() def default_paging_options do @default_paging_options diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 98416bfac481..3505353eadb5 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -311,7 +311,7 @@ defmodule Explorer.Chain.Address do """ @spec create_multiple(list()) :: {non_neg_integer(), nil | [term()]} def create_multiple(address_insert_params) do - Repo.insert_all(Address, address_insert_params, on_conflict: :nothing, returning: [:hash]) + Repo.insert_all(__MODULE__, address_insert_params, on_conflict: :nothing, returning: [:hash]) end def balance_changeset(%__MODULE__{} = address, attrs) do @@ -358,7 +358,7 @@ defmodule Explorer.Chain.Address do """ @spec address_query(Hash.Address.t() | binary()) :: Ecto.Query.t() def address_query(hash) do - from(address in Address, where: address.hash == ^hash) + from(address in __MODULE__, where: address.hash == ^hash) end def checksum(address_or_hash, iodata? \\ false) @@ -471,11 +471,11 @@ defmodule Explorer.Chain.Address do @doc """ Preloads provided contracts associations if address has contract_code which is not nil """ - @spec maybe_preload_smart_contract_associations(Address.t(), list, list) :: Address.t() - def maybe_preload_smart_contract_associations(%Address{contract_code: nil} = address, _associations, _options), + @spec maybe_preload_smart_contract_associations(__MODULE__.t(), list, list) :: __MODULE__.t() + def maybe_preload_smart_contract_associations(%__MODULE__{contract_code: nil} = address, _associations, _options), do: address - def maybe_preload_smart_contract_associations(%Address{contract_code: _} = address, associations, options), + def maybe_preload_smart_contract_associations(%__MODULE__{contract_code: _} = address, associations, options), do: Chain.select_repo(options).preload(address, associations) @doc """ @@ -483,14 +483,14 @@ defmodule Explorer.Chain.Address do """ def count_with_fetched_coin_balance do from( - a in Address, + a in __MODULE__, select: fragment("COUNT(*)"), where: a.fetched_coin_balance > ^0 ) end def fetched_coin_balance(address_hash) when not is_nil(address_hash) do - Address + __MODULE__ |> where([address], address.hash == ^address_hash) |> select([address], address.fetched_coin_balance) end @@ -530,7 +530,7 @@ defmodule Explorer.Chain.Address do Lists the top `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance and address hash. """ - @spec list_top_addresses :: [{Address.t(), non_neg_integer()}] + @spec list_top_addresses :: [{__MODULE__.t(), non_neg_integer()}] def list_top_addresses(options \\ []) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) sorting_options = Keyword.get(options, :sorting, []) @@ -565,7 +565,7 @@ defmodule Explorer.Chain.Address do joins necessary associations with `Chain.join_associations/2`, and finally selects the repository with `Chain.select_repo/1` to fetch all the addresses. """ - @spec get_addresses_by_hashes([Hash.Address.t()]) :: [Chain.Address.t()] + @spec get_addresses_by_hashes([Hash.Address.t()]) :: [__MODULE__.t()] def get_addresses_by_hashes(address_hashes) do necessity_by_association = %{:smart_contract => :optional, proxy_implementations_association() => :optional} @@ -587,7 +587,7 @@ defmodule Explorer.Chain.Address do - `address`: The address if found. - `nil`: If the address is not found. """ - @spec get_by_hash(Hash.Address.t()) :: Chain.Address.t() | nil + @spec get_by_hash(Hash.Address.t()) :: __MODULE__.t() | nil def get_by_hash(address_hash) do case Chain.hash_to_address( address_hash, @@ -699,7 +699,7 @@ defmodule Explorer.Chain.Address do _ -> base_query = - from(a in Address, + from(a in __MODULE__, where: a.fetched_coin_balance > ^0 ) @@ -756,7 +756,7 @@ defmodule Explorer.Chain.Address do """ @spec address_exists?(Hash.Address.t(), [Chain.api?()]) :: boolean() def address_exists?(address_hash, options \\ []) do - query = Address.address_query(address_hash) + query = address_query(address_hash) Chain.select_repo(options).exists?(query) end @@ -783,21 +783,19 @@ defmodule Explorer.Chain.Address do def creation_transaction(_address), do: nil - @doc """ - Creates a query for preloading contract creation transactions. + # Creates a query for preloading contract creation transactions. - This query sorts transactions by: + # This query sorts transactions by: - 1. status (descending with nulls last) - 2. block number (descending with nulls last) - 3. index (descending with nulls last), + # 1. status (descending with nulls last) + # 2. block number (descending with nulls last) + # 3. index (descending with nulls last), - and limits to one result. + # and limits to one result. - ## Returns + # ## Returns - A `Ecto.Query` that can be used to preload the contract creation transaction. - """ + # A `Ecto.Query` that can be used to preload the contract creation transaction. @spec contract_creation_transaction_preload_query() :: Ecto.Query.t() def contract_creation_transaction_preload_query do from( @@ -811,24 +809,21 @@ defmodule Explorer.Chain.Address do ) end - @doc """ - Generates a query to fetch an address with associated bytecode. - - This function constructs an Ecto query that retrieves an address - from the database where the `hash` matches the given `address_hash` - and the `contract_code` is not `nil`. + # Generates a query to fetch an address with associated bytecode. - ## Parameters + # This function constructs an Ecto query that retrieves an address + # from the database where the `hash` matches the given `address_hash` + # and the `contract_code` is not `nil`. - - `address_hash`: The hash of the address to query for. + # ## Parameters - ## Returns + # - `address_hash`: The hash of the address to query for. - An Ecto query that can be executed to fetch the desired address. + # ## Returns - """ + # An Ecto query that can be executed to fetch the desired address. @spec address_with_bytecode_query(Hash.Address.t()) :: Ecto.Query.t() - def address_with_bytecode_query(address_hash) do + defp address_with_bytecode_query(address_hash) do from( address in __MODULE__, where: address.hash == ^address_hash and not is_nil(address.contract_code) @@ -904,7 +899,7 @@ defmodule Explorer.Chain.Address do @spec contract_creation_transaction_association() :: keyword() def contract_creation_transaction_association do [ - contract_creation_transaction: Address.contract_creation_transaction_preload_query() + contract_creation_transaction: contract_creation_transaction_preload_query() ] end @@ -916,7 +911,7 @@ defmodule Explorer.Chain.Address do def contract_creation_transaction_with_from_address_association do [ contract_creation_transaction: { - Address.contract_creation_transaction_preload_query(), + contract_creation_transaction_preload_query(), :from_address } ] @@ -953,7 +948,7 @@ defmodule Explorer.Chain.Address do def contract_creation_internal_transaction_with_from_address_association do [ contract_creation_internal_transaction: { - Address.contract_creation_internal_transaction_preload_query(), + contract_creation_internal_transaction_preload_query(), :from_address } ] @@ -1010,7 +1005,7 @@ defmodule Explorer.Chain.Address do """ @spec find_contract_addresses([Hash.Address.t()], [Chain.necessity_by_association_option() | Chain.api?()]) :: - {:ok, [Address.t()]} | {:error, :not_found} + {:ok, [__MODULE__.t()]} | {:error, :not_found} def find_contract_addresses( hashes, options \\ [] @@ -1041,7 +1036,7 @@ defmodule Explorer.Chain.Address do boolean() ) :: map() | nil - def update_address_result(address_result, options, decoding_from_list?) do + defp update_address_result(address_result, options, decoding_from_list?) do LookUpSmartContractSourcesOnDemand.trigger_fetch(options[:ip], address_result) case address_result do @@ -1084,4 +1079,39 @@ defmodule Explorer.Chain.Address do limit: 1 ) end + + @doc """ + Finds an `t:Explorer.Chain.Address.t/0` that has the provided `t:Explorer.Chain.Address.t/0` `hash` and a contract. + + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association, + then the `t:Explorer.Chain.Address.t/0` will not be included in the list. + + """ + @spec find_contract_address(Hash.Address.t(), [Chain.necessity_by_association_option()]) :: + {:ok, __MODULE__.t()} | {:error, :not_found} + def find_contract_address( + %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, + options \\ [] + ) do + necessity_by_association = + options + |> Keyword.get(:necessity_by_association, %{}) + |> Map.merge(%{ + [smart_contract: :smart_contract_additional_sources] => :optional, + Implementation.proxy_implementations_association() => :optional + }) + + hash + |> address_with_bytecode_query() + |> Chain.join_associations(necessity_by_association) + |> Chain.select_repo(options).one() + |> update_address_result(options, false) + |> case do + nil -> {:error, :not_found} + address -> {:ok, address} + end + end end diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index d1abecf13f5a..1e58ad5fa8c8 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -13,7 +13,6 @@ defmodule Explorer.Chain.Address.TokenBalance do alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Block, Hash, Token} - alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.Cache.BackgroundMigrations @typedoc """ @@ -56,7 +55,7 @@ defmodule Explorer.Chain.Address.TokenBalance do @allowed_fields @optional_fields ++ @required_fields @doc false - def changeset(%TokenBalance{} = token_balance, attrs) do + def changeset(%__MODULE__{} = token_balance, attrs) do token_balance |> cast(attrs, @allowed_fields) |> validate_required(@required_fields) @@ -77,7 +76,7 @@ defmodule Explorer.Chain.Address.TokenBalance do def unfetched_token_balances do if BackgroundMigrations.get_tb_token_type_finished() do from( - tb in TokenBalance, + tb in __MODULE__, where: ((tb.address_hash != ^@burn_address_hash and tb.token_type == "ERC-721") or tb.token_type == "ERC-20" or tb.token_type == "ZRC-2" or @@ -87,7 +86,7 @@ defmodule Explorer.Chain.Address.TokenBalance do ) else from( - tb in TokenBalance, + tb in __MODULE__, join: t in Token, on: tb.token_contract_address_hash == t.contract_address_hash, where: @@ -106,7 +105,7 @@ defmodule Explorer.Chain.Address.TokenBalance do def fetch_token_balance(address_hash, token_contract_address_hash, block_number, nil) do from( - tb in TokenBalance, + tb in __MODULE__, where: tb.address_hash == ^address_hash, where: tb.token_contract_address_hash == ^token_contract_address_hash, where: tb.block_number <= ^block_number, @@ -117,7 +116,7 @@ defmodule Explorer.Chain.Address.TokenBalance do def fetch_token_balance(address_hash, token_contract_address_hash, block_number, token_id) do from( - tb in TokenBalance, + tb in __MODULE__, where: tb.address_hash == ^address_hash, where: tb.token_contract_address_hash == ^token_contract_address_hash, where: tb.token_id == ^token_id, @@ -149,4 +148,27 @@ defmodule Explorer.Chain.Address.TokenBalance do |> where([tb], is_nil(tb.value_fetched_at) or is_nil(tb.value)) |> Repo.delete_all() end + + @doc """ + Returns a stream of all token balances that weren't fetched values. + """ + @spec stream_unfetched_token_balances( + initial :: accumulator, + reducer :: (entry :: __MODULE__.t(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_unfetched_token_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + __MODULE__.unfetched_token_balances() + |> add_token_balances_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + defp add_token_balances_fetcher_limit(query, false), do: query + + defp add_token_balances_fetcher_limit(query, true) do + token_balances_fetcher_limit = Application.get_env(:indexer, :token_balances_fetcher_init_limit) + + limit(query, ^token_balances_fetcher_limit) + end end diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 812e9b28ace0..1e57e80ec2b8 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -217,9 +217,19 @@ defmodule Explorer.Chain.Block do use Explorer.Schema use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - alias Explorer.Chain.{Block, Hash, Transaction, Wei} - alias Explorer.Chain.Block.{EmissionReward, Reward} - alias Explorer.{Helper, Repo} + alias Explorer.Chain.{ + Block, + DenormalizationHelper, + Hash, + PendingBlockOperation, + PendingOperationsHelper, + PendingTransactionOperation, + Transaction, + Wei + } + + alias Explorer.{Chain, Helper, PagingOptions, Repo} + alias Explorer.Chain.Block.{EmissionReward, Reward, SecondDegreeRelation} alias Explorer.Utility.MissingBlockRange @optional_attrs ~w(size refetch_needed total_difficulty difficulty base_fee_per_gas)a @@ -310,6 +320,10 @@ defmodule Explorer.Chain.Block do |> unique_constraint(:hash, name: :blocks_pkey) end + @doc """ + Returns a query that fetches consensus blocks that do not have validator rewards. + """ + @spec blocks_without_reward_query() :: Ecto.Query.t() def blocks_without_reward_query do validator_rewards = from( @@ -755,4 +769,319 @@ defmodule Explorer.Chain.Block do priority_fees: priority_fees } end + + @doc """ + Filters block numbers that do not require refetching. + ## Parameters + - `block_numbers`: A list of block numbers to check. + ## Returns + - A list of block numbers that do not need to be refetched. + """ + @spec filter_non_refetch_needed_block_numbers([integer()]) :: [integer()] + def filter_non_refetch_needed_block_numbers(block_numbers) do + query = + from( + block in __MODULE__, + where: block.number in ^block_numbers, + where: block.consensus == true, + where: block.refetch_needed == false, + select: block.number + ) + + Repo.all(query) + end + + @doc """ + Combined block reward from all the fees. + """ + @spec block_combined_rewards(__MODULE__.t()) :: Wei.t() + def block_combined_rewards(block) do + block.rewards + |> Enum.reduce( + 0, + fn block_reward, acc -> + {:ok, decimal} = Wei.dump(block_reward.reward) + + Decimal.add(decimal, acc) + end + ) + |> Wei.cast() + |> case do + {:ok, value} -> value + _ -> %Wei{value: Decimal.new(0)} + end + end + + @doc """ + Fetches the lowest block number available in the database. + + Queries the database for the minimum block number among blocks marked as consensus + blocks. Returns 0 if no consensus blocks exist or if the query fails. + + ## Returns + - `non_neg_integer`: The lowest block number from consensus blocks, or 0 if none found + """ + @spec fetch_min_block_number() :: non_neg_integer + def fetch_min_block_number do + query = + from(block in __MODULE__, + select: block.number, + where: block.consensus == true, + order_by: [asc: block.number], + limit: 1 + ) + + Repo.one(query) || 0 + rescue + _ -> + 0 + end + + @doc """ + Fetches the highest block number available in the database. + + Queries the database for the maximum block number among blocks marked as consensus + blocks. Returns 0 if no consensus blocks exist or if the query fails. + + ## Returns + - `non_neg_integer`: The highest block number from consensus blocks, or 0 if none found + """ + @spec fetch_max_block_number() :: non_neg_integer + def fetch_max_block_number do + query = + from(block in __MODULE__, + select: block.number, + where: block.consensus == true, + order_by: [desc: block.number], + limit: 1 + ) + + Repo.one(query) || 0 + rescue + _ -> + 0 + end + + @doc """ + Fetches a block by its hash. + """ + @spec fetch_block_by_hash(Hash.Full.t()) :: Block.t() | nil + def fetch_block_by_hash(block_hash) do + Repo.get(__MODULE__, block_hash) + end + + @default_page_size 50 + @default_paging_options %PagingOptions{page_size: @default_page_size} + + @doc """ + Finds all Blocks validated by the address with the given hash. + + ## Options + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the + `t:Explorer.Chain.Block.t/0` will not be included in the page `entries`. + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and + `:key` (a tuple of the lowest/oldest `{block_number}`) and. Results will be the internal + transactions older than the `block_number` that are passed. + + Returns all blocks validated by the address given. + """ + @spec get_blocks_validated_by_address( + [Chain.paging_options() | Chain.necessity_by_association_option()], + Hash.Address.t() + ) :: [Block.t()] + def get_blocks_validated_by_address(options \\ [], address_hash) when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + case paging_options do + %PagingOptions{key: {0}} -> + [] + + _ -> + __MODULE__ + |> Chain.join_associations(necessity_by_association) + |> where(miner_hash: ^address_hash) + |> Chain.page_blocks(paging_options) + |> limit(^paging_options.page_size) + |> order_by(desc: :number) + |> Chain.select_repo(options).all() + end + end + + @doc """ + Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`. + """ + def stream_blocks_without_rewards(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + blocks_without_reward_query() + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Returns a stream of all blocks that are marked as unfetched in `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`. + For each uncle block a `hash` of nephew block and an `index` of the block in it are returned. + + When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be + returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` + `uncle_fetched_at` will be set and it won't be returned anymore. + """ + @spec stream_unfetched_uncles( + initial :: accumulator, + reducer :: (entry :: term(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_unfetched_uncles(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + query = + from(bsdr in SecondDegreeRelation, + where: is_nil(bsdr.uncle_fetched_at) and not is_nil(bsdr.index), + select: [:nephew_hash, :index] + ) + + query + |> Chain.add_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Map `block_number`s to their `t:Explorer.Chain.Block.t/0` `hash` `t:Explorer.Chain.Hash.Full.t/0`. + + Does not include non-consensus blocks. + + iex> block = insert(:block, consensus: false) + iex> Explorer.Chain.Block.block_hash_by_number([block.number]) + %{} + + """ + @spec block_hash_by_number([Block.block_number()]) :: %{Block.block_number() => Hash.Full.t()} + def block_hash_by_number(block_numbers) when is_list(block_numbers) do + query = + from(block in __MODULE__, + where: block.consensus == true and block.number in ^block_numbers, + select: {block.number, block.hash} + ) + + query + |> Repo.all() + |> Enum.into(%{}) + end + + @doc """ + Removes pending operations associated with non-consensus blocks. + """ + @spec remove_nonconsensus_blocks_from_pending_ops([Hash.Full.t()]) :: :ok + def remove_nonconsensus_blocks_from_pending_ops(block_hashes) do + query = + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> + PendingOperationsHelper.block_hash_in_query(block_hashes) + + "transactions" -> + from( + pto in PendingTransactionOperation, + join: t in assoc(pto, :transaction), + where: t.block_hash in ^block_hashes + ) + end + + {_, _} = Repo.delete_all(query) + + :ok + end + + @doc """ + Removes pending operations associated with all non-consensus blocks. + """ + @spec remove_nonconsensus_blocks_from_pending_ops() :: :ok + def remove_nonconsensus_blocks_from_pending_ops do + query = + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> + from( + pbo in PendingBlockOperation, + inner_join: block in Block, + on: block.hash == pbo.block_hash, + where: block.consensus == false + ) + + "transactions" -> + from( + pto in PendingTransactionOperation, + join: t in assoc(pto, :transaction), + where: t.block_consensus == false + ) + end + + {_, _} = Repo.delete_all(query) + + :ok + end + + @spec nonconsensus_block_by_number(Block.block_number(), [Chain.api?()]) :: {:ok, Block.t()} | {:error, :not_found} + def nonconsensus_block_by_number(number, options) do + __MODULE__ + |> where(consensus: false, number: ^number) + |> Chain.select_repo(options).one() + |> case do + nil -> {:error, :not_found} + block -> {:ok, block} + end + end + + @doc """ + The `t:Explorer.Chain.Wei.t/0` paid to the miners of the `t:Explorer.Chain.Block.t/0`s with `hash` + `Explorer.Chain.Hash.Full.t/0` by the signers of the transactions in those blocks to cover the gas fee + (`gas_used * gas_price`). + """ + @spec gas_payment_by_block_hash([Hash.Full.t()]) :: %{Hash.Full.t() => Wei.t()} + def gas_payment_by_block_hash(block_hashes) when is_list(block_hashes) do + query = + if DenormalizationHelper.transactions_denormalization_finished?() do + from( + transaction in Transaction, + where: transaction.block_hash in ^block_hashes and transaction.block_consensus == true, + group_by: transaction.block_hash, + select: {transaction.block_hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} + ) + else + from( + block in __MODULE__, + left_join: transaction in assoc(block, :transactions), + where: block.hash in ^block_hashes and block.consensus == true, + group_by: block.hash, + select: {block.hash, %Wei{value: coalesce(sum(transaction.gas_used * transaction.gas_price), 0)}} + ) + end + + initial_gas_payments = + block_hashes + |> Enum.map(&{&1, %Wei{value: Decimal.new(0)}}) + |> Enum.into(%{}) + + existing_data = + query + |> Repo.all() + |> Enum.into(%{}) + + Map.merge(initial_gas_payments, existing_data) + end + + @doc """ + The `timestamp` of the `t:Explorer.Chain.Block.t/0`s with `hash` `Explorer.Chain.Hash.Full.t/0`. + """ + @spec timestamp_by_block_hash([Hash.Full.t()]) :: %{Hash.Full.t() => non_neg_integer()} + def timestamp_by_block_hash(block_hashes) when is_list(block_hashes) do + query = + from( + block in __MODULE__, + where: block.hash in ^block_hashes and block.consensus == true, + group_by: block.hash, + select: {block.hash, block.timestamp} + ) + + query + |> Repo.all() + |> Enum.into(%{}) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/block_number.ex b/apps/explorer/lib/explorer/chain/cache/block_number.ex index 0c1aeb722d76..e5cd81ef24ef 100644 --- a/apps/explorer/lib/explorer/chain/cache/block_number.ex +++ b/apps/explorer/lib/explorer/chain/cache/block_number.ex @@ -11,7 +11,7 @@ defmodule Explorer.Chain.Cache.BlockNumber do ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval], global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl] - alias Explorer.Chain + alias Explorer.Chain.Block def handle_update(_key, nil, value), do: {:ok, value} @@ -51,8 +51,8 @@ defmodule Explorer.Chain.Cache.BlockNumber do @spec fetch_from_db(:min | :max) :: non_neg_integer() defp fetch_from_db(key) do case key do - :min -> Chain.fetch_min_block_number() - :max -> Chain.fetch_max_block_number() + :min -> Block.fetch_min_block_number() + :max -> Block.fetch_max_block_number() end end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/contracts_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/contracts_count.ex index ca6f08f48d2b..311c3a6dcc7b 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/contracts_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/contracts_count.ex @@ -15,8 +15,11 @@ defmodule Explorer.Chain.Cache.Counters.ContractsCount do enable_consolidation: [:explorer, [__MODULE__, :enable_consolidation]], update_interval_in_milliseconds: [:explorer, [__MODULE__, :update_interval_in_milliseconds]] - alias Explorer.Chain + import Ecto.Query, only: [from: 2] + + alias Explorer.Chain.Address alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Repo @counter_type "contracts_count" @@ -69,7 +72,7 @@ defmodule Explorer.Chain.Cache.Counters.ContractsCount do Consolidates the info by populating the `last_fetched_counters` table with the current database information. """ def consolidate do - all_counter = Chain.count_contracts() + all_counter = count_contracts() params = %{ counter_type: @counter_type, @@ -92,4 +95,15 @@ defmodule Explorer.Chain.Cache.Counters.ContractsCount do `config :explorer, Explorer.Chain.Cache.Counters.ContractsCount, enable_consolidation: false` """ def enable_consolidation?, do: @enable_consolidation + + defp count_contracts do + query = + from(address in Address, + select: address, + where: not is_nil(address.contract_code) + ) + + query + |> Repo.aggregate(:count, timeout: :infinity) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex index 68010009b2e9..128ce81af7d6 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex @@ -4,6 +4,7 @@ defmodule Explorer.Chain.Cache.Counters.Helper do """ alias Explorer.Chain + alias Explorer.Chain.Block alias Explorer.Chain.Cache.Counters.LastFetchedCounter @block_number_threshold_1 10_000 @@ -177,7 +178,7 @@ defmodule Explorer.Chain.Cache.Counters.Helper do @spec ttl(atom, String.t()) :: non_neg_integer() def ttl(module, management_variable) do min_blockchain_block_number = Application.get_env(:indexer, :first_block) - max_block_number = Chain.fetch_max_block_number() + max_block_number = Block.fetch_max_block_number() blocks_amount = max_block_number - min_blockchain_block_number global_ttl_from_var = Application.get_env(:explorer, module)[:global_ttl] diff --git a/apps/explorer/lib/explorer/chain/cache/counters/new_contracts_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/new_contracts_count.ex index 690939e094db..335fc03a119e 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/new_contracts_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/new_contracts_count.ex @@ -15,8 +15,11 @@ defmodule Explorer.Chain.Cache.Counters.NewContractsCount do enable_consolidation: [:explorer, [__MODULE__, :enable_consolidation]], update_interval_in_milliseconds: [:explorer, [__MODULE__, :update_interval_in_milliseconds]] - alias Explorer.Chain + import Ecto.Query, only: [from: 2] + alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.Transaction + alias Explorer.Repo @counter_type "new_contracts_count" @@ -70,7 +73,7 @@ defmodule Explorer.Chain.Cache.Counters.NewContractsCount do Consolidates the info by populating the `last_fetched_counters` table with the current database information. """ def consolidate do - new_all_counter = Chain.count_new_contracts() + new_all_counter = count_new_contracts() params = %{ counter_type: @counter_type, @@ -93,4 +96,20 @@ defmodule Explorer.Chain.Cache.Counters.NewContractsCount do `config :explorer, Explorer.Chain.Cache.Counters.NewContractsCount, enable_consolidation: false` """ def enable_consolidation?, do: @enable_consolidation + + defp count_new_contracts do + query = + from(transaction in Transaction, + select: transaction, + where: + transaction.status == ^:ok and + fragment( + "NOW() - ? at time zone 'UTC' <= interval '24 hours'", + transaction.created_contract_code_indexed_at + ) + ) + + query + |> Repo.aggregate(:count, timeout: :infinity) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/new_verified_contracts_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/new_verified_contracts_count.ex index e4313d6fac32..77525b6ff65a 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/new_verified_contracts_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/new_verified_contracts_count.ex @@ -15,8 +15,11 @@ defmodule Explorer.Chain.Cache.Counters.NewVerifiedContractsCount do enable_consolidation: [:explorer, [__MODULE__, :enable_consolidation]], update_interval_in_milliseconds: [:explorer, [__MODULE__, :update_interval_in_milliseconds]] - alias Explorer.Chain alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.SmartContract + alias Explorer.Repo + + import Ecto.Query, only: [from: 2] @counter_type "new_verified_contracts_count" @@ -70,7 +73,7 @@ defmodule Explorer.Chain.Cache.Counters.NewVerifiedContractsCount do Consolidates the info by populating the `last_fetched_counters` table with the current database information. """ def consolidate do - new_verified_counter = Chain.count_new_verified_contracts() + new_verified_counter = count_new_verified_contracts() params = %{ counter_type: @counter_type, @@ -93,4 +96,14 @@ defmodule Explorer.Chain.Cache.Counters.NewVerifiedContractsCount do `config :explorer, Explorer.Chain.Cache.Counters.NewVerifiedContractsCount, enable_consolidation: false` """ def enable_consolidation?, do: @enable_consolidation + + defp count_new_verified_contracts do + query = + from(contract in SmartContract, + where: fragment("NOW() - ? <= interval '24 hours'", contract.inserted_at) + ) + + query + |> Repo.aggregate(:count, timeout: :infinity) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/verified_contracts_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/verified_contracts_count.ex index 0989d21286fe..5620e51988b6 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/verified_contracts_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/verified_contracts_count.ex @@ -15,8 +15,9 @@ defmodule Explorer.Chain.Cache.Counters.VerifiedContractsCount do enable_consolidation: [:explorer, [__MODULE__, :enable_consolidation]], update_interval_in_milliseconds: [:explorer, [__MODULE__, :update_interval_in_milliseconds]] - alias Explorer.Chain alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.SmartContract + alias Explorer.Repo @counter_type "verified_contracts_count" @@ -69,7 +70,7 @@ defmodule Explorer.Chain.Cache.Counters.VerifiedContractsCount do Consolidates the info by populating the `last_fetched_counters` table with the current database information. """ def consolidate do - verified_counter = Chain.count_verified_contracts() + verified_counter = count_verified_contracts() params = %{ counter_type: @counter_type, @@ -92,4 +93,8 @@ defmodule Explorer.Chain.Cache.Counters.VerifiedContractsCount do `config :explorer, Explorer.Chain.Cache.Counters.VerifiedContractsCount, enable_consolidation: false` """ def enable_consolidation?, do: @enable_consolidation + + defp count_verified_contracts do + Repo.aggregate(SmartContract, :count, timeout: :infinity) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/withdrawals_sum.ex b/apps/explorer/lib/explorer/chain/cache/counters/withdrawals_sum.ex index d3d3443e26ee..d8878e63e168 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/withdrawals_sum.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/withdrawals_sum.ex @@ -11,9 +11,9 @@ defmodule Explorer.Chain.Cache.Counters.WithdrawalsSum do enable_consolidation: [:explorer, [__MODULE__, :enable_consolidation]], update_interval_in_milliseconds: [:explorer, [__MODULE__, :update_interval_in_milliseconds]] - alias Explorer.Chain alias Explorer.Chain.Cache.Counters.LastFetchedCounter - alias Explorer.Chain.Wei + alias Explorer.Chain.{Wei, Withdrawal} + alias Explorer.Repo @counter_type "withdrawals_sum" @@ -66,7 +66,7 @@ defmodule Explorer.Chain.Cache.Counters.WithdrawalsSum do Consolidates the info by populating the `last_fetched_counters` table with the current database information. """ def consolidate do - withdrawals_sum = Chain.sum_withdrawals() + withdrawals_sum = sum_withdrawals() params = %{ counter_type: @counter_type, @@ -89,4 +89,8 @@ defmodule Explorer.Chain.Cache.Counters.WithdrawalsSum do `config :explorer, #{__MODULE__}, enable_consolidation: false` """ def enable_consolidation?, do: @enable_consolidation + + defp sum_withdrawals do + Repo.aggregate(Withdrawal, :sum, :amount, timeout: :infinity) + end end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index d1ec988c8560..097ab74d42fd 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -3,6 +3,7 @@ defmodule Explorer.Chain.InternalTransaction do use Explorer.Schema + alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral @@ -11,6 +12,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.Chain.InternalTransaction.{CallType, Type} alias Explorer.Migrator.DeleteZeroValueInternalTransactions + import EthereumJSONRPC, only: [fetch_block_internal_transactions: 2] import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] @typep paging_options :: {:paging_options, PagingOptions.t()} @@ -1083,4 +1085,152 @@ defmodule Explorer.Chain.InternalTransaction do (internal_transaction.type == :call and internal_transaction.value > ^0) or internal_transaction.type != :call ) end + + @doc """ + Fetches and formats the first internal transaction trace for the given transaction parameters from the EthereumJSONRPC. + + This function retrieves the first trace by delegating to + EthereumJSONRPC.fetch_first_trace and then formats the result into a + structure suitable for insertion into the database, including type + conversions and block index calculation. + + ## Parameters + - `transactions_params`: List of transaction parameter maps containing + block_hash, block_number, hash_data, and transaction_index + - `json_rpc_named_arguments`: Named arguments for the JSON RPC call + + ## Returns + - `{:ok, [formatted_trace]}` if the trace is successfully fetched and + formatted + - `{:error, reason}` if there's an error fetching the trace + - `:ignore` if the trace should be ignored + """ + @spec fetch_first_trace(list(map()), keyword()) :: {:ok, [map()]} | {:error, term()} | :ignore + def fetch_first_trace(transactions_params, json_rpc_named_arguments) do + case EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) do + {:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} -> + format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) + + {:error, error} -> + {:error, error} + + :ignore -> + :ignore + end + end + + defp format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) do + {:ok, to_address_hash} = + if Map.has_key?(first_trace, :to_address_hash) do + Chain.string_to_address_hash(first_trace.to_address_hash) + else + {:ok, nil} + end + + {:ok, from_address_hash} = Chain.string_to_address_hash(first_trace.from_address_hash) + + {:ok, created_contract_address_hash} = + if Map.has_key?(first_trace, :created_contract_address_hash) do + Chain.string_to_address_hash(first_trace.created_contract_address_hash) + else + {:ok, nil} + end + + {:ok, transaction_hash} = Chain.string_to_full_hash(first_trace.transaction_hash) + + {:ok, call_type} = + if Map.has_key?(first_trace, :call_type) do + CallType.load(first_trace.call_type) + else + {:ok, nil} + end + + {:ok, type} = Type.load(first_trace.type) + + {:ok, input} = + if Map.has_key?(first_trace, :input) do + Data.cast(first_trace.input) + else + {:ok, nil} + end + + {:ok, output} = + if Map.has_key?(first_trace, :output) do + Data.cast(first_trace.output) + else + {:ok, nil} + end + + {:ok, created_contract_code} = + if Map.has_key?(first_trace, :created_contract_code) do + Data.cast(first_trace.created_contract_code) + else + {:ok, nil} + end + + {:ok, init} = + if Map.has_key?(first_trace, :init) do + Data.cast(first_trace.init) + else + {:ok, nil} + end + + block_index = + get_block_index(%{ + transaction_index: first_trace.transaction_index, + transaction_hash: first_trace.transaction_hash, + block_number: first_trace.block_number, + json_rpc_named_arguments: json_rpc_named_arguments + }) + + value = %Wei{value: Decimal.new(first_trace.value)} + + first_trace_formatted = + first_trace + |> Map.merge(%{ + block_index: block_index, + block_hash: block_hash, + call_type: call_type, + to_address_hash: to_address_hash, + created_contract_address_hash: created_contract_address_hash, + from_address_hash: from_address_hash, + input: input, + output: output, + created_contract_code: created_contract_code, + init: init, + transaction_hash: transaction_hash, + type: type, + value: value + }) + + {:ok, [first_trace_formatted]} + end + + defp get_block_index(%{ + transaction_index: transaction_index, + transaction_hash: transaction_hash, + block_number: block_number, + json_rpc_named_arguments: json_rpc_named_arguments + }) do + if transaction_index == 0 do + 0 + else + filtered_block_numbers = RangesHelper.filter_traceable_block_numbers([block_number]) + {:ok, traces} = fetch_block_internal_transactions(filtered_block_numbers, json_rpc_named_arguments) + + sorted_traces = + traces + |> Enum.sort_by(&{&1.transaction_index, &1.index}) + |> Enum.with_index() + + {_, block_index} = + sorted_traces + |> Enum.find({nil, -1}, fn {trace, _} -> + trace.transaction_index == transaction_index && + trace.transaction_hash == transaction_hash + end) + + block_index + end + end end diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 6f96932ffe7c..2e056b70ba39 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -79,6 +79,7 @@ defmodule Explorer.Chain.Token do * [ERC-404](https://github.com/Pandora-Labs-Org/erc404) * [ZRC-2](https://github.com/Zilliqa/ZRC/blob/main/zrcs/zrc-2.md) """ + require Logger use Explorer.Schema @@ -90,6 +91,7 @@ defmodule Explorer.Chain.Token do alias Explorer.{Chain, SortingHelper} alias Explorer.Chain.{Address, BridgedToken, Hash, Search, Token} alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Chain.Cache.Counters.{TokenHoldersCount, TokenTransfersCount} alias Explorer.Chain.Import.Runner alias Explorer.Helper, as: ExplorerHelper alias Explorer.Repo @@ -213,7 +215,7 @@ defmodule Explorer.Chain.Token do """ @spec stream_cataloged_tokens( initial :: accumulator, - reducer :: (entry :: Token.t(), accumulator -> accumulator), + reducer :: (entry :: __MODULE__.t(), accumulator -> accumulator), some_time_ago_updated :: integer(), limited? :: boolean() ) :: {:ok, accumulator} @@ -221,7 +223,7 @@ defmodule Explorer.Chain.Token do def stream_cataloged_tokens(initial, reducer, some_time_ago_updated \\ 2880, limited? \\ false) when is_function(reducer, 2) do some_time_ago_updated - |> Token.cataloged_tokens() + |> cataloged_tokens() |> Chain.add_fetcher_limit(limited?) |> order_by(asc_nulls_first: :metadata_updated_at) |> Repo.stream_reduce(initial, reducer) @@ -233,9 +235,10 @@ defmodule Explorer.Chain.Token do As part of updating token, an additional record is inserted for naming the address for reference if a name is provided for a token. """ - @spec update(Token.t(), map(), boolean(), :base | :metadata_update) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()} + @spec update(__MODULE__.t(), map(), boolean(), :base | :metadata_update) :: + {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} def update( - %Token{contract_address_hash: address_hash} = token, + %__MODULE__{contract_address_hash: address_hash} = token, params \\ %{}, info_from_admin_panel? \\ false, operation_type \\ :base @@ -251,7 +254,7 @@ defmodule Explorer.Chain.Token do token_changeset = token - |> Token.changeset( + |> __MODULE__.changeset( filtered_params |> Map.put(:updated_at, DateTime.utc_now()) ) @@ -354,7 +357,7 @@ defmodule Explorer.Chain.Token do | {:token_type, [String.t()]} | {:necessity_by_association, map()} | Chain.show_scam_tokens?() - ]) :: [Token.t()] + ]) :: [__MODULE__.t()] def list_top(filter, options \\ []) do paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) token_type = Keyword.get(options, :token_type, nil) @@ -406,7 +409,7 @@ defmodule Explorer.Chain.Token do Gets tokens with given contract address hashes. """ @spec get_by_contract_address_hashes([Hash.Address.t()], [Chain.api?() | Chain.necessity_by_association_option()]) :: - [Token.t()] + [__MODULE__.t()] def get_by_contract_address_hashes(hashes, options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) @@ -535,4 +538,154 @@ defmodule Explorer.Chain.Token do _ -> false end end + + @doc """ + Fetches token counters (transfers count and holders count) for a given token address. + + This function spawns two async tasks to fetch the token transfers count and + token holders count concurrently. If a task times out or exits, it falls back + to fetching the cached value. + + ## Parameters + - `address_hash`: The contract address hash of the token + - `timeout`: The timeout in milliseconds for the async tasks + + ## Returns + - A tuple `{transfers_count, holders_count}` where each value is an integer or nil + """ + @spec fetch_token_counters(Hash.Address.t(), timeout()) :: {integer() | nil, integer() | nil} + def fetch_token_counters(address_hash, timeout) do + total_token_transfers_task = + Task.async(fn -> + TokenTransfersCount.fetch(address_hash) + end) + + total_token_holders_task = + Task.async(fn -> + TokenHoldersCount.fetch(address_hash) + end) + + [total_token_transfers_task, total_token_holders_task] + |> Task.yield_many(timeout) + |> Enum.map(fn {task, res} -> + case res do + {:ok, result} -> + result + + {:exit, reason} -> + Logger.warning("Query fetching token counters terminated: #{inspect(reason)}") + + fallback_cached_value_based_on_async_task_pid( + task.pid, + total_token_transfers_task.pid, + total_token_holders_task.pid, + address_hash + ) + + nil -> + Logger.warning("Query fetching token counters timed out.") + + fallback_cached_value_based_on_async_task_pid( + task.pid, + total_token_transfers_task.pid, + total_token_holders_task.pid, + address_hash + ) + end + end) + |> List.to_tuple() + end + + defp fallback_cached_value_based_on_async_task_pid( + task_pid, + total_token_transfers_task_pid, + total_token_holders_task_pid, + address_hash + ) do + case task_pid do + ^total_token_transfers_task_pid -> + TokenTransfersCount.fetch_count_from_cache(address_hash) + + ^total_token_holders_task_pid -> + TokenHoldersCount.fetch_count_from_cache(address_hash) + end + end + + @doc """ + Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists. + + Returns `:ok` if found + + iex> address = insert(:address) + iex> insert(:token, contract_address: address) + iex> Explorer.Chain.Token.check_token_exists(address.hash) + :ok + + Returns `:not_found` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + iex> Explorer.Chain.Token.check_token_exists(hash) + :not_found + """ + @spec check_token_exists(Hash.Address.t()) :: :ok | :not_found + def check_token_exists(hash) do + hash + |> token_exists?() + |> Chain.boolean_to_check_result() + end + + # Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists. + + # Returns `true` if found + + # iex> address = insert(:address) + # iex> insert(:token, contract_address: address) + # iex> Explorer.Chain.token_exists?(address.hash) + # true + + # Returns `false` if not found + + # iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed") + # iex> Explorer.Chain.token_exists?(hash) + # false + @spec token_exists?(Hash.Address.t()) :: boolean() + defp token_exists?(hash) do + query = + from( + token in Token, + where: token.contract_address_hash == ^hash + ) + + Repo.exists?(query) + end + + @doc """ + Gets the token type for a given contract address hash. + """ + @spec get_token_type(Hash.Address.t()) :: String.t() | nil + def get_token_type(hash) do + query = + from( + token in __MODULE__, + where: token.contract_address_hash == ^hash, + select: token.type + ) + + Repo.one(query) + end + + @doc """ + Gets the token types for a list of contract address hashes. + """ + @spec get_token_types([Hash.Address.t()]) :: [{Hash.Address.t(), String.t()}] + def get_token_types(hashes) do + query = + from( + token in __MODULE__, + where: token.contract_address_hash in ^hashes, + select: {token.contract_address_hash, token.type} + ) + + Repo.all(query) + end end diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 73df78681e65..3573d31879a7 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -337,6 +337,8 @@ defmodule Explorer.Chain.Transaction do alias ABI.FunctionSelector alias Ecto.Association.NotLoaded alias Ecto.Changeset + alias EthereumJSONRPC + alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction alias Explorer.{Chain, Helper, PagingOptions, Repo, SortingHelper} alias Explorer.Chain.{ @@ -347,15 +349,17 @@ defmodule Explorer.Chain.Transaction do Data, DenormalizationHelper, Hash, + InternalTransaction, MethodIdentifier, SmartContract.Proxy, TokenTransfer, - Transaction, Wei } alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral + alias Explorer.Chain.Cache.Transactions + alias Explorer.Chain.SmartContract.Proxy.Models.Implementation alias Explorer.SmartContract.SigProviderInterface @@ -796,7 +800,7 @@ defmodule Explorer.Chain.Transaction do _ -> decoded_input_data( - %Transaction{ + %__MODULE__{ to_address: smart_contract, hash: hash, input: %Data{bytes: binary_revert_reason} @@ -812,7 +816,7 @@ defmodule Explorer.Chain.Transaction do # Because there is no contract association, we know the contract was not verified @spec decoded_input_data( - NotLoaded.t() | Transaction.t(), + NotLoaded.t() | __MODULE__.t(), boolean(), [Chain.api?()], methods_map, @@ -1366,7 +1370,7 @@ defmodule Explorer.Chain.Transaction do defp transactions_with_token_transfers_query(address_hash, token_hash) do from( - t in Transaction, + t in __MODULE__, inner_join: tt in TokenTransfer, on: t.hash == tt.transaction_hash, where: tt.token_contract_address_hash == ^token_hash, @@ -1388,7 +1392,7 @@ defmodule Explorer.Chain.Transaction do defp transactions_with_token_transfers_query_direction(:from, address_hash) do from( - t in Transaction, + t in __MODULE__, inner_join: tt in TokenTransfer, on: t.hash == tt.transaction_hash, where: tt.from_address_hash == ^address_hash, @@ -1398,7 +1402,7 @@ defmodule Explorer.Chain.Transaction do defp transactions_with_token_transfers_query_direction(:to, address_hash) do from( - t in Transaction, + t in __MODULE__, inner_join: tt in TokenTransfer, on: t.hash == tt.transaction_hash, where: tt.to_address_hash == ^address_hash, @@ -1408,7 +1412,7 @@ defmodule Explorer.Chain.Transaction do defp transactions_with_token_transfers_query_direction(_, address_hash) do from( - t in Transaction, + t in __MODULE__, inner_join: tt in TokenTransfer, on: t.hash == tt.transaction_hash, where: tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash, @@ -1421,7 +1425,7 @@ defmodule Explorer.Chain.Transaction do """ def transactions_with_block_number(block_number) do from( - t in Transaction, + t in __MODULE__, where: t.block_number == ^block_number ) end @@ -1432,7 +1436,7 @@ defmodule Explorer.Chain.Transaction do @spec transactions_for_block_numbers([non_neg_integer()]) :: Ecto.Query.t() def transactions_for_block_numbers(block_numbers) do from( - t in Transaction, + t in __MODULE__, where: t.block_number in ^block_numbers ) end @@ -1443,7 +1447,7 @@ defmodule Explorer.Chain.Transaction do @spec by_hashes_query([Hash.t()]) :: Ecto.Query.t() def by_hashes_query(hashes) do from( - t in Transaction, + t in __MODULE__, where: t.hash in ^hashes ) end @@ -1457,7 +1461,7 @@ defmodule Explorer.Chain.Transaction do """ def last_nonce_by_address_query(address_hash) do from( - t in Transaction, + t in __MODULE__, select: t.nonce, where: t.from_address_hash == ^address_hash, order_by: [desc: :block_number], @@ -1501,7 +1505,7 @@ defmodule Explorer.Chain.Transaction do @doc """ Returns true if the transaction is a Rootstock REMASC transaction. """ - @spec rootstock_remasc_transaction?(Explorer.Chain.Transaction.t()) :: boolean + @spec rootstock_remasc_transaction?(__MODULE__.t()) :: boolean def rootstock_remasc_transaction?(%__MODULE__{to_address_hash: to_address_hash}) do case Hash.Address.cast(Application.get_env(:explorer, __MODULE__)[:rootstock_remasc_address]) do {:ok, address} -> address == to_address_hash @@ -1512,7 +1516,7 @@ defmodule Explorer.Chain.Transaction do @doc """ Returns true if the transaction is a Rootstock bridge transaction. """ - @spec rootstock_bridge_transaction?(Explorer.Chain.Transaction.t()) :: boolean + @spec rootstock_bridge_transaction?(__MODULE__.t()) :: boolean def rootstock_bridge_transaction?(%__MODULE__{to_address_hash: to_address_hash}) do case Hash.Address.cast(Application.get_env(:explorer, __MODULE__)[:rootstock_bridge_address]) do {:ok, address} -> address == to_address_hash @@ -2005,7 +2009,7 @@ defmodule Explorer.Chain.Transaction do @doc """ Returns next page params based on the provided transaction. """ - @spec address_transactions_next_page_params(Explorer.Chain.Transaction.t()) :: %{ + @spec address_transactions_next_page_params(__MODULE__.t()) :: %{ required(atom()) => Decimal.t() | Wei.t() | non_neg_integer | DateTime.t() | Hash.t() } def address_transactions_next_page_params( @@ -2051,25 +2055,25 @@ defmodule Explorer.Chain.Transaction do {:actual, Decimal.new(4)} """ - @spec fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil} - def fee(%Transaction{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} + @spec fee(__MODULE__.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil} + def fee(%__MODULE__{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} - def fee(%Transaction{gas: gas, gas_price: _gas_price, gas_used: nil} = transaction, unit) do + def fee(%__MODULE__{gas: gas, gas_price: _gas_price, gas_used: nil} = transaction, unit) do {:maximum, fee_calc(transaction, gas, unit)} end if @chain_type == :optimism do - def fee(%Transaction{gas_price: nil, gas_used: _gas_used}, _unit) do + def fee(%__MODULE__{gas_price: nil, gas_used: _gas_used}, _unit) do {:actual, nil} end else - def fee(%Transaction{gas_price: nil, gas_used: gas_used} = transaction, unit) do + def fee(%__MODULE__{gas_price: nil, gas_used: gas_used} = transaction, unit) do gas_price = effective_gas_price(transaction) {:actual, gas_price && l2_fee_calc(gas_price, gas_used, unit)} end end - def fee(%Transaction{gas_price: _gas_price, gas_used: gas_used} = transaction, unit) do + def fee(%__MODULE__{gas_price: _gas_price, gas_used: gas_used} = transaction, unit) do {:actual, fee_calc(transaction, gas_used, unit)} end @@ -2084,7 +2088,7 @@ defmodule Explorer.Chain.Transaction do # # ## Returns # - The calculated total fee. - @spec fee_calc(Transaction.t(), Decimal.t(), :ether | :gwei | :wei) :: Decimal.t() + @spec fee_calc(__MODULE__.t(), Decimal.t(), :ether | :gwei | :wei) :: Decimal.t() defp fee_calc(transaction, gas_used, unit) do l1_fee = case Map.get(transaction, :l1_fee) do @@ -2119,9 +2123,9 @@ defmodule Explorer.Chain.Transaction do ## Returns - The calculated operator fee for the given transaction. """ - @spec operator_fee(Transaction.t()) :: Decimal.t() + @spec operator_fee(__MODULE__.t()) :: Decimal.t() def operator_fee( - %Transaction{ + %__MODULE__{ gas: gas, gas_used: gas_used } = transaction @@ -2155,19 +2159,19 @@ defmodule Explorer.Chain.Transaction do The execution fee a `transaction` paid for the `t:Explorer.Chain.Transaction.t/0` `gas`. Doesn't include L1 fee. See the description for the `fee` function for parameters and return values. """ - @spec l2_fee(Transaction.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil} - def l2_fee(%Transaction{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} + @spec l2_fee(__MODULE__.t(), :ether | :gwei | :wei) :: {:maximum, Decimal.t() | nil} | {:actual, Decimal.t() | nil} + def l2_fee(%__MODULE__{gas: _gas, gas_price: nil, gas_used: nil}, _unit), do: {:maximum, nil} - def l2_fee(%Transaction{gas: gas, gas_price: gas_price, gas_used: nil}, unit) do + def l2_fee(%__MODULE__{gas: gas, gas_price: gas_price, gas_used: nil}, unit) do {:maximum, l2_fee_calc(gas_price, gas, unit)} end - def l2_fee(%Transaction{gas_price: nil, gas_used: gas_used} = transaction, unit) do + def l2_fee(%__MODULE__{gas_price: nil, gas_used: gas_used} = transaction, unit) do gas_price = effective_gas_price(transaction) {:actual, gas_price && l2_fee_calc(gas_price, gas_used, unit)} end - def l2_fee(%Transaction{gas_price: gas_price, gas_used: gas_used}, unit) do + def l2_fee(%__MODULE__{gas_price: gas_price, gas_used: gas_used}, unit) do {:actual, l2_fee_calc(gas_price, gas_used, unit)} end @@ -2180,20 +2184,20 @@ defmodule Explorer.Chain.Transaction do @doc """ Wrapper around `effective_gas_price/2` """ - @spec effective_gas_price(Transaction.t()) :: Wei.t() | nil - def effective_gas_price(%Transaction{} = transaction), do: effective_gas_price(transaction, transaction.block) + @spec effective_gas_price(__MODULE__.t()) :: Wei.t() | nil + def effective_gas_price(%__MODULE__{} = transaction), do: effective_gas_price(transaction, transaction.block) @doc """ Calculates effective gas price for transaction with type 2 (EIP-1559) `effective_gas_price = priority_fee_per_gas + block.base_fee_per_gas` """ - @spec effective_gas_price(Transaction.t(), Block.t()) :: Wei.t() | nil + @spec effective_gas_price(__MODULE__.t(), Block.t()) :: Wei.t() | nil - def effective_gas_price(%Transaction{}, %NotLoaded{}), do: nil - def effective_gas_price(%Transaction{}, nil), do: nil + def effective_gas_price(%__MODULE__{}, %NotLoaded{}), do: nil + def effective_gas_price(%__MODULE__{}, nil), do: nil - def effective_gas_price(%Transaction{} = transaction, block) do + def effective_gas_price(%__MODULE__{} = transaction, block) do base_fee_per_gas = block.base_fee_per_gas max_priority_fee_per_gas = transaction.max_priority_fee_per_gas max_fee_per_gas = transaction.max_fee_per_gas @@ -2246,7 +2250,7 @@ defmodule Explorer.Chain.Transaction do def transaction_count_for_block_range(from..to//_) do Repo.replica().aggregate( from( - t in Transaction, + t in __MODULE__, where: t.block_number >= ^from and t.block_number <= ^to and t.block_consensus == true ), :count, @@ -2259,7 +2263,7 @@ defmodule Explorer.Chain.Transaction do Where - `decoded_input_data` is list of results: either `{:ok, _identifier, _text, _mapping}` or `nil` """ - @spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: [nil | {:ok, String.t(), String.t(), map()}] + @spec decode_transactions([__MODULE__.t()], boolean(), Keyword.t()) :: [nil | {:ok, String.t(), String.t(), map()}] def decode_transactions(transactions, skip_sig_provider?, opts) do smart_contract_full_abi_map = combine_smart_contract_full_abi_map(transactions) @@ -2334,8 +2338,8 @@ defmodule Explorer.Chain.Transaction do unique_to_address_hashes = transactions |> Enum.flat_map(fn - %Transaction{to_address: %Address{hash: hash}} -> [hash] - %Transaction{created_contract_address: %Address{hash: hash}} -> [hash] + %__MODULE__{to_address: %Address{hash: hash}} -> [hash] + %__MODULE__{created_contract_address: %Address{hash: hash}} -> [hash] _ -> [] end) |> Enum.uniq() @@ -2419,4 +2423,691 @@ defmodule Explorer.Chain.Transaction do do: "0x" <> Base.encode16(method_id, case: :lower) def method_id(_transaction), do: "0x" + + @doc """ + Fetches the revert reason of a transaction. + """ + @spec fetch_transaction_revert_reason(__MODULE__.t()) :: String.t() + def fetch_transaction_revert_reason(transaction) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + hash_string = to_string(transaction.hash) + + response = + InternalTransaction.fetch_first_trace( + [ + %{ + block_hash: transaction.block_hash, + block_number: transaction.block_number, + hash_data: hash_string, + transaction_index: transaction.index + } + ], + json_rpc_named_arguments + ) + + revert_reason = + case response do + {:ok, first_trace_params} -> + first_trace_params |> Enum.at(0) |> Map.get(:output, %Data{bytes: <<>>}) |> to_string() + + {:error, reason} -> + Logger.error(fn -> + ["Error while fetching first trace for transaction: #{hash_string} error reason: ", inspect(reason)] + end) + + fetch_transaction_revert_reason_using_call(transaction) + + :ignore -> + fetch_transaction_revert_reason_using_call(transaction) + end + + if !is_nil(revert_reason) do + transaction + |> Changeset.change(%{revert_reason: revert_reason}) + |> Repo.update() + end + + revert_reason + end + + defp fetch_transaction_revert_reason_using_call(%__MODULE__{ + block_number: block_number, + to_address_hash: to_address_hash, + from_address_hash: from_address_hash, + input: data, + gas: gas, + gas_price: gas_price, + value: value + }) do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + req = + EthereumJSONRPCTransaction.eth_call_request( + 0, + block_number, + data, + to_address_hash, + from_address_hash, + Wei.hex_format(gas), + Wei.hex_format(gas_price), + Wei.hex_format(value) + ) + + case EthereumJSONRPC.json_rpc(req, json_rpc_named_arguments) do + {:error, error} -> + Chain.parse_revert_reason_from_error(error) + + _ -> + nil + end + end + + @default_page_size 50 + @default_paging_options %PagingOptions{page_size: @default_page_size} + @limit_showing_transactions 10_000 + + @doc """ + Returns the maximum number of transactions that can be shown in the UI. + """ + @spec limit_showing_transactions :: non_neg_integer() + def limit_showing_transactions, do: @limit_showing_transactions + + @doc """ + Returns the paged list of collated transactions that occurred recently from newest to oldest using `block_number` + and `index`. + + iex> newest_first_transactions = 50 |> insert_list(:transaction) |> with_block() |> Enum.reverse() + iex> oldest_seen = Enum.at(newest_first_transactions, 9) + iex> paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} + iex> recent_collated_transactions = Explorer.Chain.Transaction.recent_collated_transactions(true, paging_options: paging_options) + iex> length(recent_collated_transactions) + 10 + iex> hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash + true + + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, + then the `t:Explorer.Chain.Transaction.t/0` will not be included in the list. + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and + `:key` (a tuple of the lowest/oldest `{block_number, index}`) and. Results will be the transactions older than + the `block_number` and `index` that are passed. + + """ + @spec recent_collated_transactions(true | false, [ + Chain.paging_options() | Chain.necessity_by_association_option() | Chain.api?() + ]) :: [ + __MODULE__.t() + ] + def recent_collated_transactions(old_ui?, options \\ []) + when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + method_id_filter = Keyword.get(options, :method) + type_filter = Keyword.get(options, :type) + + fetch_recent_collated_transactions( + old_ui?, + paging_options, + necessity_by_association, + method_id_filter, + type_filter, + options + ) + end + + defp fetch_recent_collated_transactions( + old_ui?, + paging_options, + necessity_by_association, + method_id_filter, + type_filter, + options + ) do + case paging_options do + %PagingOptions{key: {0, 0}, is_index_in_asc_order: false} -> + [] + + _ -> + paging_options + |> fetch_transactions() + |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) + |> apply_filter_by_method_id_to_transactions(method_id_filter) + |> apply_filter_by_type_to_transactions(type_filter) + |> Chain.join_associations(necessity_by_association) + |> put_has_token_transfers_to_transaction(old_ui?) + |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() + |> Chain.select_repo(options).all() + end + end + + @doc """ + Applies a filter to the query based on the method ID of the transaction. + """ + @spec apply_filter_by_method_id_to_transactions(query :: Ecto.Query.t(), filter :: list() | nil) :: Ecto.Query.t() + def apply_filter_by_method_id_to_transactions(query, nil), do: query + + def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do + method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1) + + if method_ids != [] do + query + |> where([transaction], fragment("SUBSTRING(? FOR 4)", transaction.input) in ^method_ids) + else + query + end + end + + def apply_filter_by_method_id_to_transactions(query, filter), + do: apply_filter_by_method_id_to_transactions(query, [filter]) + + @method_name_to_id_map %{ + "approve" => "095ea7b3", + "transfer" => "a9059cbb", + "multicall" => "5ae401dc", + "mint" => "40c10f19", + "commit" => "f14fcbc8" + } + + defp map_name_or_method_id_to_method_id(string) when is_binary(string) do + if id = @method_name_to_id_map[string] do + decode_method_id(id) + else + trimmed = + string + |> String.replace("0x", "", global: false) + + decode_method_id(trimmed) + end + end + + defp decode_method_id(method_id) when is_binary(method_id) do + case String.length(method_id) == 8 && Base.decode16(method_id, case: :mixed) do + {:ok, bytes} -> + [bytes] + + _ -> + [] + end + end + + @doc """ + Applies a filter to the query based on the type of transaction. + """ + @spec apply_filter_by_type_to_transactions(query :: Ecto.Query.t(), filter :: list()) :: Ecto.Query.t() + def apply_filter_by_type_to_transactions(query, [_ | _] = filter) do + {dynamic, modified_query} = apply_filter_by_type_to_transactions_inner(filter, query) + + modified_query + |> where(^dynamic) + end + + def apply_filter_by_type_to_transactions(query, _filter), do: query + + defp apply_filter_by_type_to_transactions_inner(dynamic \\ dynamic(false), filter, query) + + defp apply_filter_by_type_to_transactions_inner(dynamic, [type | remain], query) do + case type do + :contract_call -> + dynamic + |> filter_contract_call_dynamic() + |> apply_filter_by_type_to_transactions_inner( + remain, + join(query, :inner, [transaction], address in assoc(transaction, :to_address), as: :to_address) + ) + + :contract_creation -> + dynamic + |> filter_contract_creation_dynamic() + |> apply_filter_by_type_to_transactions_inner(remain, query) + + :coin_transfer -> + dynamic + |> filter_transaction_dynamic() + |> apply_filter_by_type_to_transactions_inner(remain, query) + + :token_transfer -> + dynamic + |> filter_token_transfer_dynamic() + |> apply_filter_by_type_to_transactions_inner(remain, query) + + :token_creation -> + dynamic + |> filter_token_creation_dynamic() + |> apply_filter_by_type_to_transactions_inner( + remain, + join(query, :inner, [transaction], token in Token, + on: token.contract_address_hash == transaction.created_contract_address_hash, + as: :created_token + ) + ) + + :blob_transaction -> + dynamic + |> filter_blob_transaction_dynamic() + |> apply_filter_by_type_to_transactions_inner(remain, query) + end + end + + defp apply_filter_by_type_to_transactions_inner(dynamic_query, _, query), do: {dynamic_query, query} + + defp filter_contract_creation_dynamic(dynamic) do + dynamic([transaction], ^dynamic or is_nil(transaction.to_address_hash)) + end + + defp filter_transaction_dynamic(dynamic) do + dynamic([transaction], ^dynamic or transaction.value > ^0) + end + + defp filter_contract_call_dynamic(dynamic) do + dynamic([transaction, to_address: to_address], ^dynamic or not is_nil(to_address.contract_code)) + end + + defp filter_token_transfer_dynamic(dynamic) do + # TokenTransfer.__struct__.__meta__.source + dynamic( + [transaction], + ^dynamic or + fragment( + "NOT (SELECT transaction_hash FROM token_transfers WHERE transaction_hash = ? LIMIT 1) IS NULL", + transaction.hash + ) + ) + end + + defp filter_token_creation_dynamic(dynamic) do + dynamic([transaction, created_token: created_token], ^dynamic or not is_nil(created_token)) + end + + defp filter_blob_transaction_dynamic(dynamic) do + # EIP-2718 blob transaction type + dynamic([transaction], ^dynamic or transaction.type == 3) + end + + @doc """ + Returns recently collated transactions using random access pagination (RAP). + + This function supports efficient pagination for large transaction lists by + allowing direct access to any page without requiring sequential traversal. + It uses a cache for the first page to improve performance. + + ## Parameters + - `options`: Keyword list of options including: + - `:necessity_by_association` - Associations to preload (defaults to %{}) + - `:paging_options` - Random access pagination options (defaults to @default_paging_options) + + ## Returns + - A map containing: + - `:total_transactions_count` - Total number of transactions available + - `:transactions` - List of transaction structs for the requested page + """ + @spec recent_collated_transactions_for_rap([Chain.paging_options() | Chain.necessity_by_association_option()]) :: %{ + :total_transactions_count => non_neg_integer(), + :transactions => [__MODULE__.t()] + } + def recent_collated_transactions_for_rap(options \\ []) when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + total_transactions_count = transactions_available_count() + + fetched_transactions = + if is_nil(paging_options.key) or paging_options.page_number == 1 do + paging_options.page_size + |> Kernel.+(1) + |> Transactions.atomic_take_enough() + |> case do + nil -> + transactions = fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) + Transactions.update(transactions) + transactions + + transactions -> + transactions + end + else + fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) + end + + %{total_transactions_count: total_transactions_count, transactions: fetched_transactions} + end + + defp fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) do + fetch_transactions_for_rap() + |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) + |> handle_random_access_paging_options(paging_options) + |> Chain.join_associations(necessity_by_association) + |> preload([{:token_transfers, [:token, :from_address, :to_address]}]) + |> Repo.all() + end + + defp fetch_transactions_for_rap do + __MODULE__ + |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) + end + + @doc """ + Returns the count of available transactions shown in the UI. + + This function returns the number of transactions that have been mined and are + available for display, up to a maximum of #{@limit_showing_transactions} transactions. + + ## Returns + - The count of available transactions (non-negative integer) + """ + @spec transactions_available_count() :: non_neg_integer() + def transactions_available_count do + __MODULE__ + |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) + |> limit(^@limit_showing_transactions) + |> Repo.aggregate(:count, :hash) + end + + defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}], + do: limit(query, ^(@default_page_size + 1)) + + defp handle_random_access_paging_options(query, paging_options) do + query + |> (&if(paging_options |> Map.get(:page_number, 1) |> process_page_number() == 1, + do: &1, + else: __MODULE__.page_transaction(&1, paging_options) + )).() + |> handle_page(paging_options) + end + + defp handle_page(query, paging_options) do + page_number = paging_options |> Map.get(:page_number, 1) |> process_page_number() + page_size = Map.get(paging_options, :page_size, @default_page_size) + + cond do + page_in_bounds?(page_number, page_size) && page_number == 1 -> + query + |> limit(^(page_size + 1)) + + page_in_bounds?(page_number, page_size) -> + query + |> limit(^page_size) + |> offset(^((page_number - 2) * page_size)) + + true -> + query + |> limit(^(@default_page_size + 1)) + end + end + + defp process_page_number(number) when number < 1, do: 1 + + defp process_page_number(number), do: number + + defp page_in_bounds?(page_number, page_size), + do: page_size <= @limit_showing_transactions && @limit_showing_transactions - page_number * page_size >= 0 + + @doc """ + Finds and updates replaced transactions in the database. + """ + @spec find_and_update_replaced_transactions([ + %{ + required(:nonce) => non_neg_integer, + required(:from_address_hash) => Hash.Address.t(), + required(:hash) => Hash.t() + } + ]) :: {integer(), nil | [term()]} + def find_and_update_replaced_transactions(transactions, timeout \\ :infinity) do + query = + transactions + |> Enum.reduce( + __MODULE__, + fn %{hash: hash, nonce: nonce, from_address_hash: from_address_hash}, query -> + from(t in query, + or_where: + t.nonce == ^nonce and t.from_address_hash == ^from_address_hash and t.hash != ^hash and + not is_nil(t.block_number) + ) + end + ) + # Enforce Transaction ShareLocks order (see docs: sharelocks.md) + |> order_by(asc: :hash) + |> lock("FOR NO KEY UPDATE") + + hashes = Enum.map(transactions, & &1.hash) + + transactions_to_update = + from(pending in __MODULE__, + join: duplicate in subquery(query), + on: duplicate.nonce == pending.nonce and duplicate.from_address_hash == pending.from_address_hash, + where: pending.hash in ^hashes and is_nil(pending.block_hash) + ) + + Repo.update_all(transactions_to_update, [set: [error: "dropped/replaced", status: :error]], timeout: timeout) + end + + @doc """ + Updates the replaced transactions in the database. + """ + @spec update_replaced_transactions([ + %{ + required(:nonce) => non_neg_integer, + required(:from_address_hash) => Hash.Address.t(), + required(:block_hash) => Hash.Full.t() + } + ]) :: {integer(), nil | [term()]} + def update_replaced_transactions(transactions, timeout \\ :infinity) do + filters = + transactions + |> Enum.filter(fn transaction -> + transaction.block_hash && transaction.nonce && transaction.from_address_hash + end) + |> Enum.map(fn transaction -> + {transaction.nonce, transaction.from_address_hash} + end) + |> Enum.uniq() + + if Enum.empty?(filters) do + {0, []} + else + query = + filters + |> Enum.reduce(__MODULE__, fn {nonce, from_address}, query -> + from(t in query, + or_where: + t.nonce == ^nonce and + t.from_address_hash == ^from_address and + is_nil(t.block_hash) and + (is_nil(t.error) or t.error != "dropped/replaced") + ) + end) + # Enforce Transaction ShareLocks order (see docs: sharelocks.md) + |> order_by(asc: :hash) + |> lock("FOR NO KEY UPDATE") + + Repo.update_all( + from(t in __MODULE__, join: s in subquery(query), on: t.hash == s.hash), + [set: [error: "dropped/replaced", status: :error]], + timeout: timeout + ) + end + end + + @doc """ + Streams pending transactions with the given fields. + """ + @spec stream_pending_transactions( + fields :: [ + :block_hash + | :created_contract_code_indexed_at + | :from_address_hash + | :gas + | :gas_price + | :hash + | :index + | :input + | :nonce + | :r + | :s + | :to_address_hash + | :v + | :value + ], + initial :: accumulator, + reducer :: (entry :: term(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_pending_transactions(fields, initial, reducer, limited? \\ false) when is_function(reducer, 2) do + query = + __MODULE__ + |> pending_transactions_query() + |> select(^fields) + |> Chain.add_fetcher_limit(limited?) + + Repo.stream_reduce(query, initial, reducer) + end + + @doc """ + Query to return all pending transactions + """ + @spec pending_transactions_query(Ecto.Queryable.t()) :: Ecto.Queryable.t() + def pending_transactions_query(query) do + from(transaction in query, + where: is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced") + ) + end + + @doc """ + Returns pending transactions list from the DB + """ + @spec pending_transactions_list() :: Ecto.Schema.t() | term() + def pending_transactions_list do + __MODULE__ + |> pending_transactions_query() + |> where([t], t.inserted_at < ago(1, "day")) + |> Repo.all(timeout: :infinity) + end + + @doc """ + Return the list of pending transactions that occurred recently. + + iex> 2 |> insert_list(:transaction) + iex> :transaction |> insert() |> with_block() + iex> 8 |> insert_list(:transaction) + iex> recent_pending_transactions = Explorer.Chain.Transaction.recent_pending_transactions() + iex> length(recent_pending_transactions) + 10 + iex> Enum.all?(recent_pending_transactions, fn %Explorer.Chain.Transaction{block_hash: block_hash} -> + ...> is_nil(block_hash) + ...> end) + true + + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, + then the `t:Explorer.Chain.Transaction.t/0` will not be included in the list. + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` (defaults to + `#{@default_paging_options.page_size}`) and `:key` (a tuple of the lowest/oldest `{inserted_at, hash}`) and. + Results will be the transactions older than the `inserted_at` and `hash` that are passed. + + """ + @spec recent_pending_transactions([Chain.paging_options() | Chain.necessity_by_association_option()], true | false) :: + [ + __MODULE__.t() + ] + def recent_pending_transactions(options \\ [], old_ui? \\ true) + when is_list(options) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + method_id_filter = Keyword.get(options, :method) + type_filter = Keyword.get(options, :type) + + __MODULE__ + |> page_pending_transaction(paging_options) + |> limit(^paging_options.page_size) + |> pending_transactions_query() + |> apply_filter_by_method_id_to_transactions(method_id_filter) + |> apply_filter_by_type_to_transactions(type_filter) + |> order_by([transaction], desc: transaction.inserted_at, asc: transaction.hash) + |> Chain.join_associations(necessity_by_association) + |> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).() + |> Chain.select_repo(options).all() + end + + @doc """ + Finds all transactions of a certain block number + """ + def get_transactions_of_block_number(block_number) do + block_number + |> transactions_with_block_number() + |> Repo.all() + end + + @doc """ + Finds all transactions of a certain block numbers + """ + def get_transactions_of_block_numbers(block_numbers) do + block_numbers + |> transactions_for_block_numbers() + |> Repo.all() + end + + @doc """ + Finds transactions by hashes + """ + @spec get_transactions_by_hashes([Hash.t()]) :: [__MODULE__.t()] + def get_transactions_by_hashes(transaction_hashes) do + transaction_hashes + |> by_hashes_query() + |> Repo.all() + end + + @doc """ + Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists. + + Returns `:ok` if found + + iex> %Transaction{hash: hash} = insert(:transaction) + iex> Explorer.Chain.Transaction.check_transaction_exists(hash) + :ok + + Returns `:not_found` if not found + + iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( + ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" + ...> ) + iex> Explorer.Chain.Transaction.check_transaction_exists(hash) + :not_found + """ + @spec check_transaction_exists(Hash.Full.t()) :: :ok | :not_found + def check_transaction_exists(hash) do + hash + |> transaction_exists?() + |> Chain.boolean_to_check_result() + end + + # Checks if a `t:Explorer.Chain.Transaction.t/0` with the given `hash` exists. + + # Returns `true` if found + + # iex> %Transaction{hash: hash} = insert(:transaction) + # iex> Explorer.Chain.transaction_exists?(hash) + # true + + # Returns `false` if not found + + # iex> {:ok, hash} = Explorer.Chain.string_to_full_hash( + # ...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b" + # ...> ) + # iex> Explorer.Chain.transaction_exists?(hash) + # false + @spec transaction_exists?(Hash.Full.t()) :: boolean() + defp transaction_exists?(hash) do + query = + from( + transaction in __MODULE__, + where: transaction.hash == ^hash + ) + + Repo.exists?(query) + end end diff --git a/apps/explorer/lib/explorer/chain/withdrawal.ex b/apps/explorer/lib/explorer/chain/withdrawal.ex index ecba16f92ffd..3ac453389174 100644 --- a/apps/explorer/lib/explorer/chain/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/withdrawal.ex @@ -6,6 +6,7 @@ defmodule Explorer.Chain.Withdrawal do use Explorer.Schema alias Explorer.Chain.{Address, Block, Hash, Wei} + alias Explorer.Chain.Cache.Counters.{LastFetchedCounter, WithdrawalsSum} alias Explorer.PagingOptions @required_attrs ~w(index validator_index amount address_hash block_hash)a @@ -103,4 +104,31 @@ defmodule Explorer.Chain.Withdrawal do order_by: [desc: :index] ) end + + @doc """ + Upserts the count of all withdrawals into the `last_fetched_counters` table. + """ + @spec upsert_count_withdrawals(non_neg_integer()) :: {:ok, any()} | {:error, any()} + def upsert_count_withdrawals(index) do + LastFetchedCounter.upsert(%{ + counter_type: "withdrawals_count", + value: index + }) + end + + @doc """ + Fetches the count of withdrawals from the cache and adds 1 to it. + """ + @spec count_withdrawals_from_cache(Keyword.t()) :: Decimal.t() + def count_withdrawals_from_cache(options \\ []) do + "withdrawals_count" |> LastFetchedCounter.get(options) |> Decimal.add(1) + end + + @doc """ + Fetches the sum of withdrawals from the cache. + """ + @spec sum_withdrawals_from_cache(Keyword.t()) :: Decimal.t() | nil + def sum_withdrawals_from_cache(options \\ []) do + WithdrawalsSum.fetch(options) + end end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index c8a4291bcbc5..a95563c3657c 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -461,7 +461,7 @@ defmodule Explorer.Etherscan do |> Enum.map(fn direction -> query |> where_address_match(address_hash, direction) - |> Chain.pending_transactions_query() + |> Transaction.pending_transactions_query() |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash) |> Chain.wrapped_union_subquery() end) diff --git a/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex b/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex index 9af61bee3f6e..ff57a5f9d8f0 100644 --- a/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex +++ b/apps/explorer/lib/explorer/migrator/backfill_metadata_url.ex @@ -7,8 +7,9 @@ defmodule Explorer.Migrator.BackfillMetadataURL do import Ecto.Query alias EthereumJSONRPC.NFT - alias Explorer.{Chain, MetadataURIValidator, Repo} + alias Explorer.Chain.Token alias Explorer.Chain.Token.Instance + alias Explorer.{MetadataURIValidator, Repo} alias Explorer.Migrator.FillingMigration require Logger @@ -33,7 +34,7 @@ defmodule Explorer.Migrator.BackfillMetadataURL do current_token_type_map = tokens_address_hashes_to_preload_from_db - |> Chain.get_token_types() + |> Token.get_token_types() |> Enum.map(fn {address_hash, type} -> {address_hash.bytes, type} end) |> Enum.into(%{}) |> Map.merge(tokens_address_hash_to_type) diff --git a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex index 5fabaa814e8d..e70304f7ba91 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex @@ -8,7 +8,7 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus do import Ecto.Query - alias Explorer.{Chain, Repo} + alias Explorer.Repo alias Explorer.Chain.{ Block, @@ -114,7 +114,7 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus do transactions = inserted |> Enum.map(& &1.transaction_hash) - |> Chain.get_transactions_by_hashes() + |> Transaction.get_transactions_by_hashes() {[], transactions} end diff --git a/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex b/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex index 13d73c42aa61..ca3d8e94a03a 100644 --- a/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex +++ b/apps/explorer/lib/explorer/migrator/restore_omitted_weth_transfers.ex @@ -248,7 +248,7 @@ defmodule Explorer.Migrator.RestoreOmittedWETHTransfers do defp check_token_types(token_address_hashes) do token_address_hashes - |> Chain.get_token_types() + |> Token.get_token_types() |> Enum.reduce(true, fn {token_hash, token_type}, acc -> if token_type == "ERC-20" do acc diff --git a/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs b/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs index 92dc197f372b..7ef5491a6a33 100644 --- a/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs +++ b/apps/explorer/priv/repo/migrations/20180117221923_create_transactions.exs @@ -41,7 +41,7 @@ defmodule Explorer.Repo.Migrations.CreateTransactions do add(:block_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: true) # `null` when a pending transaction - # denormalized from `blocks.number` to improve `Explorer.Chain.recent_collated_transactions/0` performance + # denormalized from `blocks.number` to improve `Explorer.Chain.Transaction.recent_collated_transactions/0` performance add(:block_number, :integer, null: true) add(:from_address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false) diff --git a/apps/explorer/test/explorer/chain/address/token_balance_test.exs b/apps/explorer/test/explorer/chain/address/token_balance_test.exs index 3e1a8018289a..0e3825e2e57c 100644 --- a/apps/explorer/test/explorer/chain/address/token_balance_test.exs +++ b/apps/explorer/test/explorer/chain/address/token_balance_test.exs @@ -119,4 +119,15 @@ defmodule Explorer.Chain.Address.TokenBalanceTest do assert(result.value == token_balance_b.value) end end + + describe "stream_unfetched_token_balances/2" do + test "executes the given reducer with the query result" do + address = insert(:address, hash: "0xc45e4830dff873cf8b70de2b194d0ddd06ef651e") + token_balance = insert(:token_balance, value_fetched_at: nil, address: address) + insert(:token_balance) + + assert TokenBalance.stream_unfetched_token_balances([], &[&1.block_number | &2]) == + {:ok, [token_balance.block_number]} + end + end end diff --git a/apps/explorer/test/explorer/chain/address_test.exs b/apps/explorer/test/explorer/chain/address_test.exs index 75af9de50b9e..15bd4193af18 100644 --- a/apps/explorer/test/explorer/chain/address_test.exs +++ b/apps/explorer/test/explorer/chain/address_test.exs @@ -4,7 +4,7 @@ defmodule Explorer.Chain.AddressTest do import Mox alias Explorer.Chain.Address - alias Explorer.Repo + alias Explorer.{Factory, Repo} setup :verify_on_exit! @@ -129,4 +129,47 @@ defmodule Explorer.Chain.AddressTest do # assert tail_result == expected_tail # end end + + describe "find_contract_address/1" do + test "doesn't find an address that doesn't have a code" do + address = insert(:address, contract_code: nil) + + response = Address.find_contract_address(address.hash) + + assert {:error, :not_found} == response + end + + test "doesn't find a nonexistent address" do + nonexistent_address_hash = Factory.address_hash() + + response = Address.find_contract_address(nonexistent_address_hash) + + assert {:error, :not_found} == response + end + + test "finds a contract address" do + address = + insert(:address, contract_code: Factory.data("contract_code"), smart_contract: nil, names: []) + |> Repo.preload( + [ + :token, + [smart_contract: :smart_contract_additional_sources], + Explorer.Chain.SmartContract.Proxy.Models.Implementation.proxy_implementations_association() + ] ++ Address.contract_creation_transaction_associations() + ) + + options = [ + necessity_by_association: %{ + :names => :optional, + :smart_contract => :optional, + :token => :optional, + Address.contract_creation_transaction_associations() => :optional + } + ] + + response = Address.find_contract_address(address.hash, options) + + assert response == {:ok, address} + end + end end diff --git a/apps/explorer/test/explorer/chain/beacon/deposit_test.exs b/apps/explorer/test/explorer/chain/beacon/deposit_test.exs index 514b6a3afe9b..37caca82eb69 100644 --- a/apps/explorer/test/explorer/chain/beacon/deposit_test.exs +++ b/apps/explorer/test/explorer/chain/beacon/deposit_test.exs @@ -12,7 +12,7 @@ defmodule Explorer.Chain.Beacon.DepositTest do actual_block = insert(:block, consensus: true, number: reorg.number) transaction = insert(:transaction) |> with_block(actual_block) - reorged_logs = [ + _reorged_logs = [ insert(:log, address: deposit_contract, block: reorg, diff --git a/apps/explorer/test/explorer/chain/block_test.exs b/apps/explorer/test/explorer/chain/block_test.exs index 932d3441cbd8..cb41e15157a1 100644 --- a/apps/explorer/test/explorer/chain/block_test.exs +++ b/apps/explorer/test/explorer/chain/block_test.exs @@ -2,7 +2,8 @@ defmodule Explorer.Chain.BlockTest do use Explorer.DataCase alias Ecto.Changeset - alias Explorer.Chain.{Block, Wei} + alias Explorer.Chain.{Address, Block, PendingBlockOperation, Wei} + alias Explorer.PagingOptions describe "changeset/2" do test "with valid attributes" do @@ -143,4 +144,259 @@ defmodule Explorer.Chain.BlockTest do assert Block.next_block_base_fee_per_gas() == Decimal.new(1100) end end + + describe "block_combined_rewards/1" do + test "sums the block_rewards values" do + block = insert(:block) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :validator, + reward: Decimal.new(1_000_000_000_000_000_000) + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :emission_funds, + reward: Decimal.new(1_000_000_000_000_000_000) + ) + + insert( + :reward, + address_hash: block.miner_hash, + block_hash: block.hash, + address_type: :uncle, + reward: Decimal.new(1_000_000_000_000_000_000) + ) + + block = Repo.preload(block, :rewards) + + {:ok, expected_value} = Wei.cast(3_000_000_000_000_000_000) + + assert Block.block_combined_rewards(block) == expected_value + end + end + + describe "fetch_min_block_number/0" do + test "fetches min block numbers" do + for index <- 5..9 do + insert(:block, number: index) + Process.sleep(200) + end + + assert 5 = Block.fetch_min_block_number() + end + + test "fetches min when there are no blocks" do + assert 0 = Block.fetch_min_block_number() + end + end + + describe "fetch_max_block_number/0" do + test "fetches max block numbers" do + for index <- 5..9 do + insert(:block, number: index) + end + + assert 9 = Block.fetch_max_block_number() + end + + test "fetches max when there are no blocks" do + assert 0 = Block.fetch_max_block_number() + end + end + + describe "get_blocks_validated_by_address/2" do + test "returns nothing when there are no blocks" do + %Address{hash: address_hash} = insert(:address) + + assert [] = Block.get_blocks_validated_by_address(address_hash) + end + + test "returns the blocks validated by a specified address" do + %Address{hash: address_hash} = address = insert(:address) + another_address = insert(:address) + + block = insert(:block, miner: address, miner_hash: address.hash) + insert(:block, miner: another_address, miner_hash: another_address.hash) + + results = + address_hash + |> Block.get_blocks_validated_by_address() + |> Enum.map(& &1.hash) + + assert results == [block.hash] + end + + test "with blocks can be paginated" do + %Address{hash: address_hash} = address = insert(:address) + + first_page_block = insert(:block, miner: address, miner_hash: address.hash, number: 0) + second_page_block = insert(:block, miner: address, miner_hash: address.hash, number: 2) + + assert [first_page_block.number] == + [paging_options: %PagingOptions{key: {1}, page_size: 1}] + |> Block.get_blocks_validated_by_address(address_hash) + |> Enum.map(& &1.number) + |> Enum.reverse() + + assert [second_page_block.number] == + [paging_options: %PagingOptions{key: {3}, page_size: 1}] + |> Block.get_blocks_validated_by_address(address_hash) + |> Enum.map(& &1.number) + |> Enum.reverse() + end + end + + describe "stream_blocks_without_rewards/2" do + test "includes consensus blocks" do + %Block{hash: consensus_hash} = insert(:block, consensus: true) + + assert {:ok, [%Block{hash: ^consensus_hash}]} = Block.stream_blocks_without_rewards([], &[&1 | &2]) + end + + test "does not include consensus block that has a reward" do + %Block{hash: consensus_hash, miner_hash: miner_hash} = insert(:block, consensus: true) + insert(:reward, address_hash: miner_hash, block_hash: consensus_hash) + + assert {:ok, []} = Block.stream_blocks_without_rewards([], &[&1 | &2]) + end + + # https://github.com/poanetwork/blockscout/issues/1310 regression test + test "does not include non-consensus blocks" do + insert(:block, consensus: false) + + assert {:ok, []} = Block.stream_blocks_without_rewards([], &[&1 | &2]) + end + end + + describe "block_hash_by_number/1" do + test "without blocks returns empty map" do + assert Block.block_hash_by_number([]) == %{} + end + + test "with consensus block returns mapping" do + block = insert(:block) + + assert Block.block_hash_by_number([block.number]) == %{block.number => block.hash} + end + + test "with non-consensus block does not return mapping" do + block = insert(:block, consensus: false) + + assert Block.block_hash_by_number([block.number]) == %{} + end + end + + describe "stream_unfetched_uncles/2" do + test "does not return uncle hashes where t:Explorer.Chain.Block.SecondDegreeRelation.t/0 uncle_fetched_at is not nil" do + %Block.SecondDegreeRelation{nephew: %Block{}, nephew_hash: nephew_hash, index: index, uncle_hash: uncle_hash} = + insert(:block_second_degree_relation) + + assert {:ok, [%{nephew_hash: ^nephew_hash, index: ^index}]} = + Block.stream_unfetched_uncles([], &[&1 | &2]) + + query = from(bsdr in Block.SecondDegreeRelation, where: bsdr.uncle_hash == ^uncle_hash) + + assert {1, _} = Repo.update_all(query, set: [uncle_fetched_at: DateTime.utc_now()]) + + assert {:ok, []} = Block.stream_unfetched_uncles([], &[&1 | &2]) + end + end + + describe "remove_nonconsensus_blocks_from_pending_ops/0" do + test "removes pending ops for nonconsensus blocks" do + block = insert(:block) + insert(:pending_block_operation, block: block, block_number: block.number) + + nonconsensus_block = insert(:block, consensus: false) + insert(:pending_block_operation, block: nonconsensus_block, block_number: nonconsensus_block.number) + + config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(config, :block_traceable?, true)) + + on_exit(fn -> Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, config) end) + + :ok = Block.remove_nonconsensus_blocks_from_pending_ops() + + assert Repo.get(PendingBlockOperation, block.hash) + assert is_nil(Repo.get(PendingBlockOperation, nonconsensus_block.hash)) + end + + test "removes pending ops for nonconsensus blocks by block hashes" do + block = insert(:block) + insert(:pending_block_operation, block: block, block_number: block.number) + + nonconsensus_block = insert(:block, consensus: false) + insert(:pending_block_operation, block: nonconsensus_block, block_number: nonconsensus_block.number) + + nonconsensus_block1 = insert(:block, consensus: false) + insert(:pending_block_operation, block: nonconsensus_block1, block_number: nonconsensus_block1.number) + + config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(config, :block_traceable?, true)) + + on_exit(fn -> Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, config) end) + + :ok = Block.remove_nonconsensus_blocks_from_pending_ops([nonconsensus_block1.hash]) + + assert Repo.get(PendingBlockOperation, block.hash) + assert Repo.get(PendingBlockOperation, nonconsensus_block.hash) + assert is_nil(Repo.get(PendingBlockOperation, nonconsensus_block1.hash)) + end + end + + describe "gas_payment_by_block_hash/1" do + setup do + number = 1 + + block = insert(:block, number: number, consensus: true) + + %{consensus_block: block, number: number} + end + + test "without consensus block hash has key with 0 value", %{consensus_block: consensus_block, number: number} do + non_consensus_block = insert(:block, number: number, consensus: false) + + :transaction + |> insert(gas_price: 1, block_consensus: false) + |> with_block(consensus_block, gas_used: 1) + + :transaction + |> insert(gas_price: 1, block_consensus: false) + |> with_block(consensus_block, gas_used: 2) + + assert Block.gas_payment_by_block_hash([non_consensus_block.hash]) == %{ + non_consensus_block.hash => %Wei{value: Decimal.new(0)} + } + end + + test "with consensus block hash without transactions has key with 0 value", %{ + consensus_block: %Block{hash: consensus_block_hash} + } do + assert Block.gas_payment_by_block_hash([consensus_block_hash]) == %{ + consensus_block_hash => %Wei{value: Decimal.new(0)} + } + end + + test "with consensus block hash with transactions has key with value", %{ + consensus_block: %Block{hash: consensus_block_hash} = consensus_block + } do + :transaction + |> insert(gas_price: 1) + |> with_block(consensus_block, gas_used: 2) + + :transaction + |> insert(gas_price: 3) + |> with_block(consensus_block, gas_used: 4) + + assert Block.gas_payment_by_block_hash([consensus_block_hash]) == %{ + consensus_block_hash => %Wei{value: Decimal.new(14)} + } + end + end end diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index a6914eb064a9..73bbb718a52a 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,8 +1,14 @@ defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase + use EthereumJSONRPC.Case - alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction} + import EthereumJSONRPC, only: [integer_to_quantity: 1] + import Mox + + alias Explorer.Chain + alias Explorer.Chain.{Address, Block, Data, InternalTransaction, Transaction, Wei} alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.InternalTransaction.Type alias Explorer.PagingOptions doctest InternalTransaction @@ -1176,4 +1182,111 @@ defmodule Explorer.Chain.InternalTransactionTest do # |> Enum.map(&{&1.transaction_hash, &1.index, &1.block_hash}) # end end + + describe "fetch_first_trace/2" do + test "fetched first trace", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + from_address_hash = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" + gas = 4_533_872 + + init = + "0x6060604052341561000f57600080fd5b60405160208061071a83398101604052808051906020019091905050806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506003600160006001600281111561007e57fe5b60ff1660ff168152602001908152602001600020819055506002600160006002808111156100a857fe5b60ff1660ff168152602001908152602001600020819055505061064a806100d06000396000f30060606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef" + + value = 0 + block_number = 39 + block_hash = "0x74c72ccabcb98b7ebbd7b31de938212b7e8814a002263b6569564e944d88f51f" + index = 0 + created_contract_address_hash = "0x1e0eaa06d02f965be2dfe0bc9ff52b2d82133461" + + created_contract_code = + "0x60606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029" + + gas_used = 382_953 + trace_address = [] + transaction_hash = "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1" + transaction_index = 0 + type = "create" + + if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do + expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options -> + {:ok, + [ + %{ + id: 0, + result: %{ + "output" => "0x", + "stateDiff" => nil, + "trace" => [ + %{ + "action" => %{ + "from" => from_address_hash, + "gas" => integer_to_quantity(gas), + "init" => init, + "value" => integer_to_quantity(value) + }, + "blockNumber" => block_number, + "index" => index, + "result" => %{ + "address" => created_contract_address_hash, + "code" => created_contract_code, + "gasUsed" => integer_to_quantity(gas_used) + }, + "traceAddress" => trace_address, + "type" => type + } + ], + "transactionHash" => transaction_hash + } + } + ]} + end) + end + + {:ok, created_contract_address_hash_bytes} = Chain.string_to_address_hash(created_contract_address_hash) + {:ok, from_address_hash_bytes} = Chain.string_to_address_hash(from_address_hash) + {:ok, created_contract_code_bytes} = Data.cast(created_contract_code) + {:ok, init_bytes} = Data.cast(init) + {:ok, transaction_hash_bytes} = Chain.string_to_full_hash(transaction_hash) + {:ok, type_bytes} = Type.load(type) + value_wei = %Wei{value: Decimal.new(value)} + + assert InternalTransaction.fetch_first_trace( + [ + %{ + hash_data: transaction_hash, + block_hash: block_hash, + block_number: block_number, + transaction_index: transaction_index + } + ], + json_rpc_named_arguments + ) == { + :ok, + [ + %{ + block_index: 0, + block_number: block_number, + block_hash: block_hash, + call_type: nil, + created_contract_address_hash: created_contract_address_hash_bytes, + created_contract_code: created_contract_code_bytes, + from_address_hash: from_address_hash_bytes, + gas: gas, + gas_used: gas_used, + index: index, + init: init_bytes, + input: nil, + output: nil, + to_address_hash: nil, + trace_address: trace_address, + transaction_hash: transaction_hash_bytes, + type: type_bytes, + value: value_wei, + transaction_index: transaction_index + } + ] + } + end + end end diff --git a/apps/explorer/test/explorer/chain/log_test.exs b/apps/explorer/test/explorer/chain/log_test.exs index e3835bf057a2..cbf620a6d9dc 100644 --- a/apps/explorer/test/explorer/chain/log_test.exs +++ b/apps/explorer/test/explorer/chain/log_test.exs @@ -5,7 +5,6 @@ defmodule Explorer.Chain.LogTest do alias Ecto.Changeset alias Explorer.Chain.{Log, SmartContract} - alias Explorer.TestHelper @first_topic_hex_string_1 "0x7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65" diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index c97680326219..1d5f30dbf449 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -879,4 +879,154 @@ defmodule Explorer.Chain.TransactionTest do assert "0x3078f114" == Transaction.get_method_name(transaction) end end + + describe "recent_collated_transactions/1" do + test "with no collated transactions it returns an empty list" do + assert [] == Transaction.recent_collated_transactions(true) + end + + test "it excludes pending transactions" do + insert(:transaction) + assert [] == Transaction.recent_collated_transactions(true) + end + + test "returns a list of recent collated transactions" do + newest_first_transactions = + 50 + |> insert_list(:transaction) + |> with_block() + |> Enum.reverse() + + oldest_seen = Enum.at(newest_first_transactions, 9) + paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} + + recent_collated_transactions = + Transaction.recent_collated_transactions(true, paging_options: paging_options) + + assert length(recent_collated_transactions) == 10 + assert hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash + end + + test "returns transactions with token_transfers preloaded" do + address = insert(:address) + token_contract_address = insert(:contract_address) + token = insert(:token, contract_address: token_contract_address) + + transaction = + :transaction + |> insert() + |> with_block() + + insert_list( + 2, + :token_transfer, + to_address: address, + transaction: transaction, + token_contract_address: token_contract_address, + token: token, + block: transaction.block + ) + + fetched_transaction = List.first(Explorer.Chain.Transaction.recent_collated_transactions(true)) + assert fetched_transaction.hash == transaction.hash + assert length(fetched_transaction.token_transfers) == 2 + end + end + + describe "update_replaced_transactions/2" do + test "update replaced transactions" do + replaced_transaction_hash = "0x2a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" + + address = insert(:address, hash: "0xb7cffe2ac19b9d5705a24cbe14fef5663af905a6") + + insert(:transaction, + from_address: address, + nonce: 1, + block_hash: nil, + index: nil, + block_number: nil, + hash: replaced_transaction_hash + ) + + mined_transaction_hash = "0x1a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" + block = insert(:block) + + mined_transaction = + insert(:transaction, + from_address: address, + nonce: 1, + index: 0, + block_hash: block.hash, + block_number: block.number, + cumulative_gas_used: 1, + gas_used: 1, + hash: mined_transaction_hash + ) + + second_mined_transaction_hash = "0x3a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" + second_block = insert(:block) + + insert(:transaction, + from_address: address, + nonce: 1, + index: 0, + block_hash: second_block.hash, + block_number: second_block.number, + cumulative_gas_used: 1, + gas_used: 1, + hash: second_mined_transaction_hash + ) + + {1, _} = + Transaction.update_replaced_transactions([ + %{ + block_hash: mined_transaction.block_hash, + nonce: mined_transaction.nonce, + from_address_hash: mined_transaction.from_address_hash + } + ]) + + replaced_transaction = Repo.get(Transaction, replaced_transaction_hash) + + assert replaced_transaction.status == :error + assert replaced_transaction.error == "dropped/replaced" + + found_mined_transaction = Repo.get(Transaction, mined_transaction_hash) + + assert found_mined_transaction.status == nil + assert found_mined_transaction.error == nil + + second_mined_transaction = Repo.get(Transaction, second_mined_transaction_hash) + + assert second_mined_transaction.status == nil + assert second_mined_transaction.error == nil + end + end + + describe "pending_transactions/0" do + test "without transactions" do + assert [] = Transaction.recent_pending_transactions() + end + + test "with transactions" do + %Transaction{hash: hash} = insert(:transaction) + + assert [%Transaction{hash: ^hash}] = Transaction.recent_pending_transactions() + end + + test "with transactions can be paginated" do + second_page_hashes = + 50 + |> insert_list(:transaction) + |> Enum.map(& &1.hash) + + %Transaction{inserted_at: inserted_at, hash: hash} = insert(:transaction) + + assert second_page_hashes == + [paging_options: %PagingOptions{key: {inserted_at, hash}, page_size: 50}] + |> Transaction.recent_pending_transactions() + |> Enum.map(& &1.hash) + |> Enum.reverse() + end + end end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index fe012ad0f667..4e3ad9f3affc 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -4,8 +4,6 @@ defmodule Explorer.ChainTest do require Ecto.Query - import Ecto.Query - import EthereumJSONRPC, only: [integer_to_quantity: 1] import Explorer.Factory import Mox @@ -18,7 +16,6 @@ defmodule Explorer.ChainTest do Hash, InternalTransaction, Log, - PendingBlockOperation, Token, TokenTransfer, Transaction, @@ -28,7 +25,6 @@ defmodule Explorer.ChainTest do alias Explorer.{Chain, Etherscan} alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.Cache.Counters.{BlocksCount, TransactionsCount, PendingBlockOperationCount} - alias Explorer.Chain.InternalTransaction.Type alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue} alias Explorer.Chain.Supply.ProofOfAuthority @@ -46,48 +42,6 @@ defmodule Explorer.ChainTest do setup :verify_on_exit! - describe "remove_nonconsensus_blocks_from_pending_ops/0" do - test "removes pending ops for nonconsensus blocks" do - block = insert(:block) - insert(:pending_block_operation, block: block, block_number: block.number) - - nonconsensus_block = insert(:block, consensus: false) - insert(:pending_block_operation, block: nonconsensus_block, block_number: nonconsensus_block.number) - - config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) - Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(config, :block_traceable?, true)) - - on_exit(fn -> Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, config) end) - - :ok = Chain.remove_nonconsensus_blocks_from_pending_ops() - - assert Repo.get(PendingBlockOperation, block.hash) - assert is_nil(Repo.get(PendingBlockOperation, nonconsensus_block.hash)) - end - - test "removes pending ops for nonconsensus blocks by block hashes" do - block = insert(:block) - insert(:pending_block_operation, block: block, block_number: block.number) - - nonconsensus_block = insert(:block, consensus: false) - insert(:pending_block_operation, block: nonconsensus_block, block_number: nonconsensus_block.number) - - nonconsensus_block1 = insert(:block, consensus: false) - insert(:pending_block_operation, block: nonconsensus_block1, block_number: nonconsensus_block1.number) - - config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) - Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(config, :block_traceable?, true)) - - on_exit(fn -> Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, config) end) - - :ok = Chain.remove_nonconsensus_blocks_from_pending_ops([nonconsensus_block1.hash]) - - assert Repo.get(PendingBlockOperation, block.hash) - assert Repo.get(PendingBlockOperation, nonconsensus_block.hash) - assert is_nil(Repo.get(PendingBlockOperation, nonconsensus_block1.hash)) - end - end - describe "address_estimated_count/0" do test "returns the number of all addresses" do insert(:address, fetched_coin_balance: 0) @@ -919,35 +873,6 @@ defmodule Explorer.ChainTest do end end - describe "fetch_min_block_number/0" do - test "fetches min block numbers" do - for index <- 5..9 do - insert(:block, number: index) - Process.sleep(200) - end - - assert 5 = Chain.fetch_min_block_number() - end - - test "fetches min when there are no blocks" do - assert 0 = Chain.fetch_min_block_number() - end - end - - describe "fetch_max_block_number/0" do - test "fetches max block numbers" do - for index <- 5..9 do - insert(:block, number: index) - end - - assert 9 = Chain.fetch_max_block_number() - end - - test "fetches max when there are no blocks" do - assert 0 = Chain.fetch_max_block_number() - end - end - describe "fetch_sum_coin_total_supply/0" do test "fetches coin total supply" do for index <- 0..4 do @@ -1293,12 +1218,12 @@ defmodule Explorer.ChainTest do gas_used: ^gas_used_int, index: 1, init: nil, - input: %Explorer.Chain.Data{ + input: %Data{ bytes: <<16, 133, 82, 105, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 134, 45, 103, 203, 7, 115, 238, 63, 140, 231, 234, 137, 179, 40, 255, 234, 134, 26, 179, 239>> }, - output: %Explorer.Chain.Data{bytes: ""}, + output: %Data{bytes: ""}, trace_address: [], type: :call, block_number: 37, @@ -1638,88 +1563,6 @@ defmodule Explorer.ChainTest do end end - describe "block_hash_by_number/1" do - test "without blocks returns empty map" do - assert Chain.block_hash_by_number([]) == %{} - end - - test "with consensus block returns mapping" do - block = insert(:block) - - assert Chain.block_hash_by_number([block.number]) == %{block.number => block.hash} - end - - test "with non-consensus block does not return mapping" do - block = insert(:block, consensus: false) - - assert Chain.block_hash_by_number([block.number]) == %{} - end - end - - describe "stream_blocks_without_rewards/2" do - test "includes consensus blocks" do - %Block{hash: consensus_hash} = insert(:block, consensus: true) - - assert {:ok, [%Block{hash: ^consensus_hash}]} = Chain.stream_blocks_without_rewards([], &[&1 | &2]) - end - - test "does not include consensus block that has a reward" do - %Block{hash: consensus_hash, miner_hash: miner_hash} = insert(:block, consensus: true) - insert(:reward, address_hash: miner_hash, block_hash: consensus_hash) - - assert {:ok, []} = Chain.stream_blocks_without_rewards([], &[&1 | &2]) - end - - # https://github.com/poanetwork/blockscout/issues/1310 regression test - test "does not include non-consensus blocks" do - insert(:block, consensus: false) - - assert {:ok, []} = Chain.stream_blocks_without_rewards([], &[&1 | &2]) - end - end - - describe "get_blocks_validated_by_address/2" do - test "returns nothing when there are no blocks" do - %Address{hash: address_hash} = insert(:address) - - assert [] = Chain.get_blocks_validated_by_address(address_hash) - end - - test "returns the blocks validated by a specified address" do - %Address{hash: address_hash} = address = insert(:address) - another_address = insert(:address) - - block = insert(:block, miner: address, miner_hash: address.hash) - insert(:block, miner: another_address, miner_hash: another_address.hash) - - results = - address_hash - |> Chain.get_blocks_validated_by_address() - |> Enum.map(& &1.hash) - - assert results == [block.hash] - end - - test "with blocks can be paginated" do - %Address{hash: address_hash} = address = insert(:address) - - first_page_block = insert(:block, miner: address, miner_hash: address.hash, number: 0) - second_page_block = insert(:block, miner: address, miner_hash: address.hash, number: 2) - - assert [first_page_block.number] == - [paging_options: %PagingOptions{key: {1}, page_size: 1}] - |> Chain.get_blocks_validated_by_address(address_hash) - |> Enum.map(& &1.number) - |> Enum.reverse() - - assert [second_page_block.number] == - [paging_options: %PagingOptions{key: {3}, page_size: 1}] - |> Chain.get_blocks_validated_by_address(address_hash) - |> Enum.map(& &1.number) - |> Enum.reverse() - end - end - describe "number_to_block/1" do test "without block" do assert {:error, :not_found} = Chain.number_to_block(-1) @@ -1732,33 +1575,6 @@ defmodule Explorer.ChainTest do end end - describe "pending_transactions/0" do - test "without transactions" do - assert [] = Chain.recent_pending_transactions() - end - - test "with transactions" do - %Transaction{hash: hash} = insert(:transaction) - - assert [%Transaction{hash: ^hash}] = Chain.recent_pending_transactions() - end - - test "with transactions can be paginated" do - second_page_hashes = - 50 - |> insert_list(:transaction) - |> Enum.map(& &1.hash) - - %Transaction{inserted_at: inserted_at, hash: hash} = insert(:transaction) - - assert second_page_hashes == - [paging_options: %PagingOptions{key: {inserted_at, hash}, page_size: 50}] - |> Chain.recent_pending_transactions() - |> Enum.map(& &1.hash) - |> Enum.reverse() - end - end - describe "transaction_estimated_count/1" do setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Counters.TransactionsCount.child_id()) @@ -2114,99 +1930,6 @@ defmodule Explorer.ChainTest do end end - describe "find_contract_address/1" do - test "doesn't find an address that doesn't have a code" do - address = insert(:address, contract_code: nil) - - response = Chain.find_contract_address(address.hash) - - assert {:error, :not_found} == response - end - - test "doesn't find a nonexistent address" do - nonexistent_address_hash = Factory.address_hash() - - response = Chain.find_contract_address(nonexistent_address_hash) - - assert {:error, :not_found} == response - end - - test "finds a contract address" do - address = - insert(:address, contract_code: Factory.data("contract_code"), smart_contract: nil, names: []) - |> Repo.preload( - [ - :token, - [smart_contract: :smart_contract_additional_sources], - Explorer.Chain.SmartContract.Proxy.Models.Implementation.proxy_implementations_association() - ] ++ Address.contract_creation_transaction_associations() - ) - - options = [ - necessity_by_association: %{ - :names => :optional, - :smart_contract => :optional, - :token => :optional, - Address.contract_creation_transaction_associations() => :optional - } - ] - - response = Chain.find_contract_address(address.hash, options) - - assert response == {:ok, address} - end - end - - describe "gas_payment_by_block_hash/1" do - setup do - number = 1 - - block = insert(:block, number: number, consensus: true) - - %{consensus_block: block, number: number} - end - - test "without consensus block hash has key with 0 value", %{consensus_block: consensus_block, number: number} do - non_consensus_block = insert(:block, number: number, consensus: false) - - :transaction - |> insert(gas_price: 1, block_consensus: false) - |> with_block(consensus_block, gas_used: 1) - - :transaction - |> insert(gas_price: 1, block_consensus: false) - |> with_block(consensus_block, gas_used: 2) - - assert Chain.gas_payment_by_block_hash([non_consensus_block.hash]) == %{ - non_consensus_block.hash => %Wei{value: Decimal.new(0)} - } - end - - test "with consensus block hash without transactions has key with 0 value", %{ - consensus_block: %Block{hash: consensus_block_hash} - } do - assert Chain.gas_payment_by_block_hash([consensus_block_hash]) == %{ - consensus_block_hash => %Wei{value: Decimal.new(0)} - } - end - - test "with consensus block hash with transactions has key with value", %{ - consensus_block: %Block{hash: consensus_block_hash} = consensus_block - } do - :transaction - |> insert(gas_price: 1) - |> with_block(consensus_block, gas_used: 2) - - :transaction - |> insert(gas_price: 3) - |> with_block(consensus_block, gas_used: 4) - - assert Chain.gas_payment_by_block_hash([consensus_block_hash]) == %{ - consensus_block_hash => %Wei{value: Decimal.new(14)} - } - end - end - describe "missing_block_number_ranges/1" do # 0000 test "0..0 without blocks" do @@ -2330,57 +2053,6 @@ defmodule Explorer.ChainTest do end end - describe "recent_collated_transactions/1" do - test "with no collated transactions it returns an empty list" do - assert [] == Explorer.Chain.recent_collated_transactions(true) - end - - test "it excludes pending transactions" do - insert(:transaction) - assert [] == Explorer.Chain.recent_collated_transactions(true) - end - - test "returns a list of recent collated transactions" do - newest_first_transactions = - 50 - |> insert_list(:transaction) - |> with_block() - |> Enum.reverse() - - oldest_seen = Enum.at(newest_first_transactions, 9) - paging_options = %Explorer.PagingOptions{page_size: 10, key: {oldest_seen.block_number, oldest_seen.index}} - recent_collated_transactions = Explorer.Chain.recent_collated_transactions(true, paging_options: paging_options) - - assert length(recent_collated_transactions) == 10 - assert hd(recent_collated_transactions).hash == Enum.at(newest_first_transactions, 10).hash - end - - test "returns transactions with token_transfers preloaded" do - address = insert(:address) - token_contract_address = insert(:contract_address) - token = insert(:token, contract_address: token_contract_address) - - transaction = - :transaction - |> insert() - |> with_block() - - insert_list( - 2, - :token_transfer, - to_address: address, - transaction: transaction, - token_contract_address: token_contract_address, - token: token, - block: transaction.block - ) - - fetched_transaction = List.first(Explorer.Chain.recent_collated_transactions(true)) - assert fetched_transaction.hash == transaction.hash - assert length(fetched_transaction.token_transfers) == 2 - end - end - describe "smart_contract_bytecode/1" do test "fetches the smart contract bytecode" do smart_contract_bytecode = @@ -2409,102 +2081,6 @@ defmodule Explorer.ChainTest do end end - describe "update_replaced_transactions/2" do - test "update replaced transactions" do - replaced_transaction_hash = "0x2a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" - - address = insert(:address, hash: "0xb7cffe2ac19b9d5705a24cbe14fef5663af905a6") - - insert(:transaction, - from_address: address, - nonce: 1, - block_hash: nil, - index: nil, - block_number: nil, - hash: replaced_transaction_hash - ) - - mined_transaction_hash = "0x1a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" - block = insert(:block) - - mined_transaction = - insert(:transaction, - from_address: address, - nonce: 1, - index: 0, - block_hash: block.hash, - block_number: block.number, - cumulative_gas_used: 1, - gas_used: 1, - hash: mined_transaction_hash - ) - - second_mined_transaction_hash = "0x3a263224a95275d77bc30a7e131bc64d948777946a790c0915ab293791fbcb61" - second_block = insert(:block) - - insert(:transaction, - from_address: address, - nonce: 1, - index: 0, - block_hash: second_block.hash, - block_number: second_block.number, - cumulative_gas_used: 1, - gas_used: 1, - hash: second_mined_transaction_hash - ) - - {1, _} = - Chain.update_replaced_transactions([ - %{ - block_hash: mined_transaction.block_hash, - nonce: mined_transaction.nonce, - from_address_hash: mined_transaction.from_address_hash - } - ]) - - replaced_transaction = Repo.get(Transaction, replaced_transaction_hash) - - assert replaced_transaction.status == :error - assert replaced_transaction.error == "dropped/replaced" - - found_mined_transaction = Repo.get(Transaction, mined_transaction_hash) - - assert found_mined_transaction.status == nil - assert found_mined_transaction.error == nil - - second_mined_transaction = Repo.get(Transaction, second_mined_transaction_hash) - - assert second_mined_transaction.status == nil - assert second_mined_transaction.error == nil - end - end - - describe "stream_unfetched_token_balances/2" do - test "executes the given reducer with the query result" do - address = insert(:address, hash: "0xc45e4830dff873cf8b70de2b194d0ddd06ef651e") - token_balance = insert(:token_balance, value_fetched_at: nil, address: address) - insert(:token_balance) - - assert Chain.stream_unfetched_token_balances([], &[&1.block_number | &2]) == {:ok, [token_balance.block_number]} - end - end - - describe "stream_unfetched_uncles/2" do - test "does not return uncle hashes where t:Explorer.Chain.Block.SecondDegreeRelation.t/0 uncle_fetched_at is not nil" do - %Block.SecondDegreeRelation{nephew: %Block{}, nephew_hash: nephew_hash, index: index, uncle_hash: uncle_hash} = - insert(:block_second_degree_relation) - - assert {:ok, [%{nephew_hash: ^nephew_hash, index: ^index}]} = - Explorer.Chain.stream_unfetched_uncles([], &[&1 | &2]) - - query = from(bsdr in Block.SecondDegreeRelation, where: bsdr.uncle_hash == ^uncle_hash) - - assert {1, _} = Repo.update_all(query, set: [uncle_fetched_at: DateTime.utc_now()]) - - assert {:ok, []} = Explorer.Chain.stream_unfetched_uncles([], &[&1 | &2]) - end - end - test "total_supply/0" do Application.put_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority) height = 2_000_000 @@ -2790,42 +2366,6 @@ defmodule Explorer.ChainTest do # end end - describe "block_combined_rewards/1" do - test "sums the block_rewards values" do - block = insert(:block) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :validator, - reward: Decimal.new(1_000_000_000_000_000_000) - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :emission_funds, - reward: Decimal.new(1_000_000_000_000_000_000) - ) - - insert( - :reward, - address_hash: block.miner_hash, - block_hash: block.hash, - address_type: :uncle, - reward: Decimal.new(1_000_000_000_000_000_000) - ) - - block = Repo.preload(block, :rewards) - - {:ok, expected_value} = Wei.cast(3_000_000_000_000_000_000) - - assert Chain.block_combined_rewards(block) == expected_value - end - end - describe "transaction_token_transfer_type/1" do test "detects erc721 token transfer" do from_address_hash = "0x7a30272c902563b712245696f0a81c5a0e45ddc8" @@ -2875,113 +2415,6 @@ defmodule Explorer.ChainTest do end end - describe "fetch_first_trace/2" do - test "fetched first trace", %{ - json_rpc_named_arguments: json_rpc_named_arguments - } do - from_address_hash = "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca" - gas = 4_533_872 - - init = - "0x6060604052341561000f57600080fd5b60405160208061071a83398101604052808051906020019091905050806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506003600160006001600281111561007e57fe5b60ff1660ff168152602001908152602001600020819055506002600160006002808111156100a857fe5b60ff1660ff168152602001908152602001600020819055505061064a806100d06000396000f30060606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef" - - value = 0 - block_number = 39 - block_hash = "0x74c72ccabcb98b7ebbd7b31de938212b7e8814a002263b6569564e944d88f51f" - index = 0 - created_contract_address_hash = "0x1e0eaa06d02f965be2dfe0bc9ff52b2d82133461" - - created_contract_code = - "0x60606040526004361061008e576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063247b3210146100935780632ffdfc8a146100bc57806374294144146100f6578063ae4b1b5b14610125578063bf7370d11461017a578063d1104cb2146101a3578063eecd1079146101f8578063fcff021c14610221575b600080fd5b341561009e57600080fd5b6100a661024a565b6040518082815260200191505060405180910390f35b34156100c757600080fd5b6100e0600480803560ff16906020019091905050610253565b6040518082815260200191505060405180910390f35b341561010157600080fd5b610123600480803590602001909190803560ff16906020019091905050610276565b005b341561013057600080fd5b61013861037a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561018557600080fd5b61018d61039f565b6040518082815260200191505060405180910390f35b34156101ae57600080fd5b6101b66104d9565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561020357600080fd5b61020b610588565b6040518082815260200191505060405180910390f35b341561022c57600080fd5b6102346105bd565b6040518082815260200191505060405180910390f35b600060c8905090565b6000600160008360ff1660ff168152602001908152602001600020549050919050565b61027e6104d9565b73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415156102b757600080fd5b60008160ff161115156102c957600080fd5b6002808111156102d557fe5b60ff168160ff16111515156102e957600080fd5b6000821180156103125750600160008260ff1660ff168152602001908152602001600020548214155b151561031d57600080fd5b81600160008360ff1660ff168152602001908152602001600020819055508060ff167fe868bbbdd6cd2efcd9ba6e0129d43c349b0645524aba13f8a43bfc7c5ffb0889836040518082815260200191505060405180910390a25050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000806000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16638b8414c46000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561042f57600080fd5b6102c65a03f1151561044057600080fd5b5050506040518051905090508073ffffffffffffffffffffffffffffffffffffffff16630eaba26a6000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b15156104b857600080fd5b6102c65a03f115156104c957600080fd5b5050506040518051905091505090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1663a3b3fff16000604051602001526040518163ffffffff167c0100000000000000000000000000000000000000000000000000000000028152600401602060405180830381600087803b151561056857600080fd5b6102c65a03f1151561057957600080fd5b50505060405180519050905090565b60006105b860016105aa600261059c61039f565b6105e590919063ffffffff16565b61060090919063ffffffff16565b905090565b60006105e06105ca61039f565b6105d261024a565b6105e590919063ffffffff16565b905090565b60008082848115156105f357fe5b0490508091505092915050565b600080828401905083811015151561061457fe5b80915050929150505600a165627a7a723058206b7eef2a57eb659d5e77e45ab5bc074e99c6a841921038cdb931e119c6aac46c0029" - - gas_used = 382_953 - trace_address = [] - transaction_hash = "0x0fa6f723216dba694337f9bb37d8870725655bdf2573526a39454685659e39b1" - transaction_index = 0 - type = "create" - - if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do - expect(EthereumJSONRPC.Mox, :json_rpc, fn _json, _options -> - {:ok, - [ - %{ - id: 0, - result: %{ - "output" => "0x", - "stateDiff" => nil, - "trace" => [ - %{ - "action" => %{ - "from" => from_address_hash, - "gas" => integer_to_quantity(gas), - "init" => init, - "value" => integer_to_quantity(value) - }, - "blockNumber" => block_number, - "index" => index, - "result" => %{ - "address" => created_contract_address_hash, - "code" => created_contract_code, - "gasUsed" => integer_to_quantity(gas_used) - }, - "traceAddress" => trace_address, - "type" => type - } - ], - "transactionHash" => transaction_hash - } - } - ]} - end) - end - - {:ok, created_contract_address_hash_bytes} = Chain.string_to_address_hash(created_contract_address_hash) - {:ok, from_address_hash_bytes} = Chain.string_to_address_hash(from_address_hash) - {:ok, created_contract_code_bytes} = Data.cast(created_contract_code) - {:ok, init_bytes} = Data.cast(init) - {:ok, transaction_hash_bytes} = Chain.string_to_full_hash(transaction_hash) - {:ok, type_bytes} = Type.load(type) - value_wei = %Wei{value: Decimal.new(value)} - - assert Chain.fetch_first_trace( - [ - %{ - hash_data: transaction_hash, - block_hash: block_hash, - block_number: block_number, - transaction_index: transaction_index - } - ], - json_rpc_named_arguments - ) == { - :ok, - [ - %{ - block_index: 0, - block_number: block_number, - block_hash: block_hash, - call_type: nil, - created_contract_address_hash: created_contract_address_hash_bytes, - created_contract_code: created_contract_code_bytes, - from_address_hash: from_address_hash_bytes, - gas: gas, - gas_used: gas_used, - index: index, - init: init_bytes, - input: nil, - output: nil, - to_address_hash: nil, - trace_address: trace_address, - transaction_hash: transaction_hash_bytes, - type: type_bytes, - value: value_wei, - transaction_index: transaction_index - } - ] - } - end - end - describe "transaction_to_revert_reason/1" do test "returns correct revert_reason from DB" do # Error("No credit of that type") @@ -2998,7 +2431,7 @@ defmodule Explorer.ChainTest do gas: 27319, gas_price: "0x1b31d2900", value: "0x86b3", - input: %Explorer.Chain.Data{bytes: <<1>>} + input: %Data{bytes: <<1>>} ) |> with_block(insert(:block, number: 1)) diff --git a/apps/explorer/test/explorer/market/fetcher/coin_test.exs b/apps/explorer/test/explorer/market/fetcher/coin_test.exs index 134eaeca17cb..0ac9cfe77e8a 100644 --- a/apps/explorer/test/explorer/market/fetcher/coin_test.exs +++ b/apps/explorer/test/explorer/market/fetcher/coin_test.exs @@ -7,7 +7,6 @@ defmodule Explorer.Market.Fetcher.CoinTest do alias Explorer.Market.Token alias Explorer.Market.Fetcher.Coin alias Explorer.Market.Source.TestSource - alias Plug.Conn @moduletag :capture_log diff --git a/apps/explorer/test/explorer/market/fetcher/history_test.exs b/apps/explorer/test/explorer/market/fetcher/history_test.exs index db62601f6c3a..4b37903ef410 100644 --- a/apps/explorer/test/explorer/market/fetcher/history_test.exs +++ b/apps/explorer/test/explorer/market/fetcher/history_test.exs @@ -1,7 +1,6 @@ defmodule Explorer.Market.Fetcher.HistoryTest do use Explorer.DataCase, async: false - import Mox import Ecto.Query, only: [limit: 2, order_by: 2] alias Explorer.Market.{MarketHistory, Source} @@ -172,7 +171,7 @@ defmodule Explorer.Market.Fetcher.HistoryTest do assert get_in(new_state.types_states, [:market_cap_history, :finished?]) assert get_in(new_state.types_states, [:market_cap_history, :records]) == market_cap_records - assert {:noreply, final_state} = + assert {:noreply, _final_state} = History.handle_info({nil, {:tvl_history, {:ok, tvl_records}}}, new_state) assert record2 = Repo.get_by(MarketHistory, date: Enum.at(price_records, 1).date) @@ -236,7 +235,7 @@ defmodule Explorer.Market.Fetcher.HistoryTest do end end) - {:ok, pid} = History.start_link([]) + {:ok, _pid} = History.start_link([]) :timer.sleep(500) diff --git a/apps/explorer/test/explorer/market/source/coin_gecko_test.exs b/apps/explorer/test/explorer/market/source/coin_gecko_test.exs index de9619a42b12..da866050fb5c 100644 --- a/apps/explorer/test/explorer/market/source/coin_gecko_test.exs +++ b/apps/explorer/test/explorer/market/source/coin_gecko_test.exs @@ -2,7 +2,6 @@ defmodule Explorer.Market.Source.CoinGeckoTest do use ExUnit.Case - alias Explorer.Market.Token alias Explorer.Market.Source.CoinGecko alias Plug.Conn diff --git a/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs b/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs index e47afe96da4d..a1d437a6f1e8 100644 --- a/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs +++ b/apps/explorer/test/explorer/migrator/transactions_denormalization_migrator_test.exs @@ -1,7 +1,6 @@ defmodule Explorer.Migrator.TransactionsDenormalizationTest do use Explorer.DataCase, async: false - alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.Transaction alias Explorer.Migrator.{MigrationStatus, TransactionsDenormalization} alias Explorer.Repo diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index d6b8c9b26d9a..89554c347519 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -15,7 +15,7 @@ defmodule Indexer.Block.Fetcher do alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries} alias Explorer.Chain - alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei} + alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei, Withdrawal} alias Explorer.Chain.Block.Reward alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions, Uncles} alias Explorer.Chain.Cache.Blocks, as: BlocksCache @@ -450,7 +450,7 @@ defmodule Indexer.Block.Fetcher do defp update_withdrawals_cache([_ | _] = withdrawals) do %{index: index} = List.last(withdrawals) - Chain.upsert_count_withdrawals(index) + Withdrawal.upsert_count_withdrawals(index) end defp update_withdrawals_cache(_) do diff --git a/apps/indexer/lib/indexer/fetcher/block_reward.ex b/apps/indexer/lib/indexer/fetcher/block_reward.ex index d2f4cf5cdcfd..75b1b65351c2 100644 --- a/apps/indexer/lib/indexer/fetcher/block_reward.ex +++ b/apps/indexer/lib/indexer/fetcher/block_reward.ex @@ -62,7 +62,7 @@ defmodule Indexer.Fetcher.BlockReward do @impl BufferedTask def init(initial, reducer, _) do {:ok, final} = - Chain.stream_blocks_without_rewards( + Block.stream_blocks_without_rewards( initial, fn %{number: number}, acc -> reducer.(number, acc) @@ -111,7 +111,7 @@ defmodule Indexer.Fetcher.BlockReward do defp hash_string_by_number(numbers) when is_list(numbers) do numbers - |> Chain.block_hash_by_number() + |> Block.block_hash_by_number() |> Enum.into(%{}, fn {number, hash} -> {number, to_string(hash)} end) @@ -189,7 +189,7 @@ defmodule Indexer.Fetcher.BlockReward do timestamp_by_block_hash = beneficiaries_params |> Enum.map(& &1.block_hash) - |> Chain.timestamp_by_block_hash() + |> Block.timestamp_by_block_hash() Enum.map(beneficiaries_params, fn %{block_hash: block_hash_str} = beneficiary -> {:ok, block_hash} = Chain.string_to_full_hash(block_hash_str) @@ -209,7 +209,7 @@ defmodule Indexer.Fetcher.BlockReward do beneficiaries_params |> Stream.filter(&(&1.address_type == :validator)) |> Enum.map(& &1.block_hash) - |> Chain.gas_payment_by_block_hash() + |> Block.gas_payment_by_block_hash() Enum.map(beneficiaries_params, fn %{block_hash: block_hash, address_type: address_type} = beneficiary -> if address_type == :validator do diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index fe421f8244df..a89e73fd9662 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -196,7 +196,7 @@ defmodule Indexer.Fetcher.InternalTransaction do Enum.reduce(block_numbers, {:ok, []}, fn block_number, {:ok, acc_list} -> block_number - |> Chain.get_transactions_of_block_number() + |> Transaction.get_transactions_of_block_number() |> filter_non_traceable_transactions() |> Enum.map(¶ms/1) |> case do @@ -278,7 +278,7 @@ defmodule Indexer.Fetcher.InternalTransaction do defp filter_block_numbers(block_numbers, :block_number, json_rpc_named_arguments) do block_numbers |> Enum.uniq() - |> Chain.filter_non_refetch_needed_block_numbers() + |> Block.filter_non_refetch_needed_block_numbers() |> RangesHelper.filter_traceable_block_numbers() |> drop_genesis(json_rpc_named_arguments) end @@ -386,7 +386,7 @@ defmodule Indexer.Fetcher.InternalTransaction do block_number_to_block_hash = transactions_params_or_unique_numbers |> data_to_block_numbers(data_type) - |> Chain.block_hash_by_number() + |> Block.block_hash_by_number() |> Map.new(fn {block_number, block_hash} -> {block_number, Hash.to_string(block_hash)} diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex index f283614d70d2..b708f9c96356 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction/delete_queue.ex @@ -9,14 +9,15 @@ defmodule Indexer.Fetcher.InternalTransaction.DeleteQueue do import Ecto.Query - alias Explorer.{Chain, Repo} + alias Explorer.Repo alias Explorer.Chain.{ Block, InternalTransaction, PendingBlockOperation, PendingOperationsHelper, - PendingTransactionOperation + PendingTransactionOperation, + Transaction } alias Explorer.Chain.InternalTransaction.DeleteQueue @@ -87,7 +88,7 @@ defmodule Indexer.Fetcher.InternalTransaction.DeleteQueue do defp insert_pending_operations(block_numbers) do case PendingOperationsHelper.pending_operations_type() do "transactions" -> - transactions = Chain.get_transactions_of_block_numbers(block_numbers) + transactions = Transaction.get_transactions_of_block_numbers(block_numbers) pto_params = transactions diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex b/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex index 4925c0f5c9e4..129566ea0627 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/first_trace.ex @@ -6,7 +6,6 @@ defmodule Indexer.Fetcher.OnDemand.FirstTrace do use GenServer use Indexer.Fetcher, restart: :permanent - alias Explorer.Chain alias Explorer.Chain.{Import, InternalTransaction} alias Explorer.Chain.Import.Runner.InternalTransactions @@ -32,7 +31,7 @@ defmodule Indexer.Fetcher.OnDemand.FirstTrace do hash_string = to_string(transaction.hash) response = - Chain.fetch_first_trace( + InternalTransaction.fetch_first_trace( [ %{ block_hash: transaction.block_hash, diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex index 0ad4d2e5b8c6..c847b70b1727 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex @@ -534,7 +534,7 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do else Enum.reduce(block_numbers, [], fn block_number, acc_list -> block_number - |> Chain.get_transactions_of_block_number() + |> Transaction.get_transactions_of_block_number() |> InternalTransactionFetcher.filter_non_traceable_transactions() |> Enum.map( &%{ diff --git a/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex b/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex index 23f49d75612e..9da58bdc7fb4 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/eip1559_config_update.ex @@ -37,10 +37,10 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do alias EthereumJSONRPC.Blocks alias Explorer.Chain + alias Explorer.Chain.{Block, Hash} alias Explorer.Chain.Block.Reader.General, as: BlockGeneralReader alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Chain.Events.Subscriber - alias Explorer.Chain.Hash alias Explorer.Chain.Optimism.EIP1559ConfigUpdate alias Indexer.Fetcher.Optimism alias Indexer.Helper @@ -655,7 +655,7 @@ defmodule Indexer.Fetcher.Optimism.EIP1559ConfigUpdate do defp block_hash_by_number(block_number, json_rpc_named_arguments) do hash_from_db = [block_number] - |> Chain.block_hash_by_number() + |> Block.block_hash_by_number() |> Map.get(block_number) if is_nil(hash_from_db) do diff --git a/apps/indexer/lib/indexer/fetcher/pending_block_operations_sanitizer.ex b/apps/indexer/lib/indexer/fetcher/pending_block_operations_sanitizer.ex index 9d77b55c21ee..d7a4c046594c 100644 --- a/apps/indexer/lib/indexer/fetcher/pending_block_operations_sanitizer.ex +++ b/apps/indexer/lib/indexer/fetcher/pending_block_operations_sanitizer.ex @@ -7,8 +7,8 @@ defmodule Indexer.Fetcher.PendingBlockOperationsSanitizer do import Ecto.Query - alias Explorer.{Chain, Repo} - alias Explorer.Chain.PendingBlockOperation + alias Explorer.Chain.{PendingBlockOperation, Transaction} + alias Explorer.Repo alias Indexer.Fetcher.InternalTransaction @interval :timer.seconds(1) @@ -60,7 +60,7 @@ defmodule Indexer.Fetcher.PendingBlockOperationsSanitizer do |> update([pbo, po, b], set: [block_number: b.number]) |> Repo.update_all([], timeout: @timeout) - transactions = Chain.get_transactions_of_block_numbers(block_numbers) + transactions = Transaction.get_transactions_of_block_numbers(block_numbers) InternalTransaction.async_fetch(block_numbers, transactions, false) diff --git a/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex b/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex index 6a4fadb339b4..47af51e90697 100644 --- a/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/replaced_transaction.ex @@ -8,8 +8,7 @@ defmodule Indexer.Fetcher.ReplacedTransaction do require Logger - alias Explorer.Chain - alias Explorer.Chain.Hash + alias Explorer.Chain.{Hash, Transaction} alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.ReplacedTransaction.Supervisor, as: ReplacedTransactionSupervisor @@ -58,7 +57,7 @@ defmodule Indexer.Fetcher.ReplacedTransaction do def init(initial, reducer, _) do {:ok, final} = [:block_hash, :nonce, :from_address_hash, :hash] - |> Chain.stream_pending_transactions( + |> Transaction.stream_pending_transactions( initial, fn transaction_fields, acc -> transaction_fields @@ -119,11 +118,11 @@ defmodule Indexer.Fetcher.ReplacedTransaction do pending |> Enum.map(&pending_params/1) - |> Chain.find_and_update_replaced_transactions() + |> Transaction.find_and_update_replaced_transactions() realtime |> Enum.map(¶ms/1) - |> Chain.update_replaced_transactions() + |> Transaction.update_replaced_transactions() :ok rescue diff --git a/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex index 9b7e7261021a..c8470d3dd1f1 100644 --- a/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex +++ b/apps/indexer/lib/indexer/fetcher/signed_authorization_status.ex @@ -219,7 +219,7 @@ defmodule Indexer.Fetcher.SignedAuthorizationStatus do defp preload_entries(%{block_hash: block_hash} = entry) do block = block_hash - |> Chain.fetch_block_by_hash() + |> Block.fetch_block_by_hash() |> Repo.preload([:transactions, [transactions: :signed_authorizations]]) entry diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index 18613a958b85..a39de159b13a 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -21,7 +21,7 @@ defmodule Indexer.Fetcher.TokenBalance do alias Explorer.Chain alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} alias Explorer.Chain.Events.Publisher - alias Explorer.Chain.Hash + alias Explorer.Chain.{Hash, Token} alias Explorer.Utility.MissingBalanceOfToken alias Indexer.{BufferedTask, TokenBalances, Tracer} alias Indexer.Fetcher.TokenBalance.Supervisor, as: TokenBalanceSupervisor @@ -76,7 +76,7 @@ defmodule Indexer.Fetcher.TokenBalance do @impl BufferedTask def init(initial, reducer, _) do {:ok, final} = - Chain.stream_unfetched_token_balances( + TokenBalance.stream_unfetched_token_balances( initial, fn token_balance, acc -> token_balance @@ -262,7 +262,7 @@ defmodule Indexer.Fetcher.TokenBalance do end defp put_token_type_to_balance_object(token_balance) do - token_type = Chain.get_token_type(token_balance.token_contract_address_hash) + token_type = Token.get_token_type(token_balance.token_contract_address_hash) if token_type do Map.put(token_balance, :token_type, token_type) diff --git a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex index c25e113fbc15..3bae7f6bf194 100644 --- a/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_counters_updater.ex @@ -6,7 +6,6 @@ defmodule Indexer.Fetcher.TokenCountersUpdater do require Logger - alias Explorer.Chain alias Explorer.Chain.Token alias Explorer.MicroserviceInterfaces.MultichainSearch alias Indexer.BufferedTask @@ -63,7 +62,7 @@ defmodule Indexer.Fetcher.TokenCountersUpdater do entries |> Enum.reduce(%{}, fn token, acc -> - {transfers_count, holders_count} = Chain.fetch_token_counters(token.contract_address_hash, :infinity) + {transfers_count, holders_count} = Token.fetch_token_counters(token.contract_address_hash, :infinity) data_for_multichain = MultichainSearch.prepare_token_counters_for_queue(transfers_count, holders_count) Map.put(acc, token.contract_address_hash.bytes, data_for_multichain) diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex index bec01eff8ddf..4f0023a8e690 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/helper.ex @@ -4,7 +4,7 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do """ alias EthereumJSONRPC.NFT - alias Explorer.Chain + alias Explorer.Chain.Token alias Explorer.Chain.Token.Instance alias Explorer.Token.MetadataRetriever alias Indexer.NFTMediaHandler.Queue @@ -86,7 +86,7 @@ defmodule Indexer.Fetcher.TokenInstance.Helper do other |> Enum.map(fn {contract_address_hash, _token_id} -> contract_address_hash end) |> Enum.uniq() - |> Chain.get_token_types() + |> Token.get_token_types() |> Map.new(fn {hash, type} -> {hash.bytes, type} end) other diff --git a/apps/indexer/lib/indexer/fetcher/transaction_action.ex b/apps/indexer/lib/indexer/fetcher/transaction_action.ex index c67bd354c705..a979b3a38a4f 100644 --- a/apps/indexer/lib/indexer/fetcher/transaction_action.ex +++ b/apps/indexer/lib/indexer/fetcher/transaction_action.ex @@ -72,7 +72,7 @@ defmodule Indexer.Fetcher.TransactionAction do logger_metadata = Logger.metadata() Logger.metadata(fetcher: :transaction_action) - max_block_number = Chain.fetch_max_block_number() + max_block_number = Block.fetch_max_block_number() if last_block > max_block_number do Logger.warning( diff --git a/apps/indexer/lib/indexer/fetcher/uncle_block.ex b/apps/indexer/lib/indexer/fetcher/uncle_block.ex index f0fa3cdaa29b..649abae3fedd 100644 --- a/apps/indexer/lib/indexer/fetcher/uncle_block.ex +++ b/apps/indexer/lib/indexer/fetcher/uncle_block.ex @@ -12,6 +12,7 @@ defmodule Indexer.Fetcher.UncleBlock do alias Ecto.Changeset alias EthereumJSONRPC.Blocks alias Explorer.Chain + alias Explorer.Chain.Block, as: ExplorerChainBlock alias Explorer.Chain.Cache.{Accounts, Uncles} alias Explorer.Chain.Hash alias Indexer.{Block, BufferedTask, Tracer} @@ -68,7 +69,7 @@ defmodule Indexer.Fetcher.UncleBlock do @impl BufferedTask def init(initial, reducer, _) do {:ok, final} = - Chain.stream_unfetched_uncles( + ExplorerChainBlock.stream_unfetched_uncles( initial, fn uncle, acc -> uncle diff --git a/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex b/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex index 7c0f1b757446..964e6fbedd2f 100644 --- a/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex +++ b/apps/indexer/lib/indexer/migrator/recovery_weth_token_transfers.ex @@ -10,7 +10,7 @@ defmodule Indexer.Migrator.RecoveryWETHTokenTransfers do import Ecto.Query alias Explorer.{Chain, Repo} - alias Explorer.Chain.{Log, TokenTransfer} + alias Explorer.Chain.{Block, Log, TokenTransfer} alias Explorer.Migrator.MigrationStatus alias Indexer.Transform.TokenTransfers @@ -32,8 +32,8 @@ defmodule Indexer.Migrator.RecoveryWETHTokenTransfers do {:stop, :normal, state} migration_status -> - max_block_number = Chain.fetch_max_block_number() - min_block_number = Chain.fetch_min_block_number() + max_block_number = Block.fetch_max_block_number() + min_block_number = Block.fetch_min_block_number() state = (migration_status && migration_status.meta) || diff --git a/apps/indexer/lib/indexer/pending_ops_cleaner.ex b/apps/indexer/lib/indexer/pending_ops_cleaner.ex index 6c12f6810d96..a3de1b80bb2f 100644 --- a/apps/indexer/lib/indexer/pending_ops_cleaner.ex +++ b/apps/indexer/lib/indexer/pending_ops_cleaner.ex @@ -7,7 +7,7 @@ defmodule Indexer.PendingOpsCleaner do require Logger - alias Explorer.Chain + alias Explorer.Chain.Block @interval :timer.minutes(60) @@ -38,7 +38,7 @@ defmodule Indexer.PendingOpsCleaner do end defp clean_nonconsensus_pending_ops do - :ok = Chain.remove_nonconsensus_blocks_from_pending_ops() + :ok = Block.remove_nonconsensus_blocks_from_pending_ops() Logger.debug(fn -> "Non-consensus pending ops are cleaned" end) end diff --git a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex index 0f3ef9545915..bc4bd35e6815 100644 --- a/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex +++ b/apps/indexer/lib/indexer/pending_transactions_sanitizer.ex @@ -12,8 +12,8 @@ defmodule Indexer.PendingTransactionsSanitizer do import EthereumJSONRPC.Receipt, only: [to_elixir: 1] alias Ecto.Changeset - alias Explorer.{Chain, Repo} alias Explorer.Chain.{Block, Transaction} + alias Explorer.Repo defstruct interval: nil, json_rpc_named_arguments: [] @@ -63,7 +63,7 @@ defmodule Indexer.PendingTransactionsSanitizer do defp sanitize_pending_transactions(json_rpc_named_arguments) do receipts_batch_size = Application.get_env(:indexer, :receipts_batch_size) - pending_transactions_list_from_db = Chain.pending_transactions_list() + pending_transactions_list_from_db = Transaction.pending_transactions_list() id_to_params = id_to_params(pending_transactions_list_from_db) with {:ok, responses} <- @@ -147,7 +147,7 @@ defmodule Indexer.PendingTransactionsSanitizer do end defp fetch_block_and_invalidate(block_hash, pending_transaction, transaction) do - case Chain.fetch_block_by_hash(block_hash) do + case Block.fetch_block_by_hash(block_hash) do %{number: number, consensus: consensus} = block -> Logger.debug( "Corresponding number of the block with hash #{block_hash} to invalidate is #{number} and consensus #{consensus}", From 6dd23e1b04f7a78b5dfaa0720477e0fa1e3786b1 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 9 Jan 2026 11:48:53 +0300 Subject: [PATCH 0573/1006] fix: Validate block number in the api/v2/blocks/:block_number API endpoint (#13795) --- .../lib/block_scout_web/chain.ex | 36 ++++++++++++++++--- .../controllers/api/rpc/block_controller.ex | 33 +++++++++++------ .../block_scout_web/schemas/api/v2/general.ex | 2 +- .../views/block_transaction_view.ex | 4 +++ .../api/rpc/block_controller_test.exs | 9 ++++- .../api/v2/block_controller_test.exs | 7 ++++ .../block_transaction_controller_test.exs | 16 +++------ .../block_withdrawal_controller_test.exs | 18 +++------- .../test/support/test_helper.ex | 21 +++++++++++ 9 files changed, 103 insertions(+), 43 deletions(-) create mode 100644 apps/block_scout_web/test/support/test_helper.ex diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 5fd9139fd974..751eaef9e6bf 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -52,6 +52,7 @@ defmodule BlockScoutWeb.Chain do Withdrawal } + alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Optimism.Deposit, as: OptimismDeposit alias Explorer.Chain.Optimism.FrameSequence, as: OptimismFrameSequence alias Explorer.Chain.Optimism.InteropMessage, as: OptimismInteropMessage @@ -833,14 +834,33 @@ defmodule BlockScoutWeb.Chain do %PagingOptions{options | page_number: new_page_number, page_size: new_page_size} end - @spec param_to_block_number(binary()) :: {:ok, integer()} | {:error, :invalid} - def param_to_block_number(formatted_number) when is_binary(formatted_number) do + @spec param_to_block_number(binary(), boolean()) :: {:ok, integer()} | {:error, :invalid} | {:error, :not_found} + def param_to_block_number(_number, validate_max_block_number? \\ true) + + def param_to_block_number(formatted_number, validate_max_block_number?) when is_binary(formatted_number) do case Integer.parse(formatted_number) do - {number, ""} -> {:ok, number} - _ -> {:error, :invalid} + {number, ""} -> + validate_block_number(number, validate_max_block_number?) + + _ -> + {:error, :invalid} end end + @spec param_to_block_number(integer(), boolean()) :: {:ok, integer()} | {:error, :invalid} | {:error, :not_found} + def param_to_block_number(number, validate_max_block_number?) when is_integer(number), + do: validate_block_number(number, validate_max_block_number?) + + defp validate_block_number(number, validate_max_block_number?) when is_integer(number) and number >= 0 do + if not validate_max_block_number? or (validate_max_block_number? and number <= BlockNumber.get_max()) do + {:ok, number} + else + {:error, :not_found} + end + end + + defp validate_block_number(_, _), do: {:error, :invalid} + @doc """ Converts a timestamp string to a `DateTime.t()` struct for block timestamp queries. @@ -1211,12 +1231,18 @@ defmodule BlockScoutWeb.Chain do {:error, :invalid} -> {:error, {:invalid, :number}} + + {:error, :not_found} -> + {:error, :not_found} end end def parse_block_hash_or_number_param(number) when is_integer(number) do - {:ok, :number, number} + case param_to_block_number(number) do + {:ok, number} -> {:ok, :number, number} + {:error, :not_found} -> {:error, :not_found} + end end @doc """ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex index 6cd7c76a3034..c5782b644d3f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex @@ -8,6 +8,9 @@ defmodule BlockScoutWeb.API.RPC.BlockController do alias Explorer.Chain.Cache.Counters.AverageBlockTime alias Timex.Duration + @invalid_block_number "Invalid block number" + @do_not_exist "Block does not exist" + @doc """ Calculates the total reward for mining a specific block. @@ -41,13 +44,13 @@ defmodule BlockScoutWeb.API.RPC.BlockController do render(conn, :block_reward, block: block) else {:block_param, :error} -> - render(conn, :error, error: "Query parameter 'blockno' is required") + render(conn, :error, error: query_param_is_required("blockno")) {:error, :invalid} -> - render(conn, :error, error: "Invalid block number") + render(conn, :error, error: @invalid_block_number) {:error, :not_found} -> - render(conn, :error, error: "Block does not exist") + render(conn, :error, error: @do_not_exist) end end @@ -74,7 +77,7 @@ defmodule BlockScoutWeb.API.RPC.BlockController do @spec getblockcountdown(Plug.Conn.t(), map()) :: Plug.Conn.t() def getblockcountdown(conn, params) do with {:block_param, {:ok, unsafe_target_block_number}} <- {:block_param, Map.fetch(params, "blockno")}, - {:ok, target_block_number} <- ChainWeb.param_to_block_number(unsafe_target_block_number), + {:ok, target_block_number} <- ChainWeb.param_to_block_number(unsafe_target_block_number, false), {:max_block, current_block_number} when not is_nil(current_block_number) <- {:max_block, BlockNumber.get_max()}, {:average_block_time, average_block_time} when is_struct(average_block_time) <- @@ -91,10 +94,10 @@ defmodule BlockScoutWeb.API.RPC.BlockController do ) else {:block_param, :error} -> - render(conn, :error, error: "Query parameter 'blockno' is required") + render(conn, :error, error: query_param_is_required("blockno")) {:error, :invalid} -> - render(conn, :error, error: "Invalid block number") + render(conn, :error, error: @invalid_block_number) {:average_block_time, {:error, :disabled}} -> render(conn, :error, error: "Average block time calculating is disabled, so getblockcountdown is not available") @@ -140,19 +143,19 @@ defmodule BlockScoutWeb.API.RPC.BlockController do render(conn, block_number: block_number) else {:timestamp_param, :error} -> - render(conn, :error, error: "Query parameter 'timestamp' is required") + render(conn, :error, error: query_param_is_required("timestamp")) {:closest_param, :error} -> - render(conn, :error, error: "Query parameter 'closest' is required") + render(conn, :error, error: query_param_is_required("closest")) {:error, :invalid_timestamp} -> - render(conn, :error, error: "Invalid `timestamp` param") + render(conn, :error, error: invalid_param("timestamp")) {:error, :invalid_closest} -> - render(conn, :error, error: "Invalid `closest` param") + render(conn, :error, error: invalid_param("closest")) {:error, :not_found} -> - render(conn, :error, error: "Block does not exist") + render(conn, :error, error: @do_not_exist) end end @@ -177,4 +180,12 @@ defmodule BlockScoutWeb.API.RPC.BlockController do render(conn, :eth_block_number, number: max_block_number, id: id) end + + defp query_param_is_required(param) do + "Query parameter '#{param}' is required" + end + + defp invalid_param(param) do + "Invalid `#{param}` param" + end end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 777cdfd5a483..841fc2867753 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -255,7 +255,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do %Parameter{ name: :block_hash_or_number_param, in: :path, - schema: %Schema{anyOf: [%Schema{type: :integer}, FullHash]}, + schema: %Schema{anyOf: [%Schema{type: :integer, minimum: 0}, FullHash]}, required: true, description: "Block hash or number in the path" } diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex index 025f2782d5c6..28747a57aad3 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex @@ -7,6 +7,10 @@ defmodule BlockScoutWeb.BlockTransactionView do gettext("Easy Cowboy! This block does not exist yet!") end + def block_not_found_message({:error, :not_found}) do + gettext("Easy Cowboy! This block does not exist yet!") + end + def block_not_found_message({:ok, false}) do gettext("This block has not been processed yet.") end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs index e9af24b278ef..ea4060ab75d8 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/block_controller_test.exs @@ -169,6 +169,9 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do start_supervised!(AverageBlockTime) Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id()) + on_exit(fn -> Application.put_env(:explorer, AverageBlockTime, enabled: false, cache_period: 1_800_000) end) @@ -183,7 +186,11 @@ defmodule BlockScoutWeb.API.RPC.BlockControllerTest do first_timestamp = Timex.now() for i <- 1..current_block_number do - insert(:block, number: i, timestamp: Timex.shift(first_timestamp, seconds: i * average_block_time)) + insert(:block, + number: i, + timestamp: Timex.shift(first_timestamp, seconds: i * average_block_time), + consensus: true + ) end AverageBlockTime.refresh() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index afcca5e7a7fa..026cf31b7fc8 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -259,6 +259,12 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do } = json_response(request_2, 422) end + test "return 404 when block number exceeds allowed range", %{conn: conn} do + request = get(conn, "/api/v2/blocks/3000000000") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + test "return 404 on non existing block", %{conn: conn} do block = build(:block) @@ -271,6 +277,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do test "get 'Block lost consensus' message", %{conn: conn} do block = insert(:block, consensus: false) + insert(:block, consensus: true) hash = to_string(block.hash) request_1 = get(conn, "/api/v2/blocks/#{block.number}") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs index a4c369877ff1..0ffce8c69189 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs @@ -3,6 +3,8 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_transaction_path: 3] + alias BlockScoutWeb.TestHelper + describe "GET index/2" do test "with invalid block number", %{conn: conn} do conn = get(conn, block_transaction_path(conn, :index, "unknown")) @@ -57,7 +59,7 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do conn = get(conn, block_transaction_path(conn, :index, block.number)) - assert_block_above_tip(conn) + TestHelper.assert_block_above_tip(conn) end test "non-consensus block number above consensus block number is treated as consensus number above tip", %{ @@ -71,7 +73,7 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do conn = get(conn, block_transaction_path(conn, :index, block.number)) - assert_block_above_tip(conn) + TestHelper.assert_block_above_tip(conn) end test "returns transactions for consensus block hash", %{conn: conn} do @@ -193,14 +195,4 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do assert html_response(conn, 200) =~ miner_name end end - - defp assert_block_above_tip(conn) do - assert conn - |> html_response(404) - |> Floki.parse_fragment() - |> elem(1) - |> Floki.find(~S|.error-descr|) - |> Floki.text() - |> String.trim() == "Easy Cowboy! This block does not exist yet!" - end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs index ef1e2222a7e4..28677283031f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs @@ -3,6 +3,8 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do import BlockScoutWeb.Routers.WebRouter.Helpers, only: [block_withdrawal_path: 3] + alias BlockScoutWeb.TestHelper + describe "GET index/2" do test "with invalid block number", %{conn: conn} do conn = get(conn, block_withdrawal_path(conn, :index, "unknown")) @@ -23,7 +25,7 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do conn = get(conn, block_withdrawal_path(conn, :index, block.number + 1)) - assert_block_above_tip(conn) + TestHelper.assert_block_above_tip(conn) end test "returns withdrawals for the block", %{conn: conn} do @@ -50,7 +52,7 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do conn = get(conn, block_withdrawal_path(conn, :index, block.number)) - assert_block_above_tip(conn) + TestHelper.assert_block_above_tip(conn) end test "non-consensus block number above consensus block number is treated as consensus number above tip", %{ @@ -64,7 +66,7 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do conn = get(conn, block_withdrawal_path(conn, :index, block.number)) - assert_block_above_tip(conn) + TestHelper.assert_block_above_tip(conn) end test "does not return transactions for invalid block hash", %{conn: conn} do @@ -128,14 +130,4 @@ defmodule BlockScoutWeb.BlockWithdrawalControllerTest do assert html_response(conn, 200) =~ miner_name end end - - defp assert_block_above_tip(conn) do - assert conn - |> html_response(404) - |> Floki.parse_fragment() - |> elem(1) - |> Floki.find(~S|.error-descr|) - |> Floki.text() - |> String.trim() == "Easy Cowboy! This block does not exist yet!" - end end diff --git a/apps/block_scout_web/test/support/test_helper.ex b/apps/block_scout_web/test/support/test_helper.ex new file mode 100644 index 000000000000..3c649934f629 --- /dev/null +++ b/apps/block_scout_web/test/support/test_helper.ex @@ -0,0 +1,21 @@ +defmodule BlockScoutWeb.TestHelper do + @moduledoc false + use BlockScoutWeb.ConnCase + + @doc """ + Asserts that the connection response contains the "block above tip" error message. + + ## Parameters + - `conn`: A Phoenix connection struct with a 404 response + + ## Returns + - Assertion passes if the error message matches, raises otherwise + """ + @spec assert_block_above_tip(Plug.Conn.t()) :: true + def assert_block_above_tip(conn) do + html = html_response(conn, 404) + {:ok, document} = Floki.parse_fragment(html) + [error_element | _] = Floki.find(document, ~S|.error-descr|) + assert Floki.text(error_element) |> String.trim() == "Easy Cowboy! This block does not exist yet!" + end +end From 6e6bc6915e9b8d3eb4ca54361493775f1b04a09b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 9 Jan 2026 11:54:15 +0300 Subject: [PATCH 0574/1006] feat: REST API endpoint to list uncompleted DB migrations (#13835) --- .../controllers/api/v2/config_controller.ex | 52 ++++++++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 1 + .../api/v2/config_controller_test.exs | 53 +++++++++++++++++++ .../lib/explorer/migrator/migration_status.ex | 15 ++++++ apps/explorer/test/support/factory.ex | 8 +++ 5 files changed, 129 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 300cdd034734..744702b5f7bb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.V2.ConfigController do use OpenApiSpex.ControllerSpecs alias Explorer.Chain.SmartContract + alias Explorer.Migrator.MigrationStatus alias OpenApiSpex.Schema plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) @@ -170,4 +171,55 @@ defmodule BlockScoutWeb.API.V2.ConfigController do |> put_status(200) |> json(%{languages: SmartContract.language_strings()}) end + + operation :db_background_migrations, + summary: "Uncompleted background migrations", + description: "Returns list of background migrations that are not yet completed.", + parameters: base_params(), + responses: [ + ok: + {"Uncompleted background migrations.", "application/json", + %Schema{ + type: :object, + properties: %{ + migrations: %Schema{ + type: :array, + items: %Schema{ + type: :object, + properties: %{ + migration_name: %Schema{type: :string}, + status: %Schema{type: :string}, + inserted_at: %Schema{type: :string, format: "date-time"}, + updated_at: %Schema{type: :string, format: "date-time"}, + meta: %Schema{type: :object, nullable: true} + } + } + } + } + }}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/config/db_background_migrations` endpoint. + """ + @spec db_background_migrations(Plug.Conn.t(), map()) :: Plug.Conn.t() + def db_background_migrations(conn, _params) do + migrations = MigrationStatus.fetch_uncompleted_migrations() + + conn + |> put_status(200) + |> json(%{ + migrations: + Enum.map(migrations, fn migration -> + %{ + migration_name: migration.migration_name, + status: migration.status, + inserted_at: migration.inserted_at, + updated_at: migration.updated_at, + meta: migration.meta + } + end) + }) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index ee1c0555b2b4..0a67cca7805d 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -143,6 +143,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do get("/indexer", V2.ConfigController, :indexer) get("/public-metrics", V2.ConfigController, :public_metrics) get("/smart-contracts/languages", V2.ConfigController, :languages_list) + get("/db-background-migrations", V2.ConfigController, :db_background_migrations) if @chain_identity == {:optimism, :celo} do get("/celo", V2.ConfigController, :celo) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs index 438694e048b9..fdc3fdc5915c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -62,4 +62,57 @@ defmodule BlockScoutWeb.API.V2.ConfigControllerTest do end end end + + describe "/config/db-background-migrations" do + test "returns empty list when there are no uncompleted migrations", %{conn: conn} do + request = get(conn, "/api/v2/config/db-background-migrations") + assert %{"migrations" => migrations} = json_response(request, 200) + assert is_list(migrations) + end + + test "returns list of uncompleted migrations", %{conn: conn} do + insert(:migration_status, migration_name: "test_migration", status: "started") + + request = get(conn, "/api/v2/config/db-background-migrations") + assert %{"migrations" => migrations} = json_response(request, 200) + + assert Enum.any?(migrations, fn m -> m["migration_name"] == "test_migration" and m["status"] == "started" end) + end + + test "does not return completed migrations", %{conn: conn} do + insert(:migration_status, migration_name: "completed_migration", status: "completed") + insert(:migration_status, migration_name: "started_migration", status: "started") + + request = get(conn, "/api/v2/config/db-background-migrations") + assert %{"migrations" => migrations} = json_response(request, 200) + + assert Enum.all?(migrations, fn m -> m["status"] != "completed" end) + assert Enum.any?(migrations, fn m -> m["migration_name"] == "started_migration" end) + end + + test "returns migration with meta data", %{conn: conn} do + insert(:migration_status, + migration_name: "test_migration", + status: "started", + meta: %{"max_block_number" => 8_151_758} + ) + + request = get(conn, "/api/v2/config/db-background-migrations") + assert %{"migrations" => migrations} = json_response(request, 200) + + migration = Enum.find(migrations, fn m -> m["migration_name"] == "test_migration" end) + assert migration["meta"]["max_block_number"] == 8_151_758 + end + + test "returns migration timestamps", %{conn: conn} do + insert(:migration_status, migration_name: "test_migration", status: "started") + + request = get(conn, "/api/v2/config/db-background-migrations") + assert %{"migrations" => migrations} = json_response(request, 200) + + migration = Enum.find(migrations, fn m -> m["migration_name"] == "test_migration" end) + assert Map.has_key?(migration, "inserted_at") + assert Map.has_key?(migration, "updated_at") + end + end end diff --git a/apps/explorer/lib/explorer/migrator/migration_status.ex b/apps/explorer/lib/explorer/migrator/migration_status.ex index a2a16b0bcd95..6b2620829348 100644 --- a/apps/explorer/lib/explorer/migrator/migration_status.ex +++ b/apps/explorer/lib/explorer/migrator/migration_status.ex @@ -169,4 +169,19 @@ defmodule Explorer.Migrator.MigrationStatus do |> fetch_migration_statuses_query() |> Repo.all() end + + @doc """ + Fetches all uncompleted migrations (status != 'completed'). + + ## Returns + + - A list of uncompleted migration status structs. + + """ + @spec fetch_uncompleted_migrations() :: list(__MODULE__.t()) + def fetch_uncompleted_migrations do + __MODULE__ + |> where([ms], ms.status != "completed") + |> Repo.all() + end end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 298ce072f84a..39adf0058007 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1679,4 +1679,12 @@ defmodule Explorer.Factory do transaction: transaction } end + + def migration_status_factory do + %MigrationStatus{ + migration_name: sequence("migration_", &"migration_#{&1}"), + status: "started", + meta: nil + } + end end From 7a598aacaede2f18c3b497e919a406436bbe1c25 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 9 Jan 2026 12:07:51 +0300 Subject: [PATCH 0575/1006] fix: Handle normal termination of Indexer.Fetcher.OnDemand.ContractCode process (#13828) --- .../lib/indexer/fetcher/on_demand/contract_code.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex index 8f19f6a4a25a..77fd227317e1 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/contract_code.ex @@ -192,6 +192,17 @@ defmodule Indexer.Fetcher.OnDemand.ContractCode do {:reply, result, state} end + @impl true + def handle_info({:DOWN, _ref, :process, _pid, :normal}, state) do + {:noreply, state} + end + + def handle_info(message, state) do + Logger.warning("Unexpected message received in handle_info/2: #{inspect(message)}") + + {:noreply, state} + end + # An initial threshold to fetch smart-contract bytecode on-demand @spec update_threshold_ms() :: non_neg_integer() defp update_threshold_ms do From 585766dc401214d637972f8aa763dd86f18b090f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 9 Jan 2026 12:30:13 +0300 Subject: [PATCH 0576/1006] chore: Set unique block numbers in handle_partially_imported_blocks/1 (#13657) --- apps/explorer/lib/explorer/chain/import.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 7dbb8bd63b3a..20027df58fad 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -445,7 +445,7 @@ defmodule Explorer.Chain.Import do end defp handle_partially_imported_blocks(%{blocks: %{params: blocks_params}}) do - block_numbers = Enum.map(blocks_params, & &1.number) + block_numbers = blocks_params |> Enum.map(& &1.number) |> Enum.uniq() Block.set_refetch_needed(block_numbers) Import.Runner.Blocks.process_blocks_consensus(blocks_params) From 456fbc3e2478da379f107e010d044e650384b77d Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 9 Jan 2026 15:54:11 +0300 Subject: [PATCH 0577/1006] feat: Expose CHAIN_TYPE in the REST API (#13805) --- .../controllers/api/v2/config_controller.ex | 25 +++++++++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 1 + .../api/v2/config_controller_test.exs | 23 +++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index 744702b5f7bb..e7d34efd9333 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -2,6 +2,8 @@ defmodule BlockScoutWeb.API.V2.ConfigController do use BlockScoutWeb, :controller use OpenApiSpex.ControllerSpecs + use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type] + alias Explorer.Chain.SmartContract alias Explorer.Migrator.MigrationStatus alias OpenApiSpex.Schema @@ -10,6 +12,29 @@ defmodule BlockScoutWeb.API.V2.ConfigController do tags(["config"]) + operation :backend, + summary: "Get backend environment configuration", + description: "Returns non-secret backend environment variables in the snake case (e.g., chain_type).", + parameters: base_params(), + responses: [ + ok: + {"Backend environment configuration.", "application/json", + %Schema{type: :object, properties: %{chain_type: %Schema{type: :string, nullable: true}}}}, + unprocessable_entity: JsonErrorResponse.response() + ] + + @doc """ + Function to handle GET requests to `/api/v2/config/backend` endpoint. + """ + @spec backend(Plug.Conn.t(), map()) :: Plug.Conn.t() + def backend(conn, _params) do + chain_type = chain_type() + + conn + |> put_status(200) + |> json(%{"chain_type" => chain_type}) + end + operation :backend_version, summary: "Get backend version", description: "Returns application backend version string.", diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 0a67cca7805d..1b192c204123 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -138,6 +138,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end scope "/config" do + get("/backend", V2.ConfigController, :backend) get("/backend-version", V2.ConfigController, :backend_version) get("/csv-export", V2.ConfigController, :csv_export) get("/indexer", V2.ConfigController, :indexer) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs index fdc3fdc5915c..7297f7332f66 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs @@ -1,6 +1,29 @@ defmodule BlockScoutWeb.API.V2.ConfigControllerTest do use BlockScoutWeb.ConnCase + @chain_type Application.compile_env(:explorer, :chain_type) + + describe "/config/backend" do + test "returns chain_type when configured", %{conn: conn} do + request = get(conn, "/api/v2/config/backend") + response = json_response(request, 200) + + assert %{"chain_type" => chain_type} = response + assert is_binary(chain_type) + end + + test "returns the configured chain type value", %{conn: conn} do + request = get(conn, "/api/v2/config/backend") + response = json_response(request, 200) + + assert %{"chain_type" => chain_type} = response + + # Compare string representations + expected_chain_type = if is_atom(@chain_type), do: Atom.to_string(@chain_type), else: @chain_type + assert chain_type == expected_chain_type + end + end + describe "/config/backend-version" do test "get json rps url if set", %{conn: conn} do version = "v6.3.0-beta" From 52b0bc0fa4e44d6bef60f137c241e6610dcbd187 Mon Sep 17 00:00:00 2001 From: Alexander Kolotov Date: Sun, 11 Jan 2026 11:38:59 -0600 Subject: [PATCH 0578/1006] feat: missed L1-to-L2 messages catchup on Arbitrum rollups (#13792) --- .../chain/arbitrum/reader/indexer/messages.ex | 150 ++++- .../arbitrum/tracking_messages_on_l1.ex | 537 ++++++++++++------ .../fetcher/arbitrum/utils/db/messages.ex | 136 ++++- .../arbitrum/workers/new_messages_to_l2.ex | 477 ++++++++++++++-- config/runtime.exs | 5 +- cspell.json | 1 + docker-compose/envs/common-blockscout.env | 2 + 7 files changed, 1064 insertions(+), 244 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex index 6c6d746c8deb..4a16c171402e 100644 --- a/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex +++ b/apps/explorer/lib/explorer/chain/arbitrum/reader/indexer/messages.ex @@ -51,16 +51,53 @@ defmodule Explorer.Chain.Arbitrum.Reader.Indexer.Messages do end @doc """ - Retrieves the number of the earliest L1 block where an L1-to-L2 message was discovered. + Retrieves the highest message ID for an L1-to-L2 message that has both the originating transaction information and has been relayed on L2, within or before a specified safe L1 block. + + This function filters messages to only include those whose originating + transaction occurred at or before the safe block number, ensuring that + only finalized L1 data is considered. + + ## Parameters + - `safe_block`: The L1 block number threshold. Only messages with + originating transaction block numbers at or below this value are + considered. ## Returns - - The number of L1 block, or `nil` if no L1-to-L2 messages are found. + - The highest message ID for a fully indexed and relayed L1-to-L2 message + within the safe block range, or `nil` if no such messages are found. """ - @spec l1_block_of_earliest_discovered_message_to_l2() :: FullBlock.block_number() | nil - def l1_block_of_earliest_discovered_message_to_l2 do + @spec highest_safe_fully_indexed_message_to_l2(FullBlock.block_number()) :: non_neg_integer() | nil + def highest_safe_fully_indexed_message_to_l2(safe_block) + when is_integer(safe_block) and safe_block >= 0 do query = from(msg in Message, - select: msg.originating_transaction_block_number, + select: msg.message_id, + where: + msg.direction == :to_l2 and + not is_nil(msg.originating_transaction_block_number) and + msg.originating_transaction_block_number <= ^safe_block and + msg.status == :relayed, + order_by: [desc: msg.message_id], + limit: 1 + ) + + query + |> Repo.one(timeout: :infinity) + end + + @doc """ + Retrieves the message ID and L1 block number of the earliest L1-to-L2 message that was discovered. + + ## Returns + - A tuple `{message_id, block_number}` for the earliest discovered L1-to-L2 message, + or `nil` if no L1-to-L2 messages are found. + """ + @spec earliest_discovered_message_to_l2() :: + {non_neg_integer(), FullBlock.block_number()} | nil + def earliest_discovered_message_to_l2 do + query = + from(msg in Message, + select: {msg.message_id, msg.originating_transaction_block_number}, where: msg.direction == :to_l2 and not is_nil(msg.originating_transaction_block_number), order_by: [asc: msg.message_id], limit: 1 @@ -370,4 +407,107 @@ defmodule Explorer.Chain.Arbitrum.Reader.Indexer.Messages do Repo.all(query) end + + @doc """ + Retrieves the message IDs of L1-to-L2 messages that have completion transactions + but are missing originating transaction information within a specified message ID range. + + This function identifies messages where the completion transaction has been discovered + on L2 (indicated by the `:relayed` status), but the corresponding originating transaction + on L1 has not yet been indexed. + + ## Parameters + - `start_message_id`: The starting message ID of the range (inclusive). + - `end_message_id`: The ending message ID of the range (inclusive). + + ## Returns + - A list of message IDs for L1-to-L2 messages that have completion transactions + but lack originating transaction information, ordered by message ID in descending + order (highest to lowest). + """ + @spec messages_to_l2_completed_but_originating_info_missed(non_neg_integer(), non_neg_integer()) :: [ + non_neg_integer() + ] + def messages_to_l2_completed_but_originating_info_missed(start_message_id, end_message_id) + when is_integer(start_message_id) and start_message_id >= 0 and + is_integer(end_message_id) and end_message_id >= 0 and + start_message_id <= end_message_id do + query = + from(msg in Message, + where: + msg.direction == :to_l2 and + msg.message_id >= ^start_message_id and + msg.message_id <= ^end_message_id and + is_nil(msg.originating_transaction_block_number) and + msg.status == :relayed, + select: msg.message_id, + order_by: [desc: msg.message_id] + ) + + Repo.all(query, timeout: :infinity) + end + + @doc """ + Retrieves the message ID and L1 block number of the closest preceding L1-to-L2 message + that has originating transaction information. + + This function finds the nearest message with a lower message ID that contains + an originating transaction block number. + + ## Parameters + - `message_id`: The message ID to find the closest preceding message for. + + ## Returns + - A tuple `{message_id, block_number}` for the closest preceding message with originating information, + or `nil` if no such message exists. + """ + @spec l1_block_of_closest_preceding_message_to_l2(non_neg_integer()) :: + {non_neg_integer(), FullBlock.block_number()} | nil + def l1_block_of_closest_preceding_message_to_l2(message_id) + when is_integer(message_id) and message_id >= 0 do + query = + from(msg in Message, + where: + msg.direction == :to_l2 and + msg.message_id < ^message_id and + not is_nil(msg.originating_transaction_block_number), + select: {msg.message_id, msg.originating_transaction_block_number}, + order_by: [desc: msg.message_id], + limit: 1 + ) + + Repo.one(query, timeout: :infinity) + end + + @doc """ + Retrieves the message ID and L1 block number of the closest following L1-to-L2 message + that has originating transaction information. + + This function finds the nearest message with a higher message ID that contains + an originating transaction block number. + + ## Parameters + - `message_id`: The message ID to find the closest following message for. + + ## Returns + - A tuple `{message_id, block_number}` for the closest following message with originating information, + or `nil` if no such message exists. + """ + @spec l1_block_of_closest_following_message_to_l2(non_neg_integer()) :: + {non_neg_integer(), FullBlock.block_number()} | nil + def l1_block_of_closest_following_message_to_l2(message_id) + when is_integer(message_id) and message_id >= 0 do + query = + from(msg in Message, + where: + msg.direction == :to_l2 and + msg.message_id > ^message_id and + not is_nil(msg.originating_transaction_block_number), + select: {msg.message_id, msg.originating_transaction_block_number}, + order_by: [asc: msg.message_id], + limit: 1 + ) + + Repo.one(query, timeout: :infinity) + end end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/tracking_messages_on_l1.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/tracking_messages_on_l1.ex index a0bd16bc36b9..6f80a5e9bae6 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/tracking_messages_on_l1.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/tracking_messages_on_l1.ex @@ -5,63 +5,102 @@ defmodule Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1 do This module is responsible for continuously monitoring and importing new messages initiated from Layer 1 (L1) to Arbitrum's Layer 2 (L2), as well as discovering and processing historical messages that were sent previously but have not yet - been processed. - - The fetcher's operation is divided into 3 phases, each initiated by sending - specific messages: - - `:init_worker`: Initializes the worker with the required configuration for message - tracking. - - `:check_new_msgs_to_rollup`: Processes new L1-to-L2 messages appearing on L1 as - the blockchain progresses. - - `:check_historical_msgs_to_rollup`: Retrieves historical L1-to-L2 messages that - were missed if the message synchronization process did not start from the - Arbitrum rollup's inception. - - While the `:init_worker` message is sent only once during the fetcher startup, - the subsequent sending of `:check_new_msgs_to_rollup` and - `:check_historical_msgs_to_rollup` forms the operation cycle of the fetcher. - - Discovery of L1-to-L2 messages is executed by requesting logs on L1 that correspond - to the `MessageDelivered` event emitted by the Arbitrum bridge contract. - Cross-chain messages are composed of information from the logs' data as well as from - the corresponding transaction details. To get the transaction details, RPC calls - `eth_getTransactionByHash` are made in chunks. + been imported into the database. + + The fetcher uses a BufferedTask-based approach for task scheduling, where each + type of task is scheduled independently with appropriate intervals. This provides + better error isolation and task management. + + Task types include: + - `:check_new`: Discovers new L1-to-L2 messages appearing on L1 as the blockchain + progresses. This task runs continuously with a configurable recheck interval. + - `:check_historical`: Retrieves historical L1-to-L2 messages that were missed + if the message synchronization process did not start from the Arbitrum rollup's + inception. This task walks backward from the current block until it reaches the + rollup initialization block, at which point it stops scheduling itself. + - `:check_missing_origination`: Discovers L1-to-L2 messages that have completion + information on L2 but are missing origination transaction details from L1. This + task works backward from the most recent fully indexed message in configurable + ranges, filling gaps where L2 indexing ran ahead of L1 event discovery. + + Task scheduling behavior: + The `:check_new` task runs continuously with a standard recheck interval, while + the `:check_historical` and `:check_missing_origination` tasks use a shorter + catchup interval (2 seconds) to expedite backfilling. Once a catchup task + completes (historical reaches the rollup init block, missing origination reaches + the earliest discovered message), it is no longer scheduled. + + Tasks that fail abnormally within a configurable threshold period will enter a + cooldown state for 10 minutes to prevent resource exhaustion. + + Initialization architecture: + The module uses a two-phase initialization process: + 1. Static configuration in `child_spec`: + - L1 RPC parameters and rollup addresses + - Recheck intervals for live and catchup work + - Failure threshold for cooldown triggering + 2. Dynamic initialization in `init/3`: + - Queries the L1 bridge address from the RPC + - Determines initial cursors from the database and L1 network state + - Prepares the task_data map with both `:check_new` and `:check_historical` + cursors + + Discovery of L1-to-L2 messages is executed by requesting logs on L1 that + correspond to the `MessageDelivered` event emitted by the Arbitrum bridge + contract. Cross-chain messages are composed of information from the logs' data + as well as from the corresponding transaction details. To get the transaction + details, RPC calls `eth_getTransactionByHash` are made in chunks. """ - use GenServer - use Indexer.Fetcher + use Indexer.Fetcher, restart: :permanent - import Indexer.Fetcher.Arbitrum.Utils.Helper, only: [increase_duration: 2] + import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_warning: 1] alias EthereumJSONRPC.Arbitrum, as: ArbitrumRpc - - alias Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 - + alias Indexer.BufferedTask alias Indexer.Fetcher.Arbitrum.Utils.Db.Messages, as: DbMessages alias Indexer.Fetcher.Arbitrum.Utils.Rpc + alias Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 alias Indexer.Helper, as: IndexerHelper - require Logger - - def child_spec(start_link_arguments) do - spec = %{ - id: __MODULE__, - start: {__MODULE__, :start_link, start_link_arguments}, - restart: :transient, - type: :worker - } - - Supervisor.child_spec(spec, []) - end - - def start_link(args, gen_server_options \\ []) do - GenServer.start_link(__MODULE__, args, Keyword.put_new(gen_server_options, :name, __MODULE__)) - end - - @impl GenServer - def init(args) do - Logger.metadata(fetcher: :arbitrum_bridge_l1) - + @behaviour BufferedTask + + # 250ms interval between processing buffered entries. + # Note: This interval has no effect on retry behavior - when a task fails or + # is explicitly retried via :retry return value, it is re-queued and the next + # available task is picked up immediately without this delay. + @idle_interval 250 + # Only one task at a time + @max_concurrency 1 + # Process one task per batch + @max_batch_size 1 + + # 10 minutes cooldown interval for failed tasks + @cooldown_interval :timer.minutes(10) + + # Catchup task (historical messages discovery) needs to run as quickly as possible + # since it is only needed when indexing a rollup chain that already has many blocks. + # This interval is hardcoded since these tasks should complete rapidly and don't need + # to be configurable. + @catchup_recheck_interval :timer.seconds(2) + + @stoppable_tasks [:check_historical, :check_missing_origination] + + @typep fetcher_task :: :check_new | :check_historical | :check_missing_origination + @typep rescheduled_tasks :: :check_historical | :check_missing_origination + @typep queued_task :: :init_worker | {non_neg_integer(), fetcher_task()} + @typep completion_status :: %{rescheduled_tasks() => boolean()} + @typep fetcher_tasks_data :: %{fetcher_task() => map()} + + # Creates a child specification for the BufferedTask supervisor. Extracts and merges + # configuration from application environment, sets up task intervals, initializes + # RPC configurations for parent and rollup chains, and creates the initial state + # with task scheduling parameters. Returns a transient supervisor child spec with + # the configured BufferedTask. + def child_spec([init_options, gen_server_options]) do + {json_rpc_named_arguments, init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) + + # Extract configuration from application environment config_common = Application.get_all_env(:indexer)[Indexer.Fetcher.Arbitrum] l1_rpc = config_common[:l1_rpc] l1_rpc_block_range = config_common[:l1_rpc_block_range] @@ -72,159 +111,313 @@ defmodule Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1 do config_tracker = Application.get_all_env(:indexer)[__MODULE__] recheck_interval = config_tracker[:recheck_interval] + missed_message_ids_range = config_tracker[:missed_message_ids_range] + + failure_interval_threshold = + config_tracker[:failure_interval_threshold] || min(20 * recheck_interval, :timer.minutes(10)) + + # Configure intervals for each task type + intervals = %{ + check_new: recheck_interval, + check_historical: @catchup_recheck_interval, + check_missing_origination: @catchup_recheck_interval + } + + # Set up initial configuration structure + initial_config = %{ + json_l1_rpc_named_arguments: IndexerHelper.json_rpc_named_arguments(l1_rpc), + json_l2_rpc_named_arguments: json_rpc_named_arguments, + l1_rpc_block_range: l1_rpc_block_range, + l1_rpc_chunk_size: l1_rpc_chunk_size, + l1_rollup_address: l1_rollup_address, + l1_start_block: l1_start_block, + l1_rollup_init_block: l1_rollup_init_block, + recheck_interval: recheck_interval, + failure_interval_threshold: failure_interval_threshold, + catchup_recheck_interval: @catchup_recheck_interval, + missed_message_ids_range: missed_message_ids_range + } - Process.send(self(), :init_worker, []) - - {:ok, - %{ - config: %{ - json_l2_rpc_named_arguments: args[:json_rpc_named_arguments], - json_l1_rpc_named_arguments: IndexerHelper.json_rpc_named_arguments(l1_rpc), - recheck_interval: recheck_interval, - l1_rpc_chunk_size: l1_rpc_chunk_size, - l1_rpc_block_range: l1_rpc_block_range, - l1_rollup_address: l1_rollup_address, - l1_start_block: l1_start_block, - l1_rollup_init_block: l1_rollup_init_block - }, - data: %{} - }} + # Initial state structure + initial_state = %{ + config: initial_config, + intervals: intervals, + task_data: %{}, + completed_tasks: %{} + } + + buffered_task_init_options = + defaults() + |> Keyword.merge(init_options) + |> Keyword.put(:state, initial_state) + + Supervisor.child_spec( + {BufferedTask, [{__MODULE__, buffered_task_init_options}, gen_server_options]}, + id: __MODULE__, + restart: :transient + ) end - @impl GenServer - def handle_info({ref, _result}, state) do - Process.demonitor(ref, [:flush]) - {:noreply, state} + defp defaults do + [ + flush_interval: @idle_interval, + max_concurrency: @max_concurrency, + max_batch_size: @max_batch_size, + poll: false, + task_supervisor: __MODULE__.TaskSupervisor, + metadata: [fetcher: :arbitrum_l1_messages_tracker] + ] end - # Initializes the worker for discovering new and historical L1-to-L2 messages. + @impl BufferedTask + def init(initial, reducer, _state) do + reducer.(:init_worker, initial) + end + + @impl BufferedTask + @spec run([queued_task()], map()) :: {:ok, map()} | {:retry, [queued_task()], map()} | :retry + def run(tasks, state) + + # Initializes the worker state and schedules all tasks for execution. Configures the + # initial state with RPC/DB values, sets up tasks in order (new batches/confirmations/ + # executions followed by historical ones), defines their completion states, and + # conditionally disables missing batches discovery and settlement transactions + # finalization based on configuration. + def run([:init_worker], state) do + # Complete configuration with RPC/DB dependent values + configured_state = initialize_workers(state) + + # Get current timestamp for initial task scheduling + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + # Define all possible tasks with their initial timestamps + default_tasks_to_run = [ + {now, :check_new}, + {now, :check_historical}, + {now, :check_missing_origination} + ] + + # Define default completion state for stoppable tasks + default_completion_state = %{ + check_historical: false, + check_missing_origination: false + } + + # Apply conditional disabling logic + {tasks_to_run, completion_state} = + {default_tasks_to_run, default_completion_state} + |> maybe_disable_check_missing_origination(configured_state) + + BufferedTask.buffer(__MODULE__, tasks_to_run, false) + + updated_state = Map.put(configured_state, :completed_tasks, completion_state) + {:ok, updated_state} + end + + # Executes or returns a task back to the queue based on its timeout and failure threshold. # - # This function prepares the initial parameters for the message discovery process. - # It fetches the Arbitrum bridge address and determines the starting block for - # new message discovery. If the starting block is not configured (set to a default - # value), the latest block number from L1 is used as the start. It also calculates - # the end block for historical message discovery. + # The function evaluates three conditions in sequence: + # 1. Whether the task's timeout has elapsed (current time >= timeout) + # 2. Whether the task hasn't exceeded the failure threshold or is an initial task (timeout == 0) # - # After setting these parameters, it immediately transitions to discovering new - # messages by sending the `:check_new_msgs_to_rollup` message. + # If all conditions are met, executes the appropriate task runner. Otherwise: + # - If timeout hasn't elapsed: Returns the task to the queue with the same timeout + # - If failure threshold exceeded: Applies a 10-minute cooldown and reschedules # # ## Parameters - # - `:init_worker`: The message triggering the initialization. - # - `state`: The current state of the process, containing configuration for data - # initialization and further message discovery. + # - `timeout`: Unix timestamp in milliseconds when the task should execute + # - `task_tag`: Atom identifying the type of task to run + # - `state`: Current state containing configuration and intervals # # ## Returns - # - `{:noreply, new_state}` where `new_state` is updated with the bridge address, - # determined start block for new messages, and calculated end block for - # historical messages. - @impl GenServer - def handle_info( - :init_worker, - %{config: %{l1_rollup_address: _, json_l1_rpc_named_arguments: _, l1_start_block: _}, data: _} = state - ) do + # - `{:ok, state}` on successful execution + # - `{:retry, [{timeout, task_tag}], state}` when task needs to be rescheduled + def run([{timeout, task_tag}], state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + with {:timeout_elapsed, true} <- {:timeout_elapsed, timeout <= now}, + {:threshold_ok, true} <- {:threshold_ok, now - timeout <= state.config.failure_interval_threshold}, + {:runner_defined, runner} when not is_nil(runner) <- {:runner_defined, Map.get(task_runners(), task_tag)} do + runner.(state) + else + {:timeout_elapsed, false} -> + {:retry, [{timeout, task_tag}], state} + + {:threshold_ok, false} -> + new_timeout = now + @cooldown_interval + + log_warning( + "Task #{task_tag} has been failing abnormally, applying cooldown for #{div(@cooldown_interval, 1000)} seconds" + ) + + {:retry, [{new_timeout, task_tag}], state} + + {:runner_defined, nil} -> + log_warning("Unknown task type: #{inspect(task_tag)}") + {:ok, state} + end + end + + defp task_runners do + %{ + check_new: &handle_check_new/1, + check_historical: &handle_check_historical/1, + check_missing_origination: &handle_check_missing_origination/1 + } + end + + # Initializes the worker state with contract addresses and block information + @spec initialize_workers(%{ + :config => map(), + optional(any()) => any() + }) :: %{:config => map(), :task_data => fetcher_tasks_data(), optional(any()) => any()} + defp initialize_workers(state) do + json_l1_rpc_named_arguments = state.config.json_l1_rpc_named_arguments + l1_rollup_address = state.config.l1_rollup_address + %{bridge: bridge_address} = ArbitrumRpc.get_contracts_for_rollup( - state.config.l1_rollup_address, + l1_rollup_address, :bridge, - state.config.json_l1_rpc_named_arguments + json_l1_rpc_named_arguments ) - l1_start_block = Rpc.get_l1_start_block(state.config.l1_start_block, state.config.json_l1_rpc_named_arguments) + l1_start_block = Rpc.get_l1_start_block(state.config.l1_start_block, json_l1_rpc_named_arguments) + + {safe_l1_block, _latest_l1_block} = + Rpc.get_safe_and_latest_l1_blocks(json_l1_rpc_named_arguments, state.config.l1_rpc_block_range) + new_msg_to_l2_start_block = DbMessages.l1_block_to_discover_latest_message_to_l2(l1_start_block) - historical_msg_to_l2_end_block = DbMessages.l1_block_to_discover_earliest_message_to_l2(l1_start_block - 1) - - Process.send(self(), :check_new_msgs_to_rollup, []) - - new_state = - state - |> Map.put( - :config, - Map.merge(state.config, %{ - l1_start_block: l1_start_block, - l1_bridge_address: bridge_address - }) - ) - |> Map.put( - :data, - Map.merge(state.data, %{ - new_msg_to_l2_start_block: new_msg_to_l2_start_block, - historical_msg_to_l2_end_block: historical_msg_to_l2_end_block - }) - ) - {:noreply, new_state} - end + %{ + l1_block_to_discover_earlier_messages: historical_msg_to_l2_end_block, + already_discovered_message_id: earliest_discovered_msg_id + } = DbMessages.inspect_earliest_discovered_message_to_l2(l1_start_block - 1) - # Initiates the process to discover and handle new L1-to-L2 messages initiated from L1. - # - # This function discovers new messages from L1 to L2 by retrieving logs for the - # calculated L1 block range. Discovered events are used to compose messages, which - # are then stored in the database. - # - # After processing, the function immediately transitions to discovering historical - # messages by sending the `:check_historical_msgs_to_rollup` message. - # - # ## Parameters - # - `:check_new_msgs_to_rollup`: The message that triggers the handler. - # - `state`: The current state of the fetcher, containing configuration and data - # needed for message discovery. - # - # ## Returns - # - `{:noreply, new_state}` where the starting block for the next new L1-to-L2 - # message discovery iteration is updated based on the results of the current - # iteration. - @impl GenServer - def handle_info(:check_new_msgs_to_rollup, %{data: _} = state) do - {handle_duration, {:ok, end_block}} = - :timer.tc(&NewMessagesToL2.discover_new_messages_to_l2/1, [ - state - ]) - - Process.send(self(), :check_historical_msgs_to_rollup, []) - - new_data = - Map.merge(state.data, %{ - duration: increase_duration(state.data, handle_duration), - new_msg_to_l2_start_block: end_block + 1 + highest_safe_fully_indexed_msg_id = + DbMessages.highest_safe_fully_indexed_message_to_l2(safe_l1_block, nil) + + updated_config = + Map.merge(state.config, %{ + l1_start_block: l1_start_block, + l1_bridge_address: bridge_address }) - {:noreply, %{state | data: new_data}} + task_data = %{ + check_new: %{ + start_block: new_msg_to_l2_start_block + }, + check_historical: %{ + end_block: historical_msg_to_l2_end_block + }, + check_missing_origination: %{ + end_message_id: + if(is_nil(highest_safe_fully_indexed_msg_id), do: nil, else: highest_safe_fully_indexed_msg_id - 1), + earliest_discovered_message_id: earliest_discovered_msg_id || 0, + safe_l1_block: safe_l1_block + } + } + + %{state | config: updated_config, task_data: task_data} + end + + # Conditionally disables missing origination discovery based on whether there + # are any fully indexed messages to work backward from. If `end_message_id` in + # the task data is nil, it means no fully indexed messages exist yet, so there's + # nothing to backfill and the task is disabled. + @spec maybe_disable_check_missing_origination({[queued_task()], completion_status()}, %{ + :task_data => %{ + :check_missing_origination => %{ + :end_message_id => non_neg_integer() | nil, + optional(any()) => any() + }, + optional(any()) => any() + }, + optional(any()) => any() + }) :: {[queued_task()], completion_status()} + defp maybe_disable_check_missing_origination({tasks, completion}, state) do + if is_nil(state.task_data.check_missing_origination.end_message_id) do + log_info("Missing origination discovery is disabled (no fully indexed messages found)") + {delete_task_by_tag(tasks, :check_missing_origination), %{completion | check_missing_origination: true}} + else + {tasks, completion} + end end - # Initiates the process to discover and handle historical L1-to-L2 messages initiated from L1. + # Helper to remove a task from the tasks list by its tag + @spec delete_task_by_tag([queued_task()], fetcher_task()) :: [queued_task()] + defp delete_task_by_tag(tasks, tag) do + Enum.reject(tasks, fn + {_timeout, task_tag} -> task_tag == tag + _other -> false + end) + end + + # Handles the discovery of new L1-to-L2 messages + defp handle_check_new(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + {:ok, updated_state} = NewMessagesToL2.check_new(state) + + next_run_time = now + updated_state.intervals[:check_new] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_new}], false) + + {:ok, updated_state} + end + + # Handles the discovery of historical L1-to-L2 messages + defp handle_check_historical(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + {:ok, updated_state} = NewMessagesToL2.check_historical(state) + + if rescheduled?(:check_historical, updated_state) do + next_run_time = now + updated_state.intervals[:check_historical] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_historical}], false) + end + + {:ok, updated_state} + end + + # Handles the discovery of L1-to-L2 messages that have completion information + # but are missing origination transaction details. # - # This function discovers historical messages by retrieving logs for a calculated L1 block range. - # The discovered events are then used to compose messages to be stored in the database. + # This function works backward from the most recent fully indexed message, + # checking ranges of message IDs for those that have been relayed on L2 but + # lack L1 origination data. It stops when reaching the earliest discovered + # message ID that was captured during initialization. # - # After processing, as it is the final handler in the loop, it schedules the - # `:check_new_msgs_to_rollup` message to initiate the next iteration. The scheduling of this - # message is delayed, taking into account the time spent on the previous handler's execution. + # The function retrieves the current safe L1 block, then calls the worker + # module to perform the actual discovery and import. Based on the worker's + # response, it either schedules the next iteration or marks the task as + # complete. # # ## Parameters - # - `:check_historical_msgs_to_rollup`: The message that triggers the handler. - # - `state`: The current state of the fetcher, containing configuration and data needed for - # message discovery. + # - `state`: Current fetcher state containing configuration, task data, and + # completion tracking. # # ## Returns - # - `{:noreply, new_state}` where the end block for the next L1-to-L2 message discovery - # iteration is updated based on the results of the current iteration. - @impl GenServer - def handle_info(:check_historical_msgs_to_rollup, %{config: %{recheck_interval: _}, data: _} = state) do - {handle_duration, {:ok, start_block}} = - :timer.tc(&NewMessagesToL2.discover_historical_messages_to_l2/1, [ - state - ]) - - next_timeout = max(state.config.recheck_interval - div(increase_duration(state.data, handle_duration), 1000), 0) - - Process.send_after(self(), :check_new_msgs_to_rollup, next_timeout) - - new_data = - Map.merge(state.data, %{ - duration: 0, - historical_msg_to_l2_end_block: start_block - 1 - }) + # - `{:ok, updated_state}` where the state includes: + # - Updated `task_data.check_missing_origination.end_message_id` cursor + # - Updated `completed_tasks.check_missing_origination` flag + defp handle_check_missing_origination(state) do + now = DateTime.to_unix(DateTime.utc_now(), :millisecond) + + {:ok, updated_state} = NewMessagesToL2.check_missing_origination(state) - {:noreply, %{state | data: new_data}} + if rescheduled?(:check_missing_origination, updated_state) do + next_run_time = now + updated_state.intervals[:check_missing_origination] + BufferedTask.buffer(__MODULE__, [{next_run_time, :check_missing_origination}], false) + end + + {:ok, updated_state} + end + + # Returns true if the task should be rescheduled (not marked as completed) + @spec rescheduled?(atom(), %{:completed_tasks => completion_status(), optional(any()) => any()}) :: boolean() + defp rescheduled?(task_tag, state) when task_tag in @stoppable_tasks do + not Map.get(state.completed_tasks, task_tag) end + + defp rescheduled?(_task_tag, _state), do: true end diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/messages.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/messages.ex index e52d2738c473..49833e9cedae 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/messages.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/utils/db/messages.ex @@ -52,26 +52,65 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Db.Messages do end @doc """ - Calculates the next L1 block number to start the search for messages sent to L2 - that precede the earliest message already discovered. + Retrieves the highest message ID for an L1-to-L2 message that has been fully indexed within or before a specified safe L1 block, meaning both the originating transaction on L1 and the completion transaction on L2 have been discovered. + + This function is useful for determining the progress of message indexing + and identifying the starting point for discovering messages that are + missing originating transaction information, while ensuring only finalized + L1 data is considered. ## Parameters - - `value_if_nil`: The default value to return if no L1-to-L2 messages have been discovered. + - `safe_block`: The L1 block number threshold for filtering messages. + - `value_if_nil`: The default value to return if no fully indexed L1-to-L2 + messages exist within the safe block range. ## Returns - - The L1 block number immediately preceding the earliest discovered message to L2, - or `value_if_nil` if no messages to L2 have been found. + - The highest message ID for a fully indexed and relayed L1-to-L2 message + within the safe block range, or `value_if_nil` if no such messages are + found. """ - @spec l1_block_to_discover_earliest_message_to_l2(nil | FullBlock.block_number()) :: nil | FullBlock.block_number() - def l1_block_to_discover_earliest_message_to_l2(value_if_nil) - when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do - case Reader.l1_block_of_earliest_discovered_message_to_l2() do + @spec highest_safe_fully_indexed_message_to_l2(FullBlock.block_number(), nil | non_neg_integer()) :: + nil | non_neg_integer() + def highest_safe_fully_indexed_message_to_l2(safe_block, value_if_nil) + when is_integer(safe_block) and safe_block >= 0 and + ((is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil)) do + case Reader.highest_safe_fully_indexed_message_to_l2(safe_block) do nil -> - log_warning(@no_messages_warning) + log_warning("No fully indexed messages to L2 found in DB within safe block #{safe_block}") value_if_nil value -> - value - 1 + value + end + end + + @doc """ + Inspects the earliest discovered L1-to-L2 message and returns its message ID + along with the L1 block number to start the search for messages that precede it. + + ## Parameters + - `value_if_nil`: The default L1 block number to use if no L1-to-L2 messages have been discovered. + + ## Returns + - A map containing: + - `already_discovered_message_id`: The message ID of the earliest discovered message, + or `nil` if no messages have been found. + - `l1_block_to_discover_earlier_messages`: The L1 block number immediately preceding + the earliest discovered message, or `value_if_nil` if no messages have been found. + """ + @spec inspect_earliest_discovered_message_to_l2(nil | FullBlock.block_number()) :: %{ + already_discovered_message_id: non_neg_integer() | nil, + l1_block_to_discover_earlier_messages: FullBlock.block_number() | nil + } + def inspect_earliest_discovered_message_to_l2(value_if_nil) + when (is_integer(value_if_nil) and value_if_nil >= 0) or is_nil(value_if_nil) do + case Reader.earliest_discovered_message_to_l2() do + nil -> + log_warning(@no_messages_warning) + %{already_discovered_message_id: nil, l1_block_to_discover_earlier_messages: value_if_nil} + + {message_id, block_number} -> + %{already_discovered_message_id: message_id, l1_block_to_discover_earlier_messages: block_number - 1} end end @@ -279,6 +318,81 @@ defmodule Indexer.Fetcher.Arbitrum.Utils.Db.Messages do Reader.get_uncompleted_l1_to_l2_messages_ids() end + @doc """ + Retrieves the message IDs of L1-to-L2 messages that have completion transactions + but are missing originating transaction information within a specified message ID range. + + This function identifies messages where the completion transaction has been discovered + on L2, but the corresponding originating transaction on L1 has not yet been indexed. + + ## Parameters + - `start_message_id`: The starting message ID of the range (inclusive). + - `end_message_id`: The ending message ID of the range (inclusive). + + ## Returns + - A list of message IDs for L1-to-L2 messages that have completion transactions + but lack originating transaction information, ordered by message ID in descending + order (highest to lowest). + """ + @spec messages_to_l2_completed_but_originating_info_missed(non_neg_integer(), non_neg_integer()) :: [ + non_neg_integer() + ] + def messages_to_l2_completed_but_originating_info_missed(start_message_id, end_message_id) + when is_integer(start_message_id) and start_message_id >= 0 and + is_integer(end_message_id) and end_message_id >= 0 and + start_message_id <= end_message_id do + Reader.messages_to_l2_completed_but_originating_info_missed(start_message_id, end_message_id) + end + + @doc """ + Determines the L1 block range to search for the originating transaction of an L1-to-L2 + message with the given message ID. + + The function finds the closest preceding and following messages that have originating + transaction information and returns their message IDs and L1 block numbers. If either + bound cannot be determined from existing messages, the provided fallback block values + are used and the message_id is set to nil. + + ## Parameters + - `message_id`: The message ID to find the L1 block range for. + - `fallback_min_block`: The fallback L1 block number to use as the lower bound if no + preceding message with originating information is found (typically the L1 block where + the rollup was initialized). + - `fallback_max_block`: The fallback L1 block number to use as the upper bound if no + following message with originating information is found (typically the current L1 chain tip). + + ## Returns + - A map with `:lower` and `:higher` keys, each containing a map with: + - `:message_id` - The message ID of the bound (or `nil` if fallback was used) + - `:block_number` - The L1 block number of the bound + """ + @spec l1_block_range_for_message_to_l2( + non_neg_integer(), + FullBlock.block_number(), + FullBlock.block_number() + ) :: %{ + lower: %{message_id: non_neg_integer() | nil, block_number: FullBlock.block_number()}, + higher: %{message_id: non_neg_integer() | nil, block_number: FullBlock.block_number()} + } + def l1_block_range_for_message_to_l2(message_id, fallback_min_block, fallback_max_block) + when is_integer(message_id) and message_id >= 0 and + is_integer(fallback_min_block) and is_integer(fallback_max_block) and + fallback_min_block <= fallback_max_block do + lower = + case Reader.l1_block_of_closest_preceding_message_to_l2(message_id) do + nil -> %{message_id: nil, block_number: fallback_min_block} + {msg_id, block_number} -> %{message_id: msg_id, block_number: block_number} + end + + higher = + case Reader.l1_block_of_closest_following_message_to_l2(message_id) do + nil -> %{message_id: nil, block_number: fallback_max_block} + {msg_id, block_number} -> %{message_id: msg_id, block_number: block_number} + end + + %{lower: lower, higher: higher} + end + @spec message_to_map(Message.t()) :: Message.to_import() defp message_to_map(message) do [ diff --git a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex index cafeda8fc507..edff8365c098 100644 --- a/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex +++ b/apps/indexer/lib/indexer/fetcher/arbitrum/workers/new_messages_to_l2.ex @@ -3,9 +3,14 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do Manages the discovery and processing of new and historical L1-to-L2 messages initiated on L1 for an Arbitrum rollup. This module is responsible for identifying and importing messages that are initiated - from Layer 1 (L1) to Arbitrum's Layer 2 (L2). It handles both new messages that are - currently being sent to L2 and historical messages that were sent in the past but - have not yet been processed by the system. + from Layer 1 (L1) to Arbitrum's Layer 2 (L2). It handles three discovery scenarios: + + 1. **New messages**: Currently being sent to L2 as the chain progresses forward + 2. **Historical messages**: Sent in the past before indexing started, discovered + by working backward from the earliest known message + 3. **Missing origination**: Messages that have completion information on L2 but + lack origination transaction details from L1, typically occurring when L2 + indexing runs ahead of L1 event discovery The initiated messages are identified by analyzing logs associated with `MessageDelivered` events emitted by the Arbitrum bridge contract. These logs @@ -21,6 +26,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do import Indexer.Fetcher.Arbitrum.Utils.Logging, only: [log_info: 1, log_debug: 1] + alias Indexer.Fetcher.Arbitrum.Utils.Db.Messages, as: DbMessages alias Indexer.Fetcher.Arbitrum.Utils.Helper, as: ArbitrumHelper alias Indexer.Fetcher.Arbitrum.Utils.Rpc alias Indexer.Helper, as: IndexerHelper @@ -47,17 +53,15 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do ## Parameters - A map containing: - `config`: Configuration settings including JSON RPC arguments for L1, Arbitrum - bridge address, RPC block range, and chunk size for RPC calls. - - `data`: Contains the starting block number for new L1-to-L2 message discovery. + bridge address, RPC block range, and chunk size for RPC calls. + - `data`: Contains the starting block number for new L1-to-L2 message discovery + and the end block number for historical messages discovery. ## Returns - - `{:ok, end_block}`: On successful discovery and processing, where `end_block` - indicates the necessity to consider next block range in the - following iteration of new message discovery. - - `{:ok, start_block - 1}`: when no new blocks on L1 produced from the last - iteration of the new message discovery. + - `{:ok, updated_state}` with `task_data.check_new.start_block` moved forward + when work was done, or left unchanged when no new blocks were present. """ - @spec discover_new_messages_to_l2(%{ + @spec check_new(%{ :config => %{ :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), :l1_bridge_address => binary(), @@ -65,14 +69,14 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do :l1_rpc_chunk_size => non_neg_integer(), optional(any()) => any() }, - :data => %{ - :new_msg_to_l2_start_block => non_neg_integer(), - :historical_msg_to_l2_end_block => non_neg_integer(), + :task_data => %{ + :check_new => %{start_block: non_neg_integer()}, + :check_historical => %{end_block: non_neg_integer()}, optional(any()) => any() }, optional(any()) => any() - }) :: {:ok, non_neg_integer()} - def discover_new_messages_to_l2( + }) :: {:ok, map()} + def check_new( %{ config: %{ json_l1_rpc_named_arguments: json_rpc_named_arguments, @@ -80,11 +84,11 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do l1_rpc_block_range: rpc_block_range, l1_bridge_address: bridge_address }, - data: %{ - new_msg_to_l2_start_block: start_block, - historical_msg_to_l2_end_block: historical_msg_to_l2_end_block + task_data: %{ + check_new: %{start_block: start_block}, + check_historical: %{end_block: historical_msg_to_l2_end_block} } - } = _state + } = state ) do # It is necessary to revisit some of the previous blocks to ensure that # no information is missed due to reorgs or RPC node inconsistency behind @@ -100,6 +104,8 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do if safe_start_block <= end_block do log_info("Block range for discovery new messages from L1: #{safe_start_block}..#{end_block}") + # Since the block range to discover messages could be wider than `rpc_block_range` + # it is required to divide it in smaller chunks. # credo:disable-for-lines:16 Credo.Check.Refactor.PipeChainStart new_messages_amount = ArbitrumHelper.execute_for_block_range_in_chunks( @@ -122,9 +128,17 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do Publisher.broadcast(%{new_messages_to_arbitrum_amount: new_messages_amount}, :realtime) end - {:ok, end_block} + # Cursor is moved forward for the next iteration of the new messages discovery + updated_state = + state + |> ArbitrumHelper.update_fetcher_task_data(:check_new, %{ + start_block: end_block + 1 + }) + + # Advance the new-message cursor for the next live run. + {:ok, updated_state} else - {:ok, start_block - 1} + {:ok, state} end end @@ -137,22 +151,23 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do from both the log and the corresponding L1 transaction, and importing them into the database. + It updates the end-block cursor and marks historical work complete when the init + block is reached. + ## Parameters - A map containing: - `config`: Configuration settings including JSON RPC arguments for L1, Arbitrum - bridge address, rollup initialization block, block range, and chunk - size for RPC calls. - - `data`: Contains the end block for historical L1-to-L2 message discovery. + bridge address, rollup initialization block, block range, and chunk size for + RPC calls. + - `task_data`: where `check_historical` contains the end block for historical + L1-to-L2 message discovery. ## Returns - - `{:ok, start_block}`: On successful discovery and processing, where `start_block` - indicates the necessity to consider another block range in - the next iteration of message discovery. - - `{:ok, l1_rollup_init_block}`: If the discovery process already reached rollup - initialization block and no discovery action was - necessary. + - `{:ok, updated_state}` with the historical cursor moved backward and + `completed_tasks.check_historical` set when the init block boundary is + reached. """ - @spec discover_historical_messages_to_l2(%{ + @spec check_historical(%{ :config => %{ :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), :l1_bridge_address => binary(), @@ -161,13 +176,11 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do :l1_rpc_chunk_size => non_neg_integer(), optional(any()) => any() }, - :data => %{ - :historical_msg_to_l2_end_block => non_neg_integer(), - optional(any()) => any() - }, + :task_data => %{:check_historical => %{end_block: non_neg_integer()}, optional(any()) => any()}, + :completed_tasks => %{:check_historical => boolean(), optional(any()) => any()}, optional(any()) => any() - }) :: {:ok, non_neg_integer()} - def discover_historical_messages_to_l2( + }) :: {:ok, map()} + def check_historical( %{ config: %{ json_l1_rpc_named_arguments: json_rpc_named_arguments, @@ -176,8 +189,8 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do l1_bridge_address: bridge_address, l1_rollup_init_block: l1_rollup_init_block }, - data: %{historical_msg_to_l2_end_block: end_block} - } = _state + task_data: %{check_historical: %{end_block: end_block}} + } = state ) do if end_block >= l1_rollup_init_block do start_block = max(l1_rollup_init_block, end_block - rpc_block_range + 1) @@ -192,12 +205,256 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do chunk_size ) - {:ok, start_block} + updated_state = + state + |> ArbitrumHelper.update_fetcher_task_data(:check_historical, %{ + end_block: start_block - 1 + }) + |> set_historical_completion(false) + + # Move the historical cursor backward for the next catchup run. + {:ok, updated_state} else - {:ok, l1_rollup_init_block} + # The rollup init block is reached, flag that next iteration of + # historical messages discovery is not needed. + updated_state = + state + |> ArbitrumHelper.update_fetcher_task_data(:check_historical, %{ + end_block: l1_rollup_init_block - 1 + }) + |> set_historical_completion(true) + + # Stop scheduling historical checks because the rollup init block was reached. + {:ok, updated_state} end end + @doc """ + Discovers L1-to-L2 messages that have completion transactions on L2 but are missing origination transaction information from L1. + + This function works backward from the most recent fully indexed message, + checking ranges of message IDs for those marked as `:relayed` but lacking + L1 origination data. For each missing message found, it determines the + appropriate L1 block range to search, discovers the originating transaction, + and imports it. + + The function processes messages in order from highest to lowest ID within + each range. When multiple missing messages are found, each is processed + sequentially, with L1 block bounds recalculated after each import to use + newly discovered messages as range delimiters. + + Note: When consecutive messages are missing (no fully indexed message between + them), processing one may also import others in the same L1 block range. This + results in some messages being imported multiple times, which is harmless due + to Chain.import's upsert behavior. + + The cursor advances by the configured range size with each iteration. The + task completes and stops when the next iteration's range would extend below + the earliest discovered message ID captured during initialization. + + **Precondition:** This function assumes it is only called when there are + fully indexed messages to work from (i.e., `end_message_id` is not nil). + The task initialization logic ensures this function is never called when + there's no work to do. + + ## Parameters + - A map containing: + - `config`: Configuration settings including: + - JSON RPC arguments for L1 + - L1 bridge address + - L1 rollup initialization block + - L1 RPC chunk size for batch requests + - Message ID range window size + - `task_data`: Contains: + - `check_missing_origination.end_message_id`: Upper bound for the next + message ID range to check (always a valid non-negative integer when this + function is called; task is disabled at init if no fully indexed messages exist) + - `check_missing_origination.earliest_discovered_message_id`: Lower bound + where discovery should stop (defaults to 0 if no historical messages discovered yet) + - `check_missing_origination.safe_l1_block`: Safe L1 block number used + as fallback upper bound when determining L1 block ranges + - `completed_tasks`: Tracking map for task completion status + + ## Returns + - `{:ok, updated_state}` where: + - `task_data.check_missing_origination.end_message_id` is moved backward + by the range size + - `completed_tasks.check_missing_origination` is set to `true` when the + next iteration would go below the earliest discovered message boundary + """ + @spec check_missing_origination(%{ + :config => %{ + :json_l1_rpc_named_arguments => EthereumJSONRPC.json_rpc_named_arguments(), + :l1_bridge_address => binary(), + :l1_rollup_init_block => non_neg_integer(), + :l1_rpc_chunk_size => non_neg_integer(), + :missed_message_ids_range => non_neg_integer(), + optional(any()) => any() + }, + :task_data => %{ + :check_missing_origination => %{ + end_message_id: non_neg_integer(), + earliest_discovered_message_id: non_neg_integer(), + safe_l1_block: non_neg_integer() + }, + optional(any()) => any() + }, + :completed_tasks => %{:check_missing_origination => boolean(), optional(any()) => any()}, + optional(any()) => any() + }) :: {:ok, map()} + def check_missing_origination( + %{ + config: %{ + json_l1_rpc_named_arguments: json_rpc_named_arguments, + l1_rpc_chunk_size: chunk_size, + l1_bridge_address: bridge_address, + l1_rollup_init_block: l1_rollup_init_block, + missed_message_ids_range: missed_message_ids_range + }, + task_data: %{ + check_missing_origination: %{ + end_message_id: end_message_id, + earliest_discovered_message_id: earliest_discovered_message_id, + safe_l1_block: safe_l1_block + } + } + } = state + ) do + # Calculate the message ID range to check: [start_message_id, end_message_id] + # Clamp the range to at least 1 to ensure start_message_id <= end_message_id + normalized_range = max(1, missed_message_ids_range) + start_message_id = max(0, end_message_id - normalized_range + 1) + + log_info("Checking message ID range #{start_message_id}..#{end_message_id} for missing origination information") + + # Query for messages in range that have completion but missing origination + missing_origination_message_ids = + DbMessages.messages_to_l2_completed_but_originating_info_missed(start_message_id, end_message_id) + + unless Enum.empty?(missing_origination_message_ids) do + log_info( + "Found #{length(missing_origination_message_ids)} messages with missing origination in range #{start_message_id}..#{end_message_id}" + ) + + # Process each missing message (already in descending order from highest to lowest ID). + # + # Note on duplicate imports: When consecutive messages are missing (no fully indexed + # message between them), processing the higher-ID message may also discover and import + # lower-ID messages within the same L1 block range. For example: + # - Messages A(indexed), B(missing), C(missing), D(indexed) + # - Processing C: L1 range spans A..D, both B and C are discovered and imported + # - Processing B: B is imported again (already has origination from previous step) + # + # This is expected and harmless - Chain.import performs upserts, so reimporting + # the same message data is idempotent. Duplicate imports only occur with consecutive + # missing messages; alternating indexed/missing patterns naturally prevent this. + missing_origination_message_ids + |> Enum.each(fn message_id -> + discover_and_import_single_missing_message( + message_id, + l1_rollup_init_block, + safe_l1_block, + bridge_address, + json_rpc_named_arguments, + chunk_size + ) + end) + end + + # Move cursor backward for next iteration + next_end_message_id = start_message_id - 1 + + # Check if next iteration would go below the earliest discovered message + should_complete = next_end_message_id < earliest_discovered_message_id + + if should_complete do + log_info("Missing origination discovery complete") + end + + updated_state = + state + |> ArbitrumHelper.update_fetcher_task_data(:check_missing_origination, %{ + end_message_id: next_end_message_id + }) + |> set_missing_origination_completion(should_complete) + + {:ok, updated_state} + end + + # Discovers and imports origination information for a single L1-to-L2 message + # that is missing such information. + # + # This function determines the appropriate L1 block range to search by finding + # the closest preceding and following messages with known origination blocks. + # It then fetches L1 logs within that range and filters to find only the + # target message, excluding any already-indexed messages. + # + # The filter bounds are derived from the discovered boundary messages: + # - `lower_bound.message_id`: May be nil if no preceding message exists; -1 is + # used as fallback (effectively "no lower bound" since all message IDs >= 0) + # - `higher_bound.message_id`: Guaranteed to be non-nil because: + # 1. check_missing_origination only runs when end_message_id is not nil + # 2. end_message_id = highest_safe_fully_indexed_msg_id - 1 + # 3. highest_safe_fully_indexed_msg_id has BOTH origination AND completion + # 4. All missing messages have IDs < highest_safe_fully_indexed_msg_id + # 5. l1_block_of_closest_following_message_to_l2 will always find at least + # highest_safe_fully_indexed_msg_id (or something closer) + # + # ## Parameters + # - `message_id`: The ID of the message to discover origination for + # - `l1_rollup_init_block`: Fallback minimum L1 block if no preceding message + # - `safe_l1_block`: Fallback maximum L1 block if no following message + # - `bridge_address`: L1 bridge contract address for log filtering + # - `json_rpc_named_arguments`: RPC configuration + # - `chunk_size`: Batch size for RPC requests + # + # ## Returns + # - `:ok` after attempting to discover and import the message + defp discover_and_import_single_missing_message( + message_id, + l1_rollup_init_block, + safe_l1_block, + bridge_address, + json_rpc_named_arguments, + chunk_size + ) do + log_debug("Discovering origination for message ID #{message_id}") + + # Determine L1 block range for this message using database helper + %{lower: lower_bound, higher: higher_bound} = + DbMessages.l1_block_range_for_message_to_l2(message_id, l1_rollup_init_block, safe_l1_block) + + l1_start_block = lower_bound.block_number + l1_end_block = higher_bound.block_number + + log_info( + "Searching L1 blocks #{l1_start_block}..#{l1_end_block} for message ID #{message_id} " <> + "(bounded by message IDs #{inspect(lower_bound.message_id)}..#{inspect(higher_bound.message_id)})" + ) + + # Build filter range from discovered bounds. + # - lower_bound.message_id may be nil if no preceding message exists; use -1 + # as fallback since message IDs are non-negative, so (msg_id > -1) is always true + # - higher_bound.message_id is guaranteed non-nil (see function documentation above) + filter_range = %{ + higher_than: lower_bound.message_id || -1, + lower_than: higher_bound.message_id + } + + # Discover messages in the L1 block range, filtering to only messages + # within the discovered bounds (exclusive) + discover( + bridge_address, + l1_start_block, + l1_end_block, + json_rpc_named_arguments, + chunk_size, + filter_range + ) + + :ok + end + # Discovers and imports L1-to-L2 messages initiated on L1 within a specified block range. # # This function discovers messages initiated on L1 for transferring information from L1 to L2 @@ -206,16 +463,32 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do # details. For information not present in the events, RPC calls are made to fetch additional # transaction details. The discovered messages are then imported into the database. # + # When `only_messages_in_range` is provided, discovered messages are filtered to only + # include those with message IDs strictly between the specified bounds (exclusive). + # This is used for targeted discovery of specific missing messages. + # # ## Parameters # - `bridge_address`: The address of the Arbitrum bridge contract used to filter the logs. # - `start_block`: The starting block number for log retrieval. # - `end_block`: The ending block number for log retrieval. # - `json_rpc_named_argument`: Configuration parameters for the JSON RPC connection. # - `chunk_size`: The size of chunks for processing RPC calls in batches. + # - `only_messages_in_range`: Optional map with `:higher_than` and `:lower_than` message + # ID bounds for filtering. When present, only messages with IDs strictly between these + # bounds (exclusive) are imported. Pass `nil` to import all discovered messages. # # ## Returns - # - amount of discovered messages - defp discover(bridge_address, start_block, end_block, json_rpc_named_argument, chunk_size) do + # - amount of discovered messages that were attempted to be imported + defp discover( + bridge_address, + start_block, + end_block, + json_rpc_named_argument, + chunk_size, + only_messages_in_range \\ nil + ) + + defp discover(bridge_address, start_block, end_block, json_rpc_named_argument, chunk_size, only_messages_in_range) do logs = get_logs_for_l1_to_l2_messages( start_block, @@ -224,7 +497,7 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do json_rpc_named_argument ) - messages = get_messages_from_logs(logs, json_rpc_named_argument, chunk_size) + messages = get_messages_from_logs(logs, json_rpc_named_argument, chunk_size, only_messages_in_range) case messages do [] -> @@ -276,10 +549,16 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do # address and setting the status to `:initiated`, making them ready for database # import. # + # When `only_messages_in_range` is provided, messages are filtered to only include + # those with message IDs strictly between the specified bounds (exclusive). This + # filtering happens after message construction but before database import. + # # ## Parameters # - `logs`: A list of log entries to be processed. # - `json_rpc_named_arguments`: Configuration parameters for the JSON RPC connection. # - `chunk_size`: The size of chunks for batch processing transactions. + # - `only_messages_in_range`: Optional map with `:higher_than` and `:lower_than` + # message ID bounds for filtering. Pass `nil` to include all messages. # # ## Returns # - A list of maps describing discovered messages compatible with the database @@ -287,24 +566,68 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do @spec get_messages_from_logs( [%{String.t() => any()}], EthereumJSONRPC.json_rpc_named_arguments(), - non_neg_integer() + non_neg_integer(), + %{higher_than: non_neg_integer(), lower_than: non_neg_integer()} | nil ) :: [Arbitrum.Message.to_import()] - defp get_messages_from_logs(logs, json_rpc_named_arguments, chunk_size) + defp get_messages_from_logs(logs, json_rpc_named_arguments, chunk_size, only_messages_in_range) - defp get_messages_from_logs([], _, _), do: [] + defp get_messages_from_logs([], _, _, _), do: [] - defp get_messages_from_logs(logs, json_rpc_named_arguments, chunk_size) do + defp get_messages_from_logs(logs, json_rpc_named_arguments, chunk_size, only_messages_in_range) do {messages, transactions_requests} = parse_logs_for_l1_to_l2_messages(logs) transactions_to_from = Rpc.execute_transactions_requests_and_get_from(transactions_requests, json_rpc_named_arguments, chunk_size) - Enum.map(messages, fn msg -> - Map.merge(msg, %{ - originator_address: transactions_to_from[msg.originating_transaction_hash], - status: :initiated - }) - end) + complete_messages = + Enum.map(messages, fn msg -> + Map.merge(msg, %{ + originator_address: transactions_to_from[msg.originating_transaction_hash], + status: :initiated + }) + end) + + filter_messages_by_id_range(complete_messages, only_messages_in_range) + end + + # Filters messages to only include those with message IDs strictly between the + # specified bounds (exclusive). + # + # This filtering is used when discovering specific missing messages to ensure + # only the target messages are imported, excluding any messages that already + # have origination information in the database. + # + # When `range` is `nil`, returns all messages without filtering. + # + # ## Parameters + # - `messages`: List of message maps to filter + # - `range`: Map with `:higher_than` and `:lower_than` message ID bounds, or `nil`. + # Note: `higher_than` can be -1 when no preceding message exists (acts as "no lower bound" + # since all message IDs are non-negative). + # + # ## Returns + # - Filtered list of messages with IDs in the exclusive range, or all messages if range is `nil` + @spec filter_messages_by_id_range( + [Arbitrum.Message.to_import()], + %{higher_than: integer(), lower_than: non_neg_integer()} | nil + ) :: [Arbitrum.Message.to_import()] + defp filter_messages_by_id_range(messages, range) + + defp filter_messages_by_id_range(messages, nil), do: messages + + defp filter_messages_by_id_range(messages, %{higher_than: lower_bound, lower_than: upper_bound}) do + filtered = + Enum.filter(messages, fn msg -> + msg.message_id > lower_bound and msg.message_id < upper_bound + end) + + if length(filtered) < length(messages) do + filtered_out_count = length(messages) - length(filtered) + + log_debug("Filtered out #{filtered_out_count} messages outside ID range #{lower_bound + 1}..#{upper_bound - 1}") + end + + filtered end # Parses logs to extract L1-to-L2 message details and prepares RPC requests for transaction data. @@ -377,4 +700,48 @@ defmodule Indexer.Fetcher.Arbitrum.Workers.NewMessagesToL2 do {message_index, kind, Timex.from_unix(timestamp)} end + + # Marks historical completion status in the state. + @spec set_historical_completion( + %{:completed_tasks => %{:check_historical => boolean(), optional(any()) => any()}, optional(any()) => any()}, + boolean() + ) :: %{ + :completed_tasks => %{:check_historical => boolean(), optional(any()) => any()}, + optional(any()) => any() + } + defp set_historical_completion(state, completed?) do + updated_completed_tasks = + state + |> Map.get(:completed_tasks, %{}) + |> Map.put(:check_historical, completed?) + + %{state | completed_tasks: updated_completed_tasks} + end + + # Marks missing origination discovery completion status in the state. + # + # ## Parameters + # - `state`: The current fetcher state + # - `completed?`: Boolean indicating if the task has completed + # + # ## Returns + # - Updated state map with the completion flag set + @spec set_missing_origination_completion( + %{ + :completed_tasks => %{:check_missing_origination => boolean(), optional(any()) => any()}, + optional(any()) => any() + }, + boolean() + ) :: %{ + :completed_tasks => %{:check_missing_origination => boolean(), optional(any()) => any()}, + optional(any()) => any() + } + defp set_missing_origination_completion(state, completed?) do + updated_completed_tasks = + state + |> Map.get(:completed_tasks, %{}) + |> Map.put(:check_missing_origination, completed?) + + %{state | completed_tasks: updated_completed_tasks} + end end diff --git a/config/runtime.exs b/config/runtime.exs index 647e2681d433..015149587297 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1394,7 +1394,10 @@ config :indexer, Indexer.Fetcher.Arbitrum, rollup_chunk_size: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_ROLLUP_CHUNK_SIZE", 20) config :indexer, Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1, - recheck_interval: ConfigHelper.parse_time_env_var("INDEXER_ARBITRUM_TRACKING_MESSAGES_ON_L1_RECHECK_INTERVAL", "20s") + recheck_interval: ConfigHelper.parse_time_env_var("INDEXER_ARBITRUM_TRACKING_MESSAGES_ON_L1_RECHECK_INTERVAL", "20s"), + failure_interval_threshold: + ConfigHelper.parse_time_env_var("INDEXER_ARBITRUM_MESSAGES_TRACKING_FAILURE_THRESHOLD", "10m"), + missed_message_ids_range: ConfigHelper.parse_integer_env_var("INDEXER_ARBITRUM_MISSED_MESSAGE_IDS_RANGE", 10_000) config :indexer, Indexer.Fetcher.Arbitrum.TrackingMessagesOnL1.Supervisor, enabled: ConfigHelper.parse_bool_env_var("INDEXER_ARBITRUM_BRIDGE_MESSAGES_TRACKING_ENABLED") diff --git a/cspell.json b/cspell.json index cd167afdada5..c42bb23f3082 100644 --- a/cspell.json +++ b/cspell.json @@ -560,6 +560,7 @@ "repayer", "reqs", "rerequest", + "rescheduled", "reshows", "Resizer", "retcode", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 558af0ff02d2..caaa2f1c7db2 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -305,6 +305,8 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_ARBITRUM_BATCHES_TRACKING_FAILURE_THRESHOLD= # INDEXER_ARBITRUM_BRIDGE_MESSAGES_TRACKING_ENABLED= # INDEXER_ARBITRUM_TRACKING_MESSAGES_ON_L1_RECHECK_INTERVAL= +# INDEXER_ARBITRUM_MESSAGES_TRACKING_FAILURE_THRESHOLD= +# INDEXER_ARBITRUM_MISSED_MESSAGE_IDS_RANGE= # INDEXER_ARBITRUM_MISSED_MESSAGES_RECHECK_INTERVAL= # INDEXER_ARBITRUM_MISSED_MESSAGES_BLOCKS_DEPTH= # INDEXER_ARBITRUM_DATA_BACKFILL_ENABLED= From 4b07ac5a9a734ab269a629a0894c0376ec1ac1b1 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Mon, 12 Jan 2026 21:18:47 +0200 Subject: [PATCH 0579/1006] chore: add swagger spec for MUD endpoints (#13793) Co-authored-by: Victor Baranov --- .credo.exs | 2 +- .github/workflows/generate-swagger.yml | 37 ++++- .../controllers/api/v2/mud_controller.ex | 143 ++++++++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 27 ++-- .../schemas/api/v2/beacon/deposit/response.ex | 3 +- .../schemas/api/v2/blob/response.ex | 3 +- .../schemas/api/v2/block/response.ex | 3 +- .../block_scout_web/schemas/api/v2/general.ex | 91 +++++++++++ .../schemas/api/v2/mud/record.ex | 43 ++++++ .../schemas/api/v2/mud/system.ex | 24 +++ .../schemas/api/v2/mud/system_details.ex | 23 +++ .../schemas/api/v2/mud/table.ex | 30 ++++ .../schemas/api/v2/mud/table_schema.ex | 27 ++++ .../schemas/api/v2/mud/table_with_schema.ex | 23 +++ .../schemas/api/v2/mud/world.ex | 26 ++++ .../schemas/api/v2/token/response.ex | 3 +- apps/explorer/config/config.exs | 2 + docker/Dockerfile | 4 + docker/oldUI.Dockerfile | 4 + 19 files changed, 489 insertions(+), 29 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/record.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system_details.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_schema.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_with_schema.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/world.ex diff --git a/.credo.exs b/.credo.exs index 37ea310347f2..7b19b35e9690 100644 --- a/.credo.exs +++ b/.credo.exs @@ -78,7 +78,7 @@ # Priority values are: `low, normal, high, higher` # {Credo.Check.Design.AliasUsage, - excluded_namespaces: ~w(Block Blocks Import Runner Socket SpandexDatadog Task), + excluded_namespaces: ~w(Block Blocks Import Runner Socket SpandexDatadog Task Schemas), excluded_lastnames: ~w(Address DateTime Exporter Fetcher Full Instrumenter Logger Monitor Name Number Repo Spec Time Unit), priority: :low}, diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 5b6b1b07262b..3584b249d05f 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -1,4 +1,4 @@ -name: Generate Swagger +name: Generate OpenAPI Specs on: push: @@ -127,6 +127,15 @@ jobs: mix openapi.spec.yaml --spec BlockScoutWeb.ApiSpec openapi.${{ matrix.chain-type }}.yaml --start-app=false env: CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} + MUD_INDEXER_ENABLED: false + + - name: Generate MUD-enabled spec for Optimism + if: matrix.chain-type == 'optimism' + run: | + mix openapi.spec.yaml --spec BlockScoutWeb.ApiSpec openapi.mud.yaml --start-app=false + env: + CHAIN_TYPE: optimism + MUD_INDEXER_ENABLED: true - name: Upload OpenAPI spec uses: actions/upload-artifact@v4 @@ -135,6 +144,14 @@ jobs: path: openapi.${{ matrix.chain-type }}.yaml retention-days: 1 + - name: Upload MUD-enabled spec + if: matrix.chain-type == 'optimism' + uses: actions/upload-artifact@v4 + with: + name: openapi-spec-mud + path: openapi.mud.yaml + retention-days: 1 + push-specs: needs: - generate-swagger @@ -169,14 +186,18 @@ jobs: for SPEC_FILE in temp-specs/*; do if [ -f "$SPEC_FILE" ]; then - # Extract chain type from filename - CHAIN_TYPE=$(echo $SPEC_FILE | cut -d'.' -f2) - - # Create directory - mkdir -p "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}" + FILENAME=$(basename "$SPEC_FILE") - # Copy spec files - cp "$SPEC_FILE" "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}/swagger.yaml" + # Handle MUD spec specially + if [ "$FILENAME" = "openapi.mud.yaml" ]; then + mkdir -p "api-specs/blockscout/${VERSION}/mud" + cp "$SPEC_FILE" "api-specs/blockscout/${VERSION}/mud/swagger.yaml" + else + # Extract chain type from filename (openapi.CHAINTYPE.yaml) + CHAIN_TYPE=$(echo "$FILENAME" | sed 's/openapi\.\(.*\)\.yaml/\1/') + mkdir -p "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}" + cp "$SPEC_FILE" "api-specs/blockscout/${VERSION}/${CHAIN_TYPE}/swagger.yaml" + fi fi done diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex index 135e2c56d624..e2f5b0cae0be 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.MudController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Chain, only: [ @@ -13,6 +14,7 @@ defmodule BlockScoutWeb.API.V2.MudController do import Explorer.MicroserviceInterfaces.BENS, only: [maybe_preload_ens: 1] import Explorer.MicroserviceInterfaces.Metadata, only: [maybe_preload_metadata: 1] + alias BlockScoutWeb.Schemas.Helper, as: SchemasHelper alias Explorer.Chain alias Explorer.Chain.{Address, Data, Hash, Mud, Mud.Schema.FieldSchema, Mud.Table} @@ -20,6 +22,30 @@ defmodule BlockScoutWeb.API.V2.MudController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + tags(["mud"]) + + operation :worlds, + summary: "List of MUD worlds.", + description: "Retrieves a paginated list of MUD worlds with basic stats.", + parameters: + base_params() ++ + define_paging_params([ + "world", + "items_count" + ]), + responses: [ + ok: + {"List of MUD worlds.", "application/json", + paginated_response( + items: Schemas.MUD.World, + next_page_params_example: %{ + "world" => "0x82cb040ff4463bff3395d52b558fd77c61583b27", + "items_count" => 50 + }, + title_prefix: "Worlds" + )} + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds` endpoint. """ @@ -60,6 +86,14 @@ defmodule BlockScoutWeb.API.V2.MudController do }) end + operation :worlds_count, + summary: "Number of known MUD worlds.", + description: "Retrieves the total number of known MUD worlds.", + parameters: base_params(), + responses: [ + ok: {"Number of known MUD worlds.", "application/json", %Schema{type: :integer}} + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/count` endpoint. """ @@ -72,6 +106,30 @@ defmodule BlockScoutWeb.API.V2.MudController do |> render(:count, %{count: count}) end + operation :world_tables, + summary: "List of MUD world tables.", + description: "Retrieves a paginated list of MUD tables in the specific MUD world.", + parameters: + base_params() ++ + [world_param()] ++ + define_paging_params([ + "table_id", + "items_count" + ]), + responses: [ + ok: + {"List of MUD tables.", "application/json", + paginated_response( + items: Schemas.MUD.TableWithSchema, + next_page_params_example: %{ + "table_id" => "0x746243484553545f5641554c5400000043686573744163636573730000000000", + "items_count" => 50 + }, + title_prefix: "Tables" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/tables` endpoint. """ @@ -96,6 +154,15 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_systems, + summary: "List of MUD world systems.", + description: "Retrieves a list of MUD systems registered in the specific MUD world.", + parameters: [world_param() | base_params()], + responses: [ + ok: {"List of MUD systems.", "application/json", %Schema{type: :array, items: Schemas.MUD.System}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/systems` endpoint. """ @@ -110,6 +177,15 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_system, + summary: "List of MUD world system ABI methods.", + description: "Retrieves a list of MUD system ABI methods registered in the specific MUD world.", + parameters: base_params() ++ [world_param(), system_param()], + responses: [ + ok: {"List of MUD world system ABI methods.", "application/json", Schemas.MUD.SystemDetails}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/systems/:system` endpoint. """ @@ -124,6 +200,15 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_tables_count, + summary: "Number of known MUD world tables.", + description: "Retrieves the total number of known MUD tables in the specific MUD world.", + parameters: [world_param() | base_params()], + responses: [ + ok: {"Number of known MUD world tables.", "application/json", %Schema{type: :integer}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/count` endpoint. """ @@ -140,6 +225,40 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_table_records, + summary: "List of MUD world table records.", + description: "Retrieves a paginated list of records in the specific MUD world table.", + parameters: + base_params() ++ + [world_param(), table_id_param()] ++ + define_paging_params([ + "key_bytes", + "key0", + "key1", + "items_count" + ]), + responses: [ + ok: + {"List of MUD world table records.", "application/json", + SchemasHelper.extend_schema( + paginated_response( + items: Schemas.MUD.Record, + next_page_params_example: %{ + "key_bytes" => "0x73796269746c7900000000000000000043686573743332000000000000000000", + "key0" => "0x73796269746c7900000000000000000043686573743332000000000000000000", + "items_count" => 50 + }, + title_prefix: "Records" + ), + properties: %{ + table: Schemas.MUD.Table, + schema: Schemas.MUD.TableSchema + }, + required: [:table, :schema] + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records` endpoint. """ @@ -176,6 +295,15 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_table_records_count, + summary: "Number of known MUD world table records.", + description: "Retrieves the total number of records in the specific MUD world table.", + parameters: base_params() ++ [world_param(), table_id_param()], + responses: [ + ok: {"Number of known MUD world table records.", "application/json", %Schema{type: :integer}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records/count` endpoint. """ @@ -194,6 +322,21 @@ defmodule BlockScoutWeb.API.V2.MudController do end end + operation :world_table_record, + summary: "Single MUD world table record.", + description: "Retrieves a single record in the specific MUD world table.", + parameters: base_params() ++ [world_param(), table_id_param(), record_id_param()], + responses: [ + ok: + {"Single MUD world table record.", "application/json", + Schemas.MUD.TableWithSchema.schema() + |> SchemasHelper.extend_schema( + properties: %{record: Schemas.MUD.Record}, + required: [:record] + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records/:record_id` endpoint. """ diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 1b192c204123..8c9ab8211bda 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -22,7 +22,8 @@ defmodule BlockScoutWeb.Routers.ApiRouter do graphql_max_complexity: [:block_scout_web, [Api.GraphQL, :max_complexity]], graphql_token_limit: [:block_scout_web, [Api.GraphQL, :token_limit]], reading_enabled: [:block_scout_web, [__MODULE__, :reading_enabled]], - writing_enabled: [:block_scout_web, [__MODULE__, :writing_enabled]] + writing_enabled: [:block_scout_web, [__MODULE__, :writing_enabled]], + mud_enabled_compile_time?: [:explorer, [Explorer.Chain.Mud, :enabled]] use Utils.RuntimeEnvHelper, mud_enabled?: [:explorer, [Explorer.Chain.Mud, :enabled]] @@ -460,17 +461,19 @@ defmodule BlockScoutWeb.Routers.ApiRouter do end end - scope "/mud" do - pipe_through(:mud) - get("/worlds", V2.MudController, :worlds) - get("/worlds/count", V2.MudController, :worlds_count) - get("/worlds/:world/tables", V2.MudController, :world_tables) - get("/worlds/:world/systems", V2.MudController, :world_systems) - get("/worlds/:world/systems/:system", V2.MudController, :world_system) - get("/worlds/:world/tables/count", V2.MudController, :world_tables_count) - get("/worlds/:world/tables/:table_id/records", V2.MudController, :world_table_records) - get("/worlds/:world/tables/:table_id/records/count", V2.MudController, :world_table_records_count) - get("/worlds/:world/tables/:table_id/records/:record_id", V2.MudController, :world_table_record) + if @mud_enabled_compile_time? do + scope "/mud" do + pipe_through(:mud) + get("/worlds", V2.MudController, :worlds) + get("/worlds/count", V2.MudController, :worlds_count) + get("/worlds/:world/tables", V2.MudController, :world_tables) + get("/worlds/:world/systems", V2.MudController, :world_systems) + get("/worlds/:world/systems/:system", V2.MudController, :world_system) + get("/worlds/:world/tables/count", V2.MudController, :world_tables_count) + get("/worlds/:world/tables/:table_id/records", V2.MudController, :world_table_records) + get("/worlds/:world/tables/:table_id/records/count", V2.MudController, :world_table_records_count) + get("/worlds/:world/tables/:table_id/records/:record_id", V2.MudController, :world_table_record) + end end scope "/arbitrum" do diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex index 9a099afdef18..7a995f852f75 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/beacon/deposit/response.ex @@ -11,8 +11,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Beacon.Deposit.Response do Deposit.schema() |> Helper.extend_schema( title: "BeaconDepositResponse", - description: "BeaconDeposit response", - additionalProperties: false + description: "BeaconDeposit response" ) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex index 4816f95363f5..11401c582afb 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/blob/response.ex @@ -11,8 +11,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Blob.Response do Blob.schema() |> Helper.extend_schema( title: "BlobResponse", - description: "Blob response", - additionalProperties: false + description: "Blob response" ) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex index 8c31054d78d8..f07f077d00da 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/block/response.ex @@ -11,8 +11,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Block.Response do Block.schema() |> Helper.extend_schema( title: "BlockResponse", - description: "Block response", - additionalProperties: false + description: "Block response" ) ) end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 841fc2867753..af2a3b5e162a 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -734,6 +734,62 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for MUD world address hash. + """ + @spec world_param() :: Parameter.t() + def world_param do + %Parameter{ + name: :world, + in: :path, + schema: AddressHash, + required: true, + description: "MUD world address hash in the path" + } + end + + @doc """ + Returns a parameter definition for MUD system address hash. + """ + @spec system_param() :: Parameter.t() + def system_param do + %Parameter{ + name: :system, + in: :path, + schema: AddressHash, + required: true, + description: "MUD system address hash in the path" + } + end + + @doc """ + Returns a parameter definition for MUD table ID. + """ + @spec table_id_param() :: Parameter.t() + def table_id_param do + %Parameter{ + name: :table_id, + in: :path, + schema: FullHash, + required: true, + description: "MUD table ID in the path" + } + end + + @doc """ + Returns a parameter definition for MUD record ID. + """ + @spec record_id_param() :: Parameter.t() + def record_id_param do + %Parameter{ + name: :record_id, + in: :path, + schema: HexString, + required: true, + description: "MUD record ID in the path" + } + end + @doc """ Returns a list of base parameters (api_key and key). """ @@ -1080,6 +1136,41 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do schema: %Schema{type: :integer, nullable: true}, required: false, description: "State changes for paging" + }, + "world" => %Parameter{ + name: :world, + in: :query, + schema: AddressHash, + required: false, + description: "MUD world address hash for paging" + }, + "table_id" => %Parameter{ + name: :table_id, + in: :query, + schema: FullHash, + required: false, + description: "MUD table ID for paging" + }, + "key_bytes" => %Parameter{ + name: :key_bytes, + in: :query, + schema: HexString, + required: false, + description: "MUD record key_bytes for paging" + }, + "key0" => %Parameter{ + name: :key0, + in: :query, + schema: FullHash, + required: false, + description: "MUD record key0 for paging" + }, + "key1" => %Parameter{ + name: :key1, + in: :query, + schema: FullHash, + required: false, + description: "MUD record key1 for paging" } } diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/record.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/record.ex new file mode 100644 index 000000000000..7e3903f16282 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/record.ex @@ -0,0 +1,43 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.Record do + @moduledoc """ + This module defines the schema for the MUD Record struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD Record struct.", + type: :object, + properties: %{ + id: General.HexString, + raw: %Schema{ + type: :object, + properties: %{ + block_number: General.IntegerString, + log_index: General.IntegerString, + dynamic_data: General.HexString, + encoded_lengths: General.FullHash, + key0: General.FullHash, + key1: General.FullHash, + key_bytes: General.HexString, + static_data: General.HexString + }, + nullable: false + }, + timestamp: General.Timestamp, + decoded: %Schema{type: :object, nullable: true}, + is_deleted: %Schema{type: :boolean, nullable: false} + }, + required: [ + :id, + :raw, + :timestamp, + :decoded, + :is_deleted + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system.ex new file mode 100644 index 000000000000..fab315e276e1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.System do + @moduledoc """ + This module defines the schema for the MUD System struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD System struct.", + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + address_hash: General.AddressHash + }, + required: [ + :name, + :address_hash + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system_details.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system_details.ex new file mode 100644 index 000000000000..c1fbb72a5ad5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/system_details.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.SystemDetails do + @moduledoc """ + This module defines the schema for the MUD SystemDetails struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD SystemDetails struct.", + type: :object, + properties: %{ + name: %Schema{type: :string, nullable: false}, + abi: %Schema{type: :array, items: %Schema{type: :object}, nullable: false} + }, + required: [ + :name, + :abi + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table.ex new file mode 100644 index 000000000000..df7451c47495 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table.ex @@ -0,0 +1,30 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.Table do + @moduledoc """ + This module defines the schema for the MUD Table struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD Table struct.", + type: :object, + properties: %{ + table_id: General.FullHash, + table_full_name: %Schema{type: :string, nullable: false}, + table_type: %Schema{type: :string, enum: ["offchain", "onchain", "unknown"], nullable: false}, + table_namespace: %Schema{type: :string, nullable: false}, + table_name: %Schema{type: :string, nullable: false} + }, + required: [ + :table_id, + :table_full_name, + :table_type, + :table_namespace, + :table_name + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_schema.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_schema.ex new file mode 100644 index 000000000000..3f4e3e864452 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_schema.ex @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.TableSchema do + @moduledoc """ + This module defines the schema for the MUD TableSchema struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD TableSchema struct.", + type: :object, + properties: %{ + key_names: %Schema{type: :array, items: %Schema{type: :string, nullable: false}, nullable: false}, + key_types: %Schema{type: :array, items: %Schema{type: :string, nullable: false}, nullable: false}, + value_names: %Schema{type: :array, items: %Schema{type: :string, nullable: false}, nullable: false}, + value_types: %Schema{type: :array, items: %Schema{type: :string, nullable: false}, nullable: false} + }, + required: [ + :key_names, + :key_types, + :value_names, + :value_types + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_with_schema.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_with_schema.ex new file mode 100644 index 000000000000..4148de82240f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/table_with_schema.ex @@ -0,0 +1,23 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.TableWithSchema do + @moduledoc """ + This module defines the schema for the MUD Table with TableSchema struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.MUD.{Table, TableSchema} + + OpenApiSpex.schema(%{ + description: "MUD Table with TableSchema struct.", + type: :object, + properties: %{ + table: Table, + schema: TableSchema + }, + required: [ + :table, + :schema + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/world.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/world.ex new file mode 100644 index 000000000000..88ce529c057c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/mud/world.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Schemas.API.V2.MUD.World do + @moduledoc """ + This module defines the schema for the MUD World struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "MUD World struct.", + type: :object, + properties: %{ + address: Address.schema(), + coin_balance: General.IntegerStringNullable, + transactions_count: %Schema{type: :integer, nullable: true} + }, + required: [ + :address, + :coin_balance, + :transactions_count + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex index 76dcaf6c75cb..8986acb28969 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token/response.ex @@ -11,8 +11,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.Response do Token.schema() |> Helper.extend_schema( title: "TokenResponse", - description: "Token response", - additionalProperties: false + description: "Token response" ) ) end diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index a53cf5a39c67..b8347d4bddc4 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -113,6 +113,8 @@ config :explorer, Explorer.TokenInstanceOwnerAddressMigration.Supervisor, enable config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, enabled: false +config :explorer, Explorer.Chain.Mud, enabled: ConfigHelper.parse_bool_env_var("MUD_INDEXER_ENABLED") + for migrator <- [ # Background migrations Explorer.Migrator.TransactionsDenormalization, diff --git a/docker/Dockerfile b/docker/Dockerfile index cfb6f78a4d33..1f3c715cddf2 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -40,6 +40,8 @@ ARG BRIDGED_TOKENS_ENABLED ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} ARG API_GRAPHQL_MAX_COMPLEXITY ENV API_GRAPHQL_MAX_COMPLEXITY=${API_GRAPHQL_MAX_COMPLEXITY} +ARG MUD_INDEXER_ENABLED +ENV MUD_INDEXER_ENABLED=${MUD_INDEXER_ENABLED} # Run backend compilation RUN mix compile @@ -76,6 +78,8 @@ ARG BRIDGED_TOKENS_ENABLED ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} ARG API_GRAPHQL_MAX_COMPLEXITY ENV API_GRAPHQL_MAX_COMPLEXITY=${API_GRAPHQL_MAX_COMPLEXITY} +ARG MUD_INDEXER_ENABLED +ENV MUD_INDEXER_ENABLED=${MUD_INDEXER_ENABLED} ARG RELEASE_VERSION ENV RELEASE_VERSION=${RELEASE_VERSION} diff --git a/docker/oldUI.Dockerfile b/docker/oldUI.Dockerfile index 661bb04827aa..29b7f7e083b4 100644 --- a/docker/oldUI.Dockerfile +++ b/docker/oldUI.Dockerfile @@ -56,6 +56,8 @@ ARG BRIDGED_TOKENS_ENABLED ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} ARG API_GRAPHQL_MAX_COMPLEXITY ENV API_GRAPHQL_MAX_COMPLEXITY=${API_GRAPHQL_MAX_COMPLEXITY} +ARG MUD_INDEXER_ENABLED +ENV MUD_INDEXER_ENABLED=${MUD_INDEXER_ENABLED} # Run backend compilation RUN mix compile @@ -92,6 +94,8 @@ ARG BRIDGED_TOKENS_ENABLED ENV BRIDGED_TOKENS_ENABLED=${BRIDGED_TOKENS_ENABLED} ARG API_GRAPHQL_MAX_COMPLEXITY ENV API_GRAPHQL_MAX_COMPLEXITY=${API_GRAPHQL_MAX_COMPLEXITY} +ARG MUD_INDEXER_ENABLED +ENV MUD_INDEXER_ENABLED=${MUD_INDEXER_ENABLED} ARG RELEASE_VERSION ENV RELEASE_VERSION=${RELEASE_VERSION} From 23550ac60da49ff46bf0f8119298f6576b3ec8b3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 11:27:51 +0300 Subject: [PATCH 0580/1006] fix: Fix NaN gas limit for `selfdestruct` internal transaction in the REST API (#13827) --- .../address/internal_transactions.ex | 2 +- .../views/api/rpc/address_view.ex | 2 +- .../views/api/v2/internal_transaction_view.ex | 2 +- .../api/v2/address_controller_test.exs | 29 +++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex index f423529e1e2e..bf393b2f09e3 100644 --- a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex +++ b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex @@ -88,7 +88,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do Address.checksum(internal_transaction.created_contract_address_hash), internal_transaction.type, internal_transaction.call_type, - internal_transaction.gas, + internal_transaction.gas || "0", internal_transaction.gas_used, Wei.to(internal_transaction.value, :wei), internal_transaction.input, diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 58e7c18f240b..5928646a8551 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -147,7 +147,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "input" => "#{internal_transaction.input}", "type" => "#{internal_transaction.type}", "callType" => "#{internal_transaction.call_type}", - "gas" => "#{internal_transaction.gas}", + "gas" => to_string(internal_transaction.gas || 0), "gasUsed" => "#{internal_transaction.gas_used}", "isError" => if(internal_transaction.error, do: "1", else: "0"), "errCode" => "#{internal_transaction.error}" diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex index d67b9f55fe0d..3e6b5fe458c6 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex @@ -51,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionView do "block_number" => internal_transaction.block_number, "timestamp" => (block && block.timestamp) || internal_transaction.block.timestamp, "index" => internal_transaction.index, - "gas_limit" => internal_transaction.gas, + "gas_limit" => internal_transaction.gas || Decimal.new(0), "block_index" => internal_transaction.block_index } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 31a93eb1e651..7d35da6c6ec6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2272,6 +2272,35 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do compare_item(internal_transaction_to, Enum.at(response["items"], 0)) end + test "returns gas_limit as 0 for selfdestruct without gas", %{conn: conn} do + address = insert(:address) + + transaction = + :transaction + |> insert(from_address: address) + |> with_block() + + internal_transaction = + insert(:internal_transaction_selfdestruct, + transaction: transaction, + index: 1, + block_index: 1, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + from_address: address, + to_address: insert(:address), + gas: nil, + gas_used: nil + ) + + request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions") + + assert %{"items" => [item], "next_page_params" => nil} = json_response(request, 200) + assert "0" == to_string(item["gas_limit"]) + assert item["index"] == internal_transaction.index + end + test "internal transactions can paginate", %{conn: conn} do address = insert(:address) From 1a72ed35a2b27b5278dc51e5d3c09107d17128b2 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 13:19:42 +0300 Subject: [PATCH 0581/1006] chore: Re-use parse_url_env_var/3 function for all *_URL env variables (#13800) --- apps/block_scout_web/config/config.exs | 4 +- .../controllers/api/v2/import_controller.ex | 10 +-- apps/explorer/config/dev/anvil.exs | 4 +- apps/explorer/config/dev/besu.exs | 4 +- apps/explorer/config/dev/erigon.exs | 4 +- apps/explorer/config/dev/filecoin.exs | 4 +- apps/explorer/config/dev/geth.exs | 4 +- apps/explorer/config/dev/nethermind.exs | 4 +- apps/explorer/config/dev/rsk.exs | 4 +- apps/explorer/config/prod/anvil.exs | 4 +- apps/explorer/config/prod/besu.exs | 4 +- apps/explorer/config/prod/erigon.exs | 4 +- apps/explorer/config/prod/filecoin.exs | 4 +- apps/explorer/config/prod/geth.exs | 4 +- apps/explorer/config/prod/nethermind.exs | 4 +- apps/explorer/config/prod/rsk.exs | 4 +- apps/explorer/config/test.exs | 2 +- apps/explorer/lib/explorer/application.ex | 3 +- .../lib/explorer/chain/blackfort/validator.ex | 5 +- .../chain/smart_contract/audit_report.ex | 3 +- .../lib/explorer/chain_spec/genesis_data.ex | 5 +- apps/explorer/lib/explorer/helper.ex | 12 --- .../lib/explorer/repo/config_helper.ex | 20 +++-- .../lib/explorer/utility/microservice.ex | 16 +--- .../utility/replica_accessibility_manager.ex | 3 +- apps/indexer/config/dev/anvil.exs | 6 +- apps/indexer/config/dev/besu.exs | 6 +- apps/indexer/config/dev/erigon.exs | 6 +- apps/indexer/config/dev/filecoin.exs | 6 +- apps/indexer/config/dev/geth.exs | 6 +- apps/indexer/config/dev/nethermind.exs | 6 +- apps/indexer/config/dev/rsk.exs | 6 +- apps/indexer/config/prod/anvil.exs | 6 +- apps/indexer/config/prod/besu.exs | 6 +- apps/indexer/config/prod/erigon.exs | 6 +- apps/indexer/config/prod/filecoin.exs | 6 +- apps/indexer/config/prod/geth.exs | 6 +- apps/indexer/config/prod/nethermind.exs | 6 +- apps/indexer/config/prod/rsk.exs | 6 +- .../optimism/interop/multichain_export.ex | 2 +- .../fetcher/on_demand/contract_code_test.exs | 12 +-- apps/utils/lib/utils/config_helper.ex | 69 ++++++++++++++++++ config/config_helper.exs | 65 +++++------------ config/runtime.exs | 73 ++++++++++--------- config/runtime/dev.exs | 4 +- config/runtime/prod.exs | 4 +- mix.exs | 13 +++- 47 files changed, 247 insertions(+), 218 deletions(-) diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index e3e905924674..1f73918bc114 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -72,8 +72,8 @@ config :block_scout_web, BlockScoutWeb.Routers.ApiRouter, config :block_scout_web, BlockScoutWeb.Routers.WebRouter, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP") config :block_scout_web, BlockScoutWeb.CSPHeader, - mixpanel_url: System.get_env("MIXPANEL_URL", "https://api-js.mixpanel.com"), - amplitude_url: System.get_env("AMPLITUDE_URL", "https://api2.amplitude.com/2/httpapi") + mixpanel_url: ConfigHelper.parse_url_env_var("MIXPANEL_URL", "https://api-js.mixpanel.com"), + amplitude_url: ConfigHelper.parse_url_env_var("AMPLITUDE_URL", "https://api2.amplitude.com/2/httpapi") config :block_scout_web, Api.GraphQL, enabled: ConfigHelper.parse_bool_env_var("API_GRAPHQL_ENABLED", "true"), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex index 59eaac18738b..7f7746774f8a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex @@ -7,6 +7,7 @@ defmodule BlockScoutWeb.API.V2.ImportController do alias Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand alias Explorer.SmartContract.EthBytecodeDBInterface alias Indexer.Fetcher.TokenUpdater + alias Utils.ConfigHelper, as: UtilsConfigHelper import Explorer.SmartContract.Helper, only: [prepare_bytecode_for_microservice: 3, fetch_data_for_verification: 2] @@ -186,15 +187,8 @@ defmodule BlockScoutWeb.API.V2.ImportController do defp params_to_contract_search_options(_), do: [] - defp valid_url?(url) when is_binary(url) do - uri = URI.parse(url) - uri.scheme != nil && uri.host =~ "." - end - - defp valid_url?(_url), do: false - defp put_icon_url(changeset, icon_url) do - if valid_url?(icon_url) do + if UtilsConfigHelper.valid_url?(icon_url) do Map.put(changeset, :icon_url, icon_url) else changeset diff --git a/apps/explorer/config/dev/anvil.exs b/apps/explorer/config/dev/anvil.exs index 58e65a7b126c..0d51a3f47302 100644 --- a/apps/explorer/config/dev/anvil.exs +++ b/apps/explorer/config/dev/anvil.exs @@ -24,8 +24,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Anvil ] diff --git a/apps/explorer/config/dev/besu.exs b/apps/explorer/config/dev/besu.exs index b761b45abbf5..bce0ddc56a6f 100644 --- a/apps/explorer/config/dev/besu.exs +++ b/apps/explorer/config/dev/besu.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Besu ] diff --git a/apps/explorer/config/dev/erigon.exs b/apps/explorer/config/dev/erigon.exs index a32d59ca293e..f3e3fae33c99 100644 --- a/apps/explorer/config/dev/erigon.exs +++ b/apps/explorer/config/dev/erigon.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Erigon ] diff --git a/apps/explorer/config/dev/filecoin.exs b/apps/explorer/config/dev/filecoin.exs index 12882aa3d139..08d93a049ada 100644 --- a/apps/explorer/config/dev/filecoin.exs +++ b/apps/explorer/config/dev/filecoin.exs @@ -27,8 +27,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Filecoin ] diff --git a/apps/explorer/config/dev/geth.exs b/apps/explorer/config/dev/geth.exs index 0537f0d4d436..141bfd68bc34 100644 --- a/apps/explorer/config/dev/geth.exs +++ b/apps/explorer/config/dev/geth.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Geth ] diff --git a/apps/explorer/config/dev/nethermind.exs b/apps/explorer/config/dev/nethermind.exs index 995621848fe0..343d2707cf9d 100644 --- a/apps/explorer/config/dev/nethermind.exs +++ b/apps/explorer/config/dev/nethermind.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Nethermind ] diff --git a/apps/explorer/config/dev/rsk.exs b/apps/explorer/config/dev/rsk.exs index c18344ec8abb..3948dc9cb3cb 100644 --- a/apps/explorer/config/dev/rsk.exs +++ b/apps/explorer/config/dev/rsk.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.RSK ] diff --git a/apps/explorer/config/prod/anvil.exs b/apps/explorer/config/prod/anvil.exs index 58e65a7b126c..0d51a3f47302 100644 --- a/apps/explorer/config/prod/anvil.exs +++ b/apps/explorer/config/prod/anvil.exs @@ -24,8 +24,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Anvil ] diff --git a/apps/explorer/config/prod/besu.exs b/apps/explorer/config/prod/besu.exs index b761b45abbf5..bce0ddc56a6f 100644 --- a/apps/explorer/config/prod/besu.exs +++ b/apps/explorer/config/prod/besu.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Besu ] diff --git a/apps/explorer/config/prod/erigon.exs b/apps/explorer/config/prod/erigon.exs index a32d59ca293e..f3e3fae33c99 100644 --- a/apps/explorer/config/prod/erigon.exs +++ b/apps/explorer/config/prod/erigon.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Erigon ] diff --git a/apps/explorer/config/prod/filecoin.exs b/apps/explorer/config/prod/filecoin.exs index c006935abd16..e8d01558045d 100644 --- a/apps/explorer/config/prod/filecoin.exs +++ b/apps/explorer/config/prod/filecoin.exs @@ -27,8 +27,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Filecoin ] diff --git a/apps/explorer/config/prod/geth.exs b/apps/explorer/config/prod/geth.exs index 0537f0d4d436..141bfd68bc34 100644 --- a/apps/explorer/config/prod/geth.exs +++ b/apps/explorer/config/prod/geth.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Geth ] diff --git a/apps/explorer/config/prod/nethermind.exs b/apps/explorer/config/prod/nethermind.exs index 995621848fe0..343d2707cf9d 100644 --- a/apps/explorer/config/prod/nethermind.exs +++ b/apps/explorer/config/prod/nethermind.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.Nethermind ] diff --git a/apps/explorer/config/prod/rsk.exs b/apps/explorer/config/prod/rsk.exs index c18344ec8abb..3948dc9cb3cb 100644 --- a/apps/explorer/config/prod/rsk.exs +++ b/apps/explorer/config/prod/rsk.exs @@ -28,8 +28,8 @@ config :explorer, transport: EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ], variant: EthereumJSONRPC.RSK ] diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index dc452c183dde..27c2694dec3f 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -3,7 +3,7 @@ import Config # Lower hashing rounds for faster tests config :bcrypt_elixir, log_rounds: 4 -database_url = System.get_env("TEST_DATABASE_URL") +database_url = ConfigHelper.parse_url_env_var("TEST_DATABASE_URL") database = if database_url, do: nil, else: "explorer_test" hostname = if database_url, do: nil, else: "localhost" diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 79ab9eac9a82..96fd8abbd8fe 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -39,6 +39,7 @@ defmodule Explorer.Application do alias Explorer.Prometheus.Instrumenter alias Explorer.Repo.PrometheusLogger alias Explorer.Utility.Hammer + alias Utils.ConfigHelper @impl Application def start(_type, _args) do @@ -496,7 +497,7 @@ defmodule Explorer.Application do end defp redix_opts do - {System.get_env("ACCOUNT_REDIS_URL") || "redis://127.0.0.1:6379", [name: :redix]} + {ConfigHelper.parse_url_env_var("ACCOUNT_REDIS_URL", "redis://127.0.0.1:6379"), [name: :redix]} end defp configure_libcluster do diff --git a/apps/explorer/lib/explorer/chain/blackfort/validator.ex b/apps/explorer/lib/explorer/chain/blackfort/validator.ex index 4245451a00fe..0d9574d4aa22 100644 --- a/apps/explorer/lib/explorer/chain/blackfort/validator.ex +++ b/apps/explorer/lib/explorer/chain/blackfort/validator.ex @@ -5,9 +5,10 @@ defmodule Explorer.Chain.Blackfort.Validator do use Explorer.Schema - alias Explorer.{Chain, Helper, HttpClient, Repo, SortingHelper} + alias Explorer.{Chain, HttpClient, Repo, SortingHelper} alias Explorer.Chain.{Address, Import} alias Explorer.Chain.Hash.Address, as: HashAddress + alias Utils.ConfigHelper, as: UtilsConfigHelper require Logger @@ -147,7 +148,7 @@ defmodule Explorer.Chain.Blackfort.Validator do def fetch_validators_list do url = validator_url() - with {:url, true} <- {:url, Helper.valid_url?(url)}, + with {:url, true} <- {:url, UtilsConfigHelper.valid_url?(url)}, {:ok, %{status_code: 200, body: body}} <- HttpClient.get(validator_url(), [], follow_redirect: true) do body |> Jason.decode() |> parse_validators_info() else diff --git a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex index f383540e49ea..52122d3326d8 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.SmartContract.AuditReport do alias Explorer.{Chain, Helper, Repo} alias Explorer.Chain.Hash alias Explorer.ThirdPartyIntegrations.AirTableAuditReport + alias Utils.ConfigHelper, as: UtilsConfigHelper @max_reports_per_day_for_contract 5 @@ -86,7 +87,7 @@ defmodule Explorer.Chain.SmartContract.AuditReport do end defp valid_url?(field, url) do - if Helper.valid_url?(url) do + if UtilsConfigHelper.valid_url?(url) do [] else [{field, "invalid url"}] diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index 3b91f1bc11c3..5eb5a5099529 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -14,7 +14,8 @@ defmodule Explorer.ChainSpec.GenesisData do alias Explorer.Chain.SmartContract alias Explorer.ChainSpec.Geth.Importer, as: GethImporter alias Explorer.ChainSpec.Parity.Importer - alias Explorer.{Helper, HttpClient} + alias Explorer.HttpClient + alias Utils.ConfigHelper, as: UtilsConfigHelper def start_link(opts) do GenServer.start_link(__MODULE__, opts, name: __MODULE__) @@ -154,7 +155,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Retrieves a JSON data from either a file or URL based on the source. @spec fetch_spec_as_json(binary()) :: {:ok, list() | map()} | {:error, any()} defp fetch_spec_as_json(path) do - if Helper.valid_url?(path) do + if UtilsConfigHelper.valid_url?(path) do fetch_from_url(path) else fetch_from_file(path) diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 9d8a06304a0e..2d38caa6fdeb 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -225,18 +225,6 @@ defmodule Explorer.Helper do def validate_url(_), do: :error - @doc """ - Validate url - """ - @spec valid_url?(String.t()) :: boolean() - def valid_url?(string) when is_binary(string) do - uri = URI.parse(string) - - !is_nil(uri.scheme) && !is_nil(uri.host) - end - - def valid_url?(_), do: false - @doc """ Compare two values and returns either :lt, :eq or :gt. diff --git a/apps/explorer/lib/explorer/repo/config_helper.ex b/apps/explorer/lib/explorer/repo/config_helper.ex index a2679e38af80..39f11eefdd74 100644 --- a/apps/explorer/lib/explorer/repo/config_helper.ex +++ b/apps/explorer/lib/explorer/repo/config_helper.ex @@ -7,6 +7,8 @@ defmodule Explorer.Repo.ConfigHelper do The priority of vars is postgrex environment vars < DATABASE_URL components, with values being overwritten by higher priority. """ + alias Utils.ConfigHelper, as: UtilsConfigHelper + # https://hexdocs.pm/postgrex/Postgrex.html#start_link/1-options @postgrex_env_vars [ username: "PGUSER", @@ -26,15 +28,23 @@ defmodule Explorer.Repo.ConfigHelper do |> Keyword.merge(extract_parameters(url)) end - def get_account_db_url, do: System.get_env("ACCOUNT_DATABASE_URL") || System.get_env("DATABASE_URL") + def get_account_db_url, + do: + UtilsConfigHelper.parse_url_env_var("ACCOUNT_DATABASE_URL") || UtilsConfigHelper.parse_url_env_var("DATABASE_URL") - def get_suave_db_url, do: System.get_env("SUAVE_DATABASE_URL") || System.get_env("DATABASE_URL") + def get_suave_db_url, + do: UtilsConfigHelper.parse_url_env_var("SUAVE_DATABASE_URL") || UtilsConfigHelper.parse_url_env_var("DATABASE_URL") - def get_api_db_url, do: System.get_env("DATABASE_READ_ONLY_API_URL") || System.get_env("DATABASE_URL") + def get_api_db_url, + do: + UtilsConfigHelper.parse_url_env_var("DATABASE_READ_ONLY_API_URL") || + UtilsConfigHelper.parse_url_env_var("DATABASE_URL") - def get_mud_db_url, do: System.get_env("MUD_DATABASE_URL") || System.get_env("DATABASE_URL") + def get_mud_db_url, + do: UtilsConfigHelper.parse_url_env_var("MUD_DATABASE_URL") || UtilsConfigHelper.parse_url_env_var("DATABASE_URL") - def get_event_notification_db_url, do: System.get_env("DATABASE_EVENT_URL") || System.get_env("DATABASE_URL") + def get_event_notification_db_url, + do: UtilsConfigHelper.parse_url_env_var("DATABASE_EVENT_URL") || UtilsConfigHelper.parse_url_env_var("DATABASE_URL") def init_repo_module(module, opts) do db_url = Application.get_env(:explorer, module)[:url] diff --git a/apps/explorer/lib/explorer/utility/microservice.ex b/apps/explorer/lib/explorer/utility/microservice.ex index d839d1940912..613449bf1c04 100644 --- a/apps/explorer/lib/explorer/utility/microservice.ex +++ b/apps/explorer/lib/explorer/utility/microservice.ex @@ -3,26 +3,16 @@ defmodule Explorer.Utility.Microservice do Module is responsible for common utils related to microservices. """ - alias Explorer.Helper + alias Utils.ConfigHelper, as: UtilsConfigHelper @doc """ Returns base url of the microservice or nil if it is invalid or not set """ - @spec base_url(atom(), atom()) :: false | nil | binary() + @spec base_url(atom(), atom()) :: nil | binary() def base_url(application \\ :explorer, module) do url = Application.get_env(application, module)[:service_url] - cond do - not Helper.valid_url?(url) -> - nil - - String.ends_with?(url, "/") -> - url - |> String.slice(0..(String.length(url) - 2)) - - true -> - url - end + if UtilsConfigHelper.valid_url?(url), do: url, else: nil end @doc """ diff --git a/apps/explorer/lib/explorer/utility/replica_accessibility_manager.ex b/apps/explorer/lib/explorer/utility/replica_accessibility_manager.ex index a3388db20c04..e4e466575eaa 100644 --- a/apps/explorer/lib/explorer/utility/replica_accessibility_manager.ex +++ b/apps/explorer/lib/explorer/utility/replica_accessibility_manager.ex @@ -6,6 +6,7 @@ defmodule Explorer.Utility.ReplicaAccessibilityManager do use GenServer alias Explorer.Repo + alias Utils.ConfigHelper @interval :timer.seconds(10) @@ -15,7 +16,7 @@ defmodule Explorer.Utility.ReplicaAccessibilityManager do end def init(_) do - if System.get_env("DATABASE_READ_ONLY_API_URL") do + if ConfigHelper.parse_url_env_var("DATABASE_READ_ONLY_API_URL") do schedule_next_check(0) {:ok, %{}} diff --git a/apps/indexer/config/dev/anvil.exs b/apps/indexer/config/dev/anvil.exs index 6022fe550aeb..7b5ba51fa874 100644 --- a/apps/indexer/config/dev/anvil.exs +++ b/apps/indexer/config/dev/anvil.exs @@ -27,11 +27,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/besu.exs b/apps/indexer/config/dev/besu.exs index 3d843dc768d1..d75cf4074def 100644 --- a/apps/indexer/config/dev/besu.exs +++ b/apps/indexer/config/dev/besu.exs @@ -34,11 +34,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/erigon.exs b/apps/indexer/config/dev/erigon.exs index baddc1e4330d..8670d834cffe 100644 --- a/apps/indexer/config/dev/erigon.exs +++ b/apps/indexer/config/dev/erigon.exs @@ -34,11 +34,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/filecoin.exs b/apps/indexer/config/dev/filecoin.exs index be5860f69f85..2b89cf37b4b3 100644 --- a/apps/indexer/config/dev/filecoin.exs +++ b/apps/indexer/config/dev/filecoin.exs @@ -30,11 +30,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/geth.exs b/apps/indexer/config/dev/geth.exs index 825d6abe8df8..055bf8726c45 100644 --- a/apps/indexer/config/dev/geth.exs +++ b/apps/indexer/config/dev/geth.exs @@ -31,11 +31,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/nethermind.exs b/apps/indexer/config/dev/nethermind.exs index 934ba050caaf..49b742043e1d 100644 --- a/apps/indexer/config/dev/nethermind.exs +++ b/apps/indexer/config/dev/nethermind.exs @@ -34,11 +34,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/dev/rsk.exs b/apps/indexer/config/dev/rsk.exs index 6b1aae31fbee..9797c51cd7d9 100644 --- a/apps/indexer/config/dev/rsk.exs +++ b/apps/indexer/config/dev/rsk.exs @@ -35,11 +35,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/anvil.exs b/apps/indexer/config/prod/anvil.exs index 6022fe550aeb..7b5ba51fa874 100644 --- a/apps/indexer/config/prod/anvil.exs +++ b/apps/indexer/config/prod/anvil.exs @@ -27,11 +27,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/besu.exs b/apps/indexer/config/prod/besu.exs index 006bf9296db8..645d8a38320e 100644 --- a/apps/indexer/config/prod/besu.exs +++ b/apps/indexer/config/prod/besu.exs @@ -33,11 +33,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/erigon.exs b/apps/indexer/config/prod/erigon.exs index 83e1be5061ee..d6b20a513b73 100644 --- a/apps/indexer/config/prod/erigon.exs +++ b/apps/indexer/config/prod/erigon.exs @@ -33,11 +33,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/filecoin.exs b/apps/indexer/config/prod/filecoin.exs index 3a720cbc0f79..c1ac0d94d6b6 100644 --- a/apps/indexer/config/prod/filecoin.exs +++ b/apps/indexer/config/prod/filecoin.exs @@ -30,11 +30,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/geth.exs b/apps/indexer/config/prod/geth.exs index 44ba660e9849..014992d687bb 100644 --- a/apps/indexer/config/prod/geth.exs +++ b/apps/indexer/config/prod/geth.exs @@ -31,11 +31,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/nethermind.exs b/apps/indexer/config/prod/nethermind.exs index 83a1145d9b70..d58dcff04fae 100644 --- a/apps/indexer/config/prod/nethermind.exs +++ b/apps/indexer/config/prod/nethermind.exs @@ -33,11 +33,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/config/prod/rsk.exs b/apps/indexer/config/prod/rsk.exs index 6b1aae31fbee..9797c51cd7d9 100644 --- a/apps/indexer/config/prod/rsk.exs +++ b/apps/indexer/config/prod/rsk.exs @@ -35,11 +35,11 @@ config :indexer, ], subscribe_named_arguments: [ transport: - System.get_env("ETHEREUM_JSONRPC_WS_URL") && System.get_env("ETHEREUM_JSONRPC_WS_URL") !== "" && + ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL") && EthereumJSONRPC.WebSocket, transport_options: [ web_socket: EthereumJSONRPC.WebSocket.WebSocketClient, - url: System.get_env("ETHEREUM_JSONRPC_WS_URL"), - fallback_url: System.get_env("ETHEREUM_JSONRPC_FALLBACK_WS_URL") + url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_WS_URL"), + fallback_url: ConfigHelper.parse_url_env_var("ETHEREUM_JSONRPC_FALLBACK_WS_URL") ] ] diff --git a/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex index 2d7dc28fc685..938e021628aa 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/interop/multichain_export.ex @@ -18,7 +18,7 @@ defmodule Indexer.Fetcher.Optimism.Interop.MultichainExport do require Logger import Ecto.Query - import Explorer.Helper, only: [valid_url?: 1] + import Utils.ConfigHelper, only: [valid_url?: 1] import Indexer.Fetcher.Optimism.Interop.Helper, only: [log_cant_get_chain_id_from_rpc: 0] alias Ecto.Multi diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs index 169b0748d51e..d99e5122afd4 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_code_test.exs @@ -220,7 +220,7 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do end defp parse_time_env_var(env_var, default_value) do - case env_var |> safe_get_env(default_value) |> String.downcase() |> Integer.parse() do + case env_var |> Utils.ConfigHelper.safe_get_env(default_value) |> String.downcase() |> Integer.parse() do {milliseconds, "ms"} -> milliseconds {hours, "h"} -> :timer.hours(hours) {minutes, "m"} -> :timer.minutes(minutes) @@ -228,16 +228,6 @@ defmodule Indexer.Fetcher.OnDemand.ContractCodeTest do _ -> 0 end end - - defp safe_get_env(env_var, default_value) do - env_var - |> System.get_env(default_value) - |> case do - "" -> default_value - value -> value - end - |> to_string() - end end describe "get_or_fetch_bytecode/1" do diff --git a/apps/utils/lib/utils/config_helper.ex b/apps/utils/lib/utils/config_helper.ex index 6541317f6bb1..40814c79368a 100644 --- a/apps/utils/lib/utils/config_helper.ex +++ b/apps/utils/lib/utils/config_helper.ex @@ -16,4 +16,73 @@ defmodule Utils.ConfigHelper do _ -> :error end end + + @doc """ + Safely gets environment variable, returning default value if the variable is not set or is an + empty string. + """ + @spec safe_get_env(String.t(), any()) :: String.t() + def safe_get_env(env_var, default_value) do + env_var + |> System.get_env(default_value) + |> case do + "" -> default_value + value -> value + end + |> then(fn + nil -> "" + value -> to_string(value) + end) + end + + @doc """ + Parses an URL from an environment variable with optional trailing slash handling. + + Retrieves the URL from the specified environment variable, normalizes it by + trimming any trailing slash, and optionally appends a trailing slash based + on the `trailing_slash_needed?` parameter. If the environment variable is + not set or is empty, returns the default value or nil. + + ## Parameters + - `env_var`: The name of the environment variable containing the URL. + - `default_value`: The value to return if the environment variable is not + set or is empty (default: nil). + - `trailing_slash_needed?`: Whether to append a trailing slash to non-empty + URLs (default: false). + + ## Returns + - A normalized URL string (with or without trailing slash based on the + `trailing_slash_needed?` parameter) if the environment variable contains + a non-empty value. + - The `default_value` if the environment variable is not set or is empty. + - nil if the environment variable is not set and no default value is + provided. + """ + @spec parse_url_env_var(String.t(), String.t() | nil, boolean()) :: String.t() | nil + def parse_url_env_var(env_var, default_value \\ nil, trailing_slash_needed? \\ false) do + with url when url != "" <- safe_get_env(env_var, default_value), + true <- valid_url?(url), + url <- String.trim_trailing(url, "/"), + {url, true} <- {url, trailing_slash_needed?} do + url <> "/" + else + {url, false} -> + url + + _ -> + default_value + end + end + + @doc """ + Checks if input string is a valid URL + """ + @spec valid_url?(term()) :: boolean() + def valid_url?(string) when is_binary(string) do + uri = URI.parse(string) + + !is_nil(uri.scheme) && !is_nil(uri.host) + end + + def valid_url?(_), do: false end diff --git a/config/config_helper.exs b/config/config_helper.exs index a26621cdb43e..2d1fbd9c3238 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -4,7 +4,6 @@ defmodule ConfigHelper do import Bitwise alias Explorer.Market.Source alias Indexer.Transform.Blocks - alias Utils.ConfigHelper def repos do base_repos = [Explorer.Repo, Explorer.Repo.Account] @@ -165,7 +164,7 @@ defmodule ConfigHelper do nil value -> - case ConfigHelper.parse_time_value(value) do + case Utils.ConfigHelper.parse_time_value(value) do :error -> raise "Invalid time format in environment variable #{env_var}: #{value}" @@ -232,16 +231,6 @@ defmodule ConfigHelper do end end - def safe_get_env(env_var, default_value) do - env_var - |> System.get_env(default_value) - |> case do - "" -> default_value - value -> value - end - |> to_string() - end - @spec parse_bool_env_var(String.t(), String.t()) :: boolean() def parse_bool_env_var(env_var, default_value \\ "false"), do: String.downcase(safe_get_env(env_var, default_value)) == "true" @@ -387,23 +376,34 @@ defmodule ConfigHelper do end end - @spec parse_url_env_var(String.t(), boolean()) :: String.t() | nil + @spec parse_url_env_var(String.t(), String.t() | nil, boolean()) :: String.t() | nil def parse_url_env_var(env_var, default_value \\ nil, trailing_slash_needed? \\ false) do - with url when not is_nil(url) <- safe_get_env(env_var, default_value), + with url when url != "" <- safe_get_env(env_var, default_value), + true <- valid_url?(url), url <- String.trim_trailing(url, "/"), - true <- url != "", {url, true} <- {url, trailing_slash_needed?} do url <> "/" else {url, false} -> url - false -> + _ -> default_value + end + end - nil -> - nil + @spec safe_get_env(String.t(), any()) :: String.t() + def safe_get_env(env_var, default_value) do + env_var + |> System.get_env(default_value) + |> case do + "" -> default_value + value -> value end + |> then(fn + nil -> "" + value -> to_string(value) + end) end @supported_chain_identities %{ @@ -538,36 +538,9 @@ defmodule ConfigHelper do end end - @doc """ - Parses and validates a microservice URL from an environment variable, removing any trailing slash. - - ## Parameters - - `env_name`: The name of the environment variable containing the URL - - ## Returns - - The validated URL string with any trailing slash removed - - `nil` if the URL is invalid or missing required components - """ - @spec parse_microservice_url(String.t()) :: String.t() | nil - def parse_microservice_url(env_name) do - url = System.get_env(env_name) - - cond do - not valid_url?(url) -> - nil - - String.ends_with?(url, "/") -> - url - |> String.slice(0..(String.length(url) - 2)) - - true -> - url - end - end - # Validates if the given string is a valid URL by checking if it has both scheme (like http, # https, ftp) and host components. - @spec valid_url?(String.t()) :: boolean() + @spec valid_url?(term()) :: boolean() defp valid_url?(string) when is_binary(string) do uri = URI.parse(string) diff --git a/config/runtime.exs b/config/runtime.exs index 015149587297..71edf1154af9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -19,7 +19,7 @@ config :logger, :api, metadata: ConfigHelper.logger_metadata(), metadata_filter: config :logger, :api_v2, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api_v2] -microservice_multichain_search_url = System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_URL") +microservice_multichain_search_url = ConfigHelper.parse_url_env_var("MICROSERVICE_MULTICHAIN_SEARCH_URL") transactions_stats_enabled = ConfigHelper.parse_bool_env_var("TXS_STATS_ENABLED", "true") ###################### @@ -34,8 +34,8 @@ config :block_scout_web, show_percentage: ConfigHelper.parse_bool_env_var("SHOW_ADDRESS_MARKETCAP_PERCENTAGE", "true"), checksum_address_hashes: ConfigHelper.parse_bool_env_var("CHECKSUM_ADDRESS_HASHES", "true"), other_networks: System.get_env("SUPPORTED_CHAINS"), - webapp_url: System.get_env("WEBAPP_URL"), - api_url: System.get_env("API_URL"), + webapp_url: ConfigHelper.parse_url_env_var("WEBAPP_URL"), + api_url: ConfigHelper.parse_url_env_var("API_URL"), apps_menu: ConfigHelper.parse_bool_env_var("APPS_MENU"), apps: System.get_env("APPS") || System.get_env("EXTERNAL_APPS"), gas_price: System.get_env("GAS_PRICE"), @@ -164,7 +164,7 @@ config :block_scout_web, :api_rate_limit, do: BlockScoutWeb.RateLimit.Hammer.ETS, else: BlockScoutWeb.RateLimit.Hammer.Redis ), - config_url: System.get_env("API_RATE_LIMIT_CONFIG_URL") + config_url: ConfigHelper.parse_url_env_var("API_RATE_LIMIT_CONFIG_URL") config :block_scout_web, :remote_ip, is_blockscout_behind_proxy: ConfigHelper.parse_bool_env_var("API_RATE_LIMIT_IS_BLOCKSCOUT_BEHIND_PROXY"), @@ -216,7 +216,7 @@ config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance, config :block_scout_web, BlockScoutWeb.API.V2, enabled: ConfigHelper.parse_bool_env_var("API_V2_ENABLED", "true") config :block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, - service_url: System.get_env("MICROSERVICE_TRANSACTION_INTERPRETATION_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_TRANSACTION_INTERPRETATION_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TRANSACTION_INTERPRETATION_ENABLED") # Configures Ueberauth's Auth0 auth provider @@ -449,11 +449,11 @@ config :explorer, Explorer.Market.Source, config :explorer, Explorer.Market.Source.CoinGecko, platform: System.get_env("MARKET_COINGECKO_PLATFORM_ID") || System.get_env("EXCHANGE_RATES_COINGECKO_PLATFORM_ID"), base_url: - System.get_env("MARKET_COINGECKO_BASE_URL") || - System.get_env("EXCHANGE_RATES_COINGECKO_BASE_URL", "https://api.coingecko.com/api/v3"), + ConfigHelper.parse_url_env_var("MARKET_COINGECKO_BASE_URL") || + ConfigHelper.parse_url_env_var("EXCHANGE_RATES_COINGECKO_BASE_URL", "https://api.coingecko.com/api/v3"), base_pro_url: - System.get_env("MARKET_COINGECKO_BASE_PRO_URL") || - System.get_env("EXCHANGE_RATES_COINGECKO_BASE_PRO_URL", "https://pro-api.coingecko.com/api/v3"), + ConfigHelper.parse_url_env_var("MARKET_COINGECKO_BASE_PRO_URL") || + ConfigHelper.parse_url_env_var("EXCHANGE_RATES_COINGECKO_BASE_PRO_URL", "https://pro-api.coingecko.com/api/v3"), api_key: System.get_env("MARKET_COINGECKO_API_KEY") || System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"), coin_id: System.get_env("MARKET_COINGECKO_COIN_ID") || System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"), secondary_coin_id: @@ -462,8 +462,8 @@ config :explorer, Explorer.Market.Source.CoinGecko, config :explorer, Explorer.Market.Source.CoinMarketCap, base_url: - System.get_env("MARKET_COINMARKETCAP_BASE_URL") || - System.get_env("EXCHANGE_RATES_COINMARKETCAP_BASE_URL", "https://pro-api.coinmarketcap.com/v2"), + ConfigHelper.parse_url_env_var("MARKET_COINMARKETCAP_BASE_URL") || + ConfigHelper.parse_url_env_var("EXCHANGE_RATES_COINMARKETCAP_BASE_URL", "https://pro-api.coinmarketcap.com/v2"), api_key: System.get_env("MARKET_COINMARKETCAP_API_KEY") || System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"), coin_id: System.get_env("MARKET_COINMARKETCAP_COIN_ID") || System.get_env("EXCHANGE_RATES_COINMARKETCAP_COIN_ID"), secondary_coin_id: @@ -472,7 +472,7 @@ config :explorer, Explorer.Market.Source.CoinMarketCap, currency_id: "2781" config :explorer, Explorer.Market.Source.CryptoCompare, - base_url: System.get_env("MARKET_CRYPTOCOMPARE_BASE_URL", "https://min-api.cryptocompare.com"), + base_url: ConfigHelper.parse_url_env_var("MARKET_CRYPTOCOMPARE_BASE_URL", "https://min-api.cryptocompare.com"), coin_symbol: System.get_env("MARKET_CRYPTOCOMPARE_COIN_SYMBOL", coin), secondary_coin_symbol: System.get_env("MARKET_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") || @@ -484,8 +484,8 @@ config :explorer, Explorer.Market.Source.CryptoRank, ConfigHelper.parse_integer_or_nil_env_var("MARKET_CRYPTORANK_PLATFORM_ID") || ConfigHelper.parse_integer_or_nil_env_var("EXCHANGE_RATES_CRYPTORANK_PLATFORM_ID"), base_url: - System.get_env("MARKET_CRYPTORANK_BASE_URL") || - System.get_env("EXCHANGE_RATES_CRYPTORANK_BASE_URL", "https://api.cryptorank.io/v1/"), + ConfigHelper.parse_url_env_var("MARKET_CRYPTORANK_BASE_URL") || + ConfigHelper.parse_url_env_var("EXCHANGE_RATES_CRYPTORANK_BASE_URL", "https://api.cryptorank.io/v1"), api_key: System.get_env("MARKET_CRYPTORANK_API_KEY") || System.get_env("EXCHANGE_RATES_CRYPTORANK_API_KEY"), coin_id: System.get_env("MARKET_CRYPTORANK_COIN_ID") || @@ -502,8 +502,8 @@ config :explorer, Explorer.Market.Source.DefiLlama, config :explorer, Explorer.Market.Source.Mobula, platform: System.get_env("MARKET_MOBULA_PLATFORM_ID") || System.get_env("EXCHANGE_RATES_MOBULA_CHAIN_ID"), base_url: - System.get_env("MARKET_MOBULA_BASE_URL") || - System.get_env("EXCHANGE_RATES_MOBULA_BASE_URL", "https://api.mobula.io/api/1"), + ConfigHelper.parse_url_env_var("MARKET_MOBULA_BASE_URL") || + ConfigHelper.parse_url_env_var("EXCHANGE_RATES_MOBULA_BASE_URL", "https://api.mobula.io/api/1"), api_key: System.get_env("MARKET_MOBULA_API_KEY") || System.get_env("EXCHANGE_RATES_MOBULA_API_KEY"), coin_id: System.get_env("MARKET_MOBULA_COIN_ID") || System.get_env("EXCHANGE_RATES_MOBULA_COIN_ID"), secondary_coin_id: @@ -511,7 +511,7 @@ config :explorer, Explorer.Market.Source.Mobula, config :explorer, Explorer.Market.Source.DIA, blockchain: System.get_env("MARKET_DIA_BLOCKCHAIN"), - base_url: System.get_env("MARKET_DIA_BASE_URL", "https://api.diadata.org/v1"), + base_url: ConfigHelper.parse_url_env_var("MARKET_DIA_BASE_URL", "https://api.diadata.org/v1"), coin_address_hash: System.get_env("MARKET_DIA_COIN_ADDRESS_HASH"), secondary_coin_address_hash: System.get_env("MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH") @@ -619,10 +619,10 @@ config :explorer, Explorer.Chain.Cache.CeloCoreContracts, contracts: ConfigHelper.parse_json_env_var("CELO_CORE_CONTRACTS") config :explorer, Explorer.ThirdPartyIntegrations.Sourcify, - server_url: System.get_env("SOURCIFY_SERVER_URL") || "https://sourcify.dev/server", + server_url: ConfigHelper.parse_url_env_var("SOURCIFY_SERVER_URL", "https://sourcify.dev/server"), enabled: ConfigHelper.parse_bool_env_var("SOURCIFY_INTEGRATION_ENABLED"), chain_id: System.get_env("CHAIN_ID"), - repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts" + repo_url: ConfigHelper.parse_url_env_var("SOURCIFY_REPO_URL", "https://repo.sourcify.dev/contracts") config :explorer, Explorer.ThirdPartyIntegrations.SolidityScan, platform_id: System.get_env("SOLIDITYSCAN_PLATFORM_ID", "16"), @@ -630,12 +630,12 @@ config :explorer, Explorer.ThirdPartyIntegrations.SolidityScan, api_key: System.get_env("SOLIDITYSCAN_API_TOKEN") config :explorer, Explorer.ThirdPartyIntegrations.NovesFi, - service_url: System.get_env("NOVES_FI_BASE_API_URL") || "https://blockscout.noves.fi", + service_url: ConfigHelper.parse_url_env_var("NOVES_FI_BASE_API_URL", "https://blockscout.noves.fi"), chain_name: System.get_env("NOVES_FI_CHAIN_NAME"), api_key: System.get_env("NOVES_FI_API_TOKEN") config :explorer, Explorer.ThirdPartyIntegrations.Xname, - service_url: System.get_env("XNAME_BASE_API_URL", "https://gateway.xname.app"), + service_url: ConfigHelper.parse_url_env_var("XNAME_BASE_API_URL", "https://gateway.xname.app"), api_key: System.get_env("XNAME_API_TOKEN") enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED", "true") @@ -643,35 +643,36 @@ enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED", " type = System.get_env("MICROSERVICE_SC_VERIFIER_TYPE", "sc_verifier") config :explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour, - service_url: System.get_env("MICROSERVICE_SC_VERIFIER_URL") || "https://eth-bytecode-db.services.blockscout.com/", + service_url: + ConfigHelper.parse_url_env_var("MICROSERVICE_SC_VERIFIER_URL", "https://eth-bytecode-db.services.blockscout.com"), enabled: enabled?, type: type, eth_bytecode_db?: enabled? && type == "eth_bytecode_db", api_key: System.get_env("MICROSERVICE_SC_VERIFIER_API_KEY") config :explorer, Explorer.Visualize.Sol2uml, - service_url: System.get_env("MICROSERVICE_VISUALIZE_SOL2UML_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_VISUALIZE_SOL2UML_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_VISUALIZE_SOL2UML_ENABLED") config :explorer, Explorer.SmartContract.SigProviderInterface, - service_url: System.get_env("MICROSERVICE_SIG_PROVIDER_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_SIG_PROVIDER_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_SIG_PROVIDER_ENABLED") config :explorer, Explorer.MicroserviceInterfaces.BENS, - service_url: System.get_env("MICROSERVICE_BENS_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_BENS_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_BENS_ENABLED") config :explorer, Explorer.MicroserviceInterfaces.AccountAbstraction, - service_url: System.get_env("MICROSERVICE_ACCOUNT_ABSTRACTION_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_ACCOUNT_ABSTRACTION_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_ACCOUNT_ABSTRACTION_ENABLED") config :explorer, Explorer.MicroserviceInterfaces.Metadata, - service_url: System.get_env("MICROSERVICE_METADATA_URL"), + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_METADATA_URL"), enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_METADATA_ENABLED"), proxy_requests_timeout: ConfigHelper.parse_time_env_var("MICROSERVICE_METADATA_PROXY_REQUESTS_TIMEOUT", "30s") config :explorer, Explorer.SmartContract.StylusVerifierInterface, - service_url: ConfigHelper.parse_microservice_url("MICROSERVICE_STYLUS_VERIFIER_URL") + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_STYLUS_VERIFIER_URL") config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, api_key: System.get_env("MICROSERVICE_MULTICHAIN_SEARCH_API_KEY"), @@ -684,7 +685,7 @@ config :explorer, Explorer.MicroserviceInterfaces.MultichainSearch, config :explorer, Explorer.MicroserviceInterfaces.TACOperationLifecycle, enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_ENABLED", "true"), - service_url: System.get_env("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") + service_url: ConfigHelper.parse_url_env_var("MICROSERVICE_TAC_OPERATION_LIFECYCLE_URL") audit_reports_table_url = System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_URL") audit_reports_api_key = System.get_env("CONTRACT_AUDIT_REPORTS_AIRTABLE_API_KEY") @@ -884,7 +885,7 @@ config :explorer, Explorer.Chain.Filecoin.NativeAddress, config :explorer, Explorer.Chain.Blackfort.Validator, api_url: System.get_env("BLACKFORT_VALIDATOR_API_URL") -addresses_blacklist_url = ConfigHelper.parse_microservice_url("ADDRESSES_BLACKLIST_URL") +addresses_blacklist_url = ConfigHelper.parse_url_env_var("ADDRESSES_BLACKLIST_URL") config :explorer, Explorer.Chain.Fetcher.AddressesBlacklist, url: addresses_blacklist_url, @@ -893,7 +894,7 @@ config :explorer, Explorer.Chain.Fetcher.AddressesBlacklist, retry_interval: ConfigHelper.parse_time_env_var("ADDRESSES_BLACKLIST_RETRY_INTERVAL", "5s"), provider: ConfigHelper.parse_catalog_value("ADDRESSES_BLACKLIST_PROVIDER", ["blockaid"], false, "blockaid") -rate_limiter_redis_url = System.get_env("RATE_LIMITER_REDIS_URL") +rate_limiter_redis_url = ConfigHelper.parse_url_env_var("RATE_LIMITER_REDIS_URL") config :explorer, Explorer.Utility.RateLimiter, storage: (rate_limiter_redis_url && :redis) || :ets, @@ -910,7 +911,7 @@ config :explorer, Explorer.Utility.RateLimiter, config :explorer, Explorer.ThirdPartyIntegrations.UniversalProxy, config_url: - System.get_env( + ConfigHelper.parse_url_env_var( "UNIVERSAL_PROXY_CONFIG_URL", "https://raw.githubusercontent.com/blockscout/backend-configs/refs/heads/main/universal-proxy-config.json" ) @@ -976,12 +977,12 @@ config :indexer, ConfigHelper.parse_catalog_value("INDEXER_INTERNAL_TRANSACTIONS_FETCH_ORDER", ["asc", "desc"], true, "asc") config :indexer, :ipfs, - gateway_url: System.get_env("IPFS_GATEWAY_URL", "https://ipfs.io/ipfs"), + gateway_url: ConfigHelper.parse_url_env_var("IPFS_GATEWAY_URL", "https://ipfs.io/ipfs"), gateway_url_param_key: System.get_env("IPFS_GATEWAY_URL_PARAM_KEY"), gateway_url_param_value: System.get_env("IPFS_GATEWAY_URL_PARAM_VALUE"), gateway_url_param_location: ConfigHelper.parse_catalog_value("IPFS_GATEWAY_URL_PARAM_LOCATION", ["query", "header"], true), - public_gateway_url: System.get_env("IPFS_PUBLIC_GATEWAY_URL", "https://ipfs.io/ipfs") + public_gateway_url: ConfigHelper.parse_url_env_var("IPFS_PUBLIC_GATEWAY_URL", "https://ipfs.io/ipfs") config :indexer, :arc, arc_native_token_decimals: ConfigHelper.parse_integer_env_var("INDEXER_ARC_NATIVE_TOKEN_DECIMALS", 6), @@ -1561,11 +1562,11 @@ config :indexer, Indexer.Fetcher.Celo.Legacy.Account.Supervisor, disabled?: not (ConfigHelper.chain_identity() == {:optimism, :celo}) config :indexer, Indexer.Fetcher.Filecoin.BeryxAPI, - base_url: ConfigHelper.safe_get_env("BERYX_API_BASE_URL", "https://api.zondax.ch/fil/data/v3/mainnet"), + base_url: ConfigHelper.parse_url_env_var("BERYX_API_BASE_URL", "https://api.zondax.ch/fil/data/v3/mainnet"), api_token: System.get_env("BERYX_API_TOKEN") config :indexer, Indexer.Fetcher.Filecoin.FilfoxAPI, - base_url: ConfigHelper.safe_get_env("FILFOX_API_BASE_URL", "https://filfox.info/api/v1") + base_url: ConfigHelper.parse_url_env_var("FILFOX_API_BASE_URL", "https://filfox.info/api/v1") config :indexer, Indexer.Fetcher.Filecoin.AddressInfo.Supervisor, disabled?: diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs index 2ca2524d4dde..f40189052ac2 100644 --- a/config/runtime/dev.exs +++ b/config/runtime/dev.exs @@ -65,7 +65,7 @@ queue_target = ConfigHelper.parse_integer_env_var("DATABASE_QUEUE_TARGET", 50) config :explorer, Explorer.Repo, database: database, hostname: hostname, - url: System.get_env("DATABASE_URL"), + url: ConfigHelper.parse_url_env_var("DATABASE_URL"), pool_size: pool_size, queue_target: queue_target @@ -147,7 +147,7 @@ for repo <- [ config :explorer, repo, database: database, hostname: hostname, - url: System.get_env("DATABASE_URL"), + url: ConfigHelper.parse_url_env_var("DATABASE_URL"), pool_size: 1 end diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs index 1096c999e81a..d00c8187744e 100644 --- a/config/runtime/prod.exs +++ b/config/runtime/prod.exs @@ -42,7 +42,7 @@ queue_target = ConfigHelper.parse_integer_env_var("DATABASE_QUEUE_TARGET", 50) # Configures the database config :explorer, Explorer.Repo, - url: System.get_env("DATABASE_URL"), + url: ConfigHelper.parse_url_env_var("DATABASE_URL"), pool_size: pool_size, ssl: ExplorerConfigHelper.ssl_enabled?(), queue_target: queue_target @@ -107,7 +107,7 @@ for repo <- [ Explorer.Repo.Neon ] do config :explorer, repo, - url: System.get_env("DATABASE_URL"), + url: ConfigHelper.parse_url_env_var("DATABASE_URL"), pool_size: 1, ssl: ExplorerConfigHelper.ssl_enabled?() end diff --git a/mix.exs b/mix.exs index eee9d0f914d9..7601eaba9f4b 100644 --- a/mix.exs +++ b/mix.exs @@ -38,7 +38,12 @@ defmodule BlockScout.Mixfile do defp copy_prod_runtime_config(%Mix.Release{path: path} = release) do File.mkdir_p!(Path.join([path, "config", "runtime"])) - File.cp!(Path.join(["config", "runtime", "prod.exs"]), Path.join([path, "config", "runtime", "prod.exs"])) + + File.cp!( + Path.join(["config", "runtime", "prod.exs"]), + Path.join([path, "config", "runtime", "prod.exs"]) + ) + File.mkdir_p!(Path.join([path, "apps", "explorer", "config", "prod"])) File.cp_r!( @@ -47,7 +52,11 @@ defmodule BlockScout.Mixfile do ) File.mkdir_p!(Path.join([path, "apps", "indexer", "config", "prod"])) - File.cp_r!(Path.join(["apps", "indexer", "config", "prod"]), Path.join([path, "apps", "indexer", "config", "prod"])) + + File.cp_r!( + Path.join(["apps", "indexer", "config", "prod"]), + Path.join([path, "apps", "indexer", "config", "prod"]) + ) release end From 251e836ff86f82405a5ada7ad80ceaacc61ad14f Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 14:51:08 +0300 Subject: [PATCH 0582/1006] refactor: Improve error handling in `EthereumJSONRPC.execute_contract_function/3` (#13764) Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- .../api/v2/token_controller_test.exs | 9 +- .../lib/ethereum_jsonrpc/contract.ex | 127 +++++++++---- .../test/ethereum_jsonrpc/contract_test.exs | 177 ++++++++++++++++++ apps/explorer/config/test.exs | 1 + .../explorer/validator/metadata_retriever.ex | 2 + .../token/metadata_retriever_test.exs | 2 +- .../fetcher/token_instance/helper_test.exs | 36 ++-- .../fetcher/token_instance/refetch_test.exs | 22 +-- .../token_instance/sanitize_erc1155_test.exs | 17 ++ .../token_instance/sanitize_erc721_test.exs | 25 +++ .../transform/token_transfers_test.exs | 12 ++ 11 files changed, 360 insertions(+), 70 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index 4a491d055539..6b1027249674 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1139,7 +1139,12 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do |> insert() |> with_block() - instance = insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash) + instance = + insert(:token_instance, + token_id: 0, + token_contract_address_hash: token.contract_address_hash, + skip_metadata_url: true + ) _transfer = insert(:token_transfer, @@ -1175,6 +1180,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash, + skip_metadata_url: true, metadata: %{ "image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/gm%20gm%20feelin%20blue%204k.png" } @@ -1211,6 +1217,7 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do insert(:token_instance, token_id: 0, token_contract_address_hash: token.contract_address_hash, + skip_metadata_url: true, metadata: %{ "image_url" => "ipfs://QmTQBtvkCQKnxbUejwYHrs2G74JR2qFwxPUqRb3BQ6BM3S/123.png" } diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex index 75b5e69ef49f..8aa833daf795 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/contract.ex @@ -30,54 +30,111 @@ defmodule EthereumJSONRPC.Contract do call_result() ] def execute_contract_functions(requests, abi, json_rpc_named_arguments, leave_error_as_map \\ false) do - parsed_abi = - abi - |> ABI.parse_specification() - + parsed_abi = ABI.parse_specification(abi) functions = Enum.into(parsed_abi, %{}, &{&1.method_id, &1}) - requests_with_index = Enum.with_index(requests) - indexed_responses = + {valid_requests, local_errors} = requests_with_index - |> Enum.map(fn {%{contract_address: contract_address, method_id: target_method_id, args: args} = request, index} -> - function = - functions - |> define_function(target_method_id) - |> Map.drop([:method_id]) + |> build_rpc_requests(functions) + |> Enum.split_with(&match?({:ok, _}, &1)) - formatted_args = format_args(function, args) + local_error_map = + local_errors + |> Enum.into(%{}, fn {:error, {index, message}} -> {index, {:local_error, message}} end) - function - |> Encoder.encode_function_call(formatted_args) - |> eth_call_request(contract_address, index, Map.get(request, :block_number), Map.get(request, :from)) - end) - |> json_rpc(json_rpc_named_arguments) - |> case do - {:ok, responses} -> responses - {:error, {:bad_gateway, _request_url}} -> raise "Bad gateway" - {:error, {reason, _request_url}} -> raise to_string(reason) - {:error, reason} when is_atom(reason) -> raise Atom.to_string(reason) - {:error, error} -> raise error + indexed_responses = + case valid_requests do + [] -> + {:ok, local_error_map} + + _ -> + valid_requests + |> Enum.map(fn {:ok, request} -> request end) + |> safe_json_rpc(json_rpc_named_arguments) + |> handle_batch_response() + |> merge_local_errors(local_error_map) end - |> Enum.into(%{}, &{&1.id, &1}) - - Enum.map(requests_with_index, fn {%{method_id: method_id}, index} -> - indexed_responses[index] - |> case do - nil -> - {:error, "No result"} - response -> - selectors = define_selectors(parsed_abi, method_id) + process_responses(indexed_responses, requests_with_index, parsed_abi, requests, leave_error_as_map) + end - {^index, result} = Encoder.decode_result(response, selectors, leave_error_as_map) - result + defp build_rpc_requests(requests_with_index, functions) do + Enum.map(requests_with_index, fn {%{contract_address: contract_address, method_id: target_method_id, args: args} = + request, index} -> + function = + functions + |> define_function(target_method_id) + |> Map.drop([:method_id]) + + with {:ok, formatted_args} <- safe_format_args(function, args), + {:ok, encoded} <- safe_encode_function_call(function, formatted_args) do + {:ok, + eth_call_request(encoded, contract_address, index, Map.get(request, :block_number), Map.get(request, :from))} + else + {:error, message} -> {:error, {index, message}} end end) + end + + defp merge_local_errors({:ok, response_map}, local_error_map), do: {:ok, Map.merge(response_map, local_error_map)} + defp merge_local_errors(other, _local_error_map), do: other + + defp handle_batch_response({:ok, responses}), do: {:ok, Enum.into(responses, %{}, &{&1.id, &1})} + defp handle_batch_response({:error, {:bad_gateway, _request_url}}), do: {:error, :batch_error, "Bad gateway"} + defp handle_batch_response({:error, {reason, _request_url}}), do: {:error, :batch_error, to_string(reason)} + defp handle_batch_response({:error, reason}) when is_atom(reason), do: {:error, :batch_error, Atom.to_string(reason)} + defp handle_batch_response({:error, error}), do: {:error, :batch_error, error} + + defp process_responses({:ok, response_map}, requests_with_index, parsed_abi, _requests, leave_error_as_map) do + Enum.map(requests_with_index, fn {%{method_id: method_id}, index} -> + process_single_response(response_map[index], index, method_id, parsed_abi, leave_error_as_map) + end) + end + + defp process_responses( + {:error, :batch_error, error}, + _requests_with_index, + _parsed_abi, + requests, + _leave_error_as_map + ) do + # Only apply error to all requests if the entire batch failed + Enum.map(requests, fn _ -> format_error(error) end) + end + + defp process_single_response(nil, _index, _method_id, _parsed_abi, _leave_error_as_map), do: {:error, "No result"} + + defp process_single_response({:local_error, message}, _index, _method_id, _parsed_abi, _leave_error_as_map), + do: {:error, message} + + defp process_single_response(response, index, method_id, parsed_abi, leave_error_as_map) do + selectors = define_selectors(parsed_abi, method_id) + {^index, result} = Encoder.decode_result(response, selectors, leave_error_as_map) + result rescue error -> - Enum.map(requests, fn _ -> format_error(error) end) + format_error(error) + end + + defp safe_format_args(function, args) do + {:ok, format_args(function, args)} + rescue + e -> {:error, Exception.message(e)} + end + + defp safe_encode_function_call(function, formatted_args) do + {:ok, Encoder.encode_function_call(function, formatted_args)} + rescue + e -> {:error, Exception.message(e)} + end + + defp safe_json_rpc(requests, json_rpc_named_arguments) do + json_rpc(requests, json_rpc_named_arguments) + rescue + error -> {:error, error} + catch + :exit, reason -> {:error, reason} end defp format_args(function, args) do diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs index 76100d469b3c..798df71234b5 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/contract_test.exs @@ -136,5 +136,182 @@ defmodule EthereumJSONRPC.ContractTest do json_rpc_named_arguments ) == blockchain_result end + + test "handles individual request processing errors without affecting other requests" do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + abi = [ + %{ + "constant" => true, + "inputs" => [], + "name" => "get1", + "method_id" => "054c1a75", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get2", + "method_id" => "d2178b08", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get3", + "method_id" => "8321045c", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ] + + contract_address = "0x0000000000000000000000000000000000000000" + + functions = [ + %{contract_address: contract_address, method_id: "054c1a75", args: []}, + %{contract_address: contract_address, method_id: "d2178b08", args: []}, + %{contract_address: contract_address, method_id: "8321045c", args: []} + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn requests, _options -> + {:ok, + requests + |> Enum.map(fn + %{id: id, method: "eth_call", params: [%{data: "0x054c1a75", to: ^contract_address}, "latest"]} -> + %{ + id: id, + result: "0x000000000000000000000000000000000000000000000000000000000000002a" + } + + %{id: id, method: "eth_call", params: [%{data: "0xd2178b08", to: ^contract_address}, "latest"]} -> + # This will cause a decoding error due to invalid hex + %{ + id: id, + result: "0xINVALID" + } + + %{id: id, method: "eth_call", params: [%{data: "0x8321045c", to: ^contract_address}, "latest"]} -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000064" + } + end) + |> Enum.shuffle()} + end + ) + + results = + EthereumJSONRPC.execute_contract_functions( + functions, + abi, + json_rpc_named_arguments + ) + + # First request should succeed + assert {:ok, [42]} = Enum.at(results, 0) + + # Second request should fail with an error (not affecting others) + assert {:error, _error_message} = Enum.at(results, 1) + + # Third request should succeed + assert {:ok, [100]} = Enum.at(results, 2) + end + + test "handles bad_gateway error for entire batch" do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + abi = [ + %{ + "constant" => false, + "inputs" => [], + "name" => "get", + "method_id" => "6d4ce63c", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ] + + contract_address = "0x0000000000000000000000000000000000000000" + + functions = [ + %{contract_address: contract_address, method_id: "6d4ce63c", args: []}, + %{contract_address: contract_address, method_id: "6d4ce63c", args: []} + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn _requests, _options -> + {:error, {:bad_gateway, "http://localhost:8545"}} + end + ) + + blockchain_result = [ + {:error, "Bad gateway"}, + {:error, "Bad gateway"} + ] + + assert EthereumJSONRPC.execute_contract_functions( + functions, + abi, + json_rpc_named_arguments + ) == blockchain_result + end + + test "handles atom error for entire batch" do + json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) + + abi = [ + %{ + "constant" => false, + "inputs" => [], + "name" => "get", + "method_id" => "6d4ce63c", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ] + + contract_address = "0x0000000000000000000000000000000000000000" + + functions = [ + %{contract_address: contract_address, method_id: "6d4ce63c", args: []}, + %{contract_address: contract_address, method_id: "6d4ce63c", args: []} + ] + + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn _requests, _options -> + {:error, :timeout} + end + ) + + blockchain_result = [ + {:error, "timeout"}, + {:error, "timeout"} + ] + + assert EthereumJSONRPC.execute_contract_functions( + functions, + abi, + json_rpc_named_arguments + ) == blockchain_result + end end end diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 27c2694dec3f..2de39c4c3e2e 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -113,5 +113,6 @@ config :logger, :explorer, path: Path.absname("logs/test/explorer.log") config :explorer, Explorer.Chain.Fetcher.CheckBytecodeMatchingOnDemand, enabled: false config :explorer, Explorer.Chain.Fetcher.FetchValidatorInfoOnDemand, enabled: false +config :explorer, Explorer.Tags.AddressTag.Cataloger, enabled: false config :tesla, adapter: Explorer.Mock.TeslaAdapter diff --git a/apps/explorer/lib/explorer/validator/metadata_retriever.ex b/apps/explorer/lib/explorer/validator/metadata_retriever.ex index fb3f8acedde4..756e5fea01fc 100644 --- a/apps/explorer/lib/explorer/validator/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/validator/metadata_retriever.ex @@ -5,6 +5,7 @@ defmodule Explorer.Validator.MetadataRetriever do alias Explorer.SmartContract.Reader + @spec fetch_data() :: [%{address_hash: String.t(), primary: true, name: String.t(), metadata: map()}] def fetch_data do fetch_validators_list() |> Enum.map(fn validator -> @@ -15,6 +16,7 @@ defmodule Explorer.Validator.MetadataRetriever do end) end + @spec fetch_validators_list() :: [String.t()] def fetch_validators_list do validators_contract_address = config(:validators_contract_address) diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index f18a07b94097..9d19c0fddbbc 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -615,7 +615,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do fn requests, _opts -> {:ok, Enum.map(requests, fn - %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} -> + %{id: id, method: "eth_call", params: [%{data: _, to: _}, "latest"]} -> %{ id: id, error: %{code: -32015, data: "something", message: "network error"}, diff --git a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs index bda1a86a3531..ee7da342f8b1 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/helper_test.exs @@ -74,14 +74,10 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do encoded_url ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Bypass.expect( - bypass, - "GET", - "/api/card/0000000000000000000000000000000000000000000000000000000000000309", - fn conn -> - Conn.resp(conn, 200, json) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: _url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: json}} end ) @@ -242,14 +238,10 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do encoded_url ) - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) - - Bypass.expect( - bypass, - "GET", - "/api/card/0000000000000000000000000000000000000000000000000000000000000309", - fn conn -> - Conn.resp(conn, 200, json) + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: _url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: json}} end ) @@ -416,10 +408,22 @@ defmodule Indexer.Fetcher.TokenInstance.HelperTest do token_instance = build(:token_instance, token_contract_address_hash: erc_721_token.contract_address_hash) + token_address = to_string(erc_721_token.contract_address_hash) + + data = + "0xc87b56dd" <> + (ABI.TypeEncoder.encode([token_instance.token_id], [{:uint, 256}]) |> Base.encode16(case: :lower)) + + # First call - should fail + error_eth_call_expectation(data, token_address) + Helper.batch_fetch_instances([ %{contract_address_hash: token_instance.token_contract_address_hash, token_id: token_instance.token_id} ]) + # Second call - should also fail + error_eth_call_expectation(data, token_address) + Helper.batch_fetch_instances([ %{contract_address_hash: token_instance.token_contract_address_hash, token_id: token_instance.token_id} ]) diff --git a/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs index 77d6b02af84e..84e0c7cf8ecb 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/refetch_test.exs @@ -7,7 +7,6 @@ defmodule Indexer.Fetcher.TokenInstance.RefetchTest do alias Explorer.Chain.Token.Instance alias Indexer.Fetcher.TokenInstance.Refetch alias Indexer.Fetcher.OnDemand.NFTCollectionMetadataRefetch, as: NFTCollectionMetadataRefetchOnDemand - alias Plug.Conn describe "child_spec/1" do test "merges default options with provided options" do @@ -76,7 +75,7 @@ defmodule Indexer.Fetcher.TokenInstance.RefetchTest do test "filters and fetches token instances marked to refetch" do bypass = Bypass.open() - Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint) + # Mock HTTP responses via Tesla.Test instead of real adapter json = """ { @@ -157,21 +156,10 @@ defmodule Indexer.Fetcher.TokenInstance.RefetchTest do end ) - Bypass.expect_once( - bypass, - "GET", - "/api/card/0000000000000000000000000000000000000000000000000000000000000001", - fn conn -> - Conn.resp(conn, 200, json) - end - ) - - Bypass.expect_once( - bypass, - "GET", - "/api/card/0000000000000000000000000000000000000000000000000000000000000002", - fn conn -> - Conn.resp(conn, 200, json) + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn %{url: _url}, _opts -> + {:ok, %Tesla.Env{status: 200, body: json, headers: [{"content-type", "application/json"}]}} end ) diff --git a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs index bb8f45e9f9c3..cdf55e8d6026 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc1155_test.exs @@ -1,9 +1,14 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155Test do use Explorer.DataCase + import Mox + alias Explorer.Repo alias Explorer.Chain.Token.Instance + setup :verify_on_exit! + setup :set_mox_global + describe "sanitizer test" do test "imports token instances" do for i <- 0..3 do @@ -17,6 +22,18 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC1155Test do ) end + # Mock the ERC-1155 uri() calls to return errors so instances get error field populated + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn _requests, _options -> + {:ok, + [ + %{id: 0, error: %{code: -32015, message: "VM execution error"}}, + %{id: 1, error: %{code: -32015, message: "VM execution error"}}, + %{id: 2, error: %{code: -32015, message: "VM execution error"}}, + %{id: 3, error: %{code: -32015, message: "VM execution error"}} + ]} + end) + assert [] = Repo.all(Instance) start_supervised!({Indexer.Fetcher.TokenInstance.SanitizeERC1155, []}) diff --git a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs index ee2ad30c8e6d..5783173a12cf 100644 --- a/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_instance/sanitize_erc721_test.exs @@ -1,6 +1,11 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do use Explorer.DataCase + import Mox + + setup :verify_on_exit! + setup :set_mox_global + alias Explorer.Repo alias Explorer.Chain.Token.Instance alias Explorer.Application.Constants @@ -18,6 +23,16 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do end test "imports token instances" do + stub(EthereumJSONRPC.Mox, :json_rpc, fn requests, _options -> + {:ok, + Enum.map(requests, fn %{id: id} -> + %{ + id: id, + error: %{code: -32000, message: "execution reverted"} + } + end)} + end) + for x <- 0..3 do erc_721_token = insert(:token, type: "ERC-721") @@ -50,6 +65,16 @@ defmodule Indexer.Fetcher.TokenInstance.SanitizeERC721Test do end test "imports token instances with low tokens queue size", %{initial_env: initial_env} do + stub(EthereumJSONRPC.Mox, :json_rpc, fn requests, _options -> + {:ok, + Enum.map(requests, fn %{id: id} -> + %{ + id: id, + error: %{code: -32000, message: "execution reverted"} + } + end)} + end) + tokens = for x <- 0..5 do erc_721_token = insert(:token, type: "ERC-721") diff --git a/apps/indexer/test/indexer/transform/token_transfers_test.exs b/apps/indexer/test/indexer/transform/token_transfers_test.exs index f25c207681bc..df4188fe8bb7 100644 --- a/apps/indexer/test/indexer/transform/token_transfers_test.exs +++ b/apps/indexer/test/indexer/transform/token_transfers_test.exs @@ -478,7 +478,19 @@ defmodule Indexer.Transform.TokenTransfersTest do expected = %{token_transfers: [], tokens: []} + env = Application.get_env(:explorer, Explorer.Chain.TokenTransfer) + + Application.put_env( + :explorer, + Explorer.Chain.TokenTransfer, + env + |> Keyword.put(:weth_token_transfers_filtering_enabled, true) + |> Keyword.put(:whitelisted_weth_contracts, []) + ) + assert TokenTransfers.parse(logs) == expected + + Application.put_env(:explorer, Explorer.Chain.TokenTransfer, env) end test "Filters duplicates WETH transfers" do From 50c5656d7330a8b4ff77c0c0dd06da91e33c04cf Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 21:45:48 +0300 Subject: [PATCH 0583/1006] chore: Add "openapi_spec_folder_name" to the response of api/v2/config/backend endpoint (#13845) --- .../controllers/api/v2/config_controller.ex | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex index e7d34efd9333..efcf7d93ab00 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/config_controller.ex @@ -19,7 +19,13 @@ defmodule BlockScoutWeb.API.V2.ConfigController do responses: [ ok: {"Backend environment configuration.", "application/json", - %Schema{type: :object, properties: %{chain_type: %Schema{type: :string, nullable: true}}}}, + %Schema{ + type: :object, + properties: %{ + chain_type: %Schema{type: :string, nullable: true}, + openapi_spec_folder_name: %Schema{type: :string, nullable: true} + } + }}, unprocessable_entity: JsonErrorResponse.response() ] @@ -32,7 +38,19 @@ defmodule BlockScoutWeb.API.V2.ConfigController do conn |> put_status(200) - |> json(%{"chain_type" => chain_type}) + |> json(%{ + "chain_type" => chain_type, + "openapi_spec_folder_name" => chain_type_translate_to_openapi_spec_folder_name() + }) + end + + @spec chain_type_translate_to_openapi_spec_folder_name() :: String.t() + defp chain_type_translate_to_openapi_spec_folder_name do + if Application.get_env(:explorer, Explorer.Chain.Mud)[:enabled] do + "mud" + else + chain_type() || "default" + end end operation :backend_version, From d83d26845806965f3769c97eb44995c8025bdd23 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 21:47:53 +0300 Subject: [PATCH 0584/1006] fix: Exclude 0 index internal transactions from /api/v2/internal-transactions endpoint (#13841) --- .../api/v2/internal_transaction_controller.ex | 1 + .../internal_transaction_controller_test.exs | 111 +++++++++++++++++- .../explorer/chain/internal_transaction.ex | 11 ++ 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex index 778283a5be48..c109202162b5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/internal_transaction_controller.ex @@ -93,6 +93,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionController do defp options(paging_options, params) do paging_options |> Keyword.put(:transaction_hash, params.transaction_hash) + |> Keyword.put(:exclude_origin_internal_transaction, true) |> Keyword.update(:paging_options, default_paging_options(), fn %PagingOptions{ page_size: page_size } = paging_options -> diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs index 35cdb17074a3..d81ccd872305 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -32,8 +32,8 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do transaction_index: 0, block_number: tx.block_number, block_hash: tx.block_hash, - index: 0, - block_index: 0 + index: 1, + block_index: 1 ) request = get(conn, "/api/v2/internal-transactions") @@ -52,14 +52,14 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do transaction_index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - index: 0, + index: 1, block_index: 0 ) transaction_2 = insert(:transaction) |> with_block() internal_transactions = - for i <- 0..49 do + for i <- 1..50 do insert(:internal_transaction, transaction: transaction_2, transaction_index: 0, @@ -80,6 +80,109 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do check_paginated_response(response, response_2nd_page, internal_transactions) end + + test "excludes zero index internal transaction when querying by transaction_hash", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + # Insert internal transaction with index 0 (origin sender transaction) - should be excluded + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: 0, + block_index: 0, + type: :call + ) + + # Insert internal transaction with index 1 - should be included + it_1 = + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: 1, + block_index: 1, + type: :call + ) + + # Insert internal transaction with index 2 - should be included + it_2 = + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: 2, + block_index: 2, + type: :call + ) + + request = get(conn, "/api/v2/internal-transactions", %{"transaction_hash" => to_string(tx.hash)}) + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 2 + assert response["next_page_params"] == nil + + # Verify that only index 1 and 2 are returned, not index 0 + returned_indices = Enum.map(response["items"], & &1["index"]) + assert 1 in returned_indices + assert 2 in returned_indices + refute 0 in returned_indices + end + + test "pagination works correctly with zero index filtering", %{conn: conn} do + tx = + :transaction + |> insert() + |> with_block() + + # Insert internal transaction with index 0 - should be excluded + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: 0, + block_index: 0, + type: :call + ) + + # Insert 51 internal transactions with index 1-51 + for i <- 1..51 do + insert(:internal_transaction, + transaction: tx, + transaction_index: 0, + block_number: tx.block_number, + block_hash: tx.block_hash, + index: i, + block_index: i, + type: :call + ) + end + + request = get(conn, "/api/v2/internal-transactions", %{"transaction_hash" => to_string(tx.hash)}) + assert response = json_response(request, 200) + + # Should return 50 items (excluding index 0) + assert Enum.count(response["items"]) == 50 + assert response["next_page_params"] != nil + + # First item should be index 1, not 0 + assert List.first(response["items"])["index"] == 1 + + # Get second page + request_2nd_page = get(conn, "/api/v2/internal-transactions", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + # Second page should have 1 item + assert Enum.count(response_2nd_page["items"]) == 1 + assert response_2nd_page["next_page_params"] == nil + end end defp compare_item(%InternalTransaction{} = internal_transaction, json) do diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 097ab74d42fd..33b1d8a432dc 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -989,10 +989,17 @@ defmodule Explorer.Chain.InternalTransaction do @doc """ Returns the ordered paginated list of internal transactions (consensus blocks only) from the DB with address, block preloads + + ## Options + * `:exclude_origin_internal_transaction` - when `true`, filters out the origin sender transaction (index 0 with type :call) + * `:paging_options` - a `t:Explorer.PagingOptions.t/0` for pagination + * `:transaction_hash` - optional transaction hash to filter by + * `:api?` - whether this is an API request """ @spec fetch([paging_options | api?]) :: [] def fetch(options) do paging_options = Keyword.get(options, :paging_options, @default_paging_options) + exclude_zero_index_internal_transaction = Keyword.get(options, :exclude_origin_internal_transaction, false) case paging_options do %PagingOptions{key: {0, 0}} -> @@ -1008,6 +1015,7 @@ defmodule Explorer.Chain.InternalTransaction do __MODULE__ |> where_nonpending_block() + |> maybe_filter_origin_transaction(exclude_zero_index_internal_transaction) |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) |> where_internal_transactions_by_transaction_hash(Keyword.get(options, :transaction_hash)) |> where_consensus_transactions() @@ -1060,6 +1068,9 @@ defmodule Explorer.Chain.InternalTransaction do |> where([internal_transaction], internal_transaction.transaction_hash == ^transaction_hash) end + defp maybe_filter_origin_transaction(query, true), do: where_is_different_from_parent_transaction(query) + defp maybe_filter_origin_transaction(query, false), do: query + @doc """ Conditionally filters internal transactions to include or exclude zero-value transfers. From 75cb377745450250872e4da2b24fac3998c8ad0b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 13 Jan 2026 21:53:20 +0300 Subject: [PATCH 0585/1006] fix: Remove contract code and verified data on lose consensus (#13829) --- .../explorer/chain/import/runner/blocks.ex | 32 +++++++++++++++++++ .../smart_contract/verification_status.ex | 10 +++--- ...ter_contract_verification_status_table.exs | 29 +++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 49172a52e75c..e31149e1d821 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -21,6 +21,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do Import, PendingBlockOperation, PendingOperationsHelper, + SmartContract, Token, Token.Instance, TokenTransfer, @@ -613,6 +614,37 @@ defmodule Explorer.Chain.Import.Runner.Blocks do timeout: timeout ) + # Query to find addresses created in lost consensus blocks + created_contract_addresses_query = + from( + t in Transaction, + join: s in subquery(acquire_query), + on: t.block_hash == s.hash, + # we don't want to remove contract code from blocks that will be upserted + where: t.block_hash not in ^hashes, + where: not is_nil(t.created_contract_address_hash), + select: t.created_contract_address_hash + ) + + # Delete smart contracts for addresses created in lost consensus blocks + repo.delete_all( + from( + sc in SmartContract, + where: sc.address_hash in subquery(created_contract_addresses_query) + ), + timeout: timeout + ) + + # Clear contract code from addresses created in lost consensus blocks + repo.update_all( + from( + address in Address, + where: address.hash in subquery(created_contract_addresses_query) + ), + [set: [contract_code: nil, updated_at: updated_at]], + timeout: timeout + ) + if Application.get_env(:explorer, :chain_type) == :zilliqa do repo.delete_all( from( diff --git a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex index beef2a0c8d89..cee840abc929 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/verification_status.ex @@ -13,20 +13,20 @@ defmodule Explorer.Chain.SmartContract.VerificationStatus do alias Que.Persistence, as: QuePersistence @typedoc """ - * `address_hash` - address of the contract which was tried to verify + * `contract_address_hash` - address of the contract which was tried to verify * `status` - try status: :pending | :pass | :fail * `uid` - unique verification try identifier """ @primary_key false - typed_schema "contract_verification_status" do + typed_schema "smart_contract_verification_statuses" do field(:uid, :string, primary_key: true, null: false) field(:status, :integer, null: false) - field(:address_hash, Hash.Address, null: false) + field(:contract_address_hash, Hash.Address, null: false) timestamps() end - @required_fields ~w(uid status address_hash)a + @required_fields ~w(uid status contract_address_hash)a def changeset(%__MODULE__{} = struct, params \\ %{}) do casted_params = encode_status(params) @@ -74,7 +74,7 @@ defmodule Explorer.Chain.SmartContract.VerificationStatus do {:ok, hash} = if is_binary(address_hash), do: Chain.string_to_address_hash(address_hash), else: address_hash %__MODULE__{} - |> changeset(%{uid: uid, status: status, address_hash: hash}) + |> changeset(%{uid: uid, status: status, contract_address_hash: hash}) |> Repo.insert() end diff --git a/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs b/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs new file mode 100644 index 000000000000..e9a8e8b325c8 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs @@ -0,0 +1,29 @@ +defmodule Explorer.Repo.Migrations.AlterContractVerificationStatusTable do + use Ecto.Migration + + def up do + rename(table(:contract_verification_status), to: table(:smart_contract_verification_statuses)) + execute("ALTER INDEX contract_verification_status_pkey RENAME TO smart_contract_verification_statuses_pkey") + rename(table(:smart_contract_verification_statuses), :address_hash, to: :contract_address_hash) + + alter table(:smart_contract_verification_statuses) do + modify( + :contract_address_hash, + references(:smart_contracts, column: :address_hash, on_delete: :delete_all, type: :bytea) + ) + end + end + + def down do + rename(table(:smart_contract_verification_statuses), to: table(:contract_verification_status)) + + execute(""" + ALTER TABLE contract_verification_status + DROP CONSTRAINT smart_contract_verification_statuses_contract_address_hash_fkey + """) + + execute("ALTER INDEX smart_contract_verification_statuses_pkey RENAME TO contract_verification_status_pkey") + + rename(table(:contract_verification_status), :contract_address_hash, to: :address_hash) + end +end From 3d854ea48e536451cd7ab1f26500535989e95b76 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 14 Jan 2026 10:23:42 +0300 Subject: [PATCH 0586/1006] fix: Prevent DeleteZeroValueInternalTransactions from running while ShrinkInternalTransactions is in progress (#13847) --- ...delete_zero_value_internal_transactions.ex | 53 +++++-- ..._zero_value_internal_transactions_test.exs | 132 ++++++++++++++++++ 2 files changed, 170 insertions(+), 15 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 71ff8c3b5a3d..223d47b96445 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -18,6 +18,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do alias Timex.Duration @migration_name "delete_zero_value_internal_transactions" + @shrink_internal_transactions_migration_name "shrink_internal_transactions" @not_completed_check_interval 10 @default_check_interval :timer.minutes(1) @default_batch_size 100 @@ -41,22 +42,13 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end @impl true - def handle_continue(:ok, _state) do - state = - case MigrationStatus.fetch(@migration_name) do - nil -> - state = %{"max_block_number" => -1} - MigrationStatus.set_status(@migration_name, "started") - MigrationStatus.update_meta(@migration_name, state) - state - - %{meta: meta} -> - meta - end - - schedule_check() + def handle_continue(:ok, state) do + check_dependency_and_start(state) + end - {:noreply, state} + @impl true + def handle_info(:check_dependency, state) do + check_dependency_and_start(state) end @impl true @@ -81,6 +73,32 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do {:noreply, new_state} end + defp check_dependency_and_start(state) do + shrink_config = Application.get_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions) || [] + shrink_enabled? = shrink_config[:enabled] != false + shrink_status = MigrationStatus.get_status(@shrink_internal_transactions_migration_name) + + if shrink_enabled? && shrink_status != "completed" do + schedule_dependency_check() + {:noreply, state} + else + state = + case MigrationStatus.fetch(@migration_name) do + nil -> + state = %{"max_block_number" => -1} + MigrationStatus.set_status(@migration_name, "started") + MigrationStatus.update_meta(@migration_name, state) + state + + %{meta: meta} -> + meta + end + + schedule_check() + {:noreply, state} + end + end + defp clear_from_delete_queue do batch_size = batch_size() @@ -232,6 +250,11 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do Process.send_after(self(), :update, (completed? && completed_check_interval()) || @not_completed_check_interval) end + defp schedule_dependency_check do + interval = Application.get_env(:explorer, __MODULE__)[:dependency_check_interval] || :timer.hours(1) + Process.send_after(self(), :check_dependency, interval) + end + defp completed_check_interval do with nil <- Application.get_env(:explorer, __MODULE__)[:check_interval], nil <- get_average_block_time() do diff --git a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs index fc6c7d4587b0..9353a61fea24 100644 --- a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs +++ b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs @@ -175,4 +175,136 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do p.address_id == address_3_id and p.block_number == block.number and p.count_tos == 4 and p.count_froms == 5 end) end + + describe "ShrinkInternalTransactions migration dependency handling" do + setup do + original_shrink_config = Application.get_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions) + original_delete_config = Application.get_env(:explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions) + + # Set a short dependency check interval for tests + Application.put_env(:explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, + dependency_check_interval: 100 + ) + + on_exit(fn -> + if original_shrink_config do + Application.put_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions, original_shrink_config) + else + Application.delete_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions) + end + + if original_delete_config do + Application.put_env(:explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, original_delete_config) + else + Application.delete_env(:explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions) + end + end) + end + + test "Waits for ShrinkInternalTransactions migration to complete before starting" do + address_1 = insert(:address) + address_2 = insert(:address) + + block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -40)) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + from_address: address_1, + to_address: address_2, + block_index: 1, + type: :call, + value: 0 + ) + + assert MigrationStatus.get_status("delete_zero_value_internal_transactions") == nil + assert MigrationStatus.get_status("shrink_internal_transactions") == nil + + # Configure ShrinkInternalTransactions as enabled + Application.put_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions, enabled: true) + + # Start the migration without ShrinkInternalTransactions being completed + DeleteZeroValueInternalTransactions.start_link([]) + + # Give it time to check for the dependency + Process.sleep(100) + + # Migration should not be initialized yet + migration_status = MigrationStatus.fetch("delete_zero_value_internal_transactions") + assert is_nil(migration_status) + + # Internal transactions should still exist (not deleted) + all_internal_transactions = Repo.all(InternalTransaction) + assert Enum.count(all_internal_transactions) == 1 + + # Now mark ShrinkInternalTransactions as completed + MigrationStatus.set_status("shrink_internal_transactions", "completed") + + # Wait for DeleteZeroValueInternalTransactions to detect completion and start + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: ms.migration_name == ^"delete_zero_value_internal_transactions" and ms.status == "completed" + ) + ) + end) + + # Now the internal transaction should be deleted + remaining_internal_transactions = Repo.all(InternalTransaction) + assert Enum.count(remaining_internal_transactions) == 0 + end + + test "Starts immediately when ShrinkInternalTransactions is disabled/not configured" do + address_1 = insert(:address) + address_2 = insert(:address) + + block = insert(:block, timestamp: Timex.shift(Timex.now(), days: -40)) + + transaction = + :transaction + |> insert() + |> with_block(block) + + insert(:internal_transaction, + index: 10, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + from_address: address_1, + to_address: address_2, + block_index: 1, + type: :call, + value: 0 + ) + + assert MigrationStatus.get_status("delete_zero_value_internal_transactions") == nil + assert MigrationStatus.get_status("shrink_internal_transactions") == nil + + # Configure ShrinkInternalTransactions as disabled + Application.put_env(:explorer, Explorer.Migrator.ShrinkInternalTransactions, enabled: false) + + # Start the migration + DeleteZeroValueInternalTransactions.start_link([]) + + # Wait for DeleteZeroValueInternalTransactions to start and complete + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: ms.migration_name == ^"delete_zero_value_internal_transactions" and ms.status == "completed" + ) + ) + end) + + # Internal transaction should be deleted + remaining_internal_transactions = Repo.all(InternalTransaction) + assert Enum.count(remaining_internal_transactions) == 0 + end + end end From 00eed85cceb563915b108ac184b9855a71261702 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 14 Jan 2026 13:35:45 +0400 Subject: [PATCH 0587/1006] chore: Duplicate created_contract_address_hash to to_address_hash (#13846) Co-authored-by: Victor Baranov --- ...s_internal_transaction_controller_test.exs | 1 + .../api/v2/csv_export_controller_test.exs | 1 + .../address/internal_transactions_test.exs | 1 + apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 8 ++ .../lib/explorer/chain/address/counters.ex | 11 +-- .../chain/cache/background_migrations.ex | 20 +++- .../import/runner/internal_transactions.ex | 15 +++ .../explorer/chain/internal_transaction.ex | 35 ++++--- apps/explorer/lib/explorer/etherscan.ex | 11 +++ ...ll_internal_transaction_to_address_hash.ex | 66 +++++++++++++ ...ted_contract_address_hash_partial_index.ex | 62 ++++++++++++ ...ernal_transaction_to_address_hash_test.exs | 95 +++++++++++++++++++ config/runtime.exs | 17 ++++ 15 files changed, 317 insertions(+), 30 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_created_contract_address_hash_partial_index.ex create mode 100644 apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs index fe92511fe0a5..0571e16c6da0 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs @@ -388,6 +388,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do :internal_transaction, transaction: transaction_3, created_contract_address: address, + to_address: nil, index: index, block_number: transaction_3.block_number, transaction_index: transaction_3.index, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index 45c35875a417..c03ab7a344b2 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -326,6 +326,7 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do index: 2, transaction: transaction_3, created_contract_address: address, + to_address: nil, block_number: transaction_3.block_number, block_hash: transaction_3.block_hash, block_index: 2, diff --git a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs index fe9f014f21b3..a2f00ee57341 100644 --- a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs @@ -179,6 +179,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do index: index, transaction: transaction, created_contract_address: address, + to_address: nil, block_number: transaction.block_number, block_hash: transaction.block_hash, block_index: index, diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index b8347d4bddc4..76514bbb7bae 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -141,7 +141,8 @@ for migrator <- [ Explorer.Migrator.ReindexDuplicatedInternalTransactions, Explorer.Migrator.MergeAdjacentMissingBlockRanges, Explorer.Migrator.UnescapeQuotesInTokens, - Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources + Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, + Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash ] do config :explorer, migrator, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 801692fd13e1..5da08c189a34 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -76,6 +76,7 @@ for migrator <- [ Explorer.Migrator.UnescapeQuotesInTokens, Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, Explorer.Migrator.DeleteZeroValueInternalTransactions, + Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 96fd8abbd8fe..0f689ef978fe 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -202,6 +202,10 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, :indexer), configure_mode_dependent_process(Explorer.Migrator.DeleteZeroValueInternalTransactions, :indexer), + configure_mode_dependent_process( + Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + :indexer + ), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer @@ -324,6 +328,10 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsCreatedContractAddressHashPartialIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index f042f9c9222f..fd4679bbae52 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -228,7 +228,7 @@ defmodule Explorer.Chain.Address.Counters do query_to_address_hash_wrapped = InternalTransaction |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(address_hash, :to_address_hash) + |> InternalTransaction.where_address_fields_match(address_hash, :to) |> InternalTransaction.where_is_different_from_parent_transaction() |> limit(@counters_limit) |> wrapped_union_subquery() @@ -241,17 +241,8 @@ defmodule Explorer.Chain.Address.Counters do |> limit(@counters_limit) |> wrapped_union_subquery() - query_created_contract_address_hash_wrapped = - InternalTransaction - |> InternalTransaction.where_nonpending_block() - |> InternalTransaction.where_address_fields_match(address_hash, :created_contract_address_hash) - |> InternalTransaction.where_is_different_from_parent_transaction() - |> limit(@counters_limit) - |> wrapped_union_subquery() - query_to_address_hash_wrapped |> union_all(^query_from_address_hash_wrapped) - |> union_all(^query_created_contract_address_hash_wrapped) |> wrapped_union_subquery() end diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 5fe698ead594..d01c831393a6 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -56,7 +56,9 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :heavy_indexes_create_logs_deposits_withdrawals_index_finished, key: :heavy_indexes_create_addresses_transactions_count_desc_partial_index_finished, key: :heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished, - key: :heavy_indexes_drop_token_instances_token_id_index_finished + key: :heavy_indexes_drop_token_instances_token_id_index_finished, + key: :fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished, + key: :heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished @dialyzer :no_match @@ -65,6 +67,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do AddressTokenBalanceTokenType, ArbitrumDaRecordsNormalization, BackfillMultichainSearchDB, + FillInternalTransactionToAddressHashWithCreatedContractAddressHash, SanitizeDuplicatedLogIndexLogs, SmartContractLanguage, TokenTransferTokenType, @@ -84,6 +87,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do CreateLogsBlockHashIndex, CreateLogsDepositsWithdrawalsIndex, CreateSmartContractsLanguageIndex, + DropInternalTransactionsCreatedContractAddressHashPartialIndex, DropInternalTransactionsFromAddressHashIndex, DropLogsAddressHashIndex, DropLogsAddressHashTransactionHashIndex, @@ -342,6 +346,20 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end + defp handle_fallback(:fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished) do + set_and_return_migration_status( + FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + &set_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished/1 + ) + end + + defp handle_fallback(:heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished) do + set_and_return_migration_status( + DropInternalTransactionsCreatedContractAddressHashPartialIndex, + &set_heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished/1 + ) + end + defp handle_fallback(:sanitize_verified_addresses_finished) do {:return, false} end diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index cae96e5c503e..c9eb56fdc4dc 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -467,6 +467,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do |> Map.put(:block_hash, block_hash) |> Map.put(:block_index, index) |> sanitize_error() + |> shift_created_contract_address_hash() end) else [] @@ -521,6 +522,20 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do Map.put(entry, :error, sanitized_error) end + # Shifts the `created_contract_address_hash` value to `to_address_hash` when applicable. + + # This function handles the migration of contract creation data by copying the + # `created_contract_address_hash` to `to_address_hash` field when: + # - `created_contract_address_hash` is present (not nil) + # - `to_address_hash` is nil + @spec shift_created_contract_address_hash(map()) :: map() + defp shift_created_contract_address_hash(entry) do + case {Map.get(entry, :created_contract_address_hash), Map.get(entry, :to_address_hash)} do + {hash, nil} when not is_nil(hash) -> Map.put(entry, :to_address_hash, hash) + _ -> entry + end + end + def defer_internal_transactions_primary_key(repo) do # Allows internal_transactions primary key to not be checked during the # DB transactions and instead be checked only at the end of it. diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 33b1d8a432dc..3bb80b4df3c8 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -7,6 +7,7 @@ defmodule Explorer.Chain.InternalTransaction do alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral + alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{CallType, Type} @@ -534,12 +535,16 @@ defmodule Explorer.Chain.InternalTransaction do from_address_hash, created_contract_address_hash from internal_transactions' table. """ def where_address_fields_match(query, address_hash, :to) do - where( - query, - [t], - t.to_address_hash == ^address_hash or - (is_nil(t.to_address_hash) and t.created_contract_address_hash == ^address_hash) - ) + if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() do + where(query, [t], t.to_address_hash == ^address_hash) + else + where( + query, + [t], + t.to_address_hash == ^address_hash or + (is_nil(t.to_address_hash) and t.created_contract_address_hash == ^address_hash) + ) + end end def where_address_fields_match(query, address_hash, :from) do @@ -547,7 +552,11 @@ defmodule Explorer.Chain.InternalTransaction do end def where_address_fields_match(query, address_hash, :to_address_hash) do - where(query, [it], it.to_address_hash == ^address_hash) + if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() do + where(query, [it], it.to_address_hash == ^address_hash and is_nil(it.created_contract_address_hash)) + else + where(query, [it], it.to_address_hash == ^address_hash) + end end def where_address_fields_match(query, address_hash, :from_address_hash) do @@ -790,7 +799,7 @@ defmodule Explorer.Chain.InternalTransaction do query_to_address_hash_wrapped = __MODULE__ |> where_nonpending_block() - |> where_address_fields_match(hash, :to_address_hash) + |> where_address_fields_match(hash, :to) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) |> where_is_different_from_parent_transaction() |> common_where_limit_order(paging_options) @@ -805,18 +814,8 @@ defmodule Explorer.Chain.InternalTransaction do |> common_where_limit_order(paging_options) |> Chain.wrapped_union_subquery() - query_created_contract_address_hash_wrapped = - __MODULE__ - |> where_nonpending_block() - |> where_address_fields_match(hash, :created_contract_address_hash) - |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) - |> where_is_different_from_parent_transaction() - |> common_where_limit_order(paging_options) - |> Chain.wrapped_union_subquery() - query_to_address_hash_wrapped |> union_all(^query_from_address_hash_wrapped) - |> union_all(^query_created_contract_address_hash_wrapped) |> Chain.wrapped_union_subquery() |> common_where_and_order(paging_options) |> preload(:block) diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index a95563c3657c..c047dfb8fcee 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -10,6 +10,7 @@ defmodule Explorer.Etherscan do alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Block, DenormalizationHelper, Hash, InternalTransaction, TokenTransfer, Transaction} alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} + alias Explorer.Chain.Cache.BackgroundMigrations alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Etherscan.Logs @@ -167,6 +168,16 @@ defmodule Explorer.Etherscan do options |> options_to_directions() + |> then(fn directions -> + if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() and + Enum.member?(directions, :to_address_hash) do + directions + |> Kernel.--([:created_contract_address_hash, :to_address_hash]) + |> Enum.concat([:to]) + else + directions + end + end) |> Enum.map(fn direction -> options |> consensus_internal_transactions_with_transactions_and_blocks_query() diff --git a/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex b/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex new file mode 100644 index 000000000000..7d18a385b625 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex @@ -0,0 +1,66 @@ +defmodule Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash do + @moduledoc """ + Fills `to_address_hash` column with the data from `created_contract_address_hash` + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.InternalTransaction + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "fill_internal_transaction_to_address_hash_with_created_contract_address_hash" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(%{"max_block_number" => -1} = state), do: {[], state} + + def last_unprocessed_identifiers(%{"max_block_number" => from_block_number} = state) do + limit = batch_size() * concurrency() + to_block_number = max(from_block_number - limit + 1, 0) + + {Enum.to_list(from_block_number..to_block_number), %{state | "max_block_number" => to_block_number - 1}} + end + + def last_unprocessed_identifiers(state) do + query = + from( + it in InternalTransaction, + where: not is_nil(it.created_contract_address_hash), + where: is_nil(it.to_address_hash), + select: max(it.block_number) + ) + + max_block_number = Repo.one(query, timeout: :infinity) + + state + |> Map.put("max_block_number", max_block_number || -1) + |> last_unprocessed_identifiers() + end + + @impl FillingMigration + def unprocessed_data_query, do: nil + + @impl FillingMigration + def update_batch(block_numbers) do + query = + from(it in InternalTransaction, + where: it.block_number in ^block_numbers, + where: not is_nil(it.created_contract_address_hash), + where: is_nil(it.to_address_hash), + update: [set: [to_address_hash: it.created_contract_address_hash]] + ) + + Repo.update_all(query, [], timeout: :infinity) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished(true) + end +end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_created_contract_address_hash_partial_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_created_contract_address_hash_partial_index.ex new file mode 100644 index 000000000000..b4b2d6774175 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_created_contract_address_hash_partial_index.ex @@ -0,0 +1,62 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsCreatedContractAddressHashPartialIndex do + @moduledoc """ + Drops index "internal_transactions_created_contract_address_hash_partial_index" on internal_transactions(created_contract_address_hash, block_number DESC, transaction_index DESC, index DESC). + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus} + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :internal_transactions + @index_name "internal_transactions_created_contract_address_hash_partial_index" + @operation_type :drop + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations do + [] + end + + @impl HeavyDbIndexOperation + def db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + operation = HeavyDbIndexOperationHelper.drop_index_query_string(@index_name) + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_dropping_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache do + BackgroundMigrations.set_heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished( + true + ) + end +end diff --git a/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs b/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs new file mode 100644 index 000000000000..41ad61cce5eb --- /dev/null +++ b/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs @@ -0,0 +1,95 @@ +defmodule Explorer.Migrator.FillInternalTransactionToAddressHashTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.InternalTransaction + alias Explorer.Migrator.{MigrationStatus, FillInternalTransactionToAddressHashWithCreatedContractAddressHash} + alias Explorer.Repo + + setup_all do + opts = Application.get_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash) + + Application.put_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + batch_size: 1, + concurrency: 1, + timeout: 0 + ) + + on_exit(fn -> + Application.put_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash, opts) + end) + end + + test "Fill to_address_hash with created_contract_address_hash" do + Enum.each(1..3, fn index -> + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction, + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ) + end) + + Enum.each(1..4, fn index -> + transaction = + :transaction + |> insert() + |> with_block() + + insert(:internal_transaction_create, + transaction: transaction, + index: index, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_index: index + ) + end) + + internal_transactions_before = Repo.all(InternalTransaction) + + assert Enum.count(internal_transactions_before, fn it -> + not is_nil(it.to_address_hash) and is_nil(it.created_contract_address_hash) + end) == 3 + + assert Enum.count(internal_transactions_before, fn it -> + is_nil(it.to_address_hash) and not is_nil(it.created_contract_address_hash) + end) == 4 + + assert MigrationStatus.get_status("fill_internal_transaction_to_address_hash_with_created_contract_address_hash") == + nil + + FillInternalTransactionToAddressHashWithCreatedContractAddressHash.start_link([]) + + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: + ms.migration_name == ^"fill_internal_transaction_to_address_hash_with_created_contract_address_hash" and + ms.status == "completed" + ) + ) + end) + + internal_transactions_after = Repo.all(InternalTransaction) + + assert Enum.count(internal_transactions_after, fn it -> + not is_nil(it.to_address_hash) and is_nil(it.created_contract_address_hash) + end) == 3 + + assert Enum.count(internal_transactions_after, fn it -> + is_nil(it.to_address_hash) and not is_nil(it.created_contract_address_hash) + end) == 0 + + assert Enum.count(internal_transactions_after, fn it -> + it.to_address_hash == it.created_contract_address_hash + end) == 4 + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 71edf1154af9..824f72b05afb 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -862,6 +862,23 @@ config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, check_interval: ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL", "1m") +config :explorer, Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + batch_size: + ConfigHelper.parse_integer_env_var( + "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_BATCH_SIZE", + 100 + ), + concurrency: + ConfigHelper.parse_integer_env_var( + "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_CONCURRENCY", + 1 + ), + timeout: + ConfigHelper.parse_time_env_var( + "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_TIMEOUT", + "0s" + ) + config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), From 33f2a8816a951e739b6b663a4a19c0841a3eac8b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 14 Jan 2026 13:42:01 +0300 Subject: [PATCH 0588/1006] feat: Setup universal proxy config from the JSON content in ENV variable (#13787) --- .../universal_proxy.ex | 30 ++++++++++---- .../universal_proxy_test.exs | 41 +++++++++++++++++++ config/runtime.exs | 19 ++++++--- 3 files changed, 77 insertions(+), 13 deletions(-) diff --git a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex index dde95bfde855..13c4aa2eb6e3 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/universal_proxy.ex @@ -357,17 +357,17 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do end defp config do - case :persistent_term.get(@cache_name, nil) do - config_string when not is_nil(config_string) -> + case config_json() do + config_string when is_binary(config_string) -> safe_parse_config_string(config_string) - nil -> - case HttpClient.get(config_url()) do - {:ok, %{status_code: 200, body: config_string}} -> - safe_parse_config_string(config_string, true) + _ -> + case :persistent_term.get(@cache_name, nil) do + config_string when not is_nil(config_string) -> + safe_parse_config_string(config_string) - _ -> - %{} + nil -> + fetch_config_from_url() end end end @@ -383,6 +383,20 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxy do end end + defp fetch_config_from_url do + case HttpClient.get(config_url()) do + {:ok, %{status_code: 200, body: config_string}} -> + safe_parse_config_string(config_string, true) + + _ -> + %{} + end + end + + defp config_json do + Application.get_env(:explorer, __MODULE__)[:config_json] + end + defp config_url do Application.get_env(:explorer, __MODULE__)[:config_url] end diff --git a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs index 97867e1c53d6..438db1ceee6e 100644 --- a/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs +++ b/apps/explorer/test/explorer/third_party_integrations/universal_proxy_test.exs @@ -573,4 +573,45 @@ defmodule Explorer.ThirdPartyIntegrations.UniversalProxyTest do } ) end + + describe "config from env (UNIVERSAL_PROXY_CONFIG)" do + test "parse_proxy_params uses inline env config without HTTP fetch" do + inline_config = %{ + "platforms" => %{ + "test_platform" => %{ + "base_url" => "https://api.test.com", + "endpoints" => %{ + "base" => %{ + "path" => "/test", + "method" => "get", + "params" => [] + } + }, + "api_key" => %{ + "location" => "header", + "param_name" => "Authorization", + "prefix" => "Bearer" + } + } + } + } + + Application.put_env( + :explorer, + Explorer.ThirdPartyIntegrations.UniversalProxy, + config_json: Jason.encode!(inline_config), + config_url: nil + ) + + on_exit(fn -> + Application.delete_env(:explorer, Explorer.ThirdPartyIntegrations.UniversalProxy) + end) + + proxy_params = UniversalProxy.parse_proxy_params(%{"platform_id" => "test_platform"}) + + assert proxy_params.url == "https://api.test.com/test" + assert proxy_params.method == :get + assert proxy_params.headers == [{"Authorization", "Bearer test_api_key"}] + end + end end diff --git a/config/runtime.exs b/config/runtime.exs index 824f72b05afb..819a222b773f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -926,12 +926,21 @@ config :explorer, Explorer.Utility.RateLimiter, hammer_backend_module: if(rate_limiter_redis_url, do: Explorer.Utility.Hammer.Redis, else: Explorer.Utility.Hammer.ETS) +universal_proxy_config_url = + ConfigHelper.parse_url_env_var( + "UNIVERSAL_PROXY_CONFIG_URL", + "https://raw.githubusercontent.com/blockscout/backend-configs/refs/heads/main/universal-proxy-config.json" + ) + +universal_proxy_config = System.get_env("UNIVERSAL_PROXY_CONFIG") + +if System.get_env("UNIVERSAL_PROXY_CONFIG_URL") && universal_proxy_config do + raise "UNIVERSAL_PROXY_CONFIG_URL and UNIVERSAL_PROXY_CONFIG are both set; choose only one" +end + config :explorer, Explorer.ThirdPartyIntegrations.UniversalProxy, - config_url: - ConfigHelper.parse_url_env_var( - "UNIVERSAL_PROXY_CONFIG_URL", - "https://raw.githubusercontent.com/blockscout/backend-configs/refs/heads/main/universal-proxy-config.json" - ) + config_url: universal_proxy_config_url, + config_json: universal_proxy_config config :explorer, Explorer.Chain.Mud, enabled: ConfigHelper.parse_bool_env_var("MUD_INDEXER_ENABLED") From 51b3bf69c84e8b22d1a34860bb8879ae120c184a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 14 Jan 2026 13:48:54 +0300 Subject: [PATCH 0589/1006] fix: Convert token id to string from refetch metadata in the socket (#13762) --- .../lib/block_scout_web/notifier.ex | 4 ++-- .../api/v2/token_controller_test.exs | 24 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 03faf243c111..3e3697dd6f1d 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -368,7 +368,7 @@ defmodule BlockScoutWeb.Notifier do Endpoint.broadcast( "token_instances:#{token_contract_address_hash_string}", "fetched_token_instance_metadata", - %{token_id: token_id, fetched_metadata: fetched_token_instance_metadata} + %{token_id: to_string(token_id), fetched_metadata: fetched_token_instance_metadata} ) end @@ -379,7 +379,7 @@ defmodule BlockScoutWeb.Notifier do Endpoint.broadcast( "token_instances:#{token_contract_address_hash_string}", "not_fetched_token_instance_metadata", - %{token_id: token_id, reason: reason} + %{token_id: to_string(token_id), reason: reason} ) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index 6b1027249674..9dfd8e840a84 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -1776,8 +1776,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, ^metadata]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + payload: %{token_id: ^token_id_string, fetched_metadata: ^metadata}, event: "fetched_token_instance_metadata", topic: ^topic }, @@ -1819,8 +1821,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, ^metadata]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + payload: %{token_id: ^token_id_string, fetched_metadata: ^metadata}, event: "fetched_token_instance_metadata", topic: ^topic }, @@ -1887,8 +1891,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, ^metadata]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + payload: %{token_id: ^token_id_string, fetched_metadata: ^metadata}, event: "fetched_token_instance_metadata", topic: ^topic }, @@ -1950,8 +1956,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, "error"]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, reason: "error"}, + payload: %{token_id: ^token_id_string, reason: "error"}, event: "not_fetched_token_instance_metadata", topic: ^topic }, @@ -2019,8 +2027,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, ^metadata]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + payload: %{token_id: ^token_id_string, fetched_metadata: ^metadata}, event: "fetched_token_instance_metadata", topic: ^topic }, @@ -2058,8 +2068,10 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do [^token_contract_address_hash_string, ^token_id, ^metadata]} ) + token_id_string = to_string(token_id) + assert_receive %Phoenix.Socket.Message{ - payload: %{token_id: ^token_id, fetched_metadata: ^metadata}, + payload: %{token_id: ^token_id_string, fetched_metadata: ^metadata}, event: "fetched_token_instance_metadata", topic: ^topic }, From 04990e3d51e2d832fab7ad11d192534c4b5d95a4 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Wed, 14 Jan 2026 18:26:33 +0400 Subject: [PATCH 0590/1006] chore: Cover Optimism API endpoints with swagger docs (#13672) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../controllers/api/v2/optimism_controller.ex | 262 +++++++++++++++--- .../lib/block_scout_web/routers/api_router.ex | 3 - .../block_scout_web/schemas/api/v2/general.ex | 21 ++ .../schemas/api/v2/optimism/batch.ex | 41 +++ .../schemas/api/v2/optimism/batch/detailed.ex | 73 +++++ .../schemas/api/v2/optimism/deposit.ex | 31 +++ .../api/v2/optimism/deposit/main_page.ex | 27 ++ .../schemas/api/v2/optimism/game.ex | 37 +++ .../schemas/api/v2/optimism/output_root.ex | 31 +++ .../schemas/api/v2/optimism/withdrawal.ex | 61 ++++ .../chain/optimism/transaction_batch.ex | 50 +--- 11 files changed, 553 insertions(+), 84 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch/detailed.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit/main_page.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/game.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/output_root.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/withdrawal.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 4b2e201f5514..9c6bdd59380c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.V2.OptimismController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs require Logger @@ -13,6 +14,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do import Explorer.Helper, only: [hash_to_binary: 1] alias BlockScoutWeb.API.V2.{ApiView, OptimismView} + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.Cache.ChainId alias Explorer.Chain.{Data, Hash, Token, Transaction} @@ -34,38 +36,28 @@ defmodule BlockScoutWeb.API.V2.OptimismController do @api_true [api?: true] - @doc """ - Function to handle GET requests to `/api/v2/optimism/txn-batches` and - `/api/v2/optimism/txn-batches/:l2_block_range_start/:l2_block_range_end` endpoints. - """ - @spec transaction_batches(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transaction_batches(conn, params) do - {batches, next_page} = - params - |> paging_options() - |> Keyword.put(:api?, true) - |> Keyword.put(:l2_block_range_start, Map.get(params, "l2_block_range_start")) - |> Keyword.put(:l2_block_range_end, Map.get(params, "l2_block_range_end")) - |> TransactionBatch.list() - |> split_list_by_page() - - next_page_params = next_page_params(next_page, batches, params) - - conn - |> put_status(200) - |> render(:optimism_transaction_batches, %{ - batches: batches, - next_page_params: next_page_params - }) - end - - @doc """ - Function to handle GET requests to `/api/v2/optimism/txn-batches/count` endpoint. - """ - @spec transaction_batches_count(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transaction_batches_count(conn, _params) do - items_count(conn, TransactionBatch) - end + operation :batches, + summary: "List batches.", + description: "Retrieves a paginated list of batches.", + parameters: + base_params() ++ + define_paging_params([ + "id", + "items_count" + ]), + responses: [ + ok: + {"List of batches.", "application/json", + paginated_response( + items: Schemas.Optimism.Batch, + next_page_params_example: %{ + "id" => 394_591, + "items_count" => 50 + }, + title_prefix: "Batches" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] @doc """ Function to handle GET requests to `/api/v2/optimism/batches` endpoint. @@ -121,6 +113,15 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :batches_count, + summary: "Number of batches in the list.", + description: "Retrieves a size of the batch list.", + parameters: base_params(), + responses: [ + ok: {"Number of items in the batch list.", "application/json", %Schema{type: :integer, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/batches/count` endpoint. """ @@ -129,6 +130,33 @@ defmodule BlockScoutWeb.API.V2.OptimismController do items_count(conn, FrameSequence) end + operation :batch_by_celestia_blob, + summary: "Batch by celestia blob.", + description: "Retrieves batch detailed info by the given celestia blob metadata (height and commitment).", + parameters: + base_params() ++ + [ + %OpenApiSpex.Parameter{ + name: :height, + in: :path, + schema: Schemas.General.IntegerString, + required: true, + description: "Celestia blob height in the path." + }, + %OpenApiSpex.Parameter{ + name: :commitment, + in: :path, + schema: Schemas.General.HexString, + required: true, + description: "Celestia blob commitment in the path." + } + ], + responses: [ + ok: {"Batch detailed info.", "application/json", Schemas.Optimism.Batch.Detailed}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/batches/da/celestia/:height/:commitment` endpoint. """ @@ -154,6 +182,25 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end end + operation :batch_by_number, + summary: "Batch by its number.", + description: "Retrieves batch detailed info by the given number.", + parameters: [ + %OpenApiSpex.Parameter{ + name: :number, + in: :path, + schema: Schemas.General.IntegerString, + required: true, + description: "Batch number in the path." + } + | base_params() + ], + responses: [ + ok: {"Batch detailed info.", "application/json", Schemas.Optimism.Batch.Detailed}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/batches/:number` endpoint. """ @@ -172,6 +219,29 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end end + operation :output_roots, + summary: "List output roots.", + description: "Retrieves a paginated list of output roots.", + parameters: + base_params() ++ + define_paging_params([ + "index", + "items_count" + ]), + responses: [ + ok: + {"List of output roots.", "application/json", + paginated_response( + items: Schemas.Optimism.OutputRoot, + next_page_params_example: %{ + "index" => 8829, + "items_count" => 50 + }, + title_prefix: "OutputRoots" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/output-roots` endpoint. """ @@ -194,6 +264,15 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :output_roots_count, + summary: "Number of output roots in the list.", + description: "Retrieves a size of the output roots list.", + parameters: base_params(), + responses: [ + ok: {"Number of items in the output roots list.", "application/json", %Schema{type: :integer, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/output-roots/count` endpoint. """ @@ -202,6 +281,29 @@ defmodule BlockScoutWeb.API.V2.OptimismController do items_count(conn, OutputRoot) end + operation :games, + summary: "List games.", + description: "Retrieves a paginated list of games.", + parameters: + base_params() ++ + define_paging_params([ + "index", + "items_count" + ]), + responses: [ + ok: + {"List of games.", "application/json", + paginated_response( + items: Schemas.Optimism.Game, + next_page_params_example: %{ + "index" => 12967, + "items_count" => 50 + }, + title_prefix: "Games" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/games` endpoint. """ @@ -224,6 +326,15 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :games_count, + summary: "Number of games in the list.", + description: "Retrieves a size of the games list.", + parameters: base_params(), + responses: [ + ok: {"Number of items in the games list.", "application/json", %Schema{type: :integer, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/games/count` endpoint. """ @@ -236,6 +347,31 @@ defmodule BlockScoutWeb.API.V2.OptimismController do |> render(:optimism_items_count, %{count: count}) end + operation :deposits, + summary: "List deposits.", + description: "Retrieves a paginated list of deposits.", + parameters: + base_params() ++ + define_paging_params([ + "items_count", + "l1_block_number", + "transaction_hash" + ]), + responses: [ + ok: + {"List of deposits.", "application/json", + paginated_response( + items: Schemas.Optimism.Deposit, + next_page_params_example: %{ + "items_count" => 50, + "l1_block_number" => 23_937_283, + "transaction_hash" => "0x5dc155c382d95353c5876e735d675d284e3b29b1379e5859dc35cfd4a1dd5188" + }, + title_prefix: "Deposits" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/deposits` endpoint. """ @@ -258,6 +394,17 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :main_page_deposits, + summary: "List deposits on the main page.", + description: "Retrieves a list of deposits for the main page.", + parameters: base_params(), + responses: [ + ok: + {"List of deposits on the main page.", "application/json", + %Schema{type: :array, items: Schemas.Optimism.Deposit.MainPage, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/main-page/optimism-deposits` endpoint. """ @@ -275,6 +422,15 @@ defmodule BlockScoutWeb.API.V2.OptimismController do |> render(:optimism_deposits, %{deposits: recent_deposits}) end + operation :deposits_count, + summary: "Number of deposits in the list.", + description: "Retrieves a size of the deposits list.", + parameters: base_params(), + responses: [ + ok: {"Number of items in the deposits list.", "application/json", %Schema{type: :integer, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/deposits/count` endpoint. """ @@ -287,6 +443,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do |> render(:optimism_items_count, %{count: count}) end + operation :interop_message, false + @doc """ Function to handle GET requests to `/api/v2/optimism/interop/messages/:unique_id` endpoint. """ @@ -360,6 +518,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end end + operation :interop_messages, false + @doc """ Function to handle GET requests to `/api/v2/optimism/interop/messages` endpoint. """ @@ -408,6 +568,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :interop_messages_count, false + @doc """ Function to handle GET requests to `/api/v2/optimism/interop/messages/count` endpoint. """ @@ -418,6 +580,29 @@ defmodule BlockScoutWeb.API.V2.OptimismController do |> render(:optimism_items_count, %{count: InteropMessage.count(@api_true)}) end + operation :withdrawals, + summary: "List withdrawals.", + description: "Retrieves a paginated list of withdrawals.", + parameters: + base_params() ++ + define_paging_params([ + "items_count", + "nonce" + ]), + responses: [ + ok: + {"List of withdrawals.", "application/json", + paginated_response( + items: Schemas.Optimism.Withdrawal, + next_page_params_example: %{ + "items_count" => 50, + "nonce" => "1766847064778384329583297500742918515827483896875618958121606201292650102" + }, + title_prefix: "Withdrawals" + )}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/withdrawals` endpoint. """ @@ -440,6 +625,15 @@ defmodule BlockScoutWeb.API.V2.OptimismController do }) end + operation :withdrawals_count, + summary: "Number of withdrawals in the list.", + description: "Retrieves a size of the withdrawals list.", + parameters: base_params(), + responses: [ + ok: {"Number of items in the withdrawals list.", "application/json", %Schema{type: :integer, nullable: false}}, + unprocessable_entity: JsonErrorResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/optimism/withdrawals/count` endpoint. """ @@ -448,6 +642,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do items_count(conn, Withdrawal) end + operation :interop_public_key, false + @doc """ Function to handle GET requests to `/api/v2/optimism/interop/public-key` endpoint. """ @@ -471,6 +667,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do end end + operation :interop_import, false + @doc """ Function to handle POST request to `/api/v2/import/optimism/interop/` endpoint. Accepts `init` part of the interop message from the source instance or diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 8c9ab8211bda..8e4e0692afad 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -307,9 +307,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do if @chain_type == :optimism do scope "/optimism" do - get("/txn-batches", V2.OptimismController, :transaction_batches) - get("/txn-batches/count", V2.OptimismController, :transaction_batches_count) - get("/txn-batches/:l2_block_range_start/:l2_block_range_end", V2.OptimismController, :transaction_batches) get("/batches", V2.OptimismController, :batches) get("/batches/count", V2.OptimismController, :batches_count) get("/batches/da/celestia/:height/:commitment", V2.OptimismController, :batch_by_celestia_blob) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index af2a3b5e162a..dbb0b64f8259 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -847,6 +847,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: "Block number for paging" }, + "l1_block_number" => %Parameter{ + name: :l1_block_number, + in: :query, + schema: %Schema{type: :integer, minimum: 0}, + required: false, + description: "L1 block number for paging" + }, "epoch_number" => %Parameter{ name: :epoch_number, in: :query, @@ -854,6 +861,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: "Epoch number for paging" }, + "nonce" => %Parameter{ + name: :nonce, + in: :query, + schema: IntegerString, + required: false, + description: "Nonce for paging" + }, "index" => %Parameter{ name: :index, in: :query, @@ -889,6 +903,13 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do required: false, description: "Transaction hash for paging" }, + "transaction_hash" => %Parameter{ + name: :transaction_hash, + in: :query, + schema: FullHash, + required: false, + description: "Transaction hash for paging" + }, # TODO: consider refactoring, to avoid ambiguity with hash param (the same name) "address_hash" => %Parameter{ name: :hash, diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch.ex new file mode 100644 index 000000000000..eb7bf386accf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch.ex @@ -0,0 +1,41 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Batch do + @moduledoc """ + This module defines the schema for the Optimism Batch struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Batch struct.", + type: :object, + properties: %{ + number: %Schema{type: :integer}, + transactions_count: %Schema{type: :integer}, + l1_timestamp: General.Timestamp, + l1_transaction_hashes: %Schema{ + type: :array, + items: General.FullHash, + nullable: false + }, + batch_data_container: %Schema{ + type: :string, + enum: ["in_blob4844", "in_celestia", "in_eigenda", "in_alt_da", "in_calldata"], + nullable: true + }, + l2_end_block_number: %Schema{type: :integer}, + l2_start_block_number: %Schema{type: :integer} + }, + required: [ + :number, + :transactions_count, + :l1_timestamp, + :l1_transaction_hashes, + :batch_data_container, + :l2_end_block_number, + :l2_start_block_number + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch/detailed.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch/detailed.ex new file mode 100644 index 000000000000..d3baccaad696 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/batch/detailed.ex @@ -0,0 +1,73 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Batch.Detailed do + @moduledoc """ + This module defines the schema for batch response from /api/v2/optimism/batches/:number and + /api/v2/optimism/batches/da/celestia/:height/:commitment + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias BlockScoutWeb.Schemas.API.V2.Optimism.Batch + alias BlockScoutWeb.Schemas.Helper + alias OpenApiSpex.Schema + + OpenApiSpex.schema( + Batch.schema() + |> Helper.extend_schema( + nullable: true, + properties: %{ + blobs: %Schema{ + type: :array, + items: %Schema{ + description: "Blob struct bound with Optimism batch.", + type: :object, + properties: %{ + hash: %Schema{ + type: :string, + pattern: General.hex_string_pattern(), + nullable: false, + description: "EIP-4844 blob hash." + }, + l1_transaction_hash: %Schema{ + type: :string, + pattern: General.full_hash_pattern(), + nullable: false, + description: "L1 transaction hash bound with the blob." + }, + l1_timestamp: %Schema{ + type: :string, + format: :"date-time", + nullable: false, + description: "L1 transaction timestamp bound with the blob." + }, + height: %Schema{type: :integer, nullable: false, description: "Celestia block height."}, + namespace: %Schema{ + type: :string, + pattern: General.hex_string_pattern(), + nullable: false, + description: "Celestia blob namespace." + }, + commitment: %Schema{ + type: :string, + pattern: General.hex_string_pattern(), + nullable: false, + description: "Celestia or Alt-DA blob commitment." + }, + cert: %Schema{ + type: :string, + pattern: General.hex_string_pattern(), + nullable: false, + description: "EigenDA cert raw bytes." + } + }, + required: [ + :l1_transaction_hash, + :l1_timestamp + ], + additionalProperties: false + }, + nullable: false + } + } + ) + ) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit.ex new file mode 100644 index 000000000000..456adb4a2c5e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit.ex @@ -0,0 +1,31 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Deposit do + @moduledoc """ + This module defines the schema for the Optimism Deposit struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Deposit struct.", + type: :object, + properties: %{ + l1_block_number: %Schema{type: :integer}, + l1_block_timestamp: General.Timestamp, + l1_transaction_hash: General.FullHash, + l1_transaction_origin: General.AddressHash, + l2_transaction_gas_limit: General.IntegerString, + l2_transaction_hash: General.FullHash + }, + required: [ + :l1_block_number, + :l1_block_timestamp, + :l1_transaction_hash, + :l1_transaction_origin, + :l2_transaction_gas_limit, + :l2_transaction_hash + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit/main_page.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit/main_page.ex new file mode 100644 index 000000000000..6ee66aeed4bb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/deposit/main_page.ex @@ -0,0 +1,27 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Deposit.MainPage do + @moduledoc """ + This module defines the schema for the Optimism Deposit struct for the main page. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Deposit struct for the main page.", + type: :object, + properties: %{ + l1_block_number: %Schema{type: :integer}, + l1_block_timestamp: General.Timestamp, + l1_transaction_hash: General.FullHash, + l2_transaction_hash: General.FullHash + }, + required: [ + :l1_block_number, + :l1_block_timestamp, + :l1_transaction_hash, + :l2_transaction_hash + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/game.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/game.ex new file mode 100644 index 000000000000..13aa84639ebf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/game.ex @@ -0,0 +1,37 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Game do + @moduledoc """ + This module defines the schema for the Optimism Dispute Game struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Dispute Game struct.", + type: :object, + properties: %{ + contract_address_hash: General.AddressHash, + created_at: General.Timestamp, + game_type: %Schema{type: :integer}, + index: %Schema{type: :integer}, + l2_block_number: %Schema{type: :integer}, + resolved_at: General.TimestampNullable, + status: %Schema{ + type: :string, + enum: ["In progress", "Challenger wins", "Defender wins"], + nullable: false + } + }, + required: [ + :contract_address_hash, + :created_at, + :game_type, + :index, + :l2_block_number, + :resolved_at, + :status + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/output_root.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/output_root.ex new file mode 100644 index 000000000000..7cd6d2087875 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/output_root.ex @@ -0,0 +1,31 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.OutputRoot do + @moduledoc """ + This module defines the schema for the Optimism Output Root struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Output Root struct.", + type: :object, + properties: %{ + l1_block_number: %Schema{type: :integer}, + l1_timestamp: General.Timestamp, + l1_transaction_hash: General.FullHash, + l2_block_number: %Schema{type: :integer}, + l2_output_index: %Schema{type: :integer}, + output_root: General.FullHash + }, + required: [ + :l1_block_number, + :l1_timestamp, + :l1_transaction_hash, + :l2_block_number, + :l2_output_index, + :output_root + ], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/withdrawal.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/withdrawal.ex new file mode 100644 index 000000000000..73f6283dece5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/optimism/withdrawal.ex @@ -0,0 +1,61 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Optimism.Withdrawal do + @moduledoc """ + This module defines the schema for the Optimism Withdrawal struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{AddressNullable, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Optimism Withdrawal struct.", + type: :object, + properties: %{ + challenge_period_end: General.TimestampNullable, + from: AddressNullable, + l1_transaction_hash: General.FullHashNullable, + l2_timestamp: General.TimestampNullable, + l2_transaction_hash: General.FullHash, + msg_data: General.HexStringNullable, + msg_gas_limit: General.IntegerStringNullable, + msg_nonce: %Schema{type: :integer}, + msg_nonce_raw: General.IntegerString, + msg_nonce_version: %Schema{type: :integer}, + msg_sender_address_hash: General.FullHashNullable, + msg_target_address_hash: General.FullHashNullable, + msg_value: General.IntegerStringNullable, + portal_contract_address_hash: General.AddressHashNullable, + status: %Schema{ + type: :string, + enum: [ + "Waiting for state root", + "Ready to prove", + "Waiting a game to resolve", + "In challenge period", + "Ready for relay", + "Proven", + "Relayed" + ], + nullable: false + } + }, + required: [ + :challenge_period_end, + :from, + :l1_transaction_hash, + :l2_timestamp, + :l2_transaction_hash, + :msg_data, + :msg_gas_limit, + :msg_nonce, + :msg_nonce_raw, + :msg_nonce_version, + :msg_sender_address_hash, + :msg_target_address_hash, + :msg_value, + :portal_contract_address_hash, + :status + ], + additionalProperties: false + }) +end diff --git a/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex b/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex index 3c17388703a7..d09aaa03134e 100644 --- a/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex +++ b/apps/explorer/lib/explorer/chain/optimism/transaction_batch.ex @@ -16,7 +16,7 @@ defmodule Explorer.Chain.Optimism.TransactionBatch do use Explorer.Schema - import Explorer.Chain, only: [default_paging_options: 0, join_association: 3, join_associations: 2, select_repo: 1] + import Explorer.Chain, only: [default_paging_options: 0, join_associations: 2, select_repo: 1] alias Explorer.Chain.Block alias Explorer.Chain.Optimism.FrameSequence @@ -98,48 +98,6 @@ defmodule Explorer.Chain.Optimism.TransactionBatch do ) end - @doc """ - Lists `t:Explorer.Chain.Optimism.TransactionBatch.t/0`'s' in descending order based on l2_block_number. - - ## Parameters - - `options`: A keyword list of options that may include whether to use a replica database, - paging options, and optional L2 block range for which to make the list of items. - - ## Returns - - A list of found entities sorted by `l2_block_number` in descending order. - """ - @spec list(list()) :: [__MODULE__.t()] - def list(options \\ []) do - paging_options = Keyword.get(options, :paging_options, default_paging_options()) - - case paging_options do - %PagingOptions{key: {0}} -> - [] - - _ -> - l2_block_range_start = Keyword.get(options, :l2_block_range_start) - l2_block_range_end = Keyword.get(options, :l2_block_range_end) - - base_query = - if is_nil(l2_block_range_start) or is_nil(l2_block_range_end) do - from(tb in __MODULE__, - order_by: [desc: tb.l2_block_number] - ) - else - from(tb in __MODULE__, - order_by: [desc: tb.l2_block_number], - where: tb.l2_block_number >= ^l2_block_range_start and tb.l2_block_number <= ^l2_block_range_end - ) - end - - base_query - |> join_association(:frame_sequence, :required) - |> page_transaction_batches(paging_options) - |> limit(^paging_options.page_size) - |> select_repo(options).all(timeout: :infinity) - end - end - @doc """ Retrieves a list of rollup blocks included into a specified batch. @@ -277,10 +235,4 @@ defmodule Explorer.Chain.Optimism.TransactionBatch do <> = bytes bytes_before <> <> <> bytes_after end - - defp page_transaction_batches(query, %PagingOptions{key: nil}), do: query - - defp page_transaction_batches(query, %PagingOptions{key: {block_number}}) do - from(tb in query, where: tb.l2_block_number < ^block_number) - end end From 9fea9845d50cf3b3d955c524b5b0ab2522baa7c2 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 15 Jan 2026 14:47:32 +0300 Subject: [PATCH 0591/1006] feat: `txlistinternal` API endpoint pending item status (#13758) --- .../controllers/api/rpc/address_controller.ex | 62 ++++++-- .../views/api/rpc/address_view.ex | 9 +- .../block_scout_web/views/api/rpc/rpc_view.ex | 8 + .../api/rpc/address_controller_test.exs | 142 ++++++++++++++++-- .../chain/pending_operations_helper.ex | 52 +++++++ 5 files changed, 247 insertions(+), 26 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index 5082b44a56ae..28bc57c6dc17 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do alias BlockScoutWeb.API.RPC.Helper alias BlockScoutWeb.Chain, as: BlockScoutWebChain alias Explorer.{Chain, Etherscan} - alias Explorer.Chain.{Address, Wei} + alias Explorer.Chain.{Address, PendingOperationsHelper, Wei} alias Explorer.Etherscan.{Addresses, Blocks} alias Explorer.Helper, as: ExplorerHelper alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand @@ -14,6 +14,7 @@ defmodule BlockScoutWeb.API.RPC.AddressController do @invalid_address_message "Invalid address format" @invalid_contract_address_message "Invalid contract address format" + @no_internal_transactions_message "No internal transactions found" @no_token_transfers_message "No token transfers found" @results_window 10000 @results_window_too_large_message "Result window is too large, PageNo x Offset size must be less than or equal to #{@results_window}" @@ -156,6 +157,8 @@ defmodule BlockScoutWeb.API.RPC.AddressController do def txlistinternal(conn, params, transaction_param, :transaction) do with {:params, {:ok, options}} <- {:params, optional_params(params)}, {:format, {:ok, transaction_hash}} <- to_transaction_hash(transaction_param), + {:ok, transaction} <- Chain.hash_to_transaction(transaction_hash), + {:pending, false} <- {:pending, PendingOperationsHelper.block_pending?(transaction.block_hash)}, {:ok, internal_transactions} <- list_internal_transactions(transaction_hash, options) do render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) else @@ -163,41 +166,76 @@ defmodule BlockScoutWeb.API.RPC.AddressController do render(conn, :error, error: "Invalid txhash format") {:error, :not_found} -> - render(conn, :error, error: "No internal transactions found", data: []) + render(conn, :error, error: @no_internal_transactions_message, data: []) {:params, {:error, :results_window_too_large}} -> render(conn, :error, error: @results_window_too_large_message, data: nil) + + {:pending, true} -> + render(conn, :pending_internal_transaction, + message: "Internal transactions for this transaction have not been processed yet", + data: [] + ) end end + @block_range_not_yet_processed_message "Some internal transactions within this block range have not yet been processed" + def txlistinternal(conn, params, address_param, :address) do with {:params, {:ok, options}} <- {:params, optional_params(params)}, {:format, {:ok, address_hash}} <- to_address_hash(address_param), {:address, :ok} <- {:address, Address.check_address_exists(address_hash, @api_true)}, - {:ok, internal_transactions} <- list_internal_transactions(address_hash, options) do - render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + {{:ok, internal_transactions}, _, _} <- + {list_internal_transactions(address_hash, options), options[:startblock], options[:endblock]} do + render_internal_transactions(conn, internal_transactions, options[:startblock], options[:endblock]) else {:format, :error} -> render(conn, :error, error: @invalid_address_message) {_, :not_found} -> - render(conn, :error, error: "No internal transactions found", data: []) + render(conn, :error, error: @no_internal_transactions_message, data: []) {:params, {:error, :results_window_too_large}} -> render(conn, :error, error: @results_window_too_large_message, data: nil) + + {{:error, :not_found}, start_block_number, end_block_number} -> + render_internal_transactions(conn, [], start_block_number, end_block_number) end end def txlistinternal(conn, params, :no_param) do - with {:params, {:ok, options}} <- {:params, optional_params(params)}, - {:ok, internal_transactions} <- list_internal_transactions(:all, options) do - render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) + with {:ok, options} <- optional_params(params), + {{:ok, internal_transactions}, _, _} <- + {list_internal_transactions(:all, options), options[:startblock], options[:endblock]} do + render_internal_transactions(conn, internal_transactions, options[:startblock], options[:endblock]) else - {:error, :not_found} -> - render(conn, :error, error: "No internal transactions found", data: []) - - {:params, {:error, :results_window_too_large}} -> + {:error, :results_window_too_large} -> render(conn, :error, error: @results_window_too_large_message, data: nil) + + {{:error, :not_found}, start_block_number, end_block_number} -> + render_internal_transactions(conn, [], start_block_number, end_block_number) + end + end + + defp render_internal_transactions(conn, [], start_block_number, end_block_number) do + if PendingOperationsHelper.blocks_pending?(start_block_number, end_block_number) do + render(conn, :pending_internal_transaction, + message: @block_range_not_yet_processed_message, + data: [] + ) + else + render(conn, :error, error: @no_internal_transactions_message, data: []) + end + end + + defp render_internal_transactions(conn, internal_transactions, start_block_number, end_block_number) do + if PendingOperationsHelper.blocks_pending?(start_block_number, end_block_number) do + render(conn, :pending_internal_transaction, + message: @block_range_not_yet_processed_message, + data: internal_transactions + ) + else + render(conn, :txlistinternal, %{internal_transactions: internal_transactions}) end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 5928646a8551..e674ef074c31 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do alias BlockScoutWeb.API.EthRPC.View, as: EthRPCView alias BlockScoutWeb.API.RPC.RPCView - alias Explorer.Chain.{DenormalizationHelper, Transaction} + alias Explorer.Chain.{DenormalizationHelper, InternalTransaction, Transaction} def render("listaccounts.json", %{accounts: accounts}) do accounts = Enum.map(accounts, &prepare_account/1) @@ -74,6 +74,11 @@ defmodule BlockScoutWeb.API.RPC.AddressView do EthRPCView.render("error.json", %{error: message, id: 0}) end + def render("pending_internal_transaction.json", %{data: data} = assigns) do + prepared_internal_transactions = Enum.map(data, &prepare_internal_transaction/1) + RPCView.render("pending_internal_transaction.json", Map.put(assigns, :data, prepared_internal_transactions)) + end + def render("error.json", assigns) do RPCView.render("error.json", assigns) end @@ -134,6 +139,8 @@ defmodule BlockScoutWeb.API.RPC.AddressView do } end + # Prepares an internal transaction for API response. + @spec prepare_internal_transaction(InternalTransaction.t()) :: map() defp prepare_internal_transaction(internal_transaction) do %{ "blockNumber" => "#{internal_transaction.block_number}", diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex index f877d947d064..e13da6831836 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/rpc_view.ex @@ -17,6 +17,14 @@ defmodule BlockScoutWeb.API.RPC.RPCView do value end + def render("pending_internal_transaction.json", %{data: data, message: message}) do + %{ + "status" => "2", + "message" => message, + "result" => data + } + end + def render("error.json", %{error: message} = assigns) do %{ "status" => "0", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index 66519a43acbe..87b15c40704f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -1880,6 +1880,74 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + test "returns status = 2 for internal transactions not yet processed", %{conn: conn, params: params} do + address = insert(:address) + address_2 = insert(:address) + + block = insert(:block) + insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + transaction = + :transaction + |> insert(from_address: address, to_address: address_2) + |> with_block(block) + + :internal_transaction + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 0, + block_number: block.number + ) + + internal_transaction = + :internal_transaction + |> insert( + transaction: transaction, + index: 1, + value: 2, + from_address: address, + to_address: address_2, + block_hash: transaction.block_hash, + block_index: 1, + block_number: block.number + ) + + expected_result = [ + %{ + "blockNumber" => "#{transaction.block_number}", + "timeStamp" => "#{DateTime.to_unix(block.timestamp)}", + "from" => "#{internal_transaction.from_address_hash}", + "to" => "#{internal_transaction.to_address_hash}", + "value" => "#{internal_transaction.value.value}", + "contractAddress" => "", + "input" => "#{internal_transaction.input}", + "type" => "#{internal_transaction.type}", + "callType" => "#{internal_transaction.call_type}", + "gas" => "#{internal_transaction.gas}", + "gasUsed" => "#{internal_transaction.gas_used}", + "index" => "#{internal_transaction.index}", + "transactionHash" => "#{transaction.hash}", + "isError" => "0", + "errCode" => "#{internal_transaction.error}" + } + ] + + assert response = + conn + |> get("/api/v1", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "2" + assert response["message"] == "Some internal transactions within this block range have not yet been processed" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + test "returns only non zero value internal transactions by default", %{conn: conn, params: params} do address = insert(:address) address_2 = insert(:address) @@ -2160,6 +2228,51 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + test "status = 2 if the block is pending", %{conn: conn} do + address = insert(:address) + contract_address = insert(:contract_address) + + block = insert(:block) + insert(:pending_block_operation, block_hash: block.hash, block_number: block.number) + + transaction = + :transaction + |> insert(from_address: address, to_address: nil) + |> with_contract_creation(contract_address) + |> with_block(block) + + _internal_transaction = + :internal_transaction_create + |> insert( + transaction: transaction, + index: 0, + value: 1, + from_address: address, + block_hash: transaction.block_hash, + block_index: 0, + block_number: transaction.block_number + ) + |> with_contract_creation(contract_address) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "txhash" => "#{transaction.hash}" + } + + expected_result = [] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "2" + assert response["message"] == "Internal transactions for this transaction have not been processed yet" + assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) + end + test "isError is true if internal transaction has an error", %{conn: conn} do transaction = :transaction @@ -2440,6 +2553,22 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do assert :ok = ExJsonSchema.Validator.validate(txlistinternal_schema(), response) end + test "doesn't raise when startblock exceeds postgres integer range", %{conn: conn} do + address = insert(:address) + + params = %{ + "module" => "account", + "action" => "txlistinternal", + "address" => "#{address.hash}", + "startblock" => "2147483648", + "endblock" => "7483099" + } + + conn + |> get("/api", params) + |> json_response(200) + end + test "response includes all the expected fields", %{conn: conn} do address = insert(:address) contract_address = insert(:contract_address) @@ -4977,17 +5106,4 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do |> put_in(["properties", "result"], result) |> ExJsonSchema.Schema.resolve() end - - defp eth_get_block_by_number_expectation(latest_block_number_hex, res) do - expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [ - %{ - id: 0, - method: "eth_getBlockByNumber", - params: [^latest_block_number_hex, true] - } - ], - _ -> - {:ok, [res]} - end) - end end diff --git a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex index 915449947729..f5afc6126505 100644 --- a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex +++ b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex @@ -154,4 +154,56 @@ defmodule Explorer.Chain.PendingOperationsHelper do where: pending_ops.block_hash in ^block_hashes ) end + + @doc """ + Checks if a block with the given hash is pending. + A block is considered pending if there exists a corresponding entry in the `PendingBlockOperation` table. + """ + @spec block_pending?(Hash.Full.t()) :: boolean() + def block_pending?(block_hash) do + [block_hash] + |> block_hash_in_query() + |> Repo.exists?() + end + + # Generates a query to find pending block operations within a specified range of block numbers. + @spec block_range_in_query(non_neg_integer(), non_neg_integer()) :: Ecto.Query.t() + defp block_range_in_query(min_block_number, max_block_number) + when is_integer(min_block_number) and is_integer(max_block_number) do + from( + pending_ops in PendingBlockOperation, + where: pending_ops.block_number >= ^min_block_number and pending_ops.block_number <= ^max_block_number + ) + end + + defp block_range_in_query(min_block_number, max_block_number) + when is_nil(min_block_number) and is_nil(max_block_number) do + from(pending_ops in PendingBlockOperation) + end + + defp block_range_in_query(min_block_number, max_block_number) when is_nil(min_block_number) do + from( + pending_ops in PendingBlockOperation, + where: pending_ops.block_number <= ^max_block_number + ) + end + + defp block_range_in_query(min_block_number, max_block_number) when is_nil(max_block_number) do + from( + pending_ops in PendingBlockOperation, + where: pending_ops.block_number >= ^min_block_number + ) + end + + @doc """ + Checks if there are any pending blocks within the specified range of block numbers. + A block is considered pending if there exists a corresponding entry in the `PendingBlockOperation` + table. + """ + @spec blocks_pending?(non_neg_integer() | nil, non_neg_integer() | nil) :: boolean() + def blocks_pending?(min_block_number, max_block_number) do + min_block_number + |> block_range_in_query(max_block_number) + |> Repo.exists?() + end end From 8a258077a9d83b04db3d2ccd8920bf862b950580 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 15 Jan 2026 15:27:59 +0300 Subject: [PATCH 0592/1006] fix: Return date to logs (#13858) --- config/runtime.exs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/config/runtime.exs b/config/runtime.exs index 819a222b773f..26f08672f9d8 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -12,12 +12,21 @@ config :logger, :default_handler, (if config_env() == :prod do LoggerJSON.Formatters.Basic.new(metadata: ConfigHelper.logger_backend_metadata()) else - Logger.Formatter.new(metadata: ConfigHelper.logger_backend_metadata()) + Logger.Formatter.new( + format: "$dateT$time $metadata[$level] $message\n", + metadata: ConfigHelper.logger_backend_metadata() + ) end) -config :logger, :api, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api] +config :logger, :api, + format: "$dateT$time $metadata[$level] $message\n", + metadata: ConfigHelper.logger_metadata(), + metadata_filter: [application: :api] -config :logger, :api_v2, metadata: ConfigHelper.logger_metadata(), metadata_filter: [application: :api_v2] +config :logger, :api_v2, + format: "$dateT$time $metadata[$level] $message\n", + metadata: ConfigHelper.logger_metadata(), + metadata_filter: [application: :api_v2] microservice_multichain_search_url = ConfigHelper.parse_url_env_var("MICROSERVICE_MULTICHAIN_SEARCH_URL") transactions_stats_enabled = ConfigHelper.parse_bool_env_var("TXS_STATS_ENABLED", "true") From 5a8a227a153b3f6b6f5c59e4b592516037cfdc46 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:46:55 +0300 Subject: [PATCH 0593/1006] fix: Fix search for ERC-1155 with null symbol (#13632) --- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 3 +- apps/explorer/lib/explorer/application.ex | 4 ++ .../lib/explorer/chain/bridged_token.ex | 5 +- .../chain/cache/background_migrations.ex | 11 ++- apps/explorer/lib/explorer/chain/search.ex | 4 +- apps/explorer/lib/explorer/chain/token.ex | 47 ++++++++++++- .../migrator/heavy_db_index_operation.ex | 2 +- .../create_tokens_name_partial_fts_index.ex | 70 +++++++++++++++++++ 9 files changed, 138 insertions(+), 11 deletions(-) create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_tokens_name_partial_fts_index.ex diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 76514bbb7bae..c1774caf0024 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -177,7 +177,8 @@ for index_operation <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, - Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex + Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex ] do config :explorer, index_operation, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 5da08c189a34..cb76d1423417 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -107,7 +107,8 @@ for migrator <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, - Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex + Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex ] do config :explorer, migrator, enabled: false end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 0f689ef978fe..fc8d7b545edd 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -332,6 +332,10 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsCreatedContractAddressHashPartialIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/chain/bridged_token.ex b/apps/explorer/lib/explorer/chain/bridged_token.ex index d82d8c9fd26f..ba4ceb291123 100644 --- a/apps/explorer/lib/explorer/chain/bridged_token.ex +++ b/apps/explorer/lib/explorer/chain/bridged_token.ex @@ -11,8 +11,7 @@ defmodule Explorer.Chain.BridgedToken do import Ecto.Query, only: [ from: 2, - limit: 2, - where: 2 + limit: 2 ] alias ABI.{TypeDecoder, TypeEncoder} @@ -957,7 +956,7 @@ defmodule Explorer.Chain.BridgedToken do case Search.prepare_search_term(filter) do {:some, filter_term} -> base_query_with_paging - |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) + |> Token.apply_fts_filter(filter_term) _ -> base_query_with_paging diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index d01c831393a6..79ec93a4faea 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -58,7 +58,8 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :heavy_indexes_create_addresses_transactions_count_asc_coin_balance_desc_hash_partial_index_finished, key: :heavy_indexes_drop_token_instances_token_id_index_finished, key: :fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished, - key: :heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished + key: :heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished, + key: :heavy_indexes_create_tokens_name_partial_fts_index_finished @dialyzer :no_match @@ -87,6 +88,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do CreateLogsBlockHashIndex, CreateLogsDepositsWithdrawalsIndex, CreateSmartContractsLanguageIndex, + CreateTokensNamePartialFtsIndex, DropInternalTransactionsCreatedContractAddressHashPartialIndex, DropInternalTransactionsFromAddressHashIndex, DropLogsAddressHashIndex, @@ -360,6 +362,13 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end + defp handle_fallback(:heavy_indexes_create_tokens_name_partial_fts_index_finished) do + set_and_return_migration_status( + CreateTokensNamePartialFtsIndex, + &set_heavy_indexes_create_tokens_name_partial_fts_index_finished/1 + ) + end + defp handle_fallback(:sanitize_verified_addresses_finished) do {:return, false} end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index b967775c63f6..eaa09d14a74f 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -584,11 +584,11 @@ defmodule Explorer.Chain.Search do as: :token, left_join: smart_contract in SmartContract, as: :smart_contract, - on: token.contract_address_hash == smart_contract.address_hash, - where: fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^term) + on: token.contract_address_hash == smart_contract.address_hash ) base_query + |> Token.apply_fts_filter(term) |> apply_sorting([], @token_sorting) |> page_search_results(paging_options, "token") |> ExplorerHelper.maybe_hide_scam_addresses_for_search(:contract_address_hash, options) diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 2e056b70ba39..dc55ac6d51cc 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -90,7 +90,7 @@ defmodule Explorer.Chain.Token do alias Ecto.{Changeset, Multi} alias Explorer.{Chain, SortingHelper} alias Explorer.Chain.{Address, BridgedToken, Hash, Search, Token} - alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Chain.Cache.{BackgroundMigrations, BlockNumber} alias Explorer.Chain.Cache.Counters.{TokenHoldersCount, TokenTransfersCount} alias Explorer.Chain.Import.Runner alias Explorer.Helper, as: ExplorerHelper @@ -380,7 +380,7 @@ defmodule Explorer.Chain.Token do case filter && filter !== "" && Search.prepare_search_term(filter) do {:some, filter_term} -> sorted_paginated_query - |> where(fragment("to_tsvector('english', symbol || ' ' || name) @@ to_tsquery(?)", ^filter_term)) + |> apply_fts_filter(filter_term) _ -> sorted_paginated_query @@ -390,6 +390,49 @@ defmodule Explorer.Chain.Token do |> Chain.select_repo(options).all() end + @doc """ + Applies full-text search filtering to a token query. + + This function handles tokens with and without symbols differently: + - For tokens with a symbol, it searches across both symbol and name. + - For tokens without a symbol (e.g., ERC-1155), it searches only the name field. + + ## Parameters + - `query`: The Ecto query to filter. + - `filter_term`: The prepared search term (from Search.prepare_search_term/1). + + ## Returns + - An Ecto query with FTS filtering applied. + """ + @spec apply_fts_filter(Ecto.Query.t(), String.t()) :: Ecto.Query.t() + def apply_fts_filter(query, filter_term) do + if BackgroundMigrations.get_heavy_indexes_create_tokens_name_partial_fts_index_finished() do + query + |> where( + [token], + (not is_nil(token.symbol) and + fragment( + "to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", + token.symbol, + token.name, + ^filter_term + )) or + (is_nil(token.symbol) and + fragment( + "to_tsvector('english', ?) @@ to_tsquery(?)", + token.name, + ^filter_term + )) + ) + else + query + |> where( + [token], + fragment("to_tsvector('english', ? || ' ' || ?) @@ to_tsquery(?)", token.symbol, token.name, ^filter_term) + ) + end + end + defp apply_filter(query, empty_type) when empty_type in [nil, []], do: query defp apply_filter(query, token_types) when is_list(token_types) do diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex index 2aa9fa8433fa..52f72a7f5c57 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation.ex @@ -32,7 +32,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation do | :smart_contracts | :arbitrum_batch_l2_blocks | :smart_contracts_additional_sources - + | :tokens @doc """ Specifies the type of operation to be performed on the database index. diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_tokens_name_partial_fts_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_tokens_name_partial_fts_index.ex new file mode 100644 index 000000000000..2d3c1b38606b --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_tokens_name_partial_fts_index.ex @@ -0,0 +1,70 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex do + @moduledoc """ + Creates partial full-text search index on the `tokens` table with filtering by `symbol IS NULL`. + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + require Logger + + alias Explorer.Chain.Cache.BackgroundMigrations + + alias Explorer.Migrator.{ + HeavyDbIndexOperation, + MigrationStatus + } + + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :tokens + @index_name "tokens_name_partial_fts_index" + @operation_type :create + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations, do: [] + + @query_string """ + CREATE INDEX #{HeavyDbIndexOperationHelper.add_concurrently_flag?()} IF NOT EXISTS "#{@index_name}" + ON #{@table_name} USING GIN (to_tsvector('english', name)) + WHERE symbol IS NULL; + """ + + @impl HeavyDbIndexOperation + def db_index_operation do + HeavyDbIndexOperationHelper.create_db_index(@query_string) + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, @query_string) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_creation_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache do + BackgroundMigrations.set_heavy_indexes_create_tokens_name_partial_fts_index_finished(true) + end +end From 120f82899a5855a938a3b912bd9b2d9475222a83 Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 15 Jan 2026 22:33:06 +0400 Subject: [PATCH 0594/1006] feat: Support EigenDA blobs by Optimism batch indexer (#13709) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../views/api/v2/optimism_view.ex | 2 +- .../explorer/chain/optimism/frame_sequence.ex | 7 +- .../chain/optimism/frame_sequence_blob.ex | 23 ++++- .../20251205112616_op_eigen_da_blobs.exs | 7 ++ .../fetcher/optimism/transaction_batch.ex | 93 ++++++++++++++++++- apps/indexer/lib/indexer/helper.ex | 33 ++++--- config/runtime.exs | 2 + docker-compose/envs/common-blockscout.env | 2 + 8 files changed, 148 insertions(+), 21 deletions(-) create mode 100644 apps/explorer/priv/optimism/migrations/20251205112616_op_eigen_da_blobs.exs diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex index 0f0b8be36ee1..a6a3d2cce268 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/optimism_view.ex @@ -368,7 +368,7 @@ defmodule BlockScoutWeb.API.V2.OptimismView do :l2_end_block_number => non_neg_integer(), :transactions_count => non_neg_integer(), :l1_transaction_hashes => list(), - :batch_data_container => :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil + :batch_data_container => :in_blob4844 | :in_celestia | :in_eigenda | :in_alt_da | :in_calldata | nil } defp render_base_info_for_batch(number, l2_block_number_from, l2_block_number_to, transactions_count, batch) do FrameSequence.prepare_base_info_for_batch( diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex index 9fe100e3937d..6d5cc3c323cf 100644 --- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex +++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence.ex @@ -145,7 +145,8 @@ defmodule Explorer.Chain.Optimism.FrameSequence do - `l2_block_number_from`: Start L2 block number of the batch block range. - `l2_block_number_to`: End L2 block number of the batch block range. - `transactions_count`: The L2 transaction count included into the blocks of the range. - - `batch_data_container`: Designates where the batch info is stored: :in_blob4844, :in_celestia, :in_alt_da, or :in_calldata. + - `batch_data_container`: Designates where the batch info is stored: + :in_blob4844, :in_celestia, :in_eigenda, :in_alt_da, or :in_calldata. Can be `nil` if the container is unknown. - `batch`: Either an `Explorer.Chain.Optimism.FrameSequence` entry or a map with the corresponding fields. @@ -158,7 +159,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do non_neg_integer(), non_neg_integer(), non_neg_integer(), - :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil, + :in_blob4844 | :in_celestia | :in_eigenda | :in_alt_da | :in_calldata | nil, __MODULE__.t() | %{:l1_timestamp => DateTime.t(), :l1_transaction_hashes => list(), optional(any()) => any()} ) :: %{ @@ -168,7 +169,7 @@ defmodule Explorer.Chain.Optimism.FrameSequence do :l2_end_block_number => non_neg_integer(), :transactions_count => non_neg_integer(), :l1_transaction_hashes => list(), - :batch_data_container => :in_blob4844 | :in_celestia | :in_alt_da | :in_calldata | nil + :batch_data_container => :in_blob4844 | :in_celestia | :in_eigenda | :in_alt_da | :in_calldata | nil } def prepare_base_info_for_batch( number, diff --git a/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex b/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex index 9ffe6aea3918..5561138ebbce 100644 --- a/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex +++ b/apps/explorer/lib/explorer/chain/optimism/frame_sequence_blob.ex @@ -21,7 +21,7 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do @typedoc """ * `key` - A unique id (key) of the blob. - * `type` - A type of the blob (`celestia`, `eip4844`, or `alt_da`). + * `type` - A type of the blob (`celestia`, `eip4844`, `alt_da`, or `eigenda`). * `metadata` - A map containing metadata of the blob. * `l1_transaction_hash` - The corresponding L1 transaction hash which point to the blob. * `l1_timestamp` - The timestamp of the L1 transaction. @@ -31,7 +31,7 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do @primary_key {:id, :integer, autogenerate: false} typed_schema "op_frame_sequence_blobs" do field(:key, :binary) - field(:type, Ecto.Enum, values: [:celestia, :eip4844, :alt_da]) + field(:type, Ecto.Enum, values: [:celestia, :eip4844, :alt_da, :eigenda]) field(:metadata, :map) field(:l1_transaction_hash, Hash.Full) field(:l1_timestamp, :utc_datetime_usec) @@ -60,10 +60,11 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do - `options`: A keyword list of options that may include whether to use a replica database. ## Returns - - A tuple {type, blobs} where `type` can be one of: `in_blob4844`, `in_celestia`, `in_alt_da`, `in_calldata`. + - A tuple {type, blobs} where `type` can be one of: `in_blob4844`, `in_celestia`, `in_eigenda`, `in_alt_da`, `in_calldata`. The `blobs` in the list of blobs related to the specified frame sequence id sorted by an entity id. """ - @spec list(non_neg_integer(), list()) :: {:in_blob4844 | :in_celestia | :in_alt_da | :in_calldata, [map()]} + @spec list(non_neg_integer(), list()) :: + {:in_blob4844 | :in_celestia | :in_eigenda | :in_alt_da | :in_calldata, [map()]} def list(frame_sequence_id, options \\ []) do repo = select_repo(options) @@ -103,6 +104,17 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do } end) + eigenda_blobs = + blobs + |> Enum.filter(fn b -> b.type == :eigenda end) + |> Enum.map(fn b -> + %{ + "cert" => b.metadata["cert"], + "l1_transaction_hash" => b.l1_transaction_hash, + "l1_timestamp" => b.l1_timestamp + } + end) + alt_da_blobs = blobs |> Enum.filter(fn b -> b.type == :alt_da end) @@ -121,6 +133,9 @@ defmodule Explorer.Chain.Optimism.FrameSequenceBlob do not Enum.empty?(celestia_blobs) -> {:in_celestia, celestia_blobs} + not Enum.empty?(eigenda_blobs) -> + {:in_eigenda, eigenda_blobs} + not Enum.empty?(alt_da_blobs) -> {:in_alt_da, alt_da_blobs} diff --git a/apps/explorer/priv/optimism/migrations/20251205112616_op_eigen_da_blobs.exs b/apps/explorer/priv/optimism/migrations/20251205112616_op_eigen_da_blobs.exs new file mode 100644 index 000000000000..9aeb849d2db6 --- /dev/null +++ b/apps/explorer/priv/optimism/migrations/20251205112616_op_eigen_da_blobs.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Optimism.Migrations.OPEigenDABlobs do + use Ecto.Migration + + def change do + execute("ALTER TYPE op_frame_sequence_blob_type ADD VALUE IF NOT EXISTS 'eigenda'") + end +end diff --git a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex index 24bd66b27393..8b5d8f217150 100644 --- a/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex +++ b/apps/indexer/lib/indexer/fetcher/optimism/transaction_batch.ex @@ -133,6 +133,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do batch_submitter: batch_submitter, eip4844_blobs_api_url: Helper.trim_url(env[:eip4844_blobs_api_url]), celestia_blobs_api_url: Helper.trim_url(env[:celestia_blobs_api_url]), + eigenda_blobs_api_url: Helper.trim_url(env[:eigenda_blobs_api_url]), alt_da_server_url: Helper.trim_url(env[:alt_da_server_url]), block_check_interval: block_check_interval, start_block: start_block, @@ -217,6 +218,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do # - `batch_submitter`: L1 address which sends L1 batch transactions to the `batch_inbox` # - `eip4844_blobs_api_url`: URL of Blockscout Blobs API to get EIP-4844 blobs # - `celestia_blobs_api_url`: URL of the server where Celestia blobs can be read from + # - `eigenda_blobs_api_url`: URL of the server where EigenDA blobs can be read from # - `alt_da_server_url`: URL of Alt-DA server where keccak commitment data can be read from # - `block_check_interval`: time interval for checking latest block number # - `start_block`: start block number of the block range @@ -236,6 +238,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do batch_submitter: batch_submitter, eip4844_blobs_api_url: eip4844_blobs_api_url, celestia_blobs_api_url: celestia_blobs_api_url, + eigenda_blobs_api_url: eigenda_blobs_api_url, alt_da_server_url: alt_da_server_url, block_check_interval: block_check_interval, start_block: start_block, @@ -279,7 +282,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do {genesis_block_l2, block_duration}, incomplete_channels_acc, {json_rpc_named_arguments, json_rpc_named_arguments_l2}, - {eip4844_blobs_api_url, celestia_blobs_api_url, alt_da_server_url, chain_id_l1}, + {eip4844_blobs_api_url, celestia_blobs_api_url, eigenda_blobs_api_url, alt_da_server_url, chain_id_l1}, Helper.infinite_retries_number() ) @@ -738,6 +741,72 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do [] end + # Gets EigenDA blob data & metadata by L1 transaction input (encoding blob's certificate). + # The data is read from the remote `da-indexer` service. + # + # ## Parameters + # - `transaction_input`: The contents of transaction `input` field. + # - `offset`: Offset (in bytes) within the `transaction_input` where blob's certificate is encoded. + # - `transaction_hash`: The L1 transaction hash for logging purposes. + # - `blobs_api_url`: The URL to `da-indexer` API. + # + # ## Returns + # - A list with a map containing blob's data and certificate. + # - An empty list in case of an error (`da-indexer` didn't respond, URL is not defined, or transaction input is incorrect). + @spec eigenda_blob_to_input(binary(), non_neg_integer(), String.t(), String.t()) :: [ + %{:bytes => binary(), :eigenda_cert => binary()} + ] + defp eigenda_blob_to_input("0x" <> transaction_input, offset, transaction_hash, blobs_api_url) do + transaction_input + |> Base.decode16!(case: :mixed) + |> eigenda_blob_to_input(offset, transaction_hash, blobs_api_url) + end + + defp eigenda_blob_to_input(transaction_input, offset, transaction_hash, blobs_api_url) when blobs_api_url != "" do + <<_::binary-size(offset), cert::binary>> = transaction_input + + url = blobs_api_url <> "/0x" <> Base.encode16(cert, case: :lower) + + url_with_proxy = + case Application.get_all_env(:indexer)[__MODULE__][:eigenda_proxy_base_url] do + nil -> + url + + eigenda_proxy_base_url -> + url <> "?proxyBaseUrl=" <> Helper.trim_url(eigenda_proxy_base_url) + end + + with {:ok, response} <- Helper.http_get_request(url_with_proxy), + data = Map.get(response, "data"), + true <- !is_nil(data), + data_binary = hash_to_binary(data), + true <- byte_size(data_binary) > 0 do + [ + %{ + bytes: data_binary, + eigenda_cert: cert + } + ] + else + false -> + Logger.error("EigenDA Proxy server response is empty for the request #{url_with_proxy}") + + [] + + _ -> + Logger.warning("Transaction hash: #{transaction_hash}") + [] + end + end + + defp eigenda_blob_to_input(_transaction_input, _offset, _transaction_hash, "") do + Logger.error( + "Cannot read EigenDA blobs from the server as the API URL is not defined. Please, check INDEXER_OPTIMISM_L1_BATCH_EIGENDA_BLOBS_API_URL env variable." + ) + + [] + end + # Gets Alt-DA data & commitment hash by L1 transaction input (encoding the commitment). # The data is then read from the remote DA server. # @@ -807,7 +876,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do {genesis_block_l2, block_duration}, incomplete_channels, json_rpc_named_arguments_l2, - {eip4844_blobs_api_url, celestia_blobs_api_url, alt_da_server_url, chain_id_l1} + {eip4844_blobs_api_url, celestia_blobs_api_url, eigenda_blobs_api_url, alt_da_server_url, chain_id_l1} ) do transactions_filtered |> Enum.reduce({:ok, incomplete_channels, [], [], []}, fn transaction, @@ -831,6 +900,10 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do # this is Celestia DA transaction, so we get the data from Celestia blob celestia_blob_to_input(transaction.input, 3, transaction.hash, celestia_blobs_api_url) + commitment_alt_da_signature(transaction.input) == 0x010100 -> + # this is Eigen DA transaction, so we get the data from EigenDA blob + eigenda_blob_to_input(transaction.input, 3, transaction.hash, eigenda_blobs_api_url) + commitment_alt_da_signature(transaction.input) == 0x0100 -> # this is Alt-DA transaction with a keccak commitment, so we get the data from a DA server alt_da_commitment_to_input(transaction.input, transaction.hash, alt_da_server_url) @@ -891,6 +964,7 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do transaction_hash: transaction.hash, eip4844_blob_hash: Map.get(input, :eip4844_blob_hash), celestia_blob_metadata: Map.get(input, :celestia_blob_metadata), + eigenda_cert: Map.get(input, :eigenda_cert), alt_da_commitment: Map.get(input, :alt_da_commitment) }) @@ -988,6 +1062,21 @@ defmodule Indexer.Fetcher.Optimism.TransactionBatch do } ] + !is_nil(Map.get(frame, :eigenda_cert)) -> + # credo:disable-for-next-line /Credo.Check.Refactor.AppendSingleItem/ + new_blobs_acc ++ + [ + %{ + id: next_blob_id, + key: :crypto.hash(:sha256, frame.eigenda_cert), + type: :eigenda, + metadata: %{cert: "0x" <> Base.encode16(frame.eigenda_cert, case: :lower)}, + l1_transaction_hash: frame.transaction_hash, + l1_timestamp: frame.block_timestamp, + frame_sequence_id: frame_sequence_id + } + ] + !is_nil(Map.get(frame, :alt_da_commitment)) -> # credo:disable-for-next-line /Credo.Check.Refactor.AppendSingleItem/ new_blobs_acc ++ diff --git a/apps/indexer/lib/indexer/helper.ex b/apps/indexer/lib/indexer/helper.ex index 4a2cefa94283..a2b5eb20555a 100644 --- a/apps/indexer/lib/indexer/helper.ex +++ b/apps/indexer/lib/indexer/helper.ex @@ -30,6 +30,7 @@ defmodule Indexer.Helper do @infinite_retries_number 100_000_000 @block_check_interval_range_size 100 @block_by_number_chunk_size 50 + @http_get_request_max_attempts 10 @beacon_blob_fetcher_reference_slot_eth 8_500_000 @beacon_blob_fetcher_reference_timestamp_eth 1_708_824_023 @@ -780,14 +781,17 @@ defmodule Indexer.Helper do - `url`: The URL which needs to be requested. - `response_format`: Can be `:json` (by default) or `:raw`. In case of `:json`, the response is decoded with `Jason.decode`. - `attempts_done`: The number of attempts done. Incremented by the function itself. + - `avoid_retry_for_statuses`: The list of http error codes we don't need to re-send the request for. + E.g. for 404 error we don't try to re-send the request. ## Returns - `{:ok, response}` where `response` is a map decoded from a JSON object, or the raw bytes (depending on the `response_format` parameter). - - `{:error, reason}` in case of failure (after three unsuccessful attempts). + - `{:error, reason}` in case of failure (after all unsuccessful attempts). """ - @spec http_get_request(String.t(), :json | :raw, non_neg_integer()) :: {:ok, map() | binary()} | {:error, any()} - def http_get_request(url, response_format \\ :json, attempts_done \\ 0) do - recv_timeout = 5_000 + @spec http_get_request(String.t(), :json | :raw, non_neg_integer(), [non_neg_integer()]) :: + {:ok, map() | binary()} | {:error, any()} + def http_get_request(url, response_format \\ :json, attempts_done \\ 0, avoid_retry_for_statuses \\ [404]) do + recv_timeout = 60_000 connect_timeout = 8_000 client = Tesla.client([{Tesla.Middleware.Timeout, timeout: recv_timeout}], Tesla.Adapter.Mint) @@ -799,11 +803,15 @@ defmodule Indexer.Helper do {:ok, body} end - {:ok, %{body: body, status: _}} -> - http_get_request_error(url, response_format, body, attempts_done) + {:ok, %{body: body, status: status}} -> + if status in avoid_retry_for_statuses do + http_get_request_error(url, response_format, body, @http_get_request_max_attempts, avoid_retry_for_statuses) + else + http_get_request_error(url, response_format, body, attempts_done, avoid_retry_for_statuses) + end {:error, error} -> - http_get_request_error(url, response_format, error, attempts_done) + http_get_request_error(url, response_format, error, attempts_done, avoid_retry_for_statuses) end end @@ -814,12 +822,15 @@ defmodule Indexer.Helper do # - `response_format`: Can be `:json` (by default) or `:raw`. In case of `:json`, the response is decoded with `Jason.decode`. # - `error`: The error description for logging purposes. # - `attempts_done`: The number of attempts done. Incremented by the function itself. + # - `avoid_retry_for_statuses`: The list of http error codes we don't need to re-send the request for. + # E.g. for 404 error we don't try to re-send the request. # # ## Returns # - `{:ok, response}` tuple if the re-call was successful. # - `{:error, reason}` if all attempts were failed. - @spec http_get_request_error(String.t(), :json | :raw, any(), non_neg_integer()) :: {:ok, map()} | {:error, any()} - defp http_get_request_error(url, response_format, error, attempts_done) do + @spec http_get_request_error(String.t(), :json | :raw, any(), non_neg_integer(), [non_neg_integer()]) :: + {:ok, map()} | {:error, any()} + defp http_get_request_error(url, response_format, error, attempts_done, avoid_retry_for_statuses) do old_truncate = Application.get_env(:logger, :truncate) Logger.configure(truncate: :infinity) @@ -835,11 +846,11 @@ defmodule Indexer.Helper do # retry to send the request attempts_done = attempts_done + 1 - if attempts_done < 10 do + if attempts_done < @http_get_request_max_attempts do # wait up to 20 minutes and then retry :timer.sleep(min(3000 * Integer.pow(2, attempts_done - 1), 1_200_000)) Logger.info("Retry to send the request to #{url} ...") - http_get_request(url, response_format, attempts_done) + http_get_request(url, response_format, attempts_done, avoid_retry_for_statuses) else {:error, "Error while sending request to #{url}"} end diff --git a/config/runtime.exs b/config/runtime.exs index 26f08672f9d8..57a6b4860d8e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1356,6 +1356,8 @@ config :indexer, Indexer.Fetcher.Optimism.TransactionBatch, blocks_chunk_size: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE", "4"), eip4844_blobs_api_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL", ""), celestia_blobs_api_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_CELESTIA_BLOBS_API_URL", ""), + eigenda_blobs_api_url: ConfigHelper.parse_url_env_var("INDEXER_OPTIMISM_L1_BATCH_EIGENDA_BLOBS_API_URL", ""), + eigenda_proxy_base_url: ConfigHelper.parse_url_env_var("INDEXER_OPTIMISM_L1_BATCH_EIGENDA_PROXY_BASE_URL"), alt_da_server_url: System.get_env("INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL", ""), genesis_block_l2: ConfigHelper.parse_integer_or_nil_env_var("INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER"), inbox: System.get_env("INDEXER_OPTIMISM_L1_BATCH_INBOX"), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index caaa2f1c7db2..79c70d754e93 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -321,6 +321,8 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_OPTIMISM_L1_BATCH_BLOCKS_CHUNK_SIZE= # INDEXER_OPTIMISM_L1_BATCH_BLOCKSCOUT_BLOBS_API_URL= # INDEXER_OPTIMISM_L1_BATCH_CELESTIA_BLOBS_API_URL= +# INDEXER_OPTIMISM_L1_BATCH_EIGENDA_BLOBS_API_URL= +# INDEXER_OPTIMISM_L1_BATCH_EIGENDA_PROXY_BASE_URL= # INDEXER_OPTIMISM_L1_BATCH_ALT_DA_SERVER_URL= # INDEXER_OPTIMISM_L2_BATCH_GENESIS_BLOCK_NUMBER= # INDEXER_OPTIMISM_BLOCK_DURATION= From 2ac8642f1271c85cd0f17e3add22840237eb14a7 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:06:42 +0300 Subject: [PATCH 0595/1006] fix: Take into account empty arrays in Explorer.Migrator.SanitizeIncorectNFTTokenTransfers (#13852) Co-authored-by: Victor Baranov --- .../migrator/sanitize_incorrect_nft_token_transfers.ex | 6 +++++- ...114143222_re_run_sanitize_incorrect_nft_migration.exs | 9 +++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/priv/repo/migrations/20260114143222_re_run_sanitize_incorrect_nft_migration.exs diff --git a/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex b/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex index cfe7b6c5de8a..02032101682b 100644 --- a/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex +++ b/apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex @@ -121,7 +121,11 @@ defmodule Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers do defp unprocessed_identifiers("delete_erc_1155") do TokenTransfer |> select([tt], {tt.transaction_hash, tt.block_hash, tt.log_index}) - |> where([tt], tt.token_type == ^"ERC-1155" and is_nil(tt.amount) and is_nil(tt.amounts) and is_nil(tt.token_ids)) + |> where( + [tt], + tt.token_type == ^"ERC-1155" and is_nil(tt.amount) and (is_nil(tt.amounts) or tt.amounts == []) and + (is_nil(tt.token_ids) or tt.token_ids == []) + ) end defp unprocessed_identifiers("refetch") do diff --git a/apps/explorer/priv/repo/migrations/20260114143222_re_run_sanitize_incorrect_nft_migration.exs b/apps/explorer/priv/repo/migrations/20260114143222_re_run_sanitize_incorrect_nft_migration.exs new file mode 100644 index 000000000000..57eb4ad3cad5 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20260114143222_re_run_sanitize_incorrect_nft_migration.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.ReRunSanitizeIncorrectNftMigration do + use Ecto.Migration + + def change do + execute( + "UPDATE migrations_status SET status = 'started', meta = '{\"step\": \"delete_erc_1155\"}' WHERE migration_name = 'sanitize_incorrect_nft' AND (meta IS NULL OR meta != '{\"step\": \"delete_erc_721\"}')" + ) + end +end From a10e5b047c2b450ea36d502adfaed8c952b5c774 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 19 Jan 2026 16:55:46 +0400 Subject: [PATCH 0596/1006] fix: Filter traceable data in InternalTransaction.async_fetch (#13872) --- apps/indexer/lib/indexer/fetcher/internal_transaction.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index a89e73fd9662..ac19f6df9402 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -57,7 +57,11 @@ defmodule Indexer.Fetcher.InternalTransaction do if InternalTransactionSupervisor.disabled?() do :ok else - data = data_for_buffer(block_numbers, transactions) + data = + block_numbers + |> data_for_buffer(transactions) + |> RangesHelper.filter_traceable_block_numbers() + BufferedTask.buffer(__MODULE__, data, realtime?, timeout) end end From 0cb34a3be3065fbd6b1910ba659fa0cccc5aef71 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 19 Jan 2026 19:41:13 +0300 Subject: [PATCH 0597/1006] fix: Add block range filtering to TokenBalance fetcher (#13874) --- .../ethereum_jsonrpc/utility/ranges_helper.ex | 59 ++++++++++++++++++- .../lib/indexer/fetcher/token_balance.ex | 25 ++++++-- 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex index 88d499db1b2d..426096447b8a 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/utility/ranges_helper.ex @@ -31,6 +31,19 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do end end + @doc """ + Filters out records from `data` that are outside the configured block ranges + """ + @spec filter_by_block_ranges([integer() | map()]) :: [integer() | map()] + def filter_by_block_ranges(data) do + if block_ranges_present?() do + block_ranges = get_block_ranges() + Enum.filter(data, &number_in_ranges?(extract_block_number(&1), block_ranges)) + else + data + end + end + @doc """ Filters elements with `filter_func` if `TRACE_BLOCK_RANGES` is set """ @@ -51,6 +64,14 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do Application.get_env(:indexer, :trace_block_ranges) != @default_trace_block_ranges end + @doc """ + Checks if block ranges are defined via env variables + """ + @spec block_ranges_present? :: boolean() + def block_ranges_present? do + Application.get_env(:indexer, :block_ranges) != nil + end + @doc """ Retrieves trace ranges from application variable in string format and parses them into Range/integer """ @@ -61,6 +82,16 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do |> parse_block_ranges() end + @doc """ + Retrieves block ranges from application variable in string format and parses them into Range/integer + """ + @spec get_block_ranges :: [Range.t() | integer()] + def get_block_ranges do + :indexer + |> Application.get_env(:block_ranges) + |> parse_block_ranges() + end + @doc """ Parse ranges from string format into Range/integer """ @@ -198,14 +229,35 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do trace_block_ranges = get_trace_block_ranges() fn data, acc -> - if number_in_ranges?(extract_block_number(data), trace_block_ranges), + block_number = extract_block_number(data) + + if number_in_ranges?(block_number, trace_block_ranges), do: reducer.(data, acc), else: acc end else - fn block_number, acc -> - reducer.(block_number, acc) + reducer + end + end + + @doc """ + Defines a stream reducer that filters out data outside configured block ranges. + Applicable for fetchers' `init` function (for modules that implement `BufferedTask`). + """ + @spec stream_reducer_by_block_ranges((any(), any() -> any())) :: (any(), any() -> any()) + def stream_reducer_by_block_ranges(reducer) do + if block_ranges_present?() do + block_ranges = get_block_ranges() + + fn data, acc -> + block_number = extract_block_number(data) + + if number_in_ranges?(block_number, block_ranges), + do: reducer.(data, acc), + else: acc end + else + reducer end end @@ -233,6 +285,7 @@ defmodule EthereumJSONRPC.Utility.RangesHelper do end) end + defp extract_block_number({_, _, block_number, _, _, _}), do: block_number defp extract_block_number(%{block_number: block_number}), do: block_number defp extract_block_number(block_number), do: block_number diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance.ex index a39de159b13a..b68aa5228e23 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance.ex @@ -18,6 +18,7 @@ defmodule Indexer.Fetcher.TokenBalance do require Logger + alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} alias Explorer.Chain.Events.Publisher @@ -49,7 +50,12 @@ defmodule Indexer.Fetcher.TokenBalance do if TokenBalanceSupervisor.disabled?() do :ok else - formatted_params = Enum.map(token_balances, &entry/1) + filtered_balances = + token_balances + |> RangesHelper.filter_by_block_ranges() + |> RangesHelper.filter_traceable_block_numbers() + + formatted_params = Enum.map(filtered_balances, &entry/1) BufferedTask.buffer(__MODULE__, formatted_params, realtime?, :infinity) end @@ -75,14 +81,21 @@ defmodule Indexer.Fetcher.TokenBalance do @impl BufferedTask def init(initial, reducer, _) do + entry_reducer = fn token_balance, acc -> + token_balance + |> entry() + |> reducer.(acc) + end + + stream_reducer = + entry_reducer + |> RangesHelper.stream_reducer_by_block_ranges() + |> RangesHelper.stream_reducer_traceable() + {:ok, final} = TokenBalance.stream_unfetched_token_balances( initial, - fn token_balance, acc -> - token_balance - |> entry() - |> reducer.(acc) - end, + stream_reducer, true ) From 0f462c9d6e39a52c9b0be55af2071bdd50aec8b3 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Mon, 19 Jan 2026 21:08:43 +0300 Subject: [PATCH 0598/1006] fix: Skip hot contracts fetching if last 30 days not indexed (#13873) --- apps/explorer/lib/explorer/chain/block.ex | 20 + .../fetcher/stats/hot_smart_contracts.ex | 96 ++- .../stats/hot_smart_contracts_test.exs | 562 ++++++++++++++++++ 3 files changed, 668 insertions(+), 10 deletions(-) create mode 100644 apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 1e57e80ec2b8..68eab61ec54d 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -1084,4 +1084,24 @@ defmodule Explorer.Chain.Block do |> Repo.all() |> Enum.into(%{}) end + + @doc """ + Fetches the second block in the database ordered by number in ascending order. + """ + @spec fetch_second_block_in_database() :: {:ok, __MODULE__.t()} | {:error, :not_found} + def fetch_second_block_in_database do + query = + from(block in __MODULE__, + where: block.consensus == true, + order_by: [asc: block.number], + offset: 1, + limit: 1, + select: block + ) + + case Repo.one(query) do + nil -> {:error, :not_found} + block -> {:ok, block} + end + end end diff --git a/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex index 5958db408f91..47f1b556d5b5 100644 --- a/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex +++ b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex @@ -5,26 +5,31 @@ defmodule Indexer.Fetcher.Stats.HotSmartContracts do use Indexer.Fetcher, restart: :permanent use GenServer + alias Explorer.Chain + alias Explorer.Chain.Block alias Explorer.Stats.HotSmartContracts require Logger @retry_interval 10_000 @max_days_ago 30 + @min_chain_age_days 30 + + @spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()} + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end @impl GenServer def init(opts) do - GenServer.cast(__MODULE__, :check_completeness) - - schedule_next_day_fetch() - - {:ok, opts} + {:ok, opts, {:continue, :check_chain_age}} end - @spec start_link(any()) :: :ignore | {:error, any()} | {:ok, pid()} - def start_link(_) do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + @impl GenServer + def handle_continue(:check_chain_age, state) do + process_chain_age_check() + {:noreply, state} end @impl GenServer @@ -46,6 +51,12 @@ defmodule Indexer.Fetcher.Stats.HotSmartContracts do {:noreply, state} end + @impl GenServer + def handle_info(:check_chain_age, state) do + process_chain_age_check() + {:noreply, state} + end + @impl GenServer def handle_info(_, state) do {:noreply, state} @@ -69,10 +80,75 @@ defmodule Indexer.Fetcher.Stats.HotSmartContracts do end defp schedule_next_day_fetch do - today = Date.utc_today() + schedule_message_on_the_next_day({:fetch_for_date, Date.utc_today(), true}) + end + + defp schedule_startup_check do + schedule_message_on_the_next_day(:check_chain_age) + end + + defp schedule_message_on_the_next_day(payload) do now = DateTime.utc_now() next_day = Date.utc_today() |> Date.add(1) |> DateTime.new!(~T[00:00:00], "Etc/UTC") delay = DateTime.diff(next_day, now, :millisecond) - Process.send_after(self(), {:fetch_for_date, today, true}, delay) + Process.send_after(self(), payload, delay) + end + + defp process_chain_age_check do + case check_chain_age() do + :ok -> + GenServer.cast(__MODULE__, :check_completeness) + schedule_next_day_fetch() + + {:wait, delay_ms} -> + Logger.info( + "Hot contracts module delayed: chain is less than #{@min_chain_age_days} days old. Rescheduling startup check." + ) + + Process.send_after(self(), :check_chain_age, delay_ms) + + {:error, :block_not_found} -> + Logger.info("Hot contracts module delayed: second block not found. Rescheduling startup check for next day.") + + schedule_startup_check() + + {:error, reason} -> + Logger.warning( + "Hot contracts module delayed: error checking chain age (#{inspect(reason)}). Rescheduling startup check for next day." + ) + + schedule_startup_check() + end + end + + defp check_chain_age do + # Get the second block (ordered by number ascending) timestamp + case Block.fetch_second_block_in_database() do + {:ok, block} -> + now = DateTime.utc_now() + age_days = DateTime.diff(now, block.timestamp, :day) + + if age_days >= @min_chain_age_days do + :ok + else + # Calculate delay until chain reaches 30 days old, or next day if that's sooner + target_date = DateTime.add(block.timestamp, @min_chain_age_days, :day) + next_day = Date.utc_today() |> Date.add(1) |> DateTime.new!(~T[00:00:00], "Etc/UTC") + + delay_target = DateTime.diff(target_date, now, :millisecond) + delay_next_day = DateTime.diff(next_day, now, :millisecond) + + # Use the smaller delay (either until 30 days or next day, whichever comes first) + delay_ms = delay_target |> min(delay_next_day) |> max(0) + + {:wait, delay_ms} + end + + {:error, :not_found} -> + {:error, :block_not_found} + end + rescue + e -> + {:error, {:exception, e}} end end diff --git a/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs b/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs new file mode 100644 index 000000000000..e7b8df52521a --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs @@ -0,0 +1,562 @@ +defmodule Indexer.Fetcher.Stats.HotSmartContractsTest do + # MUST be `async: false` due to use of named GenServer + use EthereumJSONRPC.Case, async: false + use Explorer.DataCase + + import ExUnit.CaptureLog + import Mox + + alias Explorer.Chain.Block + alias Indexer.Fetcher.Stats.HotSmartContracts + + @moduletag :capture_log + + # MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to + # use expectations and stubs from test's pid. + setup :set_mox_global + setup :verify_on_exit! + + setup do + # Clean up any existing named process before each test + if Process.whereis(HotSmartContracts) do + try do + GenServer.stop(HotSmartContracts, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + + Process.sleep(50) + end + + on_exit(fn -> + if Process.whereis(HotSmartContracts) do + try do + GenServer.stop(HotSmartContracts, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end) + + :ok + end + + describe "check_chain_age/0" do + test "returns :ok when chain is older than 30 days" do + # Create genesis block (first block) + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + + # Create second block that is 31 days old + second_block_timestamp = ~U[2024-01-01 00:00:00Z] |> DateTime.add(-31, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + # Start the GenServer + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait a bit for handle_continue to process + Process.sleep(200) + + # Verify that check_completeness was called (indirectly by checking if process is still running) + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + # Should not log delay message when chain is old enough + refute log =~ "Hot contracts module delayed" + end + + test "returns {:wait, delay_ms} when chain is less than 30 days old" do + # Create genesis block + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + + # Create second block that is 10 days old + now = DateTime.utc_now() + second_block_timestamp = DateTime.add(now, -10, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to process + Process.sleep(200) + + # Process should still be alive and waiting + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + + test "returns {:error, :block_not_found} when second block does not exist" do + # Only create genesis block, no second block + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to process + Process.sleep(200) + + # Process should still be alive + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: second block not found" + assert log =~ "Rescheduling startup check for next day" + end + + test "returns {:error, :block_not_found} when no blocks exist" do + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to process + Process.sleep(200) + + # Process should still be alive + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: second block not found" + assert log =~ "Rescheduling startup check for next day" + end + + test "correctly calculates delay when chain is close to 30 days old" do + # Create genesis block + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + + # Create second block that is 25 days old (5 days away from 30) + now = DateTime.utc_now() + second_block_timestamp = DateTime.add(now, -25, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to process + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + + test "uses next day delay when it's sooner than 30-day target" do + # Create genesis block + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + + # Create second block that is 29.9 days old + # This means we need to wait ~0.1 days to reach 30, but next day might be sooner + now = DateTime.utc_now() + second_block_timestamp = DateTime.add(now, -29, :day) |> DateTime.add(-21, :hour) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to process + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + end + + describe "process_chain_age_check/0" do + test "starts normal operation when chain age check returns :ok" do + # Create blocks older than 30 days + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + second_block_timestamp = DateTime.utc_now() |> DateTime.add(-31, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for initialization + Process.sleep(200) + + # Process should be running and should have scheduled next day fetch + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + # Should not log delay message when chain is old enough + refute log =~ "Hot contracts module delayed" + end + + test "schedules retry when chain is too young" do + # Create blocks younger than 30 days + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + second_block_timestamp = DateTime.utc_now() |> DateTime.add(-10, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for initialization + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + + test "schedules startup check for next day when block not found" do + # Only genesis block exists + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for initialization + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: second block not found" + assert log =~ "Rescheduling startup check for next day" + end + end + + describe "handle_continue/2" do + test "processes chain age check on startup" do + # Create blocks older than 30 days + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + second_block_timestamp = DateTime.utc_now() |> DateTime.add(-31, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for handle_continue to execute + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end + end + + describe "handle_info/2 for :check_chain_age" do + test "processes chain age check when message is received" do + # Create blocks older than 30 days + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + second_block_timestamp = DateTime.utc_now() |> DateTime.add(-31, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + # Wait for initial processing + Process.sleep(100) + + # Send the check_chain_age message + send(pid, :check_chain_age) + + # Wait for processing + Process.sleep(100) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end + end + + describe "fetch_second_block_in_database integration" do + test "finds second block when multiple blocks exist" do + # Create multiple blocks + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: true) + insert(:block, number: 1, timestamp: ~U[2024-01-02 00:00:00Z], consensus: true) + insert(:block, number: 2, timestamp: ~U[2024-01-03 00:00:00Z], consensus: true) + + assert {:ok, %Block{number: 1}} = Block.fetch_second_block_in_database() + end + + test "returns error when only one consensus block exists #1" do + # Create non-consensus first block and consensus second block + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: false) + insert(:block, number: 1, timestamp: ~U[2024-01-02 00:00:00Z], consensus: true) + + assert {:error, :not_found} = Block.fetch_second_block_in_database() + end + + test "returns error when only one consensus block exists #2" do + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: true) + + assert {:error, :not_found} = Block.fetch_second_block_in_database() + end + + test "returns error when no consensus blocks exist" do + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: false) + insert(:block, number: 1, timestamp: ~U[2024-01-02 00:00:00Z], consensus: false) + + assert {:error, :not_found} = Block.fetch_second_block_in_database() + end + + test "finds second block when blocks are not sequential" do + # Create blocks with gaps in numbers + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z], consensus: true) + insert(:block, number: 5, timestamp: ~U[2024-01-02 00:00:00Z], consensus: true) + insert(:block, number: 10, timestamp: ~U[2024-01-03 00:00:00Z], consensus: true) + + # Should find block number 5 as the second block (ordered by number ascending) + assert {:ok, %Block{number: 5}} = Block.fetch_second_block_in_database() + end + end + + describe "edge cases" do + test "handles chain exactly 30 days old" do + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + now = DateTime.utc_now() + second_block_timestamp = DateTime.add(now, -30, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + # Should not log delay message when chain is exactly 30 days old + refute log =~ "Hot contracts module delayed" + end + + test "handles chain just under 30 days old" do + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + now = DateTime.utc_now() + # 29 days, 23 hours, 59 minutes old + second_block_timestamp = DateTime.add(now, -29, :day) |> DateTime.add(-23, :hour) |> DateTime.add(-59, :minute) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + + test "handles very new chain (1 day old)" do + insert(:block, number: 0, timestamp: ~U[2024-01-01 00:00:00Z]) + now = DateTime.utc_now() + second_block_timestamp = DateTime.add(now, -1, :day) + insert(:block, number: 1, timestamp: second_block_timestamp, consensus: true) + + log = + capture_log(fn -> + {:ok, pid} = HotSmartContracts.start_link([]) + + try do + Process.sleep(200) + + assert Process.alive?(pid) + after + # Clean up + if Process.alive?(pid) do + try do + GenServer.stop(pid, :normal, 1000) + rescue + _ -> :ok + catch + :exit, _ -> :ok + end + end + end + end) + + assert log =~ "Hot contracts module delayed: chain is less than 30 days old" + assert log =~ "Rescheduling startup check" + end + end +end From 420f7f79bb9604db478b4db93d7c40bdf9c07d35 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Mon, 19 Jan 2026 22:12:12 +0400 Subject: [PATCH 0599/1006] chore: Refactor internal transaction logic from "block_index" to "transaction_index" and "index" (#12474) --- .../lib/block_scout_web/chain.ex | 10 +- .../address/internal_transactions.ex | 4 - .../block_scout_web/graphql/schema/types.ex | 2 - .../lib/block_scout_web/notifier.ex | 2 +- .../schemas/api/v2/internal_transaction.ex | 2 +- .../channels/address_channel_test.exs | 9 +- .../address_contract_controller_test.exs | 3 +- ...s_internal_transaction_controller_test.exs | 50 ++-- .../address_read_contract_controller_test.exs | 6 +- .../address_read_proxy_controller_test.exs | 6 +- .../address_transaction_controller_test.exs | 3 +- ...address_write_contract_controller_test.exs | 6 +- .../address_write_proxy_controller_test.exs | 6 +- .../api/rpc/address_controller_test.exs | 64 ++--- .../api/rpc/contract_controller_test.exs | 7 +- .../api/rpc/transaction_controller_test.exs | 3 +- .../api/v2/address_controller_test.exs | 13 +- .../v2/advanced_filter_controller_test.exs | 176 ++++++++------ .../api/v2/block_controller_test.exs | 6 +- .../api/v2/csv_export_controller_test.exs | 3 - .../internal_transaction_controller_test.exs | 17 +- .../api/v2/transaction_controller_test.exs | 23 +- ...n_internal_transaction_controller_test.exs | 21 +- .../address/internal_transactions_test.exs | 21 -- .../address_contract_verification_test.exs | 4 +- .../features/viewing_addresses_test.exs | 28 +-- .../features/viewing_blocks_test.exs | 3 +- .../features/viewing_transactions_test.exs | 11 +- .../graphql/schema/query/node_test.exs | 6 +- .../graphql/schema/query/transaction_test.exs | 24 +- .../views/address_view_test.exs | 3 +- apps/block_scout_web/test/test_helper.exs | 2 + .../lib/ethereum_jsonrpc/geth.ex | 11 +- .../trace_replay_block_transactions.ex | 11 +- .../test/ethereum_jsonrpc/nethermind_test.exs | 1 + apps/explorer/config/config.exs | 6 +- apps/explorer/config/runtime/test.exs | 6 +- apps/explorer/lib/explorer/application.ex | 12 +- apps/explorer/lib/explorer/chain.ex | 49 ++-- apps/explorer/lib/explorer/chain/address.ex | 3 +- .../lib/explorer/chain/address/counters.ex | 4 +- apps/explorer/lib/explorer/chain/block.ex | 2 +- .../chain/cache/background_migrations.ex | 13 +- .../explorer/chain/cache/counters/helper.ex | 17 ++ .../counters/pending_block_operation_count.ex | 13 +- .../pending_transaction_operation_count.ex | 90 +++++++ .../import/runner/internal_transactions.ex | 214 +++++++++++------ .../explorer/chain/internal_transaction.ex | 225 +++++++----------- .../explorer/chain/pending_block_operation.ex | 2 +- .../chain/pending_operations_helper.ex | 15 +- apps/explorer/lib/explorer/etherscan.ex | 4 +- apps/explorer/lib/explorer/graphql.ex | 4 +- .../migrator/backfill_multichain_search_db.ex | 4 +- ...r_transaction_index_index_unique_index.ex} | 19 +- ...lock_hash_transaction_index_index_index.ex | 66 +++++ .../heavy_db_index_operation/helper.ex | 35 +++ ...pdate_internal_transactions_primary_key.ex | 184 ++++++++++++++ .../utility/internal_transaction_helper.ex | 12 + apps/explorer/lib/test_helper.ex | 23 ++ .../chain/address/coin_balance_test.exs | 27 +-- .../runner/internal_transactions_test.exs | 49 +--- .../chain/internal_transaction_test.exs | 124 +--------- .../explorer/chain/smart_contract_test.exs | 2 - .../test/explorer/chain/transaction_test.exs | 1 - apps/explorer/test/explorer/chain_test.exs | 68 +++++- .../explorer/test/explorer/etherscan_test.exs | 75 ++---- apps/explorer/test/explorer/graphql_test.exs | 24 +- ..._zero_value_internal_transactions_test.exs | 16 +- ...ernal_transaction_to_address_hash_test.exs | 6 +- ..._duplicated_internal_transactions_test.exs | 130 ---------- ...sactions_with_incompatible_status_test.exs | 6 +- .../switch_pending_operations_test.exs | 2 - apps/explorer/test/explorer/repo_test.exs | 6 +- .../smart_contract/solidity/verifier_test.exs | 9 +- apps/explorer/test/support/factory.ex | 2 - apps/explorer/test/test_helper.exs | 2 + .../indexer/fetcher/internal_transaction.ex | 87 +------ .../delete_queue_test.exs | 21 +- .../fetcher/internal_transaction_test.exs | 24 +- .../on_demand/contract_creator_test.exs | 1 - apps/indexer/test/test_helper.exs | 2 + config/runtime.exs | 7 +- docker-compose/envs/common-blockscout.env | 1 + 83 files changed, 1229 insertions(+), 1052 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/cache/counters/pending_transaction_operation_count.ex rename apps/explorer/lib/explorer/migrator/heavy_db_index_operation/{create_internal_transactions_block_hash_transaction_index_index_unique_index.ex => create_internal_transactions_block_number_transaction_index_index_unique_index.ex} (68%) create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex create mode 100644 apps/explorer/lib/explorer/migrator/heavy_db_index_operation/update_internal_transactions_primary_key.ex create mode 100644 apps/explorer/lib/explorer/utility/internal_transaction_helper.ex delete mode 100644 apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 751eaef9e6bf..8730d1b0bf3c 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -752,8 +752,8 @@ defmodule BlockScoutWeb.Chain do ] end - # Clause for InternalTransaction by block: - # returned by `BlockScoutWeb.API.V2.BlockController.internal_transactions/2` (`/api/v2/blocks/:block_hash_or_number/internal-transactions`) + # Clause for InternalTransaction by block (for backward compatibility): + # returned by `BlockScoutWeb.API.V2.BlockController.internal_transactions/2` (`/api/v2/blocks/:block_hash_or_number/internal-transactions`) def paging_options(%{"block_index" => index_string}) when is_binary(index_string) do case Integer.parse(index_string) do {index, ""} -> @@ -768,10 +768,6 @@ defmodule BlockScoutWeb.Chain do [paging_options: %{@default_paging_options | key: %{block_index: index}}] end - def paging_options(%{block_index: index}) when is_integer(index) do - [paging_options: %{@default_paging_options | key: %{block_index: index}}] - end - # Clause for `Explorer.Chain.Blackfort.Validator`, # returned by `BlockScoutWeb.API.V2.ValidatorController.blackfort_validators_list/2` (`/api/v2/validators/blackfort`) def paging_options(%{ @@ -1391,7 +1387,7 @@ defmodule BlockScoutWeb.Chain do @spec block_to_internal_transactions(Block.t(), Keyword.t()) :: [InternalTransaction.t()] def block_to_internal_transactions(block, options \\ []) do if InternalTransaction.present_in_db?(block.number) do - InternalTransaction.block_to_internal_transactions(block.hash, options) + InternalTransaction.block_to_internal_transactions(block.number, options) else InternalTransactionOnDemand.fetch_by_block(block, options) end diff --git a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex index bf393b2f09e3..7e3a11882f19 100644 --- a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex +++ b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex @@ -49,9 +49,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do "TxHash", "Index", "BlockNumber", - "BlockHash", "TxIndex", - "BlockIndex", "UnixTimestamp", "FromAddress", "ToAddress", @@ -79,8 +77,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do to_string(internal_transaction.transaction_hash), internal_transaction.index, internal_transaction.block_number, - internal_transaction.block_hash, - internal_transaction.block_index, internal_transaction.transaction_index, internal_transaction.block.timestamp, Address.checksum(internal_transaction.from_address_hash), diff --git a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex index f710239f0c61..7b655498dfb7 100644 --- a/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/graphql/schema/types.ex @@ -226,8 +226,6 @@ defmodule BlockScoutWeb.GraphQL.Schema.Types do field(:transaction_hash, :full_hash) field(:block_number, :integer) field(:transaction_index, :integer) - field(:block_hash, :full_hash) - field(:block_index, :integer) end @desc """ diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 3e3697dd6f1d..d94cddc957ce 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -254,7 +254,7 @@ defmodule BlockScoutWeb.Notifier do def handle_event({:chain_event, :internal_transactions, :realtime, internal_transactions}) do internal_transactions |> Stream.map( - &(InternalTransaction.where_nonpending_block() + &(InternalTransaction.where_nonpending_operation() |> Repo.get_by(transaction_hash: &1.transaction_hash, index: &1.index) |> Repo.preload([:from_address, :to_address, :block])) ) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex index 26d2ad574d74..54ab28349b63 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/internal_transaction.ex @@ -47,7 +47,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.InternalTransaction do block_index: %Schema{ type: :integer, description: "The index of this internal transaction inside the block.", - nullable: false + nullable: true } }, required: [ diff --git a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs index d4da4238a8d8..83a0113466f8 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs @@ -165,9 +165,10 @@ defmodule BlockScoutWeb.AddressChannelTest do :internal_transaction, transaction: transaction, from_address: address, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) @@ -196,9 +197,10 @@ defmodule BlockScoutWeb.AddressChannelTest do insert(:internal_transaction, transaction: transaction, to_address: address, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) @@ -231,9 +233,10 @@ defmodule BlockScoutWeb.AddressChannelTest do transaction: transaction, from_address: address, to_address: address, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs index 768507b3e06b..7b420c32e24e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs @@ -50,9 +50,10 @@ defmodule BlockScoutWeb.AddressContractControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) EthereumJSONRPC.Mox diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs index 0571e16c6da0..ac137a44ef77 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_internal_transaction_controller_test.exs @@ -56,8 +56,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 1, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) to_internal_transaction = @@ -67,8 +66,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 2, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) path = address_internal_transaction_path(conn, :index, Address.checksum(address), %{"type" => "JSON"}) @@ -99,8 +97,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 1, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) to_internal_transaction = @@ -110,8 +107,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 2, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) path = @@ -150,8 +146,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 1, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) to_internal_transaction = @@ -161,8 +156,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 2, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) path = @@ -198,8 +192,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 1, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) to_internal_transaction = @@ -210,8 +203,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 2, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) path = @@ -263,8 +255,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_1.block_number, transaction_index: transaction_1.index, - block_hash: a_block.hash, - block_index: index + block_hash: a_block.hash ) end) @@ -278,8 +269,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_2.block_number, transaction_index: transaction_2.index, - block_hash: a_block.hash, - block_index: 20 + index + block_hash: a_block.hash ) end) @@ -293,8 +283,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_3.block_number, transaction_index: transaction_3.index, - block_hash: b_block.hash, - block_index: index + block_hash: b_block.hash ) end) @@ -308,8 +297,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: 11, block_number: transaction_3.block_number, transaction_index: transaction_3.index, - block_hash: b_block.hash, - block_index: 11 + block_hash: b_block.hash ) conn = @@ -361,8 +349,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_1.block_number, transaction_index: transaction_1.index, - block_hash: a_block.hash, - block_index: index + block_hash: a_block.hash ) end) @@ -376,8 +363,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_2.block_number, transaction_index: transaction_2.index, - block_hash: a_block.hash, - block_index: 55 + index + block_hash: a_block.hash ) end) @@ -392,8 +378,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction_3.block_number, transaction_index: transaction_3.index, - block_hash: b_block.hash, - block_index: index + block_hash: b_block.hash ) end) @@ -514,8 +499,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) @@ -552,9 +536,9 @@ defmodule BlockScoutWeb.AddressInternalTransactionControllerTest do :internal_transaction, transaction: transaction, from_address: address, + transaction_index: transaction.index, index: index, block_hash: transaction.block_hash, - block_index: index, block_number: transaction.block_number ) end) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs index 09d7835f6352..e3f060aab597 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_contract_controller_test.exs @@ -36,9 +36,10 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") @@ -63,9 +64,10 @@ defmodule BlockScoutWeb.AddressReadContractControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) conn = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs index b1f72acf031e..6992df38134b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_read_proxy_controller_test.exs @@ -36,9 +36,10 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") @@ -62,9 +63,10 @@ defmodule BlockScoutWeb.AddressReadProxyControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) conn = get(conn, address_read_proxy_path(BlockScoutWeb.Endpoint, :index, Address.checksum(contract_address.hash))) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs index 57e6c0f3ab29..23afdcab0b0d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs @@ -137,8 +137,9 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do created_contract_address: address, to_address: nil, transaction: transaction, + transaction_index: transaction.index, block_hash: block.hash, - block_index: 0 + block_number: transaction.block_number ) conn = get(conn, address_transaction_path(conn, :index, Address.checksum(address)), %{"type" => "JSON"}) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs index 654a57e58f25..fdd456c01906 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_contract_controller_test.exs @@ -38,9 +38,10 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") @@ -65,9 +66,10 @@ defmodule BlockScoutWeb.AddressWriteContractControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) conn = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs index f004a761072c..5f5b764c0b18 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_write_proxy_controller_test.exs @@ -36,9 +36,10 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:smart_contract, address_hash: contract_address.hash, contract_code_md5: "123") @@ -63,9 +64,10 @@ defmodule BlockScoutWeb.AddressWriteProxyControllerTest do :internal_transaction_create, index: 0, transaction: transaction, + transaction_index: transaction.index, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) conn = diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index 87b15c40704f..4580d5572378 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -1827,12 +1827,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) @@ -1840,12 +1840,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -1895,12 +1895,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) @@ -1908,12 +1908,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -1962,24 +1962,24 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) @@ -1987,12 +1987,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2044,12 +2044,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) @@ -2057,12 +2057,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2070,12 +2070,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, from_address: address, to_address: address_2, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) @@ -2182,11 +2182,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction_create |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, block_hash: transaction.block_hash, - block_index: 0, block_number: transaction.block_number ) |> with_contract_creation(contract_address) @@ -2245,11 +2245,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction_create |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, block_hash: transaction.block_hash, - block_index: 0, block_number: transaction.block_number ) |> with_contract_creation(contract_address) @@ -2281,12 +2281,13 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do internal_transaction_details = [ transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, type: :reward, error: "some error", block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ] insert(:internal_transaction_create, internal_transaction_details) @@ -2318,9 +2319,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do for index <- 0..2 do insert(:internal_transaction_create, transaction: transaction, + transaction_index: transaction.index, index: index, block_hash: transaction.block_hash, - block_index: index, + block_number: transaction.block_number, value: 1 ) end @@ -2354,20 +2356,20 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) @@ -2375,10 +2377,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2430,10 +2432,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) @@ -2441,10 +2443,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2452,10 +2454,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) @@ -2585,11 +2587,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction_create |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, from_address: address, block_number: block.number, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) |> with_contract_creation(contract_address) @@ -2641,12 +2643,12 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do internal_transaction_details = [ from_address: address, transaction: transaction, + transaction_index: transaction.index, index: 0, type: :reward, error: "some error", block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ] insert(:internal_transaction_create, internal_transaction_details) @@ -2681,10 +2683,10 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do internal_transaction_details = %{ from_address: address, transaction: transaction, + transaction_index: transaction.index, index: index, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, value: 1 } @@ -2722,22 +2724,22 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, from_address: address, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) @@ -2745,11 +2747,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2803,11 +2805,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, value: 1, from_address: address, block_hash: transaction.block_hash, - block_index: 0, block_number: block.number ) @@ -2815,11 +2817,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 1, value: 2, from_address: address, block_hash: transaction.block_hash, - block_index: 1, block_number: block.number ) @@ -2827,11 +2829,11 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do :internal_transaction |> insert( transaction: transaction, + transaction_index: transaction.index, index: 2, value: 0, from_address: address, block_hash: transaction.block_hash, - block_index: 2, block_number: block.number ) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 298737f772ac..b070d9fd2fb4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -780,7 +780,6 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do created_contract_code: smart_contract_bytecode, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -1121,7 +1120,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do transaction: transaction, index: 1, block_hash: transaction.block_hash, - block_index: transaction.index + block_number: transaction.block_number, + transaction_index: transaction.index ) address = internal_transaction.created_contract_address @@ -1176,7 +1176,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do index: 0, created_contract_address: contract_address, block_hash: transaction.block_hash, - block_index: transaction.index + block_number: transaction.block_number, + transaction_index: transaction.index ) assert %{ diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index 0ecd2fd52a2d..ba6208e432d1 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -265,11 +265,12 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do internal_transaction_details = [ transaction: transaction, + transaction_index: transaction.index, index: 0, type: :reward, error: error, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ] insert(:internal_transaction, internal_transaction_details) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 7d35da6c6ec6..adaf05f8fa29 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -2232,7 +2232,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 1, from_address: address ) @@ -2243,7 +2242,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 2, to_address: address ) @@ -2284,7 +2282,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do insert(:internal_transaction_selfdestruct, transaction: transaction, index: 1, - block_index: 1, block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, @@ -2317,7 +2314,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: i, from_address: address ) end @@ -2340,7 +2336,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: i, to_address: address ) end @@ -2621,12 +2616,12 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do type: "call", call_type: "call", transaction: transaction, + transaction_index: transaction.index, block: transaction.block, to_address: address, value: 123, block_number: transaction.block_number, - index: 1, - block_index: 1 + index: 1 ) insert(:address_coin_balance) @@ -3888,7 +3883,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: x, to_address: address ) end @@ -3932,7 +3926,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: x, from_address: address ) end @@ -3986,7 +3979,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: x, from_address: address ) end @@ -4034,7 +4026,6 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: x, from_address: address ) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs index ef7d4ce6cfe7..0d69d53441af 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/advanced_filter_controller_test.exs @@ -100,8 +100,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: first_transaction, block_hash: first_transaction.block_hash, - index: i, - block_index: i + block_number: first_transaction.block_number, + transaction_index: first_transaction.index, + index: i ) end @@ -123,8 +124,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: first_transaction, block_hash: first_transaction.block_hash, - index: i, - block_index: i + block_number: first_transaction.block_number, + transaction_index: first_transaction.index, + index: i ) end @@ -147,8 +149,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: first_transaction, block_hash: first_transaction.block_hash, - index: i, - block_index: i + block_number: first_transaction.block_number, + transaction_index: first_transaction.index, + index: i ) end @@ -178,8 +181,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: first_transaction, block_hash: first_transaction.block_hash, - index: i, - block_index: i + block_number: first_transaction.block_number, + transaction_index: first_transaction.index, + index: i ) end @@ -189,8 +193,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: second_transaction, block_hash: second_transaction.block_hash, - index: i, - block_index: i + block_number: second_transaction.block_number, + transaction_index: second_transaction.index, + index: i ) end @@ -225,8 +230,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i, - block_index: i + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i ) end @@ -270,8 +276,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do transaction: transaction, value: value, block_hash: transaction.block_hash, - index: i, - block_index: i + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i ) end @@ -315,8 +322,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i, - block_index: i + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i ) else transaction = @@ -327,8 +335,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: contract_address.hash, to_address: contract_address, block_hash: transaction.block_hash, - index: i, - block_index: i + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i ) end end @@ -372,12 +381,13 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, created_contract_address: address, created_contract_address_hash: address.hash, to_address_hash: nil, to_address: nil, - index: i, - block_index: i + index: i ) else transaction = @@ -392,8 +402,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i, - block_index: i + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i ) end end @@ -477,8 +488,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: contract_address.hash, to_address: contract_address, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: i, - block_index: i, input: method1 ) end @@ -489,8 +501,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: contract_address.hash, to_address: contract_address, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: i, - block_index: i, input: method2 ) end @@ -543,9 +556,10 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: tx, + transaction_index: tx.index, index: i + 1, - block_index: i + 1, block_hash: tx.block_hash, + block_number: tx.block_number, block: tx.block ) @@ -585,8 +599,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: address.hash, from_address: address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -600,8 +615,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -629,8 +645,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: address.hash, from_address: address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -644,8 +661,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -679,8 +697,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: address_to_include.hash, from_address: address_to_include, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -694,8 +713,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -727,8 +747,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: address.hash, to_address: address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -742,8 +763,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -771,8 +793,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: address.hash, to_address: address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -786,8 +809,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -821,8 +845,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: address_to_include.hash, to_address: address_to_include, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -836,8 +861,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -871,8 +897,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: from_address.hash, from_address: from_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -891,8 +918,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: to_address.hash, to_address: to_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -920,8 +948,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: from_address.hash, from_address: from_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -938,8 +967,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -975,8 +1005,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: 51, - block_index: 51, from_address: from_address, from_address_hash: from_address.hash, to_address: to_address, @@ -1000,8 +1031,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: i + 1, - block_index: i + 1, from_address: from_address, from_address_hash: from_address.hash ) @@ -1019,8 +1051,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: i + 1, - block_index: i + 1, to_address: to_address, to_address_hash: to_address.hash ) @@ -1062,8 +1095,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: from_address.hash, from_address: from_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -1082,8 +1116,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do to_address_hash: to_address.hash, to_address: to_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -1111,8 +1146,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do from_address_hash: from_address.hash, from_address: from_address, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, @@ -1129,8 +1165,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, - index: i + 1, - block_index: i + 1 + block_number: transaction.block_number, + transaction_index: transaction.index, + index: i + 1 ) insert(:token_transfer, transaction: transaction, block_number: transaction.block_number, log_index: i) @@ -1155,8 +1192,9 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterControllerTest do insert(:internal_transaction, transaction: transaction, block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index, index: 1, - block_index: 1, value: i * 10 ** 18 ) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index 026cf31b7fc8..927bfb581590 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -879,8 +879,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) internal_transactions = @@ -896,8 +895,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs index c03ab7a344b2..f5de376a6d1d 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/csv_export_controller_test.exs @@ -308,7 +308,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do from_address: address, block_number: transaction_1.block_number, block_hash: transaction_1.block_hash, - block_index: 0, transaction_index: transaction_1.index ) @@ -318,7 +317,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do to_address: address, block_number: transaction_2.block_number, block_hash: transaction_2.block_hash, - block_index: 1, transaction_index: transaction_2.index ) @@ -329,7 +327,6 @@ defmodule BlockScoutWeb.Api.V2.CsvExportControllerTest do to_address: nil, block_number: transaction_3.block_number, block_hash: transaction_3.block_hash, - block_index: 2, transaction_index: transaction_3.index ) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs index d81ccd872305..8a2f38381c0f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -32,8 +32,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do transaction_index: 0, block_number: tx.block_number, block_hash: tx.block_hash, - index: 1, - block_index: 1 + index: 1 ) request = get(conn, "/api/v2/internal-transactions") @@ -52,8 +51,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do transaction_index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - index: 1, - block_index: 0 + index: 1 ) transaction_2 = insert(:transaction) |> with_block() @@ -65,8 +63,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do transaction_index: 0, block_number: transaction_2.block_number, block_hash: transaction_2.block_hash, - index: i, - block_index: i + index: i ) end @@ -94,7 +91,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do block_number: tx.block_number, block_hash: tx.block_hash, index: 0, - block_index: 0, type: :call ) @@ -106,7 +102,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do block_number: tx.block_number, block_hash: tx.block_hash, index: 1, - block_index: 1, type: :call ) @@ -118,7 +113,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do block_number: tx.block_number, block_hash: tx.block_hash, index: 2, - block_index: 2, type: :call ) @@ -148,7 +142,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do block_number: tx.block_number, block_hash: tx.block_hash, index: 0, - block_index: 0, type: :call ) @@ -160,7 +153,6 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do block_number: tx.block_number, block_hash: tx.block_hash, index: i, - block_index: i, type: :call ) end @@ -190,7 +182,8 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do assert Address.checksum(internal_transaction.to_address_hash) == json["to"]["hash"] assert to_string(internal_transaction.transaction_hash) == json["transaction_hash"] assert internal_transaction.block_number == json["block_number"] - assert internal_transaction.block_index == json["block_index"] + assert internal_transaction.transaction_index == json["transaction_index"] + assert internal_transaction.index == json["index"] end defp check_paginated_response(first_page_resp, second_page_resp, internal_transactions) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 429364eeaa76..10f27d171b2a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -606,8 +606,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) internal_transaction = @@ -616,8 +615,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: 1, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) transaction_1 = @@ -632,8 +630,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: index, block_number: transaction_1.block_number, transaction_index: transaction_1.index, - block_hash: transaction_1.block_hash, - block_index: index + block_hash: transaction_1.block_hash ) end) @@ -656,8 +653,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) internal_transactions = @@ -668,8 +664,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) @@ -1455,7 +1450,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 0, value: %Wei{value: Decimal.new(7)}, from_address_hash: internal_transaction_from.hash, from_address: internal_transaction_from, @@ -1520,7 +1514,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 0, value: %Wei{value: Decimal.new(7)}, from_address_hash: internal_transaction_from.hash, from_address: internal_transaction_from, @@ -1536,7 +1529,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 1, value: %Wei{value: Decimal.new(7)}, from_address_hash: internal_transaction_from_delegatecall.hash, from_address: internal_transaction_from_delegatecall, @@ -1551,7 +1543,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 2, value: %Wei{value: Decimal.new(7)}, from_address_hash: internal_transaction_from.hash, from_address: internal_transaction_from, @@ -2451,7 +2442,6 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do block_number: transaction.block_number, transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 1, type: :reward ) @@ -2593,8 +2583,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs index 2d59ad7de257..529cf301e882 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_internal_transaction_controller_test.exs @@ -45,8 +45,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) insert(:internal_transaction, @@ -54,8 +53,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: 1, transaction_index: transaction.index, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) path = transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, transaction.hash) @@ -96,8 +94,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) |> with_contract_creation(contract_address) @@ -126,8 +123,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: 0, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 0 + block_hash: transaction.block_hash ) second_page_indexes = @@ -138,8 +134,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) |> Enum.map(& &1.index) @@ -173,8 +168,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) @@ -204,8 +198,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionControllerTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) diff --git a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs index a2f00ee57341..f707126ca447 100644 --- a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs @@ -20,7 +20,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -44,10 +43,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do _, [[], block_number], _, - [[], block_hash], - _, - [[], block_index], - _, [[], transaction_index], _, [[], timestamp], @@ -81,8 +76,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do transaction_hash: transaction_hash, index: index, block_number: block_number, - block_index: block_index, - block_hash: block_hash, transaction_index: transaction_index, timestamp: timestamp, from_address_hash: from_address_hash, @@ -103,8 +96,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do assert result.transaction_hash == to_string(internal_transaction.transaction_hash) assert result.index == to_string(internal_transaction.index) assert result.block_number == to_string(internal_transaction.block_number) - assert result.block_index == to_string(internal_transaction.block_index) - assert result.block_hash == to_string(internal_transaction.block_hash) assert result.transaction_index == to_string(internal_transaction.transaction_index) assert result.timestamp assert result.from_address_hash == Address.checksum(internal_transaction.from_address_hash) @@ -143,7 +134,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index ) end) @@ -162,7 +152,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do to_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index ) end) @@ -182,7 +171,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do to_address: nil, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index ) end) @@ -217,7 +205,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index, error: "reverted", gas_used: nil, @@ -244,10 +231,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do _, [[], block_number], _, - [[], block_hash], - _, - [[], block_index], - _, [[], transaction_index], _, [[], timestamp], @@ -281,8 +264,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do transaction_hash: transaction_hash, index: index, block_number: block_number, - block_index: block_index, - block_hash: block_hash, transaction_index: transaction_index, timestamp: timestamp, from_address_hash: from_address_hash, @@ -303,8 +284,6 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do assert result.transaction_hash == to_string(internal_transaction.transaction_hash) assert result.index == to_string(internal_transaction.index) assert result.block_number == to_string(internal_transaction.block_number) - assert result.block_index == to_string(internal_transaction.block_index) - assert result.block_hash == to_string(internal_transaction.block_hash) assert result.transaction_index == to_string(internal_transaction.transaction_index) assert result.timestamp assert result.from_address_hash == Address.checksum(internal_transaction.from_address_hash) diff --git a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs index f8451c62ffaf..148b0014bcbc 100644 --- a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs @@ -38,7 +38,9 @@ defmodule BlockScoutWeb.AddressContractVerificationTest do created_contract_address: address, created_contract_code: bytecode, index: 0, - transaction: transaction + transaction: transaction, + transaction_index: transaction.index, + block_number: transaction.block_number ) session diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs index 9dac716e910c..756f53d5292e 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs @@ -91,12 +91,13 @@ defmodule BlockScoutWeb.ViewingAddressesTest do internal_transaction = insert( :internal_transaction_create, + transaction_index: transaction.index, index: 1, transaction: transaction, from_address: address, created_contract_address: contract, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) address_hash = AddressView.trimmed_hash(address.hash) @@ -115,6 +116,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do insert( :internal_transaction, + transaction_index: transaction.index, index: 1, transaction: transaction, from_address: lincoln, @@ -122,18 +124,19 @@ defmodule BlockScoutWeb.ViewingAddressesTest do created_contract_address: contract, type: :call, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) internal_transaction = insert( :internal_transaction_create, + transaction_index: transaction.index, index: 2, transaction: transaction, from_address: contract, created_contract_address: another_contract, block_hash: transaction.block_hash, - block_index: 2 + block_number: transaction.block_number ) contract_hash = AddressView.trimmed_hash(contract.hash) @@ -218,21 +221,19 @@ defmodule BlockScoutWeb.ViewingAddressesTest do insert(:internal_transaction, transaction: transaction, to_address: address, + transaction_index: transaction.index, index: 1, block_number: 7000, - transaction_index: 1, - block_hash: transaction.block_hash, - block_index: 1 + block_hash: transaction.block_hash ) insert(:internal_transaction, transaction: transaction, from_address: address, + transaction_index: transaction.index, index: 2, block_number: 8000, - transaction_index: 2, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) {:ok, %{internal_transaction_lincoln_to_address: internal_transaction_lincoln_to_address}} @@ -268,8 +269,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: addresses.lincoln, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: 2 + block_hash: transaction.block_hash ) Notifier.handle_event({:chain_event, :internal_transactions, :realtime, [internal_transaction]}) @@ -301,8 +301,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: addresses.lincoln, block_number: from_lincoln.block_number, transaction_index: from_lincoln.index, - block_hash: from_lincoln.block_hash, - block_index: 2 + block_hash: from_lincoln.block_hash ) session @@ -334,8 +333,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: addresses.lincoln, block_number: from_lincoln.block_number, transaction_index: from_lincoln.index, - block_hash: from_lincoln.block_hash, - block_index: 2 + block_hash: from_lincoln.block_hash ) session diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs index 914ef68edb16..78cc5467c728 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_blocks_test.exs @@ -61,9 +61,10 @@ defmodule BlockScoutWeb.ViewingBlocksTest do :internal_transaction_create |> insert( transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) |> with_contract_creation(contract_address) diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs index 9931bd43a5d9..b5dff64ff317 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs @@ -54,8 +54,9 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do insert(:internal_transaction, index: 0, transaction: transaction, + transaction_index: transaction.index, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) {:ok, @@ -105,7 +106,13 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do |> with_contract_creation(contract_address) :internal_transaction_create - |> insert(transaction: transaction, index: 0, block_hash: transaction.block_hash, block_index: 0) + |> insert( + transaction: transaction, + index: 0, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + transaction_index: transaction.index + ) |> with_contract_creation(contract_address) session diff --git a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs index 05c38b5fdcd8..cccf4d336a92 100644 --- a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs +++ b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/node_test.exs @@ -63,9 +63,10 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.NodeTest do internal_transaction = insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) query = """ @@ -107,9 +108,10 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.NodeTest do internal_transaction = build(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) query = """ diff --git a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs index 3b7fd38612e0..90b1d91a15d2 100644 --- a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs +++ b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs @@ -132,11 +132,12 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do internal_transaction_attributes = %{ transaction: transaction, + transaction_index: transaction.index, index: 0, from_address: address, call_type: :call, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number } internal_transaction = @@ -265,23 +266,26 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 2, block_hash: transaction.block_hash, - block_index: 2 + block_number: transaction.block_number ) insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 1, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) query = """ @@ -393,23 +397,26 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 2, block_hash: transaction.block_hash, - block_index: 2 + block_number: transaction.block_number ) insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 1, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) query = """ @@ -448,9 +455,10 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do for index <- 0..5 do insert(:internal_transaction_create, transaction: transaction, + transaction_index: transaction.index, index: index, block_hash: transaction.block_hash, - block_index: index + block_number: transaction.block_number ) end diff --git a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs index db7ddbe10bf3..faefd2355ed3 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs @@ -17,10 +17,11 @@ defmodule BlockScoutWeb.AddressViewTest do insert(:internal_transaction, index: 1, transaction: transaction, + transaction_index: transaction.index, to_address: nil, created_contract_address_hash: nil, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) assert "Contract Address Pending" == AddressView.address_partial_selector(internal_transaction, :to, nil) diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs index 99104f43f864..bba9010220aa 100644 --- a/apps/block_scout_web/test/test_helper.exs +++ b/apps/block_scout_web/test/test_helper.exs @@ -24,6 +24,8 @@ ExUnit.start() Mox.defmock(Explorer.Market.Source.TestSource, for: Explorer.Market.Source) +Explorer.TestHelper.run_necessary_background_migrations() + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonEdge, :manual) diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex index 25b4c289c1e4..fb47de445521 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth.ex @@ -76,9 +76,16 @@ defmodule EthereumJSONRPC.Geth do json_rpc_named_arguments_corrected_timeout ) do case {traces, transactions_params} do - {[%{} = first_trace | _], [%{block_hash: block_hash} | _]} -> + {[%{} = first_trace | _], [%{block_hash: block_hash, block_number: block_number} | _]} -> {:ok, - [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} + [ + %{ + first_trace: first_trace, + block_hash: block_hash, + block_number: block_number, + json_rpc_named_arguments: json_rpc_named_arguments + } + ]} _ -> {:error, :not_found} diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex index c3aeb291ed44..0b8f084fcc17 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/trace_replay_block_transactions.ex @@ -18,12 +18,19 @@ defmodule EthereumJSONRPC.TraceReplayBlockTransactions do {:ok, responses} -> case trace_replay_transaction_responses_to_first_trace_params(responses, id_to_params, traces_module) do {:ok, [first_trace]} -> - %{block_hash: block_hash} = + %{block_hash: block_hash, block_number: block_number} = transactions_params |> Enum.at(0) {:ok, - [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} + [ + %{ + first_trace: first_trace, + block_hash: block_hash, + block_number: block_number, + json_rpc_named_arguments: json_rpc_named_arguments + } + ]} {:error, error} -> Logger.error(inspect(error)) diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs index 0d7eb0ad2988..7bf8a5720f88 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs @@ -194,6 +194,7 @@ defmodule EthereumJSONRPC.NethermindTest do transaction_index: transaction_index }, block_hash: block_hash, + block_number: block_number, json_rpc_named_arguments: [ transport: EthereumJSONRPC.Mox, transport_options: [], diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index c1774caf0024..5ab9c8c4208f 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -175,10 +175,12 @@ for index_operation <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex, + Explorer.Migrator.HeavyDbIndexOperation.UpdateInternalTransactionsPrimaryKey, + Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockHashTransactionIndexIndexIndex ] do config :explorer, index_operation, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index cb76d1423417..e890e8e2d3ad 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -105,10 +105,12 @@ for migrator <- [ Explorer.Migrator.HeavyDbIndexOperation.CreateLogsDepositsWithdrawalsIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountDescPartialIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesTransactionsCountAscCoinBalanceDescHashPartialIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.CreateSmartContractAdditionalSourcesUniqueIndex, Explorer.Migrator.HeavyDbIndexOperation.DropTokenInstancesTokenIdIndex, - Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex + Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex, + Explorer.Migrator.HeavyDbIndexOperation.UpdateInternalTransactionsPrimaryKey, + Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockHashTransactionIndexIndexIndex ] do config :explorer, migrator, enabled: false end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index fc8d7b545edd..45d1a3fffce9 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -28,6 +28,7 @@ defmodule Explorer.Application do BlocksCount, GasUsageSum, PendingBlockOperationCount, + PendingTransactionOperationCount, TransactionsCount } @@ -85,6 +86,7 @@ defmodule Explorer.Application do GasPriceOracle, GasUsageSum, PendingBlockOperationCount, + PendingTransactionOperationCount, TransactionsCount, StateChanges, Transactions, @@ -313,7 +315,7 @@ defmodule Explorer.Application do :indexer ), configure_mode_dependent_process( - Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex, :indexer ), configure_mode_dependent_process( @@ -336,6 +338,14 @@ defmodule Explorer.Application do Explorer.Migrator.HeavyDbIndexOperation.CreateTokensNamePartialFtsIndex, :indexer ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.UpdateInternalTransactionsPrimaryKey, + :indexer + ), + configure_mode_dependent_process( + Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockHashTransactionIndexIndexIndex, + :indexer + ), Explorer.Migrator.RefetchContractCodes |> configure() |> configure_chain_type_dependent_process(:zksync), configure(Explorer.Chain.Fetcher.AddressesBlacklist), Explorer.Migrator.SwitchPendingOperations, diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 7d61f0081553..494a813e0091 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -43,6 +43,7 @@ defmodule Explorer.Chain do InternalTransaction, Log, PendingOperationsHelper, + PendingTransactionOperation, SmartContract, Token, TokenTransfer, @@ -64,6 +65,8 @@ defmodule Explorer.Chain do ContractsCount, NewContractsCount, NewVerifiedContractsCount, + PendingBlockOperationCount, + PendingTransactionOperationCount, VerifiedContractsCount } @@ -564,11 +567,21 @@ defmodule Explorer.Chain do |> Decimal.to_integer() query = - from( - block in Block, - join: pending_ops in assoc(block, :pending_operations), - where: block.consensus and block.number == ^min_block_number - ) + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> + from( + block in Block, + join: pending_ops in assoc(block, :pending_operations), + where: block.consensus and block.number == ^min_block_number + ) + + "transactions" -> + from( + pto in PendingTransactionOperation, + join: t in assoc(pto, :transaction), + where: t.block_consensus and t.block_number == ^min_block_number + ) + end if select_repo(options).exists?(query) do false @@ -582,14 +595,13 @@ defmodule Explorer.Chain do end defp check_indexing_internal_transactions_threshold do - min_blockchain_trace_block_number = - RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :trace_block_ranges)) - - %{max: max_saved_block_number} = BlockNumber.get_all() - pending_ops_entity = PendingOperationsHelper.actual_entity() - pbo_count = pending_ops_entity.blocks_count_in_range(min_blockchain_trace_block_number, max_saved_block_number) + pending_ops_count = + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> PendingBlockOperationCount.get() + "transactions" -> PendingTransactionOperationCount.get() + end - if pbo_count < + if pending_ops_count < Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction)[:indexing_finished_threshold] do true else @@ -1056,6 +1068,12 @@ defmodule Explorer.Chain do if indexer_running?() and internal_transactions_fetcher_running?() do %{max: max_saved_block_number} = BlockNumber.get_all() + pending_ops_count = + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> PendingBlockOperationCount.get() + "transactions" -> PendingTransactionOperationCount.get() + end + min_blockchain_trace_block_number = Application.get_env(:indexer, :trace_first_block) case max_saved_block_number do @@ -1066,12 +1084,7 @@ defmodule Explorer.Chain do full_blocks_range = max_saved_block_number - min_blockchain_trace_block_number - BlockNumberHelper.null_rounds_count() + 1 - pending_ops_entity = PendingOperationsHelper.actual_entity() - - pbo_count = - pending_ops_entity.blocks_count_in_range(min_blockchain_trace_block_number, max_saved_block_number) - - processed_int_transactions_for_blocks_count = max(0, full_blocks_range - pbo_count) + processed_int_transactions_for_blocks_count = max(0, full_blocks_range - pending_ops_count) ratio = get_ratio(processed_int_transactions_for_blocks_count, full_blocks_range) diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 3505353eadb5..9aa87f35a538 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -854,7 +854,8 @@ defmodule Explorer.Chain.Address do order_by: [ asc_nulls_first: it.error, desc: it.block_number, - desc: it.block_index + desc: it.transaction_index, + desc: it.index ], limit: 1 ) diff --git a/apps/explorer/lib/explorer/chain/address/counters.ex b/apps/explorer/lib/explorer/chain/address/counters.ex index fd4679bbae52..1105ae2c815f 100644 --- a/apps/explorer/lib/explorer/chain/address/counters.ex +++ b/apps/explorer/lib/explorer/chain/address/counters.ex @@ -227,7 +227,7 @@ defmodule Explorer.Chain.Address.Counters do defp address_hash_to_internal_transactions_limited_count_query(address_hash) do query_to_address_hash_wrapped = InternalTransaction - |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.where_nonpending_operation() |> InternalTransaction.where_address_fields_match(address_hash, :to) |> InternalTransaction.where_is_different_from_parent_transaction() |> limit(@counters_limit) @@ -235,7 +235,7 @@ defmodule Explorer.Chain.Address.Counters do query_from_address_hash_wrapped = InternalTransaction - |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.where_nonpending_operation() |> InternalTransaction.where_address_fields_match(address_hash, :from_address_hash) |> InternalTransaction.where_is_different_from_parent_transaction() |> limit(@counters_limit) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 68eab61ec54d..34f4b603b2a1 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -190,7 +190,7 @@ defmodule Explorer.Chain.Block.Schema do has_many(:transactions, Transaction, references: :hash) has_many(:transaction_forks, Transaction.Fork, foreign_key: :uncle_hash, references: :hash) - has_many(:internal_transactions, InternalTransaction, foreign_key: :block_hash, references: :hash) + has_many(:internal_transactions, InternalTransaction, foreign_key: :block_number, references: :number) has_many(:rewards, Reward, foreign_key: :block_hash, references: :hash) diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 79ec93a4faea..7c43f7573c1c 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -59,7 +59,8 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :heavy_indexes_drop_token_instances_token_id_index_finished, key: :fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished, key: :heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished, - key: :heavy_indexes_create_tokens_name_partial_fts_index_finished + key: :heavy_indexes_create_tokens_name_partial_fts_index_finished, + key: :heavy_indexes_update_internal_transactions_primary_key_finished @dialyzer :no_match @@ -103,7 +104,8 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do DropTokenTransfersTokenContractAddressHashTransactionHashIndex, DropTransactionsCreatedContractAddressHashWithPendingIndex, DropTransactionsFromAddressHashWithPendingIndex, - DropTransactionsToAddressHashWithPendingIndex + DropTransactionsToAddressHashWithPendingIndex, + UpdateInternalTransactionsPrimaryKey } defp handle_fallback(:transactions_denormalization_finished) do @@ -369,6 +371,13 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end + defp handle_fallback(:heavy_indexes_update_internal_transactions_primary_key_finished) do + set_and_return_migration_status( + UpdateInternalTransactionsPrimaryKey, + &set_heavy_indexes_update_internal_transactions_primary_key_finished/1 + ) + end + defp handle_fallback(:sanitize_verified_addresses_finished) do {:return, false} end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex index 128ce81af7d6..c910475b0987 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/helper.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/helper.ex @@ -264,4 +264,21 @@ defmodule Explorer.Chain.Cache.Counters.Helper do if is_nil(count), do: 0, else: max(count, 0) end + + @doc """ + Returns the estimated count of pending transaction operations. + + This function retrieves the estimated count from the "pending_transaction_operations" cache. + If the count is `nil`, it returns `0`. Otherwise, it ensures the count is non-negative + by returning the maximum of the count and `0`. + + ## Returns + - `integer`: The estimated count of pending transaction operations, or `0` if the count is `nil`. + """ + @spec estimated_pending_transaction_operations_count() :: non_neg_integer() + def estimated_pending_transaction_operations_count do + count = estimated_count_from("pending_transaction_operations") + + if is_nil(count), do: 0, else: max(count, 0) + end end diff --git a/apps/explorer/lib/explorer/chain/cache/counters/pending_block_operation_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/pending_block_operation_count.ex index db043acfd741..9a86c13ca6da 100644 --- a/apps/explorer/lib/explorer/chain/cache/counters/pending_block_operation_count.ex +++ b/apps/explorer/lib/explorer/chain/cache/counters/pending_block_operation_count.ex @@ -13,10 +13,11 @@ defmodule Explorer.Chain.Cache.Counters.PendingBlockOperationCount do require Logger + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.Chain.Cache.BlockNumber alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.Cache.Counters.LastFetchedCounter alias Explorer.Chain.PendingBlockOperation - alias Explorer.Repo @cache_key "pending_block_operations_count" @@ -46,7 +47,10 @@ defmodule Explorer.Chain.Cache.Counters.PendingBlockOperationCount do {:ok, task} = Task.start_link(fn -> try do - result = Repo.aggregate(PendingBlockOperation, :count, timeout: :infinity) + min_blockchain_trace_block_number = + RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :trace_block_ranges)) + + result = PendingBlockOperation.blocks_count_in_range(min_blockchain_trace_block_number, BlockNumber.get_max()) params = %{ counter_type: @cache_key, @@ -55,7 +59,10 @@ defmodule Explorer.Chain.Cache.Counters.PendingBlockOperationCount do LastFetchedCounter.upsert(params) - set_count(%ConCache.Item{ttl: CacheCountersHelper.ttl(__MODULE__, "CACHE_PBO_COUNT_PERIOD"), value: result}) + set_count(%ConCache.Item{ + ttl: CacheCountersHelper.ttl(__MODULE__, "CACHE_PENDING_OPERATIONS_COUNT_PERIOD"), + value: result + }) rescue e -> Logger.debug([ diff --git a/apps/explorer/lib/explorer/chain/cache/counters/pending_transaction_operation_count.ex b/apps/explorer/lib/explorer/chain/cache/counters/pending_transaction_operation_count.ex new file mode 100644 index 000000000000..806eb8a19e1a --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/counters/pending_transaction_operation_count.ex @@ -0,0 +1,90 @@ +defmodule Explorer.Chain.Cache.Counters.PendingTransactionOperationCount do + @moduledoc """ + Cache for estimated `pending_transaction_operations` count. + """ + + use Explorer.Chain.MapCache, + name: :pending_transaction_operations_count, + key: :count, + key: :async_task, + global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl], + ttl_check_interval: :timer.seconds(1), + callback: &async_task_on_deletion(&1) + + require Logger + + alias EthereumJSONRPC.Utility.RangesHelper + alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper + alias Explorer.Chain.Cache.Counters.LastFetchedCounter + alias Explorer.Chain.PendingTransactionOperation + + @cache_key "pending_transaction_operations_count" + + @doc """ + Gets count of `t:Explorer.Chain.PendingTransactionOperation.t/0`. + + """ + @spec get() :: non_neg_integer() + def get do + cached_value_from_ets = __MODULE__.get_count() + + CacheCountersHelper.evaluate_count( + @cache_key, + cached_value_from_ets, + :estimated_pending_transaction_operations_count + ) + end + + defp handle_fallback(:count) do + # This will get the task PID if one exists, check if it's running and launch + # a new task if task doesn't exist or it's not running. + # See next `handle_fallback` definition + safe_get_async_task() + + {:return, nil} + end + + defp handle_fallback(:async_task) do + # If this gets called it means an async task was requested, but none exists + # so a new one needs to be launched + {:ok, task} = + Task.start_link(fn -> + try do + min_blockchain_trace_block_number = + RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :trace_block_ranges)) + + result = + PendingTransactionOperation.blocks_count_in_range(min_blockchain_trace_block_number, BlockNumber.get_max()) + + params = %{ + counter_type: @cache_key, + value: result + } + + LastFetchedCounter.upsert(params) + + set_count(%ConCache.Item{ + ttl: CacheCountersHelper.ttl(__MODULE__, "CACHE_PENDING_OPERATIONS_COUNT_PERIOD"), + value: result + }) + rescue + e -> + Logger.debug([ + "Couldn't update pending_transaction_operations count: ", + Exception.format(:error, e, __STACKTRACE__) + ]) + end + + set_async_task(nil) + end) + + {:update, task} + end + + # By setting this as a `callback` an async task will be started each time the + # `count` expires (unless there is one already running) + defp async_task_on_deletion({:delete, _, :count}), do: safe_get_async_task() + + defp async_task_on_deletion(_data), do: nil +end diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index c9eb56fdc4dc..78b0ee15f0e7 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -26,7 +26,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias Explorer.Migrator.DeleteZeroValueInternalTransactions alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo - alias Explorer.Utility.MissingBlockRange + alias Explorer.Utility.{InternalTransactionHelper, MissingBlockRange} import Ecto.Query @@ -79,7 +79,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end) |> Multi.run(:acquire_pending_internal_transactions, fn repo, %{acquire_blocks: block_hashes} -> Instrumenter.block_import_stage_runner( - fn -> acquire_pending_internal_transactions(repo, block_hashes) end, + fn -> acquire_pending_internal_transactions(repo, block_hashes, changes_list) end, :block_pending, :internal_transactions, :acquire_pending_internal_transactions @@ -241,11 +241,18 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do ordered_changes_list = Enum.sort_by(valid_internal_transactions, &{&1.transaction_hash, &1.index}) + conflict_target = + if InternalTransactionHelper.primary_key_updated?() do + [:block_number, :transaction_index, :index] + else + [:block_hash, :block_index] + end + {:ok, internal_transactions} = Import.insert_changes_list( repo, ordered_changes_list, - conflict_target: [:block_hash, :block_index], + conflict_target: conflict_target, for: InternalTransaction, on_conflict: on_conflict, returning: true, @@ -257,57 +264,107 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end defp default_on_conflict do - from( - internal_transaction in InternalTransaction, - update: [ - set: [ - block_number: fragment("EXCLUDED.block_number"), - call_type: fragment("EXCLUDED.call_type"), - created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), - created_contract_code: fragment("EXCLUDED.created_contract_code"), - error: fragment("EXCLUDED.error"), - from_address_hash: fragment("EXCLUDED.from_address_hash"), - gas: fragment("EXCLUDED.gas"), - gas_used: fragment("EXCLUDED.gas_used"), - index: fragment("EXCLUDED.index"), - init: fragment("EXCLUDED.init"), - input: fragment("EXCLUDED.input"), - output: fragment("EXCLUDED.output"), - to_address_hash: fragment("EXCLUDED.to_address_hash"), - trace_address: fragment("EXCLUDED.trace_address"), - transaction_hash: fragment("EXCLUDED.transaction_hash"), - transaction_index: fragment("EXCLUDED.transaction_index"), - type: fragment("EXCLUDED.type"), - value: fragment("EXCLUDED.value"), - inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", internal_transaction.inserted_at), - updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", internal_transaction.updated_at) - # Don't update `block_hash` as it is used for the conflict target - # Don't update `block_index` as it is used for the conflict target - ] - ], - # `IS DISTINCT FROM` is used because it allows `NULL` to be equal to itself - where: - fragment( - "(EXCLUDED.transaction_hash, EXCLUDED.index, EXCLUDED.call_type, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.trace_address, EXCLUDED.transaction_index, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - internal_transaction.transaction_hash, - internal_transaction.index, - internal_transaction.call_type, - internal_transaction.created_contract_address_hash, - internal_transaction.created_contract_code, - internal_transaction.error, - internal_transaction.from_address_hash, - internal_transaction.gas, - internal_transaction.gas_used, - internal_transaction.init, - internal_transaction.input, - internal_transaction.output, - internal_transaction.to_address_hash, - internal_transaction.trace_address, - internal_transaction.transaction_index, - internal_transaction.type, - internal_transaction.value - ) - ) + if InternalTransactionHelper.primary_key_updated?() do + from( + internal_transaction in InternalTransaction, + update: [ + set: [ + call_type: fragment("EXCLUDED.call_type"), + created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), + created_contract_code: fragment("EXCLUDED.created_contract_code"), + error: fragment("EXCLUDED.error"), + from_address_hash: fragment("EXCLUDED.from_address_hash"), + gas: fragment("EXCLUDED.gas"), + gas_used: fragment("EXCLUDED.gas_used"), + init: fragment("EXCLUDED.init"), + input: fragment("EXCLUDED.input"), + output: fragment("EXCLUDED.output"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + trace_address: fragment("EXCLUDED.trace_address"), + transaction_hash: fragment("EXCLUDED.transaction_hash"), + type: fragment("EXCLUDED.type"), + value: fragment("EXCLUDED.value"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", internal_transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", internal_transaction.updated_at) + # Don't update `block_number` as it is used for the conflict target + # Don't update `transaction_index` as it is used for the conflict target + # Don't update `index` as it is used for the conflict target + ] + ], + # `IS DISTINCT FROM` is used because it allows `NULL` to be equal to itself + where: + fragment( + "(EXCLUDED.transaction_hash, EXCLUDED.call_type, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.trace_address, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + internal_transaction.transaction_hash, + internal_transaction.call_type, + internal_transaction.created_contract_address_hash, + internal_transaction.created_contract_code, + internal_transaction.error, + internal_transaction.from_address_hash, + internal_transaction.gas, + internal_transaction.gas_used, + internal_transaction.init, + internal_transaction.input, + internal_transaction.output, + internal_transaction.to_address_hash, + internal_transaction.trace_address, + internal_transaction.type, + internal_transaction.value + ) + ) + else + from( + internal_transaction in InternalTransaction, + update: [ + set: [ + block_number: fragment("EXCLUDED.block_number"), + call_type: fragment("EXCLUDED.call_type"), + created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), + created_contract_code: fragment("EXCLUDED.created_contract_code"), + error: fragment("EXCLUDED.error"), + from_address_hash: fragment("EXCLUDED.from_address_hash"), + gas: fragment("EXCLUDED.gas"), + gas_used: fragment("EXCLUDED.gas_used"), + index: fragment("EXCLUDED.index"), + init: fragment("EXCLUDED.init"), + input: fragment("EXCLUDED.input"), + output: fragment("EXCLUDED.output"), + to_address_hash: fragment("EXCLUDED.to_address_hash"), + trace_address: fragment("EXCLUDED.trace_address"), + transaction_hash: fragment("EXCLUDED.transaction_hash"), + transaction_index: fragment("EXCLUDED.transaction_index"), + type: fragment("EXCLUDED.type"), + value: fragment("EXCLUDED.value"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", internal_transaction.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", internal_transaction.updated_at) + # Don't update `block_hash` as it is used for the conflict target + # Don't update `block_index` as it is used for the conflict target + ] + ], + # `IS DISTINCT FROM` is used because it allows `NULL` to be equal to itself + where: + fragment( + "(EXCLUDED.transaction_hash, EXCLUDED.index, EXCLUDED.call_type, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.trace_address, EXCLUDED.transaction_index, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + internal_transaction.transaction_hash, + internal_transaction.index, + internal_transaction.call_type, + internal_transaction.created_contract_address_hash, + internal_transaction.created_contract_code, + internal_transaction.error, + internal_transaction.from_address_hash, + internal_transaction.gas, + internal_transaction.gas_used, + internal_transaction.init, + internal_transaction.input, + internal_transaction.output, + internal_transaction.to_address_hash, + internal_transaction.trace_address, + internal_transaction.transaction_index, + internal_transaction.type, + internal_transaction.value + ) + ) + end end defp acquire_blocks(repo, changes_list) do @@ -329,7 +386,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do {:ok, repo.all(query)} end - defp acquire_pending_internal_transactions(repo, block_hashes) do + defp acquire_pending_internal_transactions(repo, block_hashes, changes_list) do case PendingOperationsHelper.pending_operations_type() do "blocks" -> query = @@ -342,11 +399,16 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do {:ok, {:block_hashes, repo.all(query)}} "transactions" -> + transaction_hashes = + changes_list + |> Enum.map(&Map.get(&1, :transaction_hash)) + |> Enum.uniq() + |> Enum.reject(&is_nil/1) + query = from( pending_ops in PendingTransactionOperation, - join: transaction in assoc(pending_ops, :transaction), - where: transaction.block_hash in ^block_hashes, + where: pending_ops.transaction_hash in ^transaction_hashes, select: pending_ops.transaction_hash, # Enforce PendingTransactionOperation ShareLocks order (see docs: sharelocks.md) order_by: [asc: pending_ops.transaction_hash], @@ -455,20 +517,29 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do defp compose_entry(entries, blocks_map, block_number) do if Map.has_key?(blocks_map, block_number) do - block_hash = Map.fetch!(blocks_map, block_number) + if InternalTransactionHelper.primary_key_updated?() do + Enum.map(entries, fn entry -> + entry + |> sanitize_error() + |> shift_created_contract_address_hash() + end) + else + block_hash = Map.fetch!(blocks_map, block_number) - entries - |> Enum.sort_by( - &{(Map.has_key?(&1, :transaction_index) && &1.transaction_index) || &1.transaction_hash, &1.index} - ) - |> Enum.with_index() - |> Enum.map(fn {entry, index} -> - entry - |> Map.put(:block_hash, block_hash) - |> Map.put(:block_index, index) - |> sanitize_error() - |> shift_created_contract_address_hash() - end) + entries + |> Enum.sort_by( + &{(Map.has_key?(&1, :transaction_index) && &1.transaction_index) || &1.transaction_hash, &1.index} + ) + |> Enum.with_index() + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + |> Enum.map(fn {entry, index} -> + entry + |> Map.put(:block_hash, block_hash) + |> Map.put(:block_index, index) + |> sanitize_error() + |> shift_created_contract_address_hash() + end) + end else [] end @@ -560,7 +631,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end) |> Enum.map(fn trace -> %{ - block_hash: Map.get(trace, :block_hash), block_number: Map.get(trace, :block_number), gas_used: Map.get(trace, :gas_used), transaction_hash: Map.get(trace, :transaction_hash), @@ -569,7 +639,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do status: if(is_nil(Map.get(trace, :error)), do: :ok, else: :error) } end) - |> Enum.filter(fn transaction_hash -> transaction_hash != nil end) transaction_hashes = valid_internal_transactions @@ -777,7 +846,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do default_set |> put_status_in_update_set(first_trace, transaction_from_db) |> put_error_in_update_set(first_trace, transaction_from_db, transaction_receipt_from_node) - |> Keyword.put_new(:block_hash, first_trace.block_hash) |> Keyword.put_new(:block_number, first_trace.block_number) |> Keyword.put_new(:index, transaction_receipt_from_node && transaction_receipt_from_node.transaction_index) |> Keyword.put_new( diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 3bb80b4df3c8..29e448639c2f 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -3,17 +3,25 @@ defmodule Explorer.Chain.InternalTransaction do use Explorer.Schema - alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.{Chain, PagingOptions} - alias Explorer.Chain.{Address, Block, Data, Hash, PendingBlockOperation, Transaction, Wei} + + alias Explorer.Chain.{ + Address, + Block, + Data, + Hash, + PendingOperationsHelper, + Transaction, + Wei + } + alias Explorer.Chain.Block.Reader.General, as: BlockReaderGeneral alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.Cache.Counters.Helper, as: CacheCountersHelper alias Explorer.Chain.DenormalizationHelper alias Explorer.Chain.InternalTransaction.{CallType, Type} alias Explorer.Migrator.DeleteZeroValueInternalTransactions + alias Explorer.Utility.InternalTransactionHelper - import EthereumJSONRPC, only: [fetch_block_internal_transactions: 2] import Explorer.Chain.SmartContract.Proxy.Models.Implementation, only: [proxy_implementations_association: 0] @typep paging_options :: {:paging_options, PagingOptions.t()} @@ -45,7 +53,6 @@ defmodule Explorer.Chain.InternalTransaction do * `block` - block in which this internal transaction occurred * `block_hash` - foreign key for `block` * `block_index` - the index of this internal transaction inside the `block` - * `pending_block` - `nil` if `block` has all its internal transactions fetched """ @primary_key false typed_schema "internal_transactions" do @@ -63,9 +70,10 @@ defmodule Explorer.Chain.InternalTransaction do # todo: consider using enum field(:type, Type, null: false) field(:value, Wei, null: false) - field(:block_number, :integer) - field(:transaction_index, :integer) - field(:block_index, :integer, null: false) + field(:block_number, :integer, primary_key: true) + field(:transaction_index, :integer, primary_key: true, null: false) + # TODO: remove field after update PK migration is completed + field(:block_index, :integer) timestamps() @@ -96,25 +104,18 @@ defmodule Explorer.Chain.InternalTransaction do belongs_to(:transaction, Transaction, foreign_key: :transaction_hash, - primary_key: true, references: :hash, type: Hash.Full, null: false ) + # TODO: remove field after update PK migration is completed belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full, null: false ) - - belongs_to(:pending_block, PendingBlockOperation, - foreign_key: :block_hash, - define_field: false, - references: :block_hash, - type: Hash.Full - ) end @doc """ @@ -136,11 +137,11 @@ defmodule Explorer.Chain.InternalTransaction do ...> init: "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029", ...> trace_address: [], ...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", + ...> transaction_index: 0, ...> type: "create", ...> value: 0, ...> block_number: 35, - ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0 + ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd" ...> } ...> ) iex> changeset.valid? @@ -181,7 +182,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> value: 0, ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0 ...> } iex> ) @@ -200,7 +200,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> %{ ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0, ...> transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", ...> index: 0, @@ -226,7 +225,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> %{ ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0, ...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b", ...> index: 0, @@ -253,7 +251,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> %{ ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0, ...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b", ...> index: 0, @@ -280,7 +277,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> %{ ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0, ...> transaction_hash: "0xcd7c15dbbc797722bef6e1d551edfd644fc7f4fb2ccd6a7947b2d1ade9ed140b", ...> index: 0, @@ -323,7 +319,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> value: 0, ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0 ...> } iex> ) @@ -345,7 +340,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> value: 0, ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0 ...> } ...> ) @@ -372,7 +366,6 @@ defmodule Explorer.Chain.InternalTransaction do ...> value: 0, ...> block_number: 35, ...> block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - ...> block_index: 0, ...> transaction_index: 0 ...> } ...> ) @@ -381,22 +374,30 @@ defmodule Explorer.Chain.InternalTransaction do """ def changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do + base_attributes = ~w(transaction_index index type)a + + all_attributes = + if InternalTransactionHelper.primary_key_updated?() do + [:block_number | base_attributes] + else + [:block_hash, :block_index | base_attributes] + end + internal_transaction - |> cast(attrs, ~w(type)a) - |> validate_required(~w(type)a) - |> validate_block_required(attrs) + |> cast(attrs, all_attributes) + |> validate_required(all_attributes) |> type_changeset(attrs) end @doc """ Accepts changes without `:type` but with `:block_number`, if `:type` is defined - works like `changeset`, except allowing `:block_hash` and `:block_index` to be undefined. + works like `changeset`, except allowing `:block_hash` to be undefined. This is used because the `internal_transactions` runner can derive such values on its own or use empty types to know that a block has no internal transactions. """ def blockless_changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do - changeset = cast(internal_transaction, attrs, ~w(type block_number)a) + changeset = cast(internal_transaction, attrs, ~w(type block_number transaction_index index)a) if validate_required(changeset, ~w(type)a).valid? do type_changeset(changeset, attrs) @@ -405,21 +406,14 @@ defmodule Explorer.Chain.InternalTransaction do end end - defp validate_block_required(changeset, attrs) do - changeset - |> cast(attrs, ~w(block_hash block_index)a) - |> validate_required(~w(block_hash block_index)a) - |> foreign_key_constraint(:block_hash) - end - defp type_changeset(changeset, attrs) do type = get_field(changeset, :type) type_changeset(changeset, attrs, type) end - @call_optional_fields ~w(error gas_used output block_number transaction_index)a - @call_required_fields ~w(call_type from_address_hash gas index input to_address_hash trace_address transaction_hash value)a + @call_optional_fields ~w(error gas_used output block_number)a + @call_required_fields ~w(call_type from_address_hash gas input to_address_hash trace_address transaction_hash value)a @call_allowed_fields @call_optional_fields ++ @call_required_fields defp type_changeset(changeset, attrs, :call) do @@ -431,11 +425,10 @@ defmodule Explorer.Chain.InternalTransaction do |> check_constraint(:call_type, message: ~S|can't be blank when type is 'call'|, name: :call_has_call_type) |> check_constraint(:input, message: ~S|can't be blank when type is 'call'|, name: :call_has_input) |> foreign_key_constraint(:transaction_hash) - |> unique_constraint(:index) end - @create_optional_fields ~w(error created_contract_code created_contract_address_hash gas_used block_number transaction_index)a - @create_required_fields ~w(from_address_hash gas index init trace_address transaction_hash value)a + @create_optional_fields ~w(error created_contract_code created_contract_address_hash gas_used block_number)a + @create_required_fields ~w(from_address_hash gas init trace_address transaction_hash value)a @create_allowed_fields @create_optional_fields ++ @create_required_fields defp type_changeset(changeset, attrs, type) when type in [:create, :create2] do @@ -446,29 +439,26 @@ defmodule Explorer.Chain.InternalTransaction do |> validate_create_error_or_result() |> check_constraint(:init, message: ~S|can't be blank when type is 'create'|, name: :create_has_init) |> foreign_key_constraint(:transaction_hash) - |> unique_constraint(:index) end - @selfdestruct_optional_fields ~w(block_number transaction_index)a - @selfdestruct_required_fields ~w(from_address_hash index to_address_hash trace_address transaction_hash type value)a + @selfdestruct_optional_fields ~w(block_number)a + @selfdestruct_required_fields ~w(from_address_hash to_address_hash trace_address transaction_hash type value)a @selfdestruct_allowed_fields @selfdestruct_optional_fields ++ @selfdestruct_required_fields defp type_changeset(changeset, attrs, :selfdestruct) do changeset |> cast(attrs, @selfdestruct_allowed_fields) |> validate_required(@selfdestruct_required_fields) - |> unique_constraint(:index) end @stop_optional_fields ~w(from_address_hash gas gas_used error)a - @stop_required_fields ~w(block_number transaction_hash transaction_index index type value trace_address)a + @stop_required_fields ~w(block_number transaction_hash type value trace_address)a @stop_allowed_fields @stop_optional_fields ++ @stop_required_fields defp type_changeset(changeset, attrs, :stop) do changeset |> cast(attrs, @stop_allowed_fields) |> validate_required(@stop_required_fields) - |> unique_constraint(:index) end defp type_changeset(changeset, _, nil), do: changeset @@ -589,30 +579,28 @@ defmodule Explorer.Chain.InternalTransaction do end @doc """ - Filters out internal_transactions of blocks that are flagged as needing fetching + Filters out internal_transactions of blocks or transactions that are flagged as needing fetching of internal_transactions """ - def where_nonpending_block(query \\ nil) do - estimated_pbo_count = CacheCountersHelper.estimated_count_from("pending_block_operations") || 0 - - # NOT EXISTS query practically performs better on smaller tables - # while the indexed LEFT JOIN IS NULL query is better for larger tables - # The estimated count is used as a heuristic to decide which query to use - if estimated_pbo_count < 1_000 do - (query || __MODULE__) - |> where( - [it], - fragment("NOT EXISTS (SELECT 1 FROM pending_block_operations WHERE block_hash = ?)", it.block_hash) - ) - else - (query || __MODULE__) - |> where( - [it], - fragment( - "(SELECT block_hash FROM pending_block_operations WHERE block_hash = ? LIMIT 1) IS NULL", - it.block_hash + @spec where_nonpending_operation(Ecto.Query.t() | module()) :: Ecto.Query.t() + def where_nonpending_operation(query \\ __MODULE__) do + case PendingOperationsHelper.pending_operations_type() do + "blocks" -> + where( + query, + [it], + fragment("(SELECT 1 FROM pending_block_operations WHERE block_number = ? LIMIT 1) IS NULL", it.block_number) + ) + + "transactions" -> + where( + query, + [it], + fragment( + "(SELECT 1 FROM pending_transaction_operations WHERE transaction_hash = ? LIMIT 1) IS NULL", + it.transaction_hash + ) ) - ) end end @@ -641,7 +629,7 @@ defmodule Explorer.Chain.InternalTransaction do __MODULE__ |> for_parent_transaction(hash) |> Chain.join_associations(necessity_by_association) - |> where_nonpending_block() + |> where_nonpending_operation() |> page_internal_transaction(paging_options) |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.index) @@ -662,7 +650,7 @@ defmodule Explorer.Chain.InternalTransaction do |> for_parent_transaction(hash) |> Chain.join_associations(necessity_by_association) |> where_is_different_from_parent_transaction() - |> where_nonpending_block() + |> where_nonpending_operation() |> page_internal_transaction(paging_options) |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.index) @@ -670,23 +658,23 @@ defmodule Explorer.Chain.InternalTransaction do |> Chain.select_repo(options).all() end - @spec block_to_internal_transactions(Hash.Full.t(), [ + @spec block_to_internal_transactions(non_neg_integer(), [ Chain.paging_options() | Chain.necessity_by_association_option() | Chain.api?() ]) :: [ __MODULE__.t() ] - def block_to_internal_transactions(hash, options \\ []) when is_list(options) do + def block_to_internal_transactions(block_number, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, @default_paging_options) type_filter = Keyword.get(options, :type) call_type_filter = Keyword.get(options, :call_type) __MODULE__ - |> where([internal_transaction], internal_transaction.block_hash == ^hash) + |> where([internal_transaction], internal_transaction.block_number == ^block_number) |> Chain.join_associations(necessity_by_association) |> where_is_different_from_parent_transaction() - |> where_nonpending_block() + |> where_nonpending_operation() |> page_block_internal_transaction(paging_options) |> filter_by_type(type_filter, call_type_filter) |> filter_by_call_type(call_type_filter) @@ -798,7 +786,7 @@ defmodule Explorer.Chain.InternalTransaction do if direction == nil || direction == "" do query_to_address_hash_wrapped = __MODULE__ - |> where_nonpending_block() + |> where_nonpending_operation() |> where_address_fields_match(hash, :to) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) |> where_is_different_from_parent_transaction() @@ -807,7 +795,7 @@ defmodule Explorer.Chain.InternalTransaction do query_from_address_hash_wrapped = __MODULE__ - |> where_nonpending_block() + |> where_nonpending_operation() |> where_address_fields_match(hash, :from_address_hash) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) |> where_is_different_from_parent_transaction() @@ -824,7 +812,7 @@ defmodule Explorer.Chain.InternalTransaction do |> deduplicate_and_trim_internal_transactions(paging_options) else __MODULE__ - |> where_nonpending_block() + |> where_nonpending_operation() |> where_address_fields_match(hash, direction) |> BlockReaderGeneral.where_block_number_in_period(from_block, to_block) |> where_is_different_from_parent_transaction() @@ -964,8 +952,7 @@ defmodule Explorer.Chain.InternalTransaction do from( child in query, inner_join: transaction in assoc(child, :transaction), - where: transaction.hash == ^hash, - where: child.block_hash == transaction.block_hash + where: transaction.hash == ^hash ) end @@ -1013,11 +1000,10 @@ defmodule Explorer.Chain.InternalTransaction do ]) __MODULE__ - |> where_nonpending_block() + |> where_nonpending_operation() |> maybe_filter_origin_transaction(exclude_zero_index_internal_transaction) |> page_internal_transaction(paging_options, %{index_internal_transaction_desc_order: true}) |> where_internal_transactions_by_transaction_hash(Keyword.get(options, :transaction_hash)) - |> where_consensus_transactions() |> order_by([internal_transaction], desc: internal_transaction.block_number, desc: internal_transaction.transaction_index, @@ -1029,6 +1015,11 @@ defmodule Explorer.Chain.InternalTransaction do end end + defp page_block_internal_transaction(query, %PagingOptions{key: %{block_index: block_index}}) do + query + |> where([internal_transaction], internal_transaction.block_index > ^block_index) + end + defp page_block_internal_transaction(query, %PagingOptions{key: %{transaction_index: transaction_index, index: index}}) do query |> where( @@ -1040,23 +1031,15 @@ defmodule Explorer.Chain.InternalTransaction do defp page_block_internal_transaction(query, _), do: query - def internal_transaction_to_block_paging_options(%__MODULE__{transaction_index: transaction_index, index: index}) do - %{"transaction_index" => transaction_index, "index" => index} - end - - defp where_consensus_transactions(query) do - if DenormalizationHelper.transactions_denormalization_finished?() do - query - |> join(:inner, [internal_transaction], transaction in assoc(internal_transaction, :transaction)) - # todo: this additional check causes performance issues at /api/v2/internal-transactions endpoint - # In the future we plan to extract reorg data into separate tables, so the main tables will no longer contain - # reorged data, and this check will no longer be necessary.. - # |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) - |> where([_internal_transaction, transaction], transaction.block_consensus == true) + def internal_transaction_to_block_paging_options(%__MODULE__{ + transaction_index: transaction_index, + index: index, + block_index: block_index + }) do + if InternalTransactionHelper.primary_key_updated?() do + %{"transaction_index" => transaction_index, "index" => index} else - query - |> join(:inner, [internal_transaction], block in assoc(internal_transaction, :block)) - |> where([_internal_transaction, block], block.consensus == true) + %{"block_index" => block_index} end end @@ -1118,8 +1101,8 @@ defmodule Explorer.Chain.InternalTransaction do @spec fetch_first_trace(list(map()), keyword()) :: {:ok, [map()]} | {:error, term()} | :ignore def fetch_first_trace(transactions_params, json_rpc_named_arguments) do case EthereumJSONRPC.fetch_first_trace(transactions_params, json_rpc_named_arguments) do - {:ok, [%{first_trace: first_trace, block_hash: block_hash, json_rpc_named_arguments: json_rpc_named_arguments}]} -> - format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) + {:ok, [%{first_trace: first_trace, block_hash: block_hash, block_number: block_number}]} -> + format_transaction_first_trace(first_trace, block_hash, block_number) {:error, error} -> {:error, error} @@ -1129,7 +1112,7 @@ defmodule Explorer.Chain.InternalTransaction do end end - defp format_transaction_first_trace(first_trace, block_hash, json_rpc_named_arguments) do + defp format_transaction_first_trace(first_trace, block_hash, block_number) do {:ok, to_address_hash} = if Map.has_key?(first_trace, :to_address_hash) do Chain.string_to_address_hash(first_trace.to_address_hash) @@ -1185,21 +1168,13 @@ defmodule Explorer.Chain.InternalTransaction do {:ok, nil} end - block_index = - get_block_index(%{ - transaction_index: first_trace.transaction_index, - transaction_hash: first_trace.transaction_hash, - block_number: first_trace.block_number, - json_rpc_named_arguments: json_rpc_named_arguments - }) - value = %Wei{value: Decimal.new(first_trace.value)} first_trace_formatted = first_trace |> Map.merge(%{ - block_index: block_index, block_hash: block_hash, + block_number: block_number, call_type: call_type, to_address_hash: to_address_hash, created_contract_address_hash: created_contract_address_hash, @@ -1215,32 +1190,4 @@ defmodule Explorer.Chain.InternalTransaction do {:ok, [first_trace_formatted]} end - - defp get_block_index(%{ - transaction_index: transaction_index, - transaction_hash: transaction_hash, - block_number: block_number, - json_rpc_named_arguments: json_rpc_named_arguments - }) do - if transaction_index == 0 do - 0 - else - filtered_block_numbers = RangesHelper.filter_traceable_block_numbers([block_number]) - {:ok, traces} = fetch_block_internal_transactions(filtered_block_numbers, json_rpc_named_arguments) - - sorted_traces = - traces - |> Enum.sort_by(&{&1.transaction_index, &1.index}) - |> Enum.with_index() - - {_, block_index} = - sorted_traces - |> Enum.find({nil, -1}, fn {trace, _} -> - trace.transaction_index == transaction_index && - trace.transaction_hash == transaction_hash - end) - - block_index - end - end end diff --git a/apps/explorer/lib/explorer/chain/pending_block_operation.ex b/apps/explorer/lib/explorer/chain/pending_block_operation.ex index ca205f978fee..c58094cb2767 100644 --- a/apps/explorer/lib/explorer/chain/pending_block_operation.ex +++ b/apps/explorer/lib/explorer/chain/pending_block_operation.ex @@ -55,7 +55,7 @@ defmodule Explorer.Chain.PendingBlockOperation do |> where([pbo], pbo.block_number >= ^from_block_number) |> where([pbo], pbo.block_number <= ^to_block_number) |> select([pbo], count(pbo.block_number)) - |> Repo.one() + |> Repo.one(timeout: :infinity) end @doc """ diff --git a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex index f5afc6126505..1a6c49c55bf2 100644 --- a/apps/explorer/lib/explorer/chain/pending_operations_helper.ex +++ b/apps/explorer/lib/explorer/chain/pending_operations_helper.ex @@ -10,17 +10,10 @@ defmodule Explorer.Chain.PendingOperationsHelper do @blocks_batch_size 10 def pending_operations_type do - # TODO: bring back this condition after the migration of internal transactions PK to [:block_hash, :transaction_index, :index] - # if Application.get_env(:explorer, :json_rpc_named_arguments)[:variant] == EthereumJSONRPC.Geth and - # not Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth)[:block_traceable?], - # do: "transactions", - # else: "blocks" - - if Application.get_env(:explorer, :non_existing_variable, false) do - "transactions" - else - "blocks" - end + if Application.get_env(:explorer, :json_rpc_named_arguments)[:variant] == EthereumJSONRPC.Geth and + !Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth)[:block_traceable?], + do: "transactions", + else: "blocks" end @doc """ diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index c047dfb8fcee..df71d6bf0422 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -155,7 +155,7 @@ defmodule Explorer.Etherscan do query |> InternalTransaction.where_is_different_from_parent_transaction() - |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.where_nonpending_operation() |> InternalTransaction.include_zero_value(options.include_zero_value) |> Repo.replica().all() end @@ -186,7 +186,7 @@ defmodule Explorer.Etherscan do |> InternalTransaction.include_zero_value(options.include_zero_value) |> where_start_block_match_internal_transaction(options) |> where_end_block_match_internal_transaction(options) - |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.where_nonpending_operation() |> Chain.wrapped_union_subquery() end) |> Enum.reduce(fn query, acc -> diff --git a/apps/explorer/lib/explorer/graphql.ex b/apps/explorer/lib/explorer/graphql.ex index 90ecfb41bd6b..93ba3b1d8416 100644 --- a/apps/explorer/lib/explorer/graphql.ex +++ b/apps/explorer/lib/explorer/graphql.ex @@ -44,7 +44,7 @@ defmodule Explorer.GraphQL do """ @spec get_internal_transaction(map()) :: {:ok, InternalTransaction.t()} | {:error, String.t()} def get_internal_transaction(%{transaction_hash: _, index: _} = clauses) do - if internal_transaction = Repo.replica().get_by(InternalTransaction.where_nonpending_block(), clauses) do + if internal_transaction = Repo.replica().get_by(InternalTransaction.where_nonpending_operation(), clauses) do {:ok, internal_transaction} else {:error, "Internal transaction not found."} @@ -69,7 +69,7 @@ defmodule Explorer.GraphQL do ) query - |> InternalTransaction.where_nonpending_block() + |> InternalTransaction.where_nonpending_operation() |> InternalTransaction.where_is_different_from_parent_transaction() end diff --git a/apps/explorer/lib/explorer/migrator/backfill_multichain_search_db.ex b/apps/explorer/lib/explorer/migrator/backfill_multichain_search_db.ex index 67ed5897ff6a..7296844d10d9 100644 --- a/apps/explorer/lib/explorer/migrator/backfill_multichain_search_db.ex +++ b/apps/explorer/lib/explorer/migrator/backfill_multichain_search_db.ex @@ -77,10 +77,10 @@ defmodule Explorer.Migrator.BackfillMultichainSearchDB do |> Repo.all(timeout: :infinity) end) - block_hashes = blocks |> Enum.map(& &1.hash) + block_numbers = Enum.map(blocks, & &1.number) internal_transactions_query = - from(internal_transaction in InternalTransaction, where: internal_transaction.block_hash in ^block_hashes) + from(internal_transaction in InternalTransaction, where: internal_transaction.block_number in ^block_numbers) internal_transactions_task = Task.async(fn -> diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex similarity index 68% rename from apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex rename to apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex index ad5b3fff3719..0e79a16a998d 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_hash_transaction_index_index_unique_index.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex @@ -1,19 +1,25 @@ -defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockHashTransactionIndexIndexUniqueIndex do +defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex do @moduledoc """ - Create unique B-tree index `internal_transactions_block_hash_transaction_index_index_index` on `internal_transactions` table for (`block_hash`, `transaction_index`, `index`) columns. + Create unique B-tree index `internal_transactions_block_number_transaction_index_index_index` on `internal_transactions` table for (`block_number`, `transaction_index`, `index`) columns. """ use Explorer.Migrator.HeavyDbIndexOperation require Logger - alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus, ReindexDuplicatedInternalTransactions} + alias Explorer.Migrator.{ + FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + HeavyDbIndexOperation, + MigrationStatus, + ReindexDuplicatedInternalTransactions + } + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper @table_name :internal_transactions - @index_name "internal_transactions_block_hash_transaction_index_index_index" + @index_name "internal_transactions_block_number_transaction_index_index_index" @operation_type :create - @table_columns ["block_hash", "transaction_index", "index"] + @table_columns ["block_number", "transaction_index", "index"] @impl HeavyDbIndexOperation def table_name, do: @table_name @@ -27,7 +33,8 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBloc @impl HeavyDbIndexOperation def dependent_from_migrations, do: [ - ReindexDuplicatedInternalTransactions.migration_name() + ReindexDuplicatedInternalTransactions.migration_name(), + FillInternalTransactionToAddressHashWithCreatedContractAddressHash.migration_name() ] @impl HeavyDbIndexOperation diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex new file mode 100644 index 000000000000..0e0a2a82a41a --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex @@ -0,0 +1,66 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockHashTransactionIndexIndexIndex do + @moduledoc """ + Drops index "internal_transactions_block_hash_transaction_index_index_index" btree (block_hash, transaction_index, index). + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + alias Explorer.Migrator.{ + FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + HeavyDbIndexOperation, + MigrationStatus + } + + alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper + + @table_name :internal_transactions + @index_name "internal_transactions_block_hash_transaction_index_index_index" + @operation_type :drop + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations do + [ + FillInternalTransactionToAddressHashWithCreatedContractAddressHash.migration_name() + ] + end + + @impl HeavyDbIndexOperation + def db_index_operation do + with :ok <- HeavyDbIndexOperationHelper.cancel_index_creating_query(@index_name) do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + operation = HeavyDbIndexOperationHelper.drop_index_query_string(@index_name) + HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation) + end + + @impl HeavyDbIndexOperation + def db_index_operation_status do + HeavyDbIndexOperationHelper.db_index_dropping_status(@index_name) + end + + @impl HeavyDbIndexOperation + def restart_db_index_operation do + HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name) + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache, do: :ok +end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex index d9d167e2717a..f8cff0586826 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/helper.ex @@ -176,6 +176,24 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.Helper do end end + @doc """ + Cancels currently active index creating query. + """ + @spec cancel_index_creating_query(String.t()) :: :ok | :error + # sobelow_skip ["SQL"] + def cancel_index_creating_query(raw_index_name) do + index_name = sanitize_index_name(raw_index_name) + + case SQL.query(Repo, cancel_index_query_string(index_name), [], timeout: :infinity) do + {:ok, _} -> + :ok + + {:error, error} -> + Logger.error("Failed to cancel creating DB index '#{index_name}': #{inspect(error)}") + :error + end + end + @doc """ Returns the prefix used for naming heavy database operation migrations. @@ -214,6 +232,23 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.Helper do "DROP INDEX #{add_concurrently_flag?()} IF EXISTS \"#{index_name}\";" end + @doc """ + Generates a SQL query string to cancel a currently active `CREATE INDEX` query. + """ + @spec cancel_index_query_string(String.t()) :: String.t() + def cancel_index_query_string(raw_index_name) do + index_name = sanitize_index_name(raw_index_name) + + """ + SELECT pg_cancel_backend(pid) + FROM pg_stat_activity + WHERE pid <> pg_backend_pid() + AND query ILIKE '%create%index%' + AND query ILIKE '%#{index_name}%' + AND state = 'active'; + """ + end + @doc """ As a workaround we have to remove `CONCURRENTLY` in tests since the error like "DROP INDEX CONCURRENTLY cannot run inside a transaction diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/update_internal_transactions_primary_key.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/update_internal_transactions_primary_key.ex new file mode 100644 index 000000000000..966c9b7bec48 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/update_internal_transactions_primary_key.ex @@ -0,0 +1,184 @@ +defmodule Explorer.Migrator.HeavyDbIndexOperation.UpdateInternalTransactionsPrimaryKey do + @moduledoc """ + Update primary key on `internal_transactions` table from (`block_hash`, `block_index`) to (`block_number`, `transaction_index`, `index`). + """ + + use Explorer.Migrator.HeavyDbIndexOperation + + require Logger + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus} + alias Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex + alias Explorer.Repo + + @table_name :internal_transactions + @index_name "internal_transactions_pkey" + @operation_type :create + + @impl HeavyDbIndexOperation + def table_name, do: @table_name + + @impl HeavyDbIndexOperation + def operation_type, do: @operation_type + + @impl HeavyDbIndexOperation + def index_name, do: @index_name + + @impl HeavyDbIndexOperation + def dependent_from_migrations, + do: [ + CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex.migration_name() + ] + + @impl HeavyDbIndexOperation + # sobelow_skip ["SQL"] + def db_index_operation do + result = + Repo.transaction(fn -> + with {:ok, _} <- Repo.query(drop_pk_constraint_query_string()), + {:ok, _} <- Repo.query(rename_index_query_string()), + {:ok, _} <- Repo.query(add_new_pk_query_string()), + {:ok, _} <- Repo.query(drop_block_index_not_null_query_string()), + {:ok, _} <- Repo.query(drop_block_hash_not_null_query_string()) do + update_cache() + :ok + else + {:error, error} -> + Repo.rollback(error) + end + end) + + case result do + {:ok, :ok} -> + :ok + + {:error, error} -> + Logger.error("Migration UpdateInternalTransactionsPrimaryKey failed: #{inspect(error)}") + :error + end + end + + @impl HeavyDbIndexOperation + def check_db_index_operation_progress do + all_operations = [ + drop_pk_constraint_query_string(), + rename_index_query_string(), + add_new_pk_query_string(), + drop_block_index_not_null_query_string(), + drop_block_hash_not_null_query_string() + ] + + Enum.reduce_while(all_operations, :finished_or_not_started, fn operation, acc -> + case HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation) do + :finished_or_not_started -> {:cont, acc} + progress -> {:halt, progress} + end + end) + end + + @impl HeavyDbIndexOperation + # credo:disable-for-next-line /Complexity/ + def db_index_operation_status do + completed? = + case Repo.query(""" + SELECT is_nullable + FROM information_schema.columns + WHERE table_name = '#{@table_name}' AND column_name = 'block_index'; + """) do + {:ok, %Postgrex.Result{rows: [["YES"]]}} -> true + {:ok, %Postgrex.Result{rows: [["NO"]]}} -> false + _ -> nil + end + + started? = + case Repo.query(""" + SELECT EXISTS ( + SELECT 1 + FROM information_schema.table_constraints + WHERE table_schema = 'public' + AND table_name = '#{@table_name}' + AND constraint_type = 'PRIMARY KEY' + ); + """) do + {:ok, %Postgrex.Result{rows: [[false]]}} -> true + {:ok, %Postgrex.Result{rows: [[true]]}} -> false + _ -> nil + end + + cond do + completed? == true -> :completed + started? == true -> :not_completed + is_nil(completed?) or is_nil(started?) -> :unknown + true -> :not_initialized + end + end + + @impl HeavyDbIndexOperation + # sobelow_skip ["SQL"] + def restart_db_index_operation do + result = + Repo.transaction(fn -> + with {:ok, _} <- Repo.query(drop_pk_constraint_query_string()), + {:ok, _} <- Repo.query(set_block_index_not_null_query_string()), + {:ok, _} <- Repo.query(set_block_hash_not_null_query_string()), + {:ok, _} <- Repo.query(add_old_pk_query_string()) do + :ok + else + {:error, error} -> + Repo.rollback(error) + end + end) + + case result do + {:ok, :ok} -> + :ok + + {:error, error} -> + Logger.error("Migration UpdateInternalTransactionsPrimaryKey rollback failed: #{inspect(error)}") + :error + end + end + + @impl HeavyDbIndexOperation + def running_other_heavy_migration_exists?(migration_name) do + MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name) + end + + @impl HeavyDbIndexOperation + def update_cache do + BackgroundMigrations.set_heavy_indexes_update_internal_transactions_primary_key_finished(true) + end + + defp drop_pk_constraint_query_string do + "ALTER TABLE #{@table_name} DROP CONSTRAINT #{@index_name};" + end + + defp rename_index_query_string do + "ALTER INDEX #{CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex.index_name()} RENAME TO #{@index_name};" + end + + defp add_new_pk_query_string do + "ALTER TABLE #{@table_name} ADD PRIMARY KEY USING INDEX #{@index_name};" + end + + defp add_old_pk_query_string do + "ALTER TABLE #{@table_name} ADD PRIMARY KEY (block_hash, block_index);" + end + + defp drop_block_index_not_null_query_string do + "ALTER TABLE #{@table_name} ALTER COLUMN block_index DROP NOT NULL;" + end + + defp set_block_index_not_null_query_string do + "ALTER TABLE #{@table_name} ALTER COLUMN block_index SET NOT NULL;" + end + + defp drop_block_hash_not_null_query_string do + "ALTER TABLE #{@table_name} ALTER COLUMN block_hash DROP NOT NULL;" + end + + defp set_block_hash_not_null_query_string do + "ALTER TABLE #{@table_name} ALTER COLUMN block_hash SET NOT NULL;" + end +end diff --git a/apps/explorer/lib/explorer/utility/internal_transaction_helper.ex b/apps/explorer/lib/explorer/utility/internal_transaction_helper.ex new file mode 100644 index 000000000000..56373bb90e90 --- /dev/null +++ b/apps/explorer/lib/explorer/utility/internal_transaction_helper.ex @@ -0,0 +1,12 @@ +defmodule Explorer.Utility.InternalTransactionHelper do + @moduledoc """ + Internal transactions helping functions. + """ + + alias Explorer.Chain.Cache.BackgroundMigrations + + @spec primary_key_updated? :: boolean() + def primary_key_updated? do + BackgroundMigrations.get_heavy_indexes_update_internal_transactions_primary_key_finished() + end +end diff --git a/apps/explorer/lib/test_helper.ex b/apps/explorer/lib/test_helper.ex index ffc419dc903b..ea3697eed2d0 100644 --- a/apps/explorer/lib/test_helper.ex +++ b/apps/explorer/lib/test_helper.ex @@ -16,6 +16,29 @@ defmodule Explorer.TestHelper do # cast cd 'getAddress(string)' IMPLEMENTATION @address_manager_calldata "0xbf40fac10000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000e494d504c454d454e544154494f4e000000000000000000000000000000000000" + # TODO: Execute this for all background migrations + def run_necessary_background_migrations do + for background_migration <- [ + Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBlockNumberTransactionIndexIndexUniqueIndex, + Explorer.Migrator.HeavyDbIndexOperation.UpdateInternalTransactionsPrimaryKey + ] do + case background_migration.db_index_operation_status() do + :completed -> + :ok + + _ -> + case background_migration.db_index_operation() do + :ok -> + background_migration.update_cache() + :ok + + :error -> + raise "Background migrations failed" + end + end + end + end + def mock_erc7760_basic_requests( mox, error?, diff --git a/apps/explorer/test/explorer/chain/address/coin_balance_test.exs b/apps/explorer/test/explorer/chain/address/coin_balance_test.exs index b87e25750b46..b80190ca02fe 100644 --- a/apps/explorer/test/explorer/chain/address/coin_balance_test.exs +++ b/apps/explorer/test/explorer/chain/address/coin_balance_test.exs @@ -428,10 +428,9 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do created_contract_address: created_contract_address, index: 0, transaction: transaction, + transaction_index: transaction.index, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index + block_hash: transaction.block_hash ) balance = insert(:unfetched_balance, address_hash: created_contract_address.hash, block_number: block.number) @@ -476,10 +475,9 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do from_address: from_address, index: 0, transaction: transaction, + transaction_index: transaction.index, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index + block_hash: transaction.block_hash ) balance = insert(:unfetched_balance, address_hash: from_address.hash, block_number: block.number) @@ -518,10 +516,9 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do to_address: to_address, index: 0, transaction: transaction, + transaction_index: transaction.index, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: 0, - transaction_index: transaction.index + block_hash: transaction.block_hash ) balance = insert(:unfetched_balance, address_hash: to_address.hash, block_number: block.number) @@ -589,10 +586,9 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do from_address: miner, index: 0, transaction: from_internal_transaction_transaction, + transaction_index: from_internal_transaction_transaction.index, block_number: from_internal_transaction_transaction.block_number, - block_hash: from_internal_transaction_transaction.block_hash, - block_index: 0, - transaction_index: from_internal_transaction_transaction.index + block_hash: from_internal_transaction_transaction.block_hash ) insert(:unfetched_balance, address_hash: miner.hash, block_number: from_internal_transaction_block.number) @@ -609,10 +605,9 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do index: 0, to_address: miner, transaction: to_internal_transaction_transaction, + transaction_index: to_internal_transaction_transaction.index, block_number: to_internal_transaction_transaction.block_number, - block_hash: to_internal_transaction_transaction.block_hash, - block_index: 0, - transaction_index: to_internal_transaction_transaction.index + block_hash: to_internal_transaction_transaction.block_hash ) insert(:unfetched_balance, address_hash: miner.hash, block_number: to_internal_transaction_block.number) @@ -669,7 +664,6 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do transaction: from_internal_transaction_transaction, block_number: from_internal_transaction_transaction.block_number, block_hash: from_internal_transaction_transaction.block_hash, - block_index: 0, transaction_index: from_internal_transaction_transaction.index ) @@ -685,7 +679,6 @@ defmodule Explorer.Chain.Address.CoinBalanceTest do transaction: to_internal_transaction_transaction, block_number: to_internal_transaction_transaction.block_number, block_hash: to_internal_transaction_transaction.block_hash, - block_index: 1, transaction_index: to_internal_transaction_transaction.index ) diff --git a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs index c4553d3e7ce3..5acce9517ef2 100644 --- a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs @@ -139,53 +139,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do assert :ok == Repo.get(Transaction, transaction2.hash).status end - test "for block with simple coin transfer and method calls, method calls internal transactions have correct block_index" do - a_block = insert(:block, number: 1000) - transaction0 = insert(:transaction) |> with_block(a_block, status: :ok) - transaction1 = insert(:transaction) |> with_block(a_block, status: :ok) - transaction2 = insert(:transaction) |> with_block(a_block, status: :ok) - insert(:pending_block_operation, block_hash: a_block.hash, block_number: a_block.number) - - assert :ok == transaction0.status - assert :ok == transaction1.status - assert :ok == transaction2.status - - index = 0 - - internal_transaction_changes_0 = make_internal_transaction_changes(transaction0, index, nil) - internal_transaction_changes_0_1 = make_internal_transaction_changes(transaction0, 1, nil) - - internal_transaction_changes_1 = - make_internal_transaction_changes_for_simple_coin_transfers(transaction1, index, nil) - - internal_transaction_changes_2 = make_internal_transaction_changes(transaction2, index, nil) - internal_transaction_changes_2_1 = make_internal_transaction_changes(transaction2, 1, nil) - - assert {:ok, _} = - run_internal_transactions([ - internal_transaction_changes_0, - internal_transaction_changes_0_1, - internal_transaction_changes_1, - internal_transaction_changes_2, - internal_transaction_changes_2_1 - ]) - - # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth - - # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction0.hash, where: i.index == 0) - # |> Repo.one() - # |> is_nil() - - assert 1 == Repo.get_by!(InternalTransaction, transaction_hash: transaction0.hash, index: 1).block_index - # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction1.hash) |> Repo.one() |> is_nil() - - # assert from(i in InternalTransaction, where: i.transaction_hash == ^transaction2.hash, where: i.index == 0) - # |> Repo.one() - # |> is_nil() - - assert 4 == Repo.get_by!(InternalTransaction, transaction_hash: transaction2.hash, index: 1).block_index - end - # test "simple coin transfer has no internal transaction inserted for Nethermind" do # transaction = insert(:transaction) |> with_block(status: :ok) # insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) @@ -441,6 +394,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do index: index, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, type: :call, value: Wei.from(Decimal.new(1), :wei), error: error, @@ -470,6 +424,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do index: index, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, type: :call, value: Wei.from(Decimal.new(1), :wei), error: error, diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index 73bbb718a52a..9492160d9322 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -29,11 +29,11 @@ defmodule Explorer.Chain.InternalTransactionTest do to_address_hash: "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f", trace_address: [0, 1], transaction_hash: transaction.hash, + transaction_index: 0, type: "call", value: 100, block_number: 35, - block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - block_index: 0 + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd" }) assert changeset.valid? @@ -81,8 +81,7 @@ defmodule Explorer.Chain.InternalTransactionTest do error: "execution stopped", value: 0, block_number: 35, - block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", - block_index: 0 + block_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd" }) assert changeset.valid? @@ -110,7 +109,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -119,7 +117,6 @@ defmodule Explorer.Chain.InternalTransactionTest do transaction: transaction, index: 1, block_hash: transaction.block_hash, - block_index: 1, block_number: transaction.block_number, transaction_index: transaction.index ) @@ -151,7 +148,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -191,7 +187,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -212,7 +207,6 @@ defmodule Explorer.Chain.InternalTransactionTest do transaction: transaction, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -234,7 +228,6 @@ defmodule Explorer.Chain.InternalTransactionTest do type: :reward, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -257,7 +250,6 @@ defmodule Explorer.Chain.InternalTransactionTest do type: :selfdestruct, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -278,7 +270,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -288,7 +279,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -298,7 +288,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 2, transaction_index: transaction.index ) @@ -323,7 +312,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -333,7 +321,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -343,7 +330,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 2, transaction_index: transaction.index ) @@ -391,7 +377,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -400,7 +385,6 @@ defmodule Explorer.Chain.InternalTransactionTest do transaction: transaction, index: 1, block_hash: transaction.block_hash, - block_index: 1, block_number: transaction.block_number, transaction_index: transaction.index ) @@ -432,7 +416,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -472,7 +455,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -493,7 +475,6 @@ defmodule Explorer.Chain.InternalTransactionTest do transaction: transaction, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -515,7 +496,6 @@ defmodule Explorer.Chain.InternalTransactionTest do type: :reward, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -538,7 +518,6 @@ defmodule Explorer.Chain.InternalTransactionTest do type: :selfdestruct, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -559,7 +538,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -569,7 +547,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -593,7 +570,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -603,7 +579,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -613,7 +588,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 2, transaction_index: transaction.index ) @@ -658,7 +632,6 @@ defmodule Explorer.Chain.InternalTransactionTest do to_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -669,7 +642,6 @@ defmodule Explorer.Chain.InternalTransactionTest do to_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 2, transaction_index: transaction.index ) @@ -697,7 +669,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -707,7 +678,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index ) @@ -756,7 +726,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: pending_transaction.block_number, block_hash: pending_transaction.block_hash, - block_index: 1, transaction_index: pending_transaction.index ) @@ -768,7 +737,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: pending_transaction.block_number, block_hash: pending_transaction.block_hash, - block_index: 2, transaction_index: pending_transaction.index ) @@ -787,7 +755,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: first_a_transaction.block_number, block_hash: a_block.hash, - block_index: 1, transaction_index: first_a_transaction.index ) @@ -799,7 +766,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: first_a_transaction.block_number, block_hash: a_block.hash, - block_index: 2, transaction_index: first_a_transaction.index ) @@ -816,7 +782,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: second_a_transaction.block_number, block_hash: a_block.hash, - block_index: 4, transaction_index: second_a_transaction.index ) @@ -828,7 +793,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: second_a_transaction.block_number, block_hash: a_block.hash, - block_index: 5, transaction_index: second_a_transaction.index ) @@ -847,7 +811,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: first_b_transaction.block_number, block_hash: b_block.hash, - block_index: 1, transaction_index: first_b_transaction.index ) @@ -859,7 +822,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: first_b_transaction.block_number, block_hash: b_block.hash, - block_index: 2, transaction_index: first_b_transaction.index ) @@ -885,26 +847,6 @@ defmodule Explorer.Chain.InternalTransactionTest do pending_transaction = insert(:transaction) - old_block = insert(:block, consensus: false) - - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - block_hash: old_block.hash, - block_index: 1, - index: 1 - ) - - insert( - :internal_transaction, - transaction: pending_transaction, - to_address: address, - block_hash: old_block.hash, - block_index: 2, - index: 2 - ) - a_block = insert(:block, number: 2000) first_a_transaction = @@ -920,7 +862,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: first_a_transaction.block_number, block_hash: a_block.hash, - block_index: 1, transaction_index: first_a_transaction.index ) @@ -932,7 +873,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: first_a_transaction.block_number, block_hash: a_block.hash, - block_index: 2, transaction_index: first_a_transaction.index ) @@ -949,7 +889,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: second_a_transaction.block_number, block_hash: a_block.hash, - block_index: 4, transaction_index: second_a_transaction.index ) @@ -961,7 +900,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: second_a_transaction.block_number, block_hash: a_block.hash, - block_index: 5, transaction_index: second_a_transaction.index ) @@ -980,7 +918,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 1, block_number: first_b_transaction.block_number, block_hash: b_block.hash, - block_index: 1, transaction_index: first_b_transaction.index ) @@ -992,7 +929,6 @@ defmodule Explorer.Chain.InternalTransactionTest do index: 2, block_number: first_b_transaction.block_number, block_hash: b_block.hash, - block_index: 2, transaction_index: first_b_transaction.index ) @@ -1061,7 +997,6 @@ defmodule Explorer.Chain.InternalTransactionTest do transaction: transaction, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -1083,7 +1018,6 @@ defmodule Explorer.Chain.InternalTransactionTest do from_address: address, transaction: transaction, block_hash: transaction.block_hash, - block_index: 0, block_number: transaction.block_number, transaction_index: transaction.index ) @@ -1095,55 +1029,6 @@ defmodule Explorer.Chain.InternalTransactionTest do end describe "fetch/1" do - test "with consensus transactions only" do - block_non_consensus = insert(:block, number: 2000, consensus: false) - block_consensus = insert(:block, number: 3000) - - transaction_1 = - :transaction - |> insert( - block_hash: block_non_consensus.hash, - block_number: block_non_consensus.number, - block_consensus: false, - cumulative_gas_used: 100_500, - gas_used: 100_500, - index: 1 - ) - - transaction_2 = - :transaction - |> insert() - |> with_block(block_consensus) - - %InternalTransaction{transaction_hash: first_transaction_hash, index: first_index} = - insert(:internal_transaction, - index: 1, - transaction: transaction_1, - block_number: transaction_1.block_number, - block_hash: transaction_1.block_hash, - block_index: 1, - transaction_index: transaction_1.index - ) - - %InternalTransaction{transaction_hash: second_transaction_hash, index: second_index} = - insert(:internal_transaction, - index: 2, - transaction: transaction_2, - block_number: transaction_2.block_number, - block_hash: transaction_2.block_hash, - block_index: 2, - transaction_index: transaction_2.index - ) - - result = - [] - |> InternalTransaction.fetch() - |> Enum.map(&{&1.transaction_hash, &1.index}) - - refute Enum.member?(result, {first_transaction_hash, first_index}) - assert Enum.member?(result, {second_transaction_hash, second_index}) - end - # todo: This test is temporarily disabled because this check is removed for the sake of performance: # |> where([internal_transaction, transaction], transaction.block_hash == internal_transaction.block_hash) # Return the test back when reorg data will be moved out from the main tables. @@ -1162,7 +1047,6 @@ defmodule Explorer.Chain.InternalTransactionTest do # transaction: transaction, # block_number: transaction.block_number, # block_hash: block_non_consensus.hash, - # block_index: 1, # transaction_index: transaction.index # ) @@ -1172,7 +1056,6 @@ defmodule Explorer.Chain.InternalTransactionTest do # transaction: transaction, # block_number: transaction.block_number, # block_hash: block_consensus.hash, - # block_index: 2, # transaction_index: transaction.index # ) @@ -1265,7 +1148,6 @@ defmodule Explorer.Chain.InternalTransactionTest do :ok, [ %{ - block_index: 0, block_number: block_number, block_hash: block_hash, call_type: nil, diff --git a/apps/explorer/test/explorer/chain/smart_contract_test.exs b/apps/explorer/test/explorer/chain/smart_contract_test.exs index a774c5354a9b..a501af868afc 100644 --- a/apps/explorer/test/explorer/chain/smart_contract_test.exs +++ b/apps/explorer/test/explorer/chain/smart_contract_test.exs @@ -34,7 +34,6 @@ defmodule Explorer.Chain.SmartContractTest do created_contract_code: smart_contract_bytecode, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -136,7 +135,6 @@ defmodule Explorer.Chain.SmartContractTest do created_contract_code: smart_contract_bytecode, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) diff --git a/apps/explorer/test/explorer/chain/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs index 1d5f30dbf449..6ac5e8bba1c7 100644 --- a/apps/explorer/test/explorer/chain/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -542,7 +542,6 @@ defmodule Explorer.Chain.TransactionTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 4e3ad9f3affc..3f4223f31490 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -24,7 +24,14 @@ defmodule Explorer.ChainTest do alias Explorer.{Chain, Etherscan} alias Explorer.Chain.Cache.ChainId - alias Explorer.Chain.Cache.Counters.{BlocksCount, TransactionsCount, PendingBlockOperationCount} + + alias Explorer.Chain.Cache.Counters.{ + BlocksCount, + TransactionsCount, + PendingBlockOperationCount, + PendingTransactionOperationCount + } + alias Explorer.Chain.MultichainSearchDb.{BalancesExportQueue, MainExportQueue} alias Explorer.Chain.Supply.ProofOfAuthority @@ -518,7 +525,13 @@ defmodule Explorer.ChainTest do setup do Supervisor.terminate_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) Supervisor.restart_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) - on_exit(fn -> Supervisor.terminate_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) end) + Supervisor.terminate_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) + Supervisor.restart_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) + + on_exit(fn -> + Supervisor.terminate_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) + end) end test "finished indexing" do @@ -547,8 +560,12 @@ defmodule Explorer.ChainTest do configuration = Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor) Application.put_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor, disabled?: false) + geth_config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(geth_config, :block_traceable?, true)) + on_exit(fn -> Application.put_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor, configuration) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, geth_config) end) refute Chain.finished_indexing_internal_transactions?() @@ -649,7 +666,6 @@ defmodule Explorer.ChainTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) @@ -659,7 +675,6 @@ defmodule Explorer.ChainTest do index: index, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index ) end) @@ -821,6 +836,8 @@ defmodule Explorer.ChainTest do setup do Supervisor.terminate_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) Supervisor.restart_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) + Supervisor.restart_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) configuration = Application.get_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor) Application.put_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor, disabled?: false) @@ -828,10 +845,11 @@ defmodule Explorer.ChainTest do Application.put_env(:indexer, :trace_first_block, 0) Application.put_env(:indexer, Indexer.Fetcher.InternalTransaction.Supervisor, configuration) Supervisor.terminate_child(Explorer.Supervisor, PendingBlockOperationCount.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, PendingTransactionOperationCount.child_id()) end) end - test "returns indexed ratio" do + test "returns indexed ratio (pending_block_operation)" do for index <- 0..9 do block = insert(:block, number: index) @@ -850,6 +868,41 @@ defmodule Explorer.ChainTest do assert Decimal.compare(Chain.indexed_ratio_internal_transactions(), Decimal.from_float(0.7)) == :eq end + test "returns indexed ratio (pending_transaction_operation)" do + for index <- 0..9 do + block = insert(:block, number: index) + + transaction_hashes = + 2 + |> insert_list(:transaction) + |> with_block(block) + |> Enum.map(& &1.hash) + + if index === 0 || index === 5 || index === 7 do + insert(:pending_transaction_operation, transaction_hash: List.first(transaction_hashes)) + end + end + + geth_config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + json_rpc_config = Application.get_env(:explorer, :json_rpc_named_arguments) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(geth_config, :block_traceable?, false)) + + Application.put_env( + :explorer, + :json_rpc_named_arguments, + Keyword.put(json_rpc_config, :variant, EthereumJSONRPC.Geth) + ) + + on_exit(fn -> + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, geth_config) + Application.put_env(:explorer, :json_rpc_named_arguments, json_rpc_config) + end) + + Chain.indexed_ratio_internal_transactions() + + assert Decimal.compare(Chain.indexed_ratio_internal_transactions(), Decimal.from_float(0.7)) == :eq + end + test "returns 0 if no blocks" do assert Decimal.new(0) == Chain.indexed_ratio_internal_transactions() end @@ -1025,6 +1078,7 @@ defmodule Explorer.ChainTest do %{ block_number: 37, transaction_hash: "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5", + transaction_index: 1, # transaction with index 0 is ignored in Nethermind JSON RPC Variant and not ignored in case of Geth index: 1, trace_address: [], @@ -1227,8 +1281,7 @@ defmodule Explorer.ChainTest do trace_address: [], type: :call, block_number: 37, - transaction_index: nil, - block_index: 0, + transaction_index: 1, created_contract_address_hash: nil, from_address_hash: %Explorer.Chain.Hash{ byte_count: 20, @@ -2073,7 +2126,6 @@ defmodule Explorer.ChainTest do created_contract_code: smart_contract_bytecode, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index a6c83f33508b..ca7f9160700b 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -69,7 +69,6 @@ defmodule Explorer.EtherscanTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) |> with_contract_creation(contract_address) @@ -147,7 +146,6 @@ defmodule Explorer.EtherscanTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) |> with_contract_creation(contract_address) @@ -643,7 +641,6 @@ defmodule Explorer.EtherscanTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) |> with_contract_creation(contract_address) @@ -688,7 +685,6 @@ defmodule Explorer.EtherscanTest do value: index + 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index ) end @@ -716,7 +712,6 @@ defmodule Explorer.EtherscanTest do value: 1, block_number: transaction1.block_number, block_hash: transaction1.block_hash, - block_index: 0, transaction_index: transaction1.index ) @@ -726,7 +721,6 @@ defmodule Explorer.EtherscanTest do value: 2, block_number: transaction1.block_number, block_hash: transaction1.block_hash, - block_index: 1, transaction_index: transaction1.index ) @@ -737,7 +731,6 @@ defmodule Explorer.EtherscanTest do type: :reward, block_number: transaction2.block_number, block_hash: transaction2.block_hash, - block_index: 2, transaction_index: transaction2.index ) @@ -760,24 +753,22 @@ defmodule Explorer.EtherscanTest do for index <- 0..2 do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: index, value: index + 1, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash ) end for index <- 3..5 do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash ) end @@ -795,24 +786,22 @@ defmodule Explorer.EtherscanTest do for index <- 0..2 do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: index, value: index + 1, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash ) end for index <- 3..5 do insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash ) end @@ -863,7 +852,6 @@ defmodule Explorer.EtherscanTest do from_address: address, block_number: transaction.block_number, block_hash: block.hash, - block_index: 0, transaction_index: transaction.index ) |> with_contract_creation(contract_address) @@ -915,7 +903,6 @@ defmodule Explorer.EtherscanTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index } @@ -941,7 +928,6 @@ defmodule Explorer.EtherscanTest do index: 0, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index, created_contract_address: address1 ) @@ -951,7 +937,6 @@ defmodule Explorer.EtherscanTest do index: 1, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 1, transaction_index: transaction.index, from_address: address1 ) @@ -961,7 +946,6 @@ defmodule Explorer.EtherscanTest do index: 2, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 2, transaction_index: transaction.index, to_address: address1 ) @@ -971,7 +955,6 @@ defmodule Explorer.EtherscanTest do index: 3, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 3, transaction_index: transaction.index, from_address: address2 ) @@ -1001,7 +984,6 @@ defmodule Explorer.EtherscanTest do from_address: address, block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: index, transaction_index: transaction.index } @@ -1044,7 +1026,6 @@ defmodule Explorer.EtherscanTest do from_address: address, block_number: block.number, block_hash: block.hash, - block_index: index, transaction_index: transaction.index, value: 1 } @@ -1081,13 +1062,12 @@ defmodule Explorer.EtherscanTest do for index <- 0..3 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1096,13 +1076,12 @@ defmodule Explorer.EtherscanTest do for index <- 4..5 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: index, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1124,13 +1103,12 @@ defmodule Explorer.EtherscanTest do for index <- 0..3 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1139,13 +1117,12 @@ defmodule Explorer.EtherscanTest do for index <- 4..5 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: index, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1183,13 +1160,12 @@ defmodule Explorer.EtherscanTest do for index <- 0..3 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1198,13 +1174,12 @@ defmodule Explorer.EtherscanTest do for index <- 4..5 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: index, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1226,13 +1201,12 @@ defmodule Explorer.EtherscanTest do for index <- 0..3 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: 0, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) @@ -1241,13 +1215,12 @@ defmodule Explorer.EtherscanTest do for index <- 4..5 do internal_transaction_details = %{ transaction: transaction, + transaction_index: transaction.index, index: index, value: index, from_address: address, block_number: transaction.block_number, - block_hash: transaction.block_hash, - block_index: index, - transaction_index: transaction.index + block_hash: transaction.block_hash } insert(:internal_transaction, internal_transaction_details) diff --git a/apps/explorer/test/explorer/graphql_test.exs b/apps/explorer/test/explorer/graphql_test.exs index 4e6915e2f7e4..7496dcb71e11 100644 --- a/apps/explorer/test/explorer/graphql_test.exs +++ b/apps/explorer/test/explorer/graphql_test.exs @@ -97,9 +97,10 @@ defmodule Explorer.GraphQLTest do internal_transaction = insert(:internal_transaction, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) clauses = %{transaction_hash: transaction.hash, index: internal_transaction.index} @@ -129,16 +130,18 @@ defmodule Explorer.GraphQLTest do internal_transaction = insert(:internal_transaction_create, transaction: transaction1, + transaction_index: transaction1.index, index: 0, block_hash: transaction1.block_hash, - block_index: 0 + block_number: transaction1.block_number ) insert(:internal_transaction_create, transaction: transaction2, + transaction_index: transaction2.index, index: 0, block_hash: transaction2.block_hash, - block_index: 0 + block_number: transaction2.block_number ) [found_internal_transaction] = @@ -157,17 +160,19 @@ defmodule Explorer.GraphQLTest do for index <- 0..2 do insert(:internal_transaction_create, transaction: transaction1, + transaction_index: transaction1.index, index: index, block_hash: transaction1.block_hash, - block_index: index + block_number: transaction1.block_number ) end insert(:internal_transaction_create, transaction: transaction2, + transaction_index: transaction2.index, index: 0, block_hash: transaction2.block_hash, - block_index: 0 + block_number: transaction2.block_number ) found_internal_transactions = @@ -187,23 +192,26 @@ defmodule Explorer.GraphQLTest do insert(:internal_transaction_create, transaction: transaction, + transaction_index: transaction.index, index: 2, block_hash: transaction.block_hash, - block_index: 2 + block_number: transaction.block_number ) insert(:internal_transaction_create, transaction: transaction, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) insert(:internal_transaction_create, transaction: transaction, + transaction_index: transaction.index, index: 1, block_hash: transaction.block_hash, - block_index: 1 + block_number: transaction.block_number ) found_internal_transactions = diff --git a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs index 9353a61fea24..0ad062a92b37 100644 --- a/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs +++ b/apps/explorer/test/explorer/migrator/delete_zero_value_internal_transactions_test.exs @@ -26,7 +26,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do block_number: transaction.block_number, from_address: address_1, to_address: address_2, - block_index: i, + transaction_index: transaction.index, type: :call, value: 0 ) @@ -45,7 +45,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do block_number: transaction.block_number, from_address: address_2, to_address: address_3, - block_index: i + 3, + transaction_index: transaction.index, type: :call, value: 0 ) @@ -64,7 +64,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do block_number: transaction.block_number, from_address: address_3, to_address: address_1, - block_index: i + 7, + transaction_index: transaction.index, type: :call, value: 0 ) @@ -81,7 +81,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, - block_index: i, + transaction_index: transaction.index, type: :call, value: 1 ) @@ -98,7 +98,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, - block_index: i, + transaction_index: transaction.index, value: 0 ) end) @@ -114,7 +114,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, - block_index: i, + transaction_index: transaction.index, type: :call, value: 0 ) @@ -217,9 +217,9 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, + transaction_index: transaction.index, from_address: address_1, to_address: address_2, - block_index: 1, type: :call, value: 0 ) @@ -277,9 +277,9 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactionsTest do transaction: transaction, block_hash: transaction.block_hash, block_number: transaction.block_number, + transaction_index: transaction.index, from_address: address_1, to_address: address_2, - block_index: 1, type: :call, value: 0 ) diff --git a/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs b/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs index 41ad61cce5eb..e25cde671c4e 100644 --- a/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs +++ b/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs @@ -32,8 +32,7 @@ defmodule Explorer.Migrator.FillInternalTransactionToAddressHashTest do index: index, block_number: transaction.block_number, transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_index: index + block_hash: transaction.block_hash ) end) @@ -48,8 +47,7 @@ defmodule Explorer.Migrator.FillInternalTransactionToAddressHashTest do index: index, block_hash: transaction.block_hash, block_number: transaction.block_number, - transaction_index: transaction.index, - block_index: index + transaction_index: transaction.index ) end) diff --git a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs b/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs deleted file mode 100644 index cacb7c825f75..000000000000 --- a/apps/explorer/test/explorer/migrator/reindex_duplicated_internal_transactions_test.exs +++ /dev/null @@ -1,130 +0,0 @@ -defmodule Explorer.Migrator.ReindexDuplicatedInternalTransactionsTest do - use Explorer.DataCase, async: false - - alias Explorer.Chain.{InternalTransaction, PendingBlockOperation} - alias Explorer.Migrator.{MigrationStatus, ReindexDuplicatedInternalTransactions} - alias Explorer.Repo - - setup_all do - opts = Application.get_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions) - - Application.put_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, - batch_size: 1, - concurrency: 1, - timeout: 0 - ) - - on_exit(fn -> - Application.put_env(:explorer, Explorer.Migrator.ReindexDuplicatedInternalTransactions, opts) - end) - end - - test "Reindex duplicated internal transactions" do - transaction_from_invalid_block_1 = - :transaction - |> insert() - |> with_block() - - Enum.each(1..10, fn index -> - insert( - :internal_transaction, - transaction: transaction_from_invalid_block_1, - index: index, - block_number: transaction_from_invalid_block_1.block_number, - transaction_index: transaction_from_invalid_block_1.index, - block_hash: transaction_from_invalid_block_1.block_hash, - block_index: index - ) - end) - - Enum.each(11..13, fn index -> - insert( - :internal_transaction, - transaction: transaction_from_invalid_block_1, - index: index - 10, - block_number: transaction_from_invalid_block_1.block_number, - transaction_index: transaction_from_invalid_block_1.index, - block_hash: transaction_from_invalid_block_1.block_hash, - block_index: index - ) - end) - - transaction_from_invalid_block_2 = - :transaction - |> insert() - |> with_block() - - Enum.each(1..10, fn index -> - insert( - :internal_transaction, - transaction: transaction_from_invalid_block_2, - index: index, - block_number: transaction_from_invalid_block_2.block_number, - transaction_index: transaction_from_invalid_block_2.index, - block_hash: transaction_from_invalid_block_2.block_hash, - block_index: index - ) - end) - - Enum.each(11..13, fn index -> - insert( - :internal_transaction, - transaction: transaction_from_invalid_block_2, - index: index - 10, - block_number: transaction_from_invalid_block_2.block_number, - transaction_index: transaction_from_invalid_block_2.index, - block_hash: transaction_from_invalid_block_2.block_hash, - block_index: index - ) - end) - - transaction_from_valid_block = - :transaction - |> insert() - |> with_block() - - Enum.each(1..10, fn index -> - insert( - :internal_transaction, - transaction: transaction_from_valid_block, - index: index, - block_number: transaction_from_valid_block.block_number, - transaction_index: transaction_from_valid_block.index, - block_hash: transaction_from_valid_block.block_hash, - block_index: index - ) - end) - - assert MigrationStatus.get_status("reindex_duplicated_internal_transactions") == nil - - ReindexDuplicatedInternalTransactions.start_link([]) - - wait_for_results(fn -> - Repo.one!(from(pbo in PendingBlockOperation, limit: 1)) - end) - - assert MigrationStatus.get_status("reindex_duplicated_internal_transactions") == "completed" - - internal_transactions = Repo.all(InternalTransaction) - - assert Enum.count(internal_transactions) == 10 - - Enum.each(internal_transactions, fn it -> - assert it.block_hash == transaction_from_valid_block.block_hash - end) - - pending_operations = Repo.all(PendingBlockOperation) - - assert Enum.count(pending_operations) == 2 - - assert Enum.any?(pending_operations, fn pbo -> - pbo.block_hash == transaction_from_invalid_block_1.block_hash and - pbo.block_number == transaction_from_invalid_block_1.block_number - end) - - assert Enum.any?(pending_operations, fn pbo -> - pbo.block_hash == transaction_from_invalid_block_2.block_hash and - pbo.block_number == transaction_from_invalid_block_2.block_number - end) - end -end diff --git a/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs b/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs index 77eb929fde9a..df99bb065a32 100644 --- a/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs +++ b/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs @@ -22,9 +22,9 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatusTes insert(:internal_transaction, index: 10, transaction: transaction, + transaction_index: transaction.index, block: block, block_number: block.number, - block_index: i, error: nil ) @@ -38,9 +38,9 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatusTes insert(:internal_transaction, index: 10, transaction: transaction, + transaction_index: transaction.index, block: block, block_number: block.number, - block_index: i, error: "error", output: nil ) @@ -53,9 +53,9 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatusTes insert(:internal_transaction, index: 10, transaction: transaction, + transaction_index: transaction.index, block: block, block_number: block.number, - block_index: i, error: nil ) end) diff --git a/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs b/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs index 64e5c23091f0..43575036c1c3 100644 --- a/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs +++ b/apps/explorer/test/explorer/migrator/switch_pending_operations_test.exs @@ -16,8 +16,6 @@ defmodule Explorer.Migrator.SwitchPendingOperationsTest do end) end - # TODO: remove tag after the migration of internal transactions PK to [:block_hash, :transaction_index, :index] - @tag :skip test "from pbo to pto" do first_block = insert(:block) second_block = insert(:block) diff --git a/apps/explorer/test/explorer/repo_test.exs b/apps/explorer/test/explorer/repo_test.exs index e414f8745df4..50c68953dfdd 100644 --- a/apps/explorer/test/explorer/repo_test.exs +++ b/apps/explorer/test/explorer/repo_test.exs @@ -21,7 +21,6 @@ defmodule Explorer.RepoTest do index: 0, block_number: 35, block_hash: transaction.block_hash, - block_index: 0, transaction_index: 0 ) @@ -35,7 +34,7 @@ defmodule Explorer.RepoTest do Repo.safe_insert_all( InternalTransaction, [timestamped_changes, timestamped_changes], - conflict_target: [:block_hash, :block_index], + conflict_target: [:block_number, :transaction_index, :index], on_conflict: :replace_all ) end @@ -44,7 +43,8 @@ defmodule Explorer.RepoTest do assert log =~ "Chunk:\n" assert log =~ "index: 0" - assert log =~ "Options:\n\n[conflict_target: [:block_hash, :block_index], on_conflict: :replace_all]\n\n" + assert log =~ + "Options:\n\n[conflict_target: [:block_number, :transaction_index, :index], on_conflict: :replace_all]\n\n" assert log =~ "Exception:\n\n** (Postgrex.Error) ERROR 21000 (cardinality_violation) ON CONFLICT DO UPDATE command cannot affect row a second time\n" diff --git a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs index 4880da3fba9c..aaedf5aaf441 100644 --- a/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs +++ b/apps/explorer/test/explorer/smart_contract/solidity/verifier_test.exs @@ -515,9 +515,10 @@ defmodule Explorer.SmartContract.Solidity.VerifierTest do created_contract_code: bytecode, input: nil, transaction_hash: transaction.hash, + transaction_index: transaction.index, index: 0, block_hash: transaction.block_hash, - block_index: 0 + block_number: transaction.block_number ) params = %{ @@ -573,9 +574,10 @@ defmodule Explorer.SmartContract.Solidity.VerifierTest do created_contract_code: bytecode, input: nil, transaction_hash: transaction_success.hash, + transaction_index: transaction_success.index, index: 0, block_hash: transaction_success.block_hash, - block_index: 0 + block_number: transaction_success.block_number ) :internal_transaction @@ -586,9 +588,10 @@ defmodule Explorer.SmartContract.Solidity.VerifierTest do created_contract_code: bytecode, input: nil, transaction_hash: transaction_failure.hash, + transaction_index: transaction_failure.index, index: 0, block_hash: transaction_failure.block_hash, - block_index: 0 + block_number: transaction_failure.block_number ) params = %{ diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 39adf0058007..f66a64092ff6 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -837,7 +837,6 @@ defmodule Explorer.Factory do # caller MUST supply `transaction` because it can't be built lazily to allow overrides without creating an extra # transaction # caller MUST supply `block_hash` (usually the same as the transaction's) - # caller MUST supply `block_index` type: :call, value: sequence("internal_transaction_value", &Decimal.new(&1)) } @@ -861,7 +860,6 @@ defmodule Explorer.Factory do # caller MUST supply `transaction` because it can't be built lazily to allow overrides without creating an extra # transaction # caller MUST supply `block_hash` (usually the same as the transaction's) - # caller MUST supply `block_index` type: :create, value: sequence("internal_transaction_value", &Decimal.new(&1)) } diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs index 24ceddad9e10..55deb8e9949c 100644 --- a/apps/explorer/test/test_helper.exs +++ b/apps/explorer/test/test_helper.exs @@ -11,6 +11,8 @@ ExUnit.start() {:ok, _} = Application.ensure_all_started(:ex_machina) +Explorer.TestHelper.run_necessary_background_migrations() + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :auto) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.PolygonEdge, :auto) diff --git a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex index ac19f6df9402..fb80e9b64ed9 100644 --- a/apps/indexer/lib/indexer/fetcher/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/internal_transaction.ex @@ -1,4 +1,3 @@ -# credo:disable-for-this-file defmodule Indexer.Fetcher.InternalTransaction do @moduledoc """ Fetches and indexes `t:Explorer.Chain.InternalTransaction.t/0`. @@ -28,8 +27,8 @@ defmodule Indexer.Fetcher.InternalTransaction do alias Explorer.Chain.Zilliqa.Helper, as: ZilliqaHelper alias Indexer.{BufferedTask, Tracer} alias Indexer.Fetcher.InternalTransaction.Supervisor, as: InternalTransactionSupervisor - alias Indexer.Transform.Celo.TransactionTokenTransfers, as: CeloTransactionTokenTransfers alias Indexer.Transform.{AddressCoinBalances, Addresses, AddressTokenBalances} + alias Indexer.Transform.Celo.TransactionTokenTransfers, as: CeloTransactionTokenTransfers @behaviour BufferedTask @@ -174,7 +173,7 @@ defmodule Indexer.Fetcher.InternalTransaction do block_numbers_or_transactions |> check_and_filter_block_numbers() - |> fetch_block_internal_transactions(json_rpc_named_arguments) + |> EthereumJSONRPC.fetch_block_internal_transactions(json_rpc_named_arguments) :transaction_params -> Logger.debug("fetching internal transactions by transactions") @@ -190,42 +189,6 @@ defmodule Indexer.Fetcher.InternalTransaction do end end - # TODO: remove this function after the migration of internal transactions PK to [:block_hash, :transaction_index, :index] - defp fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) do - variant = Keyword.fetch!(json_rpc_named_arguments, :variant) - - if variant in block_traceable_variants() do - EthereumJSONRPC.fetch_block_internal_transactions(block_numbers, json_rpc_named_arguments) - else - Enum.reduce(block_numbers, {:ok, []}, fn - block_number, {:ok, acc_list} -> - block_number - |> Transaction.get_transactions_of_block_number() - |> filter_non_traceable_transactions() - |> Enum.map(¶ms/1) - |> case do - [] -> - {:ok, []} - - transactions -> - try do - EthereumJSONRPC.fetch_internal_transactions(transactions, json_rpc_named_arguments) - catch - :exit, error -> - {:error, error, __STACKTRACE__} - end - end - |> case do - {:ok, internal_transactions} -> {:ok, internal_transactions ++ acc_list} - error_or_ignore -> error_or_ignore - end - - _, error_or_ignore -> - error_or_ignore - end) - end - end - @default_block_traceable_variants [ EthereumJSONRPC.Nethermind, EthereumJSONRPC.Erigon, @@ -258,27 +221,6 @@ defmodule Indexer.Fetcher.InternalTransaction do end end - def import_first_trace(internal_transactions_params) do - imports = - Chain.import(%{ - internal_transactions: %{params: internal_transactions_params, with: :blockless_changeset}, - timeout: :infinity - }) - - case imports do - {:error, step, reason, _changes_so_far} -> - Logger.error( - fn -> - [ - "failed to import first trace for transaction: ", - inspect(reason) - ] - end, - step: step - ) - end - end - defp filter_block_numbers(block_numbers, :block_number, json_rpc_named_arguments) do block_numbers |> Enum.uniq() @@ -349,8 +291,8 @@ defmodule Indexer.Fetcher.InternalTransaction do defp safe_import_internal_transaction(internal_transactions_params, block_numbers, data_type) do import_internal_transaction(internal_transactions_params, block_numbers, data_type) rescue - Postgrex.Error -> - handle_foreign_key_violation(internal_transactions_params, block_numbers, data_type) + exception in Postgrex.Error -> + handle_foreign_key_violation(exception, internal_transactions_params, block_numbers, data_type) {:retry, block_numbers} end @@ -499,7 +441,7 @@ defmodule Indexer.Fetcher.InternalTransaction do defp handle_unique_key_violation(_reason, _identifiers, _data_type), do: :ok - defp handle_foreign_key_violation(internal_transactions_params, block_numbers_or_transactions, data_type) do + defp handle_foreign_key_violation(reason, internal_transactions_params, block_numbers_or_transactions, data_type) do block_numbers = data_to_block_numbers(block_numbers_or_transactions, data_type) Block.set_refetch_needed(block_numbers) @@ -511,7 +453,7 @@ defmodule Indexer.Fetcher.InternalTransaction do Logger.error(fn -> [ - "foreign_key_violation on internal transactions import, foreign transactions hashes: ", + "foreign_key_violation on internal transactions import: #{inspect(reason)}, foreign transactions hashes: ", Enum.join(transaction_hashes, ", ") ] end) @@ -538,20 +480,13 @@ defmodule Indexer.Fetcher.InternalTransaction do defp invalidate_block_from_error(_error_data), do: :ok - defp queue_data_type(_json_rpc_named_arguments) do - # TODO: bring back after the migration of internal transactions PK to [:block_hash, :transaction_index, :index] - # variant = Keyword.fetch!(json_rpc_named_arguments, :variant) - - # if variant in block_traceable_variants() do - # :block_number - # else - # :transaction_params - # end + defp queue_data_type(json_rpc_named_arguments) do + variant = Keyword.fetch!(json_rpc_named_arguments, :variant) - if Application.get_env(:explorer, :non_existing_variable, false) do - :transaction_params - else + if variant in block_traceable_variants() do :block_number + else + :transaction_params end end diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs index b17826dc03fe..94177fe185ed 100644 --- a/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs +++ b/apps/indexer/test/indexer/fetcher/internal_transaction/delete_queue_test.exs @@ -10,6 +10,19 @@ defmodule Indexer.Fetcher.InternalTransaction.DeleteQueueTest do setup :verify_on_exit! setup :set_mox_global + setup do + geth_config = Application.get_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth) + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, Keyword.put(geth_config, :block_traceable?, true)) + + fetcher_config = Application.get_env(:indexer, DeleteQueueFetcher) + Application.put_env(:indexer, DeleteQueueFetcher, Keyword.put(fetcher_config, :threshold, :timer.minutes(10))) + + on_exit(fn -> + Application.put_env(:ethereum_jsonrpc, EthereumJSONRPC.Geth, geth_config) + Application.put_env(:indexer, DeleteQueueFetcher, fetcher_config) + end) + end + test "deletes internal transactions and inserts pending operations" do transaction_1 = :transaction @@ -34,16 +47,16 @@ defmodule Indexer.Fetcher.InternalTransaction.DeleteQueueTest do transaction: transaction_1, index: 0, block_hash: transaction_1.block_hash, - block_index: 0, - block_number: fresh_block_number + block_number: fresh_block_number, + transaction_index: transaction_1.index ) insert(:internal_transaction, transaction: transaction_2, index: 0, block_hash: transaction_2.block_hash, - block_index: 0, - block_number: expired_block_number + block_number: expired_block_number, + transaction_index: transaction_2.index ) pid = DeleteQueueFetcher.Supervisor.Case.start_supervised!() diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs index 89b0ba925e59..1f372c52f785 100644 --- a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs +++ b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs @@ -202,7 +202,8 @@ defmodule Indexer.Fetcher.InternalTransactionTest do block = insert(:block) transaction = insert(:transaction) |> with_block(block) block_hash = block.hash - insert(:pending_block_operation, block_hash: block_hash, block_number: block.number) + block_number = block.number + insert(:pending_block_operation, block_hash: block_hash, block_number: block_number) if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do case Keyword.fetch!(json_rpc_named_arguments, :variant) do @@ -301,7 +302,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do assert nil == Repo.get(PendingBlockOperation, block_hash) - assert Repo.exists?(from(i in Chain.InternalTransaction, where: i.block_hash == ^block_hash)) + assert Repo.exists?(from(i in Chain.InternalTransaction, where: i.block_number == ^block_number)) end test "handles failure by retrying only unique numbers", %{ @@ -482,7 +483,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do end) assert %{consensus: true, refetch_needed: true} = Repo.reload(block) - assert logs =~ "foreign_key_violation on internal transactions import, foreign transactions hashes:" + assert logs =~ "foreign_key_violation on internal transactions import" end end @@ -535,10 +536,9 @@ defmodule Indexer.Fetcher.InternalTransactionTest do |> Enum.concat([{:transport_options, [http_options: []]}]) block = insert(:block, number: 1) - _transaction = :transaction |> insert() |> with_block(block) + transaction = :transaction |> insert() |> with_block(block) block_number = block.number - block_hash = block.hash - insert(:pending_block_operation, block_hash: block_hash, block_number: block_number) + insert(:pending_transaction_operation, transaction_hash: transaction.hash) EthereumJSONRPC.Mox |> expect(:json_rpc, fn [%{id: id, method: "debug_traceTransaction"}], _options -> @@ -611,13 +611,17 @@ defmodule Indexer.Fetcher.InternalTransactionTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - assert %{block_hash: block_hash} = Repo.get(PendingBlockOperation, block_hash) + assert %{} = Repo.get(PendingTransactionOperation, transaction.hash) - assert :ok == InternalTransaction.run([block_number], json_rpc_named_arguments) + assert :ok == + InternalTransaction.run( + [%{block_number: transaction.block_number, hash: transaction.hash, index: transaction.index}], + json_rpc_named_arguments + ) - assert nil == Repo.get(PendingBlockOperation, block_hash) + assert nil == Repo.get(PendingTransactionOperation, transaction.hash) - internal_transactions = Repo.all(from(i in Chain.InternalTransaction, where: i.block_hash == ^block_hash)) + internal_transactions = Repo.all(from(i in Chain.InternalTransaction, where: i.block_number == ^block_number)) assert Enum.count(internal_transactions) > 0 diff --git a/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs b/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs index 26ee35968e33..7beb6b8deffd 100644 --- a/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs +++ b/apps/indexer/test/indexer/fetcher/on_demand/contract_creator_test.exs @@ -61,7 +61,6 @@ defmodule Indexer.Fetcher.OnDemand.ContractCreatorTest do created_contract_code: "0x1234", block_number: transaction.block_number, block_hash: transaction.block_hash, - block_index: 0, transaction_index: transaction.index ) diff --git a/apps/indexer/test/test_helper.exs b/apps/indexer/test/test_helper.exs index a14c7297ce32..4b2f51050ce1 100644 --- a/apps/indexer/test/test_helper.exs +++ b/apps/indexer/test/test_helper.exs @@ -22,5 +22,7 @@ Mox.defmock(Explorer.Mock.TeslaAdapter, for: Tesla.Adapter) ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter]) ExUnit.start() +Explorer.TestHelper.run_necessary_background_migrations() + Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual) Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.EventNotifications, :manual) diff --git a/config/runtime.exs b/config/runtime.exs index 57a6b4860d8e..6fb24231da94 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -373,7 +373,10 @@ config :explorer, Explorer.Chain.Cache.Counters.TransactionsCount, global_ttl: ConfigHelper.parse_time_env_var("CACHE_TXS_COUNT_PERIOD", "2h") config :explorer, Explorer.Chain.Cache.Counters.PendingBlockOperationCount, - global_ttl: ConfigHelper.parse_time_env_var("CACHE_PBO_COUNT_PERIOD", "20m") + global_ttl: ConfigHelper.parse_time_env_var("CACHE_PENDING_OPERATIONS_COUNT_PERIOD", "5m") + +config :explorer, Explorer.Chain.Cache.Counters.PendingTransactionOperationCount, + global_ttl: ConfigHelper.parse_time_env_var("CACHE_PENDING_OPERATIONS_COUNT_PERIOD", "5m") config :explorer, Explorer.Chain.Cache.GasPriceOracle, global_ttl: ConfigHelper.parse_time_env_var("GAS_PRICE_ORACLE_CACHE_PERIOD", "30s"), @@ -1225,7 +1228,7 @@ config :indexer, Indexer.Fetcher.InternalTransaction, config :indexer, Indexer.Fetcher.InternalTransaction.DeleteQueue, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_CONCURRENCY", 1), - threshold: ConfigHelper.parse_time_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_THRESHOLD", "10m") + threshold: ConfigHelper.parse_time_env_var("INDEXER_INTERNAL_TRANSACTIONS_DELETE_QUEUE_THRESHOLD", "0s") coin_balances_batch_size = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 100) coin_balances_concurrency = ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 79c70d754e93..0c7fb176db1a 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -128,6 +128,7 @@ RELEASE_LINK= # CACHE_ADDRESS_TOKEN_TRANSFERS_COUNTER_PERIOD=3600 # CACHE_TRANSACTIONS_24H_STATS_PERIOD= # CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD= +# CACHE_PENDING_OPERATIONS_COUNT_PERIOD= # TOKEN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD= # COIN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD= # CONTRACT_CODE_ON_DEMAND_FETCHER_THRESHOLD= From 8a4e178c3c981a006a0f056c1ed540e0cd74d66b Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Mon, 19 Jan 2026 21:16:07 +0300 Subject: [PATCH 0600/1006] feat: return timestamps in the event logs list API endpoints (#13779) --- .../controllers/api/v2/address_controller.ex | 3 ++- .../controllers/api/v2/transaction_controller.ex | 3 ++- .../lib/block_scout_web/schemas/api/v2/log.ex | 6 ++++-- .../lib/block_scout_web/views/api/v2/transaction_view.ex | 7 ++++++- .../controllers/api/v2/address_controller_test.exs | 1 + .../controllers/api/v2/transaction_controller_test.exs | 1 + 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index ef4dac14d6d6..0f85998254ff 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -668,7 +668,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do |> paging_options() |> Keyword.merge( necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional, + :block => :optional } ) |> Keyword.merge(@api_true) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 5165584f5aa1..fe344113a32e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -801,7 +801,8 @@ defmodule BlockScoutWeb.API.V2.TransactionController do full_options = [ necessity_by_association: %{ - [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional + [address: [:names, :smart_contract, proxy_implementations_smart_contracts_association()]] => :optional, + :block => :optional } ] |> Keyword.merge(paging_options(params)) diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex index 97fb02286e61..53e0ca1af5fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/log.ex @@ -18,7 +18,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Log do decoded: %Schema{allOf: [General.DecodedLogInput], nullable: true}, smart_contract: %Schema{allOf: [Address], nullable: true}, block_hash: General.FullHash, - block_number: %Schema{type: :integer, nullable: false} + block_number: %Schema{type: :integer, nullable: false}, + block_timestamp: General.TimestampNullable }, required: [ :transaction_hash, @@ -29,7 +30,8 @@ defmodule BlockScoutWeb.Schemas.API.V2.Log do :decoded, :smart_contract, :block_hash, - :block_number + :block_number, + :block_timestamp ], additionalProperties: false }) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index f1d7ce7f30eb..d65180bcdbc5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -366,7 +366,12 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "decoded" => decoded, "smart_contract" => smart_contract_info(transaction_or_hash), "block_number" => log.block_number, - "block_hash" => log.block_hash + "block_hash" => log.block_hash, + "block_timestamp" => + case log.block do + %Block{timestamp: timestamp} -> timestamp + _ -> nil + end } end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index adaf05f8fa29..f8c2ff96d305 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -5624,6 +5624,7 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert to_string(log.transaction_hash) == json["transaction_hash"] assert json["block_number"] == log.block_number assert json["block_hash"] == to_string(log.block_hash) + assert json["block_timestamp"] != nil end defp compare_item(%Withdrawal{} = withdrawal, json) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 10f27d171b2a..9e4e1020be1b 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -2186,6 +2186,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do assert Address.checksum(log.address_hash) == json["address"]["hash"] assert to_string(log.transaction_hash) == json["transaction_hash"] assert json["block_number"] == log.block_number + assert json["block_timestamp"] != nil end defp compare_item(%TokenTransfer{} = token_transfer, json) do From 9c7954a2ad4691281483f62409a2fe66e785b676 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 07:48:20 +0300 Subject: [PATCH 0601/1006] chore(deps-dev): bump wallaby from 0.30.11 to 0.30.12 (#13878) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.lock b/mix.lock index f0811603639a..0298fa064c9a 100644 --- a/mix.lock +++ b/mix.lock @@ -35,7 +35,7 @@ "credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"}, "csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"}, "dataloader": {:hex, :dataloader, "2.0.2", "c45075e0692e68638a315e14f747bd8d7065fb5f38705cf980f62d4cd344401f", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4c6cabc0b55e96e7de74d14bf37f4a5786f0ab69aa06764a1f39dda40079b098"}, - "db_connection": {:hex, :db_connection, "2.8.1", "9abdc1e68c34c6163f6fb96a96532272d13ad7ca45262156ae8b7ec6d9dc4bec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a61a3d489b239d76f326e03b98794fb8e45168396c925ef25feb405ed09da8fd"}, + "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "ddskerl": {:hex, :ddskerl, "0.4.3", "bb97b90eef1b5906520cebdd38da29b18a770b5567f6297927f53566eae9bebb", [:rebar3], [], "hexpm", "4e0f6047c6a002ce38f6ea155276dd918cd635fd0a5edb9e0b46aeb6f7aff2c2"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "decorator": {:hex, :decorator, "1.4.0", "a57ac32c823ea7e4e67f5af56412d12b33274661bb7640ec7fc882f8d23ac419", [:mix], [], "hexpm", "0a07cedd9083da875c7418dea95b78361197cf2bf3211d743f6f7ce39656597f"}, @@ -143,7 +143,7 @@ "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"}, - "postgrex": {:hex, :postgrex, "0.21.1", "2c5cc830ec11e7a0067dd4d623c049b3ef807e9507a424985b8dcf921224cd88", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "27d8d21c103c3cc68851b533ff99eef353e6a0ff98dc444ea751de43eb48bdac"}, + "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, "prometheus": {:hex, :prometheus, "6.1.2", "e5c3c567cd8b0994425920763405635211183c15bdc47d0cd524807dde20ca1d", [:rebar3], [{:ddskerl, "0.4.3", [hex: :ddskerl, repo: "hexpm", optional: false]}], "hexpm", "109662001328112b860de112d49b575310be8ff33fe04bf0482eec4b1e5a1278"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.3", "3dd4da1812b8e0dbee81ea58bb3b62ed7588f2eae0c9e97e434c46807ff82311", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8d66289f77f913b37eda81fd287340c17e61a447549deb28efc254532b2bed82"}, "prometheus_ex": {:hex, :prometheus_ex, "5.0.0", "8e5514e385d5408b559792698dee2cfa60faed3b354d2a1046622923ecad4a68", [:mix], [{:prometheus, "~> 6.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "148bf80b110799362152e4bb154b508cba233456a171b5de6fe97e652bc90019"}, @@ -180,7 +180,7 @@ "unsafe": {:hex, :unsafe, "1.0.2", "23c6be12f6c1605364801f4b47007c0c159497d0446ad378b5cf05f1855c0581", [:mix], [], "hexpm", "b485231683c3ab01a9cd44cb4a79f152c6f3bb87358439c6f68791b85c2df675"}, "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, "vix": {:hex, :vix, "0.35.0", "f6319b715e3b072e53eba456a21af5f2ff010a7a7b19b884600ea98a0609b18c", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "a3e80067a89d0631b6cf2b93594e03c1b303a2c7cddbbdd28040750d521984e5"}, - "wallaby": {:hex, :wallaby, "0.30.11", "93b635bb3d01b11b5a80f83a5d9be59e87ccc8c8390136b7c526dd4fc7e485e3", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "407b50972e3827ce77e3b8292c36dcbd6b21b6837cc4f12ee8767e92a72610ac"}, + "wallaby": {:hex, :wallaby, "0.30.12", "1736bad495b222053a04307e22075bf3a38e7978363294d1eacdea1417bbb208", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0 or ~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "03b33244213b53af7c7e354a6ce129b1ffec9c57cac613eb76543f1fd46fcf70"}, "web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, From 344a89051e843fa4ac0156264d417ef0cf82f609 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 11:11:32 +0300 Subject: [PATCH 0602/1006] chore(deps): bump ex_cldr_units from 3.20.0 to 3.20.1 (#13876) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mix.lock b/mix.lock index 0298fa064c9a..268631633088 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "cldr_utils": {:hex, :cldr_utils, "2.29.1", "11ff0a50a36a7e5f3bd9fc2fb8486a4c1bcca3081d9c080bf9e48fe0e6742e2d", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "3844a0a0ed7f42e6590ddd8bd37eb4b1556b112898f67dea3ba068c29aabd6c2"}, + "cldr_utils": {:hex, :cldr_utils, "2.29.2", "58355887416ac8e015aa2fc1ca23a594e7b3bf670bb860ac61cc635ce83b18e3", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c3e29ea34d88c6ae2e7c35f8db5040a249584f7edd7db697b129ba2bc8f25845"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, @@ -55,11 +55,11 @@ "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, - "ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"}, - "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.16.5", "9b8065605bf0c77b6b07b8fd05d26f34f848b8f5ead2343837f0f4bbf3ac2f10", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4397179028f0a7389de278afd0239771f39ba8d1984ce072bc9b715fa28f30d3"}, + "ex_cldr": {:hex, :ex_cldr, "2.45.2", "3295bf1ffd82768035e5e5a1b4fc646905422eff3874b6b639c3c1288edf8d57", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "82671e5e4e320bfc54d422ce25858f542d593804a3417e664c9e5e5a35f741da"}, + "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.17.0", "c38d76339dbee413f7dd1aba4cdf05758bd4c0bbfe9c3b1c8602f96082c2890a", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "9af59bd29407dcca59fa39ded8c1649ae1cf6ec29fd0611576dcad0279bce0db"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, - "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.37.0", "701922f371d5569f4aef4dd81958231fc15dd9976564251871977f21215f4bc7", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.44", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.16", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "adc011aad34ab545e1d53ae248891479efcd25ba51f662822ec7c5083d0122f8"}, - "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.0", "7ade6ae1017a8f650f83f79a378ad9ad5bcc4eacfa2704349590c1f91f2eac19", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "eb93f99005f8d3f1b1b12c85a45323937350e11d3e79f8375ab51e2ff44392f2"}, + "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.0", "b5564b57d3769c85e16689472a9bb65804f71ccd3484144e31998398fda25ad1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b29e4d723c69db5d0a3f3bcef7583a0bc87dda1cd642187c589fec4bfc59a703"}, + "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.1", "b27ec81814a67ed77d1c06c64ef74519b141497b74244ec18e1a1a2f78f8e313", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "879af22563b06570f16c28bed3decaadc0c1233906f4516b2d5d28e2bbbadee0"}, "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, @@ -103,7 +103,7 @@ "logger_json": {:hex, :logger_json, "7.0.4", "e315f2b9a755504658a745f3eab90d88d2cd7ac2ecfd08c8da94d8893965ab5c", [:mix], [{:decimal, ">= 0.0.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d1369f8094e372db45d50672c3b91e8888bcd695fdc444a37a0734e96717c45c"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, "memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"}, From 2170814edb577dc91c8d0263df8f54e18b31024c Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 20 Jan 2026 12:20:56 +0300 Subject: [PATCH 0603/1006] fix: Clear bytecode for smart-contracts self destructed in a separate transaction (#13834) --- .../import/runner/internal_transactions.ex | 60 +++ ...code_for_selfdestructed_smart_contracts.ex | 152 ++++++ .../runner/internal_transactions_test.exs | 200 ++++++++ ...or_selfdestructed_smart_contracts_test.exs | 457 ++++++++++++++++++ 4 files changed, 869 insertions(+) create mode 100644 apps/explorer/lib/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts.ex create mode 100644 apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index 78b0ee15f0e7..07a6d2e4753e 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -11,6 +11,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain.{ + Address, Block, Hash, Import, @@ -201,6 +202,17 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do :save_zero_value_to_delete ) end) + |> Multi.run(:empty_selfdestructed_contracts_bytecode, fn repo, + %{ + valid_internal_transactions: valid_internal_transactions + } -> + Instrumenter.block_import_stage_runner( + fn -> empty_selfdestructed_contracts_bytecode(repo, valid_internal_transactions, timestamps) end, + :block_pending, + :internal_transactions, + :empty_selfdestructed_contracts_bytecode + ) + end) end def run_insert_only(changes_list, %{timestamps: timestamps} = options) when is_map(options) do @@ -984,6 +996,54 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end + defp empty_selfdestructed_contracts_bytecode(repo, valid_internal_transactions, timestamps) do + # Find all selfdestruct internal transactions + selfdestruct_addresses = + valid_internal_transactions + |> Enum.filter(&(&1.type == :selfdestruct)) + |> Enum.map(&{&1.transaction_hash, &1.from_address_hash}) + |> MapSet.new() + + # Find all create/create2 internal transactions in the same transactions + created_addresses = + valid_internal_transactions + |> Enum.filter(&(&1.type in [:create, :create2])) + |> Enum.map(&{&1.transaction_hash, Map.get(&1, :created_contract_address_hash)}) + |> Enum.reject(fn {_tx_hash, address_hash} -> is_nil(address_hash) end) + |> MapSet.new() + + # Filter to find addresses that were selfdestructed but NOT created in the same transaction + addresses_to_empty = + selfdestruct_addresses + |> Enum.reject(fn {tx_hash, address_hash} -> + MapSet.member?(created_addresses, {tx_hash, address_hash}) + end) + |> Enum.map(fn {_tx_hash, address_hash} -> address_hash end) + |> Enum.uniq() + + if Enum.empty?(addresses_to_empty) do + {:ok, []} + else + # Update the addresses to have empty contract_code + empty_contract_code = %Explorer.Chain.Data{bytes: <<>>} + + update_query = + from( + address in Address, + where: address.hash in ^addresses_to_empty, + update: [set: [contract_code: ^empty_contract_code, updated_at: ^timestamps.updated_at]] + ) + + {count, _} = repo.update_all(update_query, []) + + Logger.info( + "Emptied contract_code for #{count} selfdestructed contracts: #{inspect(addresses_to_empty, limit: :infinity)}" + ) + + {:ok, count} + end + end + defp traceable_blocks_dynamic_query do if RangesHelper.trace_ranges_present?() do block_ranges = RangesHelper.get_trace_block_ranges() diff --git a/apps/explorer/lib/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts.ex b/apps/explorer/lib/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts.ex new file mode 100644 index 000000000000..b9d484934373 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts.ex @@ -0,0 +1,152 @@ +defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContracts do + @moduledoc """ + Finds all existing selfdestruct internal transactions and empties the contract_code + for addresses that still have bytecode, excluding contracts that were created and + selfdestructed in the same transaction. + + This migration processes blocks from the head of the chain down to the first block, + identifying selfdestruct operations and clearing the bytecode of affected contracts. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + + alias Explorer.Chain.{Address, Data, InternalTransaction} + alias Explorer.Chain.Cache.BlockNumber + alias Explorer.Migrator.{FillingMigration, MigrationStatus} + alias Explorer.Repo + + require Logger + + @migration_name "empty_bytecode_for_selfdestructed_smart_contracts" + @empty_contract_code %Data{bytes: <<>>} + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(%{"min_block_number" => min_block_number} = state) + when not is_nil(min_block_number) and min_block_number < 0 do + {[], state} + end + + def last_unprocessed_identifiers(%{"min_block_number" => min_block_number} = state) + when not is_nil(min_block_number) do + limit = batch_size() * concurrency() + from_block_number = min_block_number + to_block_number = max(from_block_number - limit + 1, 0) + + block_numbers = Enum.to_list(from_block_number..to_block_number//-1) + + {block_numbers, %{state | "min_block_number" => to_block_number - 1}} + end + + def last_unprocessed_identifiers(state) do + query = + from( + migration_status in MigrationStatus, + where: migration_status.migration_name == ^@migration_name, + select: migration_status.meta + ) + + meta = Repo.one(query, timeout: :infinity) + + state + |> Map.put("min_block_number", (meta && meta["min_block_number"]) || BlockNumber.get_max()) + |> last_unprocessed_identifiers() + end + + @impl FillingMigration + def unprocessed_data_query, do: nil + + @impl FillingMigration + def update_batch(block_numbers) do + if Enum.empty?(block_numbers) do + {:ok, []} + else + # Find all selfdestruct internal transactions in these blocks + selfdestruct_query = + from( + it in InternalTransaction, + where: it.block_number in ^block_numbers, + where: it.type == :selfdestruct, + select: %{ + transaction_hash: it.transaction_hash, + from_address_hash: it.from_address_hash, + block_number: it.block_number + } + ) + + selfdestruct_transactions = Repo.all(selfdestruct_query, timeout: :infinity) + + if Enum.empty?(selfdestruct_transactions) do + {:ok, []} + else + # Get unique transaction hashes to check for create/create2 + transaction_hashes = + selfdestruct_transactions + |> Enum.map(& &1.transaction_hash) + |> Enum.uniq() + + # Find all create/create2 internal transactions in the same transactions + create_query = + from( + it in InternalTransaction, + where: it.transaction_hash in ^transaction_hashes, + where: it.type in [:create, :create2], + select: %{ + transaction_hash: it.transaction_hash, + created_contract_address_hash: it.created_contract_address_hash + } + ) + + created_contracts = Repo.all(create_query, timeout: :infinity) + + # Build a set of {transaction_hash, address_hash} for contracts created in same tx + created_in_same_tx = + created_contracts + |> Enum.map(&{&1.transaction_hash, &1.created_contract_address_hash}) + |> MapSet.new() + + # Filter to find addresses that were selfdestructed but NOT created in the same transaction + addresses_to_empty = + selfdestruct_transactions + |> Enum.reject(fn sd -> + MapSet.member?(created_in_same_tx, {sd.transaction_hash, sd.from_address_hash}) + end) + |> Enum.map(& &1.from_address_hash) + |> Enum.uniq() + + if Enum.empty?(addresses_to_empty) do + {:ok, []} + else + # Only update addresses that still have non-empty contract_code + update_query = + from( + address in Address, + where: address.hash in ^addresses_to_empty, + where: not is_nil(address.contract_code), + where: fragment("octet_length(?) > 0", address.contract_code), + update: [set: [contract_code: ^@empty_contract_code]], + select: address.hash + ) + + {count, updated_hashes} = Repo.update_all(update_query, [], timeout: :infinity) + + # credo:disable-for-next-line Credo.Check.Refactor.Nesting + if count > 0 do + Logger.info( + "Emptied contract_code for #{count} selfdestructed contracts in blocks #{inspect(block_numbers)}: #{inspect(updated_hashes, limit: :infinity)}" + ) + end + + {:ok, count} + end + end + end + end + + @impl FillingMigration + def update_cache, do: :ok +end diff --git a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs index 5acce9517ef2..73facf6db4a2 100644 --- a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs @@ -359,6 +359,206 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do assert {:ok, _} = run_internal_transactions([internal_transaction_changes]) end + + test "empties contract_code for addresses selfdestructed in different transaction than creation" do + block = insert(:block) + transaction = insert(:transaction) |> with_block(block, status: :ok) + insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + + # Create a contract address with some bytecode + contract_address = insert(:address, contract_code: "0x6080604052") + + # Create a selfdestruct internal transaction + selfdestruct_changes = %{ + block_number: block.number, + from_address_hash: contract_address.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 0, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + assert {:ok, _} = run_internal_transactions([selfdestruct_changes]) + + # Verify the contract_code was emptied + updated_address = Repo.get(Explorer.Chain.Address, contract_address.hash) + assert %Data{bytes: <<>>} = updated_address.contract_code + end + + test "does not empty contract_code when contract is created and selfdestructed in same transaction" do + block = insert(:block) + transaction = insert(:transaction) |> with_block(block, status: :ok) + insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + + # Create a contract address + contract_address = insert(:address, contract_code: "0x6080604052") + + # Create internal transaction: first create the contract + create_changes = %{ + block_number: block.number, + created_contract_address_hash: contract_address.hash, + created_contract_code: %Data{bytes: <<0x60, 0x80, 0x60, 0x40, 0x52>>}, + from_address_hash: insert(:address).hash, + gas: 50000, + gas_used: 25000, + index: 0, + init: %Data{bytes: <<1, 2, 3>>}, + trace_address: [], + transaction_hash: transaction.hash, + type: :create, + value: Wei.from(Decimal.new(0), :wei) + } + + # Then selfdestruct it in the same transaction + selfdestruct_changes = %{ + block_number: block.number, + from_address_hash: contract_address.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 1, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + assert {:ok, _} = run_internal_transactions([create_changes, selfdestruct_changes]) + + # Verify the contract_code was NOT emptied (contract created and destroyed in same tx) + updated_address = Repo.get(Explorer.Chain.Address, contract_address.hash) + assert updated_address.contract_code != %Data{bytes: <<>>} + end + + test "empties contract_code for multiple selfdestructed contracts" do + block = insert(:block) + transaction = insert(:transaction) |> with_block(block, status: :ok) + insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + + # Create multiple contract addresses with bytecode + contract_address_1 = insert(:address, contract_code: "0x6080604052") + contract_address_2 = insert(:address, contract_code: "0x608060405260") + contract_address_3 = insert(:address, contract_code: "0x60806040") + + # Create selfdestruct internal transactions for multiple contracts + selfdestruct_changes_1 = %{ + block_number: block.number, + from_address_hash: contract_address_1.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 0, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + selfdestruct_changes_2 = %{ + block_number: block.number, + from_address_hash: contract_address_2.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 1, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + selfdestruct_changes_3 = %{ + block_number: block.number, + from_address_hash: contract_address_3.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 2, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + assert {:ok, _} = + run_internal_transactions([ + selfdestruct_changes_1, + selfdestruct_changes_2, + selfdestruct_changes_3 + ]) + + # Verify all contract_codes were emptied + assert %Data{bytes: <<>>} = Repo.get(Explorer.Chain.Address, contract_address_1.hash).contract_code + assert %Data{bytes: <<>>} = Repo.get(Explorer.Chain.Address, contract_address_2.hash).contract_code + assert %Data{bytes: <<>>} = Repo.get(Explorer.Chain.Address, contract_address_3.hash).contract_code + end + + test "does not empty contract_code when only create2 and selfdestruct in same transaction" do + block = insert(:block) + transaction = insert(:transaction) |> with_block(block, status: :ok) + insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + + # Create a contract address + contract_address = insert(:address, contract_code: "0x6080604052") + + # Create internal transaction: first create2 the contract + create2_changes = %{ + block_number: block.number, + created_contract_address_hash: contract_address.hash, + created_contract_code: %Data{bytes: <<0x60, 0x80, 0x60, 0x40, 0x52>>}, + from_address_hash: insert(:address).hash, + gas: 50000, + gas_used: 25000, + index: 0, + init: %Data{bytes: <<1, 2, 3>>}, + trace_address: [], + transaction_hash: transaction.hash, + type: :create2, + value: Wei.from(Decimal.new(0), :wei) + } + + # Then selfdestruct it in the same transaction + selfdestruct_changes = %{ + block_number: block.number, + from_address_hash: contract_address.hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 1, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + assert {:ok, _} = run_internal_transactions([create2_changes, selfdestruct_changes]) + + # Verify the contract_code was NOT emptied (contract created and destroyed in same tx) + updated_address = Repo.get(Explorer.Chain.Address, contract_address.hash) + assert updated_address.contract_code != %Data{bytes: <<>>} + end + + test "handles selfdestruct with no matching address gracefully" do + block = insert(:block) + transaction = insert(:transaction) |> with_block(block, status: :ok) + insert(:pending_block_operation, block_hash: transaction.block_hash, block_number: transaction.block_number) + + # Create a selfdestruct for a non-existent address + non_existent_address_hash = insert(:address).hash + + selfdestruct_changes = %{ + block_number: block.number, + from_address_hash: non_existent_address_hash, + to_address_hash: insert(:address).hash, + gas: nil, + index: 0, + trace_address: [], + transaction_hash: transaction.hash, + type: :selfdestruct, + value: Wei.from(Decimal.new(0), :wei) + } + + # Should not raise an error + assert {:ok, _} = run_internal_transactions([selfdestruct_changes]) + end end defp run_internal_transactions(changes_list, multi \\ Multi.new()) when is_list(changes_list) do diff --git a/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs b/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs new file mode 100644 index 000000000000..29d6bd1dfcc8 --- /dev/null +++ b/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs @@ -0,0 +1,457 @@ +defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do + use Explorer.DataCase, async: false + + alias Explorer.Chain.{Address, Data} + alias Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContracts + alias Explorer.Repo + + describe "migration_name/0" do + test "returns the correct migration name" do + assert EmptyBytecodeForSelfdestructedSmartContracts.migration_name() == + "empty_bytecode_for_selfdestructed_smart_contracts" + end + end + + describe "update_batch/1" do + test "returns {:ok, []} when block_numbers is empty" do + assert {:ok, []} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([]) + end + + test "returns {:ok, []} when no selfdestruct transactions exist in blocks" do + block = insert(:block, number: 100) + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + type: :call + ) + + assert {:ok, []} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + end + + test "empties contract_code for address with selfdestruct transaction" do + block = insert(:block, number: 200) + contract_code = %Data{bytes: <<1, 2, 3, 4, 5>>} + + # Create a contract with bytecode + contract_address = insert(:address, contract_code: contract_code) + recipient_address = insert(:address) + + # Create a transaction with selfdestruct + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + assert {:ok, 1} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code is now empty + updated_address = Repo.get(Address, contract_address.hash) + assert updated_address.contract_code == %Data{bytes: <<>>} + end + + test "does NOT empty contract_code when contract created and selfdestructed in same transaction" do + block = insert(:block, number: 300) + contract_code = %Data{bytes: <<1, 2, 3, 4, 5>>} + + # Create a contract with bytecode + contract_address = insert(:address, contract_code: contract_code) + recipient_address = insert(:address) + + # Create a transaction with both create and selfdestruct + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction_create, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + created_contract_address: contract_address, + created_contract_code: contract_code + ) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 1, + block_index: 1, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + assert {:ok, []} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code is still present + updated_address = Repo.get(Address, contract_address.hash) + assert updated_address.contract_code == contract_code + end + + test "does NOT empty contract_code when contract created with create2 and selfdestructed in same transaction" do + block = insert(:block, number: 350) + contract_code = %Data{bytes: <<1, 2, 3, 4, 5>>} + + # Create a contract with bytecode + contract_address = insert(:address, contract_code: contract_code) + recipient_address = insert(:address) + + # Create a transaction with both create2 and selfdestruct + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction_create, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + type: :create2, + created_contract_address: contract_address, + created_contract_code: contract_code + ) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 1, + block_index: 1, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + assert {:ok, []} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code is still present + updated_address = Repo.get(Address, contract_address.hash) + assert updated_address.contract_code == contract_code + end + + test "handles multiple selfdestruct transactions in same block" do + block = insert(:block, number: 400) + contract_code_1 = %Data{bytes: <<1, 2, 3>>} + contract_code_2 = %Data{bytes: <<4, 5, 6>>} + + # Create two contracts with bytecode + contract_address_1 = insert(:address, contract_code: contract_code_1) + contract_address_2 = insert(:address, contract_code: contract_code_2) + recipient_address_1 = insert(:address) + recipient_address_2 = insert(:address) + + # Create separate transactions for each selfdestruct + transaction_1 = insert(:transaction, gas: 21_000) |> with_block(block) + transaction_2 = insert(:transaction, gas: 22_000) |> with_block(block) + + insert(:internal_transaction, + transaction: transaction_1, + block_hash: transaction_1.block_hash, + block_number: transaction_1.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address_1, + to_address: recipient_address_1, + gas: nil + ) + + insert(:internal_transaction, + transaction: transaction_2, + block_hash: transaction_2.block_hash, + block_number: transaction_2.block_number, + index: 0, + block_index: 1, + type: :selfdestruct, + from_address: contract_address_2, + to_address: recipient_address_2, + gas: nil + ) + + assert {:ok, 2} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify both contracts have empty contract_code + updated_address_1 = Repo.get(Address, contract_address_1.hash) + updated_address_2 = Repo.get(Address, contract_address_2.hash) + assert updated_address_1.contract_code == %Data{bytes: <<>>} + assert updated_address_2.contract_code == %Data{bytes: <<>>} + end + + test "does NOT update address that already has empty contract_code" do + block = insert(:block, number: 500) + empty_contract_code = %Data{bytes: <<>>} + + # Create a contract with already empty bytecode + contract_address = insert(:address, contract_code: empty_contract_code) + recipient_address = insert(:address) + + # Create a transaction with selfdestruct + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + assert {:ok, 0} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code remains empty (no update occurred) + updated_address = Repo.get(Address, contract_address.hash) + assert updated_address.contract_code == empty_contract_code + end + + test "does NOT update address with nil contract_code" do + block = insert(:block, number: 550) + + # Create a regular address (not a contract, contract_code is nil) + address = insert(:address, contract_code: nil) + recipient_address = insert(:address) + + # Create a transaction with selfdestruct + transaction = insert(:transaction) |> with_block(block) + + insert(:internal_transaction, + transaction: transaction, + block_hash: transaction.block_hash, + block_number: transaction.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: address, + to_address: recipient_address, + gas: nil + ) + + assert {:ok, 0} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code remains nil + updated_address = Repo.get(Address, address.hash) + assert updated_address.contract_code == nil + end + + test "handles multiple blocks in one batch" do + block_1 = insert(:block, number: 600) + block_2 = insert(:block, number: 601) + contract_code_1 = %Data{bytes: <<1, 2, 3>>} + contract_code_2 = %Data{bytes: <<4, 5, 6>>} + + # Create two contracts with bytecode + contract_address_1 = insert(:address, contract_code: contract_code_1) + contract_address_2 = insert(:address, contract_code: contract_code_2) + recipient_address_1 = insert(:address) + recipient_address_2 = insert(:address) + + # Create selfdestruct in first block + transaction_1 = insert(:transaction, gas: 21_000) |> with_block(block_1) + + insert(:internal_transaction, + transaction: transaction_1, + block_hash: transaction_1.block_hash, + block_number: transaction_1.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address_1, + to_address: recipient_address_1, + gas: nil + ) + + # Create selfdestruct in second block + transaction_2 = insert(:transaction, gas: 22_000) |> with_block(block_2) + + insert(:internal_transaction, + transaction: transaction_2, + block_hash: transaction_2.block_hash, + block_number: transaction_2.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address_2, + to_address: recipient_address_2, + gas: nil + ) + + assert {:ok, 2} = + EmptyBytecodeForSelfdestructedSmartContracts.update_batch([ + block_1.number, + block_2.number + ]) + + # Verify both contracts have empty contract_code + updated_address_1 = Repo.get(Address, contract_address_1.hash) + updated_address_2 = Repo.get(Address, contract_address_2.hash) + assert updated_address_1.contract_code == %Data{bytes: <<>>} + assert updated_address_2.contract_code == %Data{bytes: <<>>} + end + + test "handles mixed scenario: one should empty, one should not" do + block = insert(:block, number: 700) + contract_code_1 = %Data{bytes: <<1, 2, 3>>} + contract_code_2 = %Data{bytes: <<4, 5, 6>>} + + # Create two contracts + contract_address_1 = insert(:address, contract_code: contract_code_1) + contract_address_2 = insert(:address, contract_code: contract_code_2) + recipient_address_1 = insert(:address) + recipient_address_2 = insert(:address) + + # First transaction: selfdestruct without create (should empty) + transaction_1 = insert(:transaction, gas: 21_000) |> with_block(block) + + insert(:internal_transaction, + transaction_hash: transaction_1.hash, + block_hash: transaction_1.block_hash, + block_number: transaction_1.block_number, + index: 0, + block_index: 0, + type: :selfdestruct, + from_address: contract_address_1, + to_address: recipient_address_1, + gas: nil + ) + + # Second transaction: create + selfdestruct (should NOT empty) + transaction_2 = insert(:transaction, gas: 22_000) |> with_block(block) + + insert(:internal_transaction_create, + transaction_hash: transaction_2.hash, + block_hash: transaction_2.block_hash, + block_number: transaction_2.block_number, + index: 0, + transaction_index: transaction_2.index, + block_index: 1, + created_contract_address: contract_address_2, + created_contract_code: contract_code_2 + ) + + insert(:internal_transaction, + transaction_hash: transaction_2.hash, + block_hash: transaction_2.block_hash, + block_number: transaction_2.block_number, + index: 1, + transaction_index: transaction_2.index, + block_index: 2, + type: :selfdestruct, + from_address: contract_address_2, + to_address: recipient_address_2, + gas: nil + ) + + assert {:ok, 1} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify first contract is empty, second is not + updated_address_1 = Repo.get(Address, contract_address_1.hash) + updated_address_2 = Repo.get(Address, contract_address_2.hash) + assert updated_address_1.contract_code == %Data{bytes: <<>>} + assert updated_address_2.contract_code == contract_code_2 + end + + test "handles same contract selfdestructed multiple times in different transactions" do + block = insert(:block, number: 800) + contract_code = %Data{bytes: <<1, 2, 3, 4, 5>>} + + # Create a contract with bytecode + contract_address = insert(:address, contract_code: contract_code) + recipient_address = insert(:address) + + # Create two different transactions with selfdestruct for the same address + transaction_1 = insert(:transaction, gas: 21_000) |> with_block(block) + transaction_2 = insert(:transaction, gas: 22_000) |> with_block(block) + + insert(:internal_transaction, + transaction_hash: transaction_1.hash, + block_hash: transaction_1.block_hash, + block_number: transaction_1.block_number, + index: 0, + transaction_index: transaction_1.index, + block_index: 0, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + insert(:internal_transaction, + transaction_hash: transaction_2.hash, + block_hash: transaction_2.block_hash, + block_number: transaction_2.block_number, + index: 0, + transaction_index: transaction_2.index, + block_index: 1, + type: :selfdestruct, + from_address: contract_address, + to_address: recipient_address, + gas: nil + ) + + # Should still only update once since it's the same address + assert {:ok, 1} = EmptyBytecodeForSelfdestructedSmartContracts.update_batch([block.number]) + + # Verify contract_code is empty + updated_address = Repo.get(Address, contract_address.hash) + assert updated_address.contract_code == %Data{bytes: <<>>} + end + end + + describe "last_unprocessed_identifiers/1" do + test "returns empty list when min_block_number is negative" do + state = %{"min_block_number" => -1} + + assert {[], %{"min_block_number" => -1}} = + EmptyBytecodeForSelfdestructedSmartContracts.last_unprocessed_identifiers(state) + end + + test "returns block numbers in descending order" do + state = %{"min_block_number" => 100} + + {block_numbers, new_state} = + EmptyBytecodeForSelfdestructedSmartContracts.last_unprocessed_identifiers(state) + + # Should return blocks in descending order + assert length(block_numbers) > 0 + assert block_numbers == Enum.sort(block_numbers, :desc) + assert hd(block_numbers) == 100 + assert new_state["min_block_number"] < 100 + end + + test "handles blocks down to zero" do + # Set a small number to test boundary + state = %{"min_block_number" => 5} + + {block_numbers, new_state} = + EmptyBytecodeForSelfdestructedSmartContracts.last_unprocessed_identifiers(state) + + assert length(block_numbers) > 0 + assert Enum.min(block_numbers) >= 0 + assert new_state["min_block_number"] <= 0 + end + end + + describe "update_cache/0" do + test "returns :ok" do + assert :ok = EmptyBytecodeForSelfdestructedSmartContracts.update_cache() + end + end +end From 082763edb61fff8674f75379fa01471b43c0c428 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 14:32:45 +0300 Subject: [PATCH 0604/1006] chore(deps): bump open_api_spex from 3.22.1 to 3.22.2 (#13879) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 268631633088..6094b57cbb22 100644 --- a/mix.lock +++ b/mix.lock @@ -125,7 +125,7 @@ "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, "nx": {:hex, :nx, "0.10.0", "128e4a094cb790f663e20e1334b127c1f2a4df54edfb8b13c22757ec33133b4f", [:mix], [{:complex, "~> 0.6", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3db8892c124aeee091df0e6fbf8e5bf1b81f502eb0d4f5ba63e6378ebcae7da4"}, "oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"}, - "open_api_spex": {:hex, :open_api_spex, "3.22.1", "38c99d8bf107dc7ffb112dc669f33e0a287396c6c9fd6bb7eeb27e3fe8dbba0e", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "fa51ecd04ececbad89a8ede55ebd9db7aa9e55cc7ddbb46455522e0f3c098290"}, + "open_api_spex": {:hex, :open_api_spex, "3.22.2", "0b3c4f572ee69cb6c936abf426b9d84d8eebd34960871fd77aead746f0d69cb0", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "0a4fc08472d75e9cfe96e0748c6b1565b3b4398f97bf43fcce41b41b6fd3fb33"}, "optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"}, "parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"}, "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, From ab574e2be82e0aceea3976757429cee566779ab6 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Tue, 20 Jan 2026 18:48:04 +0300 Subject: [PATCH 0605/1006] fix: Actualize indexer tests (#13887) --- .../runner/internal_transactions_test.exs | 27 +++++++++++------ ...or_selfdestructed_smart_contracts_test.exs | 30 ++++++++----------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs index 73facf6db4a2..a6dfac8d47f7 100644 --- a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs @@ -374,9 +374,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 0, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 0, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -404,10 +405,11 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: insert(:address).hash, gas: 50000, gas_used: 25000, - index: 0, init: %Data{bytes: <<1, 2, 3>>}, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 0, type: :create, value: Wei.from(Decimal.new(0), :wei) } @@ -418,9 +420,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 1, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 1, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -448,9 +451,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address_1.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 0, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 0, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -460,9 +464,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address_2.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 1, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 1, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -472,9 +477,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address_3.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 2, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 2, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -509,9 +515,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do gas: 50000, gas_used: 25000, index: 0, - init: %Data{bytes: <<1, 2, 3>>}, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + init: %Data{bytes: <<1, 2, 3>>}, type: :create2, value: Wei.from(Decimal.new(0), :wei) } @@ -522,9 +529,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: contract_address.hash, to_address_hash: insert(:address).hash, gas: nil, - index: 1, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 1, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } @@ -549,9 +557,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do from_address_hash: non_existent_address_hash, to_address_hash: insert(:address).hash, gas: nil, - index: 0, trace_address: [], transaction_hash: transaction.hash, + transaction_index: transaction.index, + index: 0, type: :selfdestruct, value: Wei.from(Decimal.new(0), :wei) } diff --git a/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs b/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs index 29d6bd1dfcc8..24de2c157fa0 100644 --- a/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs +++ b/apps/explorer/test/explorer/migrator/empty_bytecode_for_selfdestructed_smart_contracts_test.exs @@ -26,7 +26,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, type: :call ) @@ -49,7 +49,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, @@ -79,7 +79,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, created_contract_address: contract_address, created_contract_code: contract_code ) @@ -89,7 +89,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 1, - block_index: 1, + transaction_index: transaction.index, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, @@ -119,7 +119,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, type: :create2, created_contract_address: contract_address, created_contract_code: contract_code @@ -130,7 +130,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 1, - block_index: 1, + transaction_index: transaction.index, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, @@ -164,7 +164,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction_1.block_hash, block_number: transaction_1.block_number, index: 0, - block_index: 0, + transaction_index: transaction_1.index, type: :selfdestruct, from_address: contract_address_1, to_address: recipient_address_1, @@ -176,7 +176,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction_2.block_hash, block_number: transaction_2.block_number, index: 0, - block_index: 1, + transaction_index: transaction_2.index, type: :selfdestruct, from_address: contract_address_2, to_address: recipient_address_2, @@ -208,7 +208,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, @@ -237,7 +237,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction.block_hash, block_number: transaction.block_number, index: 0, - block_index: 0, + transaction_index: transaction.index, type: :selfdestruct, from_address: address, to_address: recipient_address, @@ -271,7 +271,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction_1.block_hash, block_number: transaction_1.block_number, index: 0, - block_index: 0, + transaction_index: transaction_1.index, type: :selfdestruct, from_address: contract_address_1, to_address: recipient_address_1, @@ -286,7 +286,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction_2.block_hash, block_number: transaction_2.block_number, index: 0, - block_index: 0, + transaction_index: transaction_2.index, type: :selfdestruct, from_address: contract_address_2, to_address: recipient_address_2, @@ -325,7 +325,7 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_hash: transaction_1.block_hash, block_number: transaction_1.block_number, index: 0, - block_index: 0, + transaction_index: transaction_1.index, type: :selfdestruct, from_address: contract_address_1, to_address: recipient_address_1, @@ -341,7 +341,6 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_number: transaction_2.block_number, index: 0, transaction_index: transaction_2.index, - block_index: 1, created_contract_address: contract_address_2, created_contract_code: contract_code_2 ) @@ -352,7 +351,6 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_number: transaction_2.block_number, index: 1, transaction_index: transaction_2.index, - block_index: 2, type: :selfdestruct, from_address: contract_address_2, to_address: recipient_address_2, @@ -386,7 +384,6 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_number: transaction_1.block_number, index: 0, transaction_index: transaction_1.index, - block_index: 0, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, @@ -399,7 +396,6 @@ defmodule Explorer.Migrator.EmptyBytecodeForSelfdestructedSmartContractsTest do block_number: transaction_2.block_number, index: 0, transaction_index: transaction_2.index, - block_index: 1, type: :selfdestruct, from_address: contract_address, to_address: recipient_address, From 426e4b5a3724cb21c2a1eb697c27e41916739503 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:41:34 +0300 Subject: [PATCH 0606/1006] feat: distributed cache (#13698) --- .github/workflows/config.yml | 1 + apps/block_scout_web/config/config.exs | 3 +- .../controllers/transaction_controller.ex | 93 +--------- .../api/v2/main_page_controller_test.exs | 4 +- .../api/v2/transaction_controller_test.exs | 4 +- .../transaction_controller_test.exs | 137 -------------- .../features/viewing_transactions_test.exs | 170 ------------------ .../routers/chain_type_scope_test.exs | 5 + apps/explorer/lib/explorer.ex | 15 +- apps/explorer/lib/explorer/application.ex | 2 - apps/explorer/lib/explorer/chain.ex | 2 +- .../lib/explorer/chain/cache/transactions.ex | 5 +- .../chain/cache/transactions_api_v2.ex | 27 --- .../lib/explorer/chain/ordered_cache.ex | 70 ++++++-- .../lib/explorer/chain/transaction.ex | 128 ++----------- .../explorer/lib/explorer/chain/withdrawal.ex | 12 +- apps/explorer/lib/explorer/market/market.ex | 3 +- .../chain/cache/transactions_test.exs | 12 +- .../catchup/bound_interval_supervisor.ex | 2 +- .../block/catchup/massive_blocks_fetcher.ex | 34 +++- .../lib/indexer/block/catchup/supervisor.ex | 11 +- apps/indexer/lib/indexer/block/fetcher.ex | 75 ++++++-- .../lib/indexer/block/fetcher/receipts.ex | 2 +- .../lib/indexer/block/realtime/fetcher.ex | 8 +- .../lib/indexer/block/realtime/supervisor.ex | 14 +- apps/indexer/lib/indexer/supervisor.ex | 1 + .../bound_interval_supervisor_test.exs | 33 +++- .../indexer/block/catchup/fetcher_test.exs | 128 ++++++------- .../indexer/block/fetcher/receipts_test.exs | 3 + .../test/indexer/block/fetcher_test.exs | 26 ++- .../indexer/block/realtime/fetcher_test.exs | 17 +- .../test/indexer/fetcher/uncle_block_test.exs | 15 +- config/runtime.exs | 16 +- cspell.json | 1 + 34 files changed, 366 insertions(+), 713 deletions(-) delete mode 100644 apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs delete mode 100644 apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs delete mode 100644 apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index afdb51c01855..baac41c80403 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -827,3 +827,4 @@ jobs: CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} WETH_TOKEN_TRANSFERS_FILTERING_ENABLED: "true" BRIDGED_TOKENS_ENABLED: ${{ matrix.bridged-tokens }} + DISABLE_WEBAPP: "false" diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 1f73918bc114..83b4b2468f06 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -69,7 +69,8 @@ config :block_scout_web, BlockScoutWeb.Routers.ApiRouter, writing_enabled: !ConfigHelper.parse_bool_env_var("API_V1_WRITE_METHODS_DISABLED"), reading_enabled: !ConfigHelper.parse_bool_env_var("API_V1_READ_METHODS_DISABLED") -config :block_scout_web, BlockScoutWeb.Routers.WebRouter, enabled: !ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP") +config :block_scout_web, BlockScoutWeb.Routers.WebRouter, + enabled: !ConfigHelper.parse_bool_env_var("DISABLE_WEBAPP", "true") config :block_scout_web, BlockScoutWeb.CSPHeader, mixpanel_url: ConfigHelper.parse_url_env_var("MIXPANEL_URL", "https://api-js.mixpanel.com"), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index 6fa1798d7136..7444cac188d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -2,19 +2,8 @@ defmodule BlockScoutWeb.TransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - - import BlockScoutWeb.Chain, - only: [ - fetch_page_number: 1, - paging_options: 1, - next_page_params: 3, - update_page_parameters: 3, - split_list_by_page: 1 - ] - import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] import BlockScoutWeb.Models.GetTransactionTags, only: [get_transaction_with_addresses_tags: 2] - import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] alias BlockScoutWeb.{ AccessHelper, @@ -26,8 +15,7 @@ defmodule BlockScoutWeb.TransactionController do alias Explorer.{Chain, Market} alias Explorer.Chain.Cache.Counters.TransactionsCount - alias Explorer.Chain.{DenormalizationHelper, Transaction} - alias Phoenix.View + alias Explorer.Chain.Transaction @necessity_by_association %{ :block => :optional, @@ -38,85 +26,6 @@ defmodule BlockScoutWeb.TransactionController do :token_transfers => :optional } - {:ok, burn_address_hash} = Chain.string_to_address_hash(burn_address_hash_string()) - @burn_address_hash burn_address_hash - - @default_options [ - necessity_by_association: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - [created_contract_address: :smart_contract] => :optional, - [from_address: :smart_contract] => :optional, - [to_address: :smart_contract] => :optional - } - ] - - def index(conn, %{"type" => "JSON"} = params) do - options = - @default_options - |> DenormalizationHelper.extend_block_necessity(:required) - |> Keyword.merge(paging_options(params)) - - full_options = - options - |> Keyword.put( - :paging_options, - params - |> fetch_page_number() - |> update_page_parameters(Chain.default_page_size(), Keyword.get(options, :paging_options)) - ) - - %{total_transactions_count: transactions_count, transactions: transactions_plus_one} = - Transaction.recent_collated_transactions_for_rap(full_options) - - {transactions, next_page} = - if fetch_page_number(params) == 1 do - split_list_by_page(transactions_plus_one) - else - {transactions_plus_one, nil} - end - - next_page_params = - if fetch_page_number(params) == 1 do - page_size = Chain.default_page_size() - - pages_limit = transactions_count |> Kernel./(page_size) |> Float.ceil() |> trunc() - - case next_page_params(next_page, transactions, params) do - nil -> - nil - - next_page_params -> - next_page_params - |> Map.delete("type") - |> Map.delete("items_count") - |> Map.put("pages_limit", pages_limit) - |> Map.put("page_size", page_size) - |> Map.put("page_number", 1) - end - else - Map.delete(params, "type") - end - - json( - conn, - %{ - items: - Enum.map(transactions, fn transaction -> - View.render_to_string( - TransactionView, - "_tile.html", - transaction: transaction, - burn_address_hash: @burn_address_hash, - conn: conn - ) - end), - next_page_params: next_page_params - } - ) - end - def index(conn, _params) do transaction_estimated_count = TransactionsCount.get() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs index 3445aa5699ae..e76eeed5c514 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs @@ -10,8 +10,8 @@ defmodule BlockScoutWeb.API.V2.MainPageControllerTest do setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) - Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) - Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) :ok end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 9e4e1020be1b..4c3a8aaf5517 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -16,8 +16,8 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do require Logger setup do - Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) - Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id()) + Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) + Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id()) :ok end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs deleted file mode 100644 index 99d09fcb44a8..000000000000 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs +++ /dev/null @@ -1,137 +0,0 @@ -defmodule BlockScoutWeb.TransactionControllerTest do - use BlockScoutWeb.ConnCase - - import BlockScoutWeb.Routers.WebRouter.Helpers, - only: [transaction_path: 3] - - alias Explorer.Chain.Transaction - - describe "GET index/2" do - test "returns a collated transactions", %{conn: conn} do - :transaction - |> insert() - |> with_block() - - conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) - - transactions_html = - conn - |> json_response(200) - |> Map.get("items") - - assert length(transactions_html) == 1 - end - - test "returns a count of transactions", %{conn: conn} do - :transaction - |> insert() - |> with_block() - - conn = get(conn, "/txs") - - assert is_integer(conn.assigns.transaction_estimated_count) - end - - test "excludes pending transactions", %{conn: conn} do - %Transaction{hash: transaction_hash} = - :transaction - |> insert() - |> with_block() - - %Transaction{hash: pending_transaction_hash} = insert(:transaction) - - conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) - - transactions_html = - conn - |> json_response(200) - |> Map.get("items") - - assert Enum.any?(transactions_html, &String.contains?(&1, to_string(transaction_hash))) - refute Enum.any?(transactions_html, &String.contains?(&1, to_string(pending_transaction_hash))) - end - - test "returns next page of results based on last seen transaction", %{conn: conn} do - second_page_hashes = - 50 - |> insert_list(:transaction) - |> with_block() - |> Enum.map(&to_string(&1.hash)) - - %Transaction{block_number: block_number, index: index} = - :transaction - |> insert() - |> with_block() - - conn = - get( - conn, - transaction_path(conn, :index, %{ - "type" => "JSON", - "block_number" => Integer.to_string(block_number), - "index" => Integer.to_string(index) - }) - ) - - transactions_html = - conn - |> json_response(200) - |> Map.get("items") - - assert length(second_page_hashes) == length(transactions_html) - end - - test "next_page_params exist if not on last page", %{conn: conn} do - address = insert(:address) - block = insert(:block) - - 60 - |> insert_list(:transaction, from_address: address) - |> with_block(block) - - conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) - - assert conn |> json_response(200) |> Map.get("next_page_params") - end - - test "next_page_params are empty if on last page", %{conn: conn} do - address = insert(:address) - - :transaction - |> insert(from_address: address) - |> with_block() - - conn = get(conn, transaction_path(conn, :index, %{"type" => "JSON"})) - - refute conn |> json_response(200) |> Map.get("next_page_path") - end - - test "works when there are no transactions", %{conn: conn} do - conn = get(conn, "/txs") - - assert html_response(conn, 200) - end - end - - describe "GET show/3" do - test "responds with 404 with the transaction missing", %{conn: conn} do - hash = transaction_hash() - conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, hash)) - - assert html_response(conn, 404) - end - - test "responds with 422 when the hash is invalid", %{conn: conn} do - conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, "wrong")) - - assert html_response(conn, 422) - end - - test "no redirect from transaction page", %{conn: conn} do - transaction = insert(:transaction) - conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, transaction)) - - assert html_response(conn, 200) - end - end -end diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs deleted file mode 100644 index b5dff64ff317..000000000000 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs +++ /dev/null @@ -1,170 +0,0 @@ -defmodule BlockScoutWeb.ViewingTransactionsTest do - @moduledoc false - - import Mox - - use BlockScoutWeb.FeatureCase, async: false - - alias BlockScoutWeb.{AddressPage, TransactionListPage, TransactionLogsPage, TransactionPage} - alias Explorer.Chain.Wei - - setup :set_mox_global - - setup do - block = - insert(:block, %{ - timestamp: Timex.now() |> Timex.shift(hours: -2), - gas_used: 123_987 - }) - - 3 - |> insert_list(:transaction) - |> with_block() - - pending = insert(:transaction, block_hash: nil, gas: 5891, index: nil) - pending_contract = insert(:transaction, to_address: nil, block_hash: nil, gas: 5891, index: nil) - - lincoln = insert(:address) - taft = insert(:address) - - # From Lincoln to Taft. - transaction_from_lincoln = - :transaction - |> insert(from_address: lincoln, to_address: taft) - |> with_block(block) - - transaction = - :transaction - |> insert( - value: Wei.from(Decimal.new(5656), :ether), - gas: Decimal.new(1_230_000_000_000_123_123), - gas_price: Decimal.new(7_890_000_000_898_912_300_045), - input: "0x000012", - nonce: 99045, - inserted_at: Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}"), - updated_at: Timex.parse!("1980-01-01T00:00:18-00:00", "{ISO:Extended}"), - from_address: taft, - to_address: lincoln - ) - |> with_block(block, gas_used: Decimal.new(1_230_000_000_000_123_000), status: :ok) - - insert(:log, address: lincoln, index: 0, transaction: transaction, block: block, block_number: block.number) - - internal = - insert(:internal_transaction, - index: 0, - transaction: transaction, - transaction_index: transaction.index, - block_hash: transaction.block_hash, - block_number: transaction.block_number - ) - - {:ok, - %{ - pending: pending, - pending_contract: pending_contract, - internal: internal, - lincoln: lincoln, - taft: taft, - transaction: transaction, - transaction_from_lincoln: transaction_from_lincoln - }} - end - - describe "viewing transaction lists" do - test "viewing the default transactions tab", %{ - session: session, - transaction: transaction, - pending: pending - } do - session - |> TransactionListPage.visit_page() - |> assert_has(TransactionListPage.transaction(transaction)) - |> assert_has(TransactionListPage.transaction_status(transaction)) - |> refute_has(TransactionListPage.transaction(pending)) - end - - test "viewing the pending transactions list", %{ - pending: pending, - pending_contract: pending_contract, - session: session - } do - session - |> TransactionListPage.visit_pending_transactions_page() - |> assert_has(TransactionListPage.transaction(pending)) - |> assert_has(TransactionListPage.transaction(pending_contract)) - |> assert_has(TransactionListPage.transaction_status(pending_contract)) - end - - test "contract creation is shown for to_address on list page", %{session: session} do - contract_address = insert(:contract_address) - - transaction = - :transaction - |> insert(to_address: nil) - |> with_block() - |> with_contract_creation(contract_address) - - :internal_transaction_create - |> insert( - transaction: transaction, - index: 0, - block_hash: transaction.block_hash, - block_number: transaction.block_number, - transaction_index: transaction.index - ) - |> with_contract_creation(contract_address) - - session - |> TransactionListPage.visit_page() - |> assert_has(TransactionListPage.contract_creation(transaction)) - end - end - - describe "viewing a pending transaction page" do - test "can see a pending transaction's details", %{session: session, pending: pending} do - EthereumJSONRPC.Mox - |> expect(:json_rpc, fn %{id: _id, method: "net_version", params: []}, _options -> - {:ok, "100"} - end) - - session - |> TransactionPage.visit_page(pending) - |> assert_has(TransactionPage.detail_hash(pending)) - |> assert_has(TransactionPage.is_pending()) - end - end - - describe "viewing a transaction page" do - test "can navigate to transaction show from list page", %{session: session, transaction: transaction} do - session - |> TransactionListPage.visit_page() - |> TransactionListPage.click_transaction(transaction) - |> assert_has(TransactionPage.detail_hash(transaction)) - end - - test "can see a transaction's details", %{session: session, transaction: transaction} do - session - |> TransactionPage.visit_page(transaction) - |> assert_has(TransactionPage.detail_hash(transaction)) - end - - test "can view a transaction's logs", %{session: session, transaction: transaction} do - session - |> TransactionPage.visit_page(transaction) - |> TransactionPage.click_logs() - |> assert_has(TransactionLogsPage.logs(count: 1)) - end - - test "can visit an address from the transaction logs page", %{ - lincoln: lincoln, - session: session, - transaction: transaction - } do - session - |> TransactionLogsPage.visit_page(transaction) - |> TransactionLogsPage.click_address(lincoln) - |> assert_has(AddressPage.detail_hash(lincoln)) - end - end -end diff --git a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs index ea2d2fe0314a..acabb81702cf 100644 --- a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs +++ b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs @@ -34,8 +34,13 @@ defmodule BlockScoutWeb.Routers.ChainTypeScopeTest do test "blackfort validators counters are accessible when chain type is blackfort and stability is not", %{conn: conn} do + chain_type = Application.get_env(:explorer, :chain_type) Application.put_env(:explorer, :chain_type, :blackfort) + on_exit(fn -> + Application.put_env(:explorer, :chain_type, chain_type) + end) + assert conn |> get("/api/v2/validators/blackfort/counters") |> json_response(200) diff --git a/apps/explorer/lib/explorer.ex b/apps/explorer/lib/explorer.ex index bf774e3c05e8..e3c09512fbb1 100644 --- a/apps/explorer/lib/explorer.ex +++ b/apps/explorer/lib/explorer.ex @@ -27,14 +27,25 @@ defmodule Explorer do - `:all` - Both API web server and blockchain indexer run together - `:indexer` - Only blockchain indexer modules run without the web server - `:api` - Only the web server runs without performing indexing operations + - `:media_worker` - Standalone NFT media processing worker mode + + The function first checks if the application is configured as a standalone + media worker. If so, it returns `:media_worker` regardless of the configured + explorer mode. Otherwise, it returns the mode configured for the explorer + application. ## Returns + - `:media_worker` if configured as a standalone NFT media worker - `:all` if both API and indexer components should be active - `:indexer` if only the indexer component should be active - `:api` if only the API web server component should be active """ - @spec mode() :: :all | :indexer | :api + @spec mode() :: :all | :indexer | :api | :media_worker def mode do - Application.get_env(:explorer, :mode) + if Application.get_env(:nft_media_handler, :standalone_media_worker?) do + :media_worker + else + Application.get_env(:explorer, :mode) + end end end diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 45d1a3fffce9..14afbe71fda5 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -17,7 +17,6 @@ defmodule Explorer.Application do MinMissingBlockNumber, StateChanges, Transactions, - TransactionsApiV2, Uncles } @@ -90,7 +89,6 @@ defmodule Explorer.Application do TransactionsCount, StateChanges, Transactions, - TransactionsApiV2, Uncles, AddressTabsElementsCount, con_cache_child_spec(MarketHistoryCache.cache_name()), diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 494a813e0091..ddfde10ea3e0 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1163,7 +1163,7 @@ defmodule Explorer.Chain do elements blocks -> - blocks |> Repo.preload(Map.keys(necessity_by_association)) + blocks |> select_repo(options).preload(Map.keys(necessity_by_association)) end end diff --git a/apps/explorer/lib/explorer/chain/cache/transactions.ex b/apps/explorer/lib/explorer/chain/cache/transactions.ex index e9b8a40a9efe..b79ed4cef7a9 100644 --- a/apps/explorer/lib/explorer/chain/cache/transactions.ex +++ b/apps/explorer/lib/explorer/chain/cache/transactions.ex @@ -12,10 +12,7 @@ defmodule Explorer.Chain.Cache.Transactions do :block, created_contract_address: :names, from_address: :names, - to_address: :names, - token_transfers: :token, - token_transfers: :from_address, - token_transfers: :to_address + to_address: :names ], ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval], global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl] diff --git a/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex deleted file mode 100644 index 0a5cf715c1bc..000000000000 --- a/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Explorer.Chain.Cache.TransactionsApiV2 do - @moduledoc """ - Caches the latest imported transactions - """ - - alias Explorer.Chain.Transaction - - use Explorer.Chain.OrderedCache, - name: :transactions_api_v2, - max_size: 51, - preloads: [ - :block, - created_contract_address: :names, - from_address: :names, - to_address: :names - ], - ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval], - global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl] - - @type element :: Transaction.t() - - @type id :: {non_neg_integer(), non_neg_integer()} - - def element_to_id(%Transaction{block_number: block_number, index: index}) do - {block_number, index} - end -end diff --git a/apps/explorer/lib/explorer/chain/ordered_cache.ex b/apps/explorer/lib/explorer/chain/ordered_cache.ex index 62704e95244e..601daf2c1afd 100644 --- a/apps/explorer/lib/explorer/chain/ordered_cache.ex +++ b/apps/explorer/lib/explorer/chain/ordered_cache.ex @@ -151,6 +151,7 @@ defmodule Explorer.Chain.OrderedCache do # credo:disable-for-next-line Credo.Check.Refactor.LongQuoteBlocks quote do + require Logger alias Explorer.Chain.OrderedCache @behaviour OrderedCache @@ -255,25 +256,64 @@ defmodule Explorer.Chain.OrderedCache do def update(elements) when is_nil(elements), do: :ok def update(elements) when is_list(elements) do - prepared_elements = - elements - |> Enum.sort_by(&element_to_id(&1), &prevails?(&1, &2)) - |> Enum.take(max_size()) - |> do_preloads() - |> Enum.map(&{element_to_id(&1), sanitize_before_update(&1)}) - - ConCache.update(cache_name(), ids_list_key(), fn ids -> - updated_list = - prepared_elements - |> merge_and_update(ids || [], max_size()) - - # ids_list is set to never expire - {:ok, %ConCache.Item{value: updated_list, ttl: :infinity}} - end) + case Explorer.mode() do + mode when mode in [:all, :api, :indexer] -> + elements_for_preload = + elements + |> Enum.sort_by(&element_to_id(&1), &prevails?(&1, &2)) + |> Enum.take(max_size()) + + preloaded_elements = + try do + do_preloads(elements_for_preload) + rescue + postgrex_error in Postgrex.Error -> + Logger.error(fn -> + [ + "Error while preloading elements for ordered cache: ", + Exception.format(:error, postgrex_error, __STACKTRACE__) + ] + end) + + elements_for_preload + end + + preloaded_elements + |> Enum.map(&{element_to_id(&1), sanitize_before_update(&1)}) + |> do_raw_update(true) + + _ -> + :ok + end end def update(element), do: update([element]) + def do_raw_update(prepared_elements, propagate) do + case Explorer.mode() do + mode when mode in [:all, :api] -> + ConCache.update(cache_name(), ids_list_key(), fn ids -> + updated_list = + prepared_elements + |> merge_and_update(ids || [], max_size()) + + # ids_list is set to never expire + {:ok, %ConCache.Item{value: updated_list, ttl: :infinity}} + end) + + :indexer -> + if propagate do + Node.list() |> :erpc.multicast(__MODULE__, :do_raw_update, [prepared_elements, false]) + else + Logger.error("Indexer got unexpected propagation call to do_raw_update/2") + :ok + end + + _ -> + :ok + end + end + defp do_preloads(elements) do if Enum.empty?(preloads()) do elements diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 3573d31879a7..fe4a41a85562 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -2538,9 +2538,7 @@ defmodule Explorer.Chain.Transaction do """ @spec recent_collated_transactions(true | false, [ Chain.paging_options() | Chain.necessity_by_association_option() | Chain.api?() - ]) :: [ - __MODULE__.t() - ] + ]) :: [t()] def recent_collated_transactions(old_ui?, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) @@ -2548,14 +2546,21 @@ defmodule Explorer.Chain.Transaction do method_id_filter = Keyword.get(options, :method) type_filter = Keyword.get(options, :type) - fetch_recent_collated_transactions( - old_ui?, - paging_options, - necessity_by_association, - method_id_filter, - type_filter, - options - ) + case !paging_options.key && !method_id_filter && !type_filter && + Transactions.atomic_take_enough(paging_options.page_size) do + transactions when is_list(transactions) -> + transactions |> Chain.select_repo(options).preload(Map.keys(necessity_by_association)) + + _ -> + fetch_recent_collated_transactions( + old_ui?, + paging_options, + necessity_by_association, + method_id_filter, + type_filter, + options + ) + end end defp fetch_recent_collated_transactions( @@ -2726,68 +2731,6 @@ defmodule Explorer.Chain.Transaction do dynamic([transaction], ^dynamic or transaction.type == 3) end - @doc """ - Returns recently collated transactions using random access pagination (RAP). - - This function supports efficient pagination for large transaction lists by - allowing direct access to any page without requiring sequential traversal. - It uses a cache for the first page to improve performance. - - ## Parameters - - `options`: Keyword list of options including: - - `:necessity_by_association` - Associations to preload (defaults to %{}) - - `:paging_options` - Random access pagination options (defaults to @default_paging_options) - - ## Returns - - A map containing: - - `:total_transactions_count` - Total number of transactions available - - `:transactions` - List of transaction structs for the requested page - """ - @spec recent_collated_transactions_for_rap([Chain.paging_options() | Chain.necessity_by_association_option()]) :: %{ - :total_transactions_count => non_neg_integer(), - :transactions => [__MODULE__.t()] - } - def recent_collated_transactions_for_rap(options \\ []) when is_list(options) do - necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - total_transactions_count = transactions_available_count() - - fetched_transactions = - if is_nil(paging_options.key) or paging_options.page_number == 1 do - paging_options.page_size - |> Kernel.+(1) - |> Transactions.atomic_take_enough() - |> case do - nil -> - transactions = fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) - Transactions.update(transactions) - transactions - - transactions -> - transactions - end - else - fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) - end - - %{total_transactions_count: total_transactions_count, transactions: fetched_transactions} - end - - defp fetch_recent_collated_transactions_for_rap(paging_options, necessity_by_association) do - fetch_transactions_for_rap() - |> where([transaction], not is_nil(transaction.block_number) and not is_nil(transaction.index)) - |> handle_random_access_paging_options(paging_options) - |> Chain.join_associations(necessity_by_association) - |> preload([{:token_transfers, [:token, :from_address, :to_address]}]) - |> Repo.all() - end - - defp fetch_transactions_for_rap do - __MODULE__ - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) - end - @doc """ Returns the count of available transactions shown in the UI. @@ -2805,45 +2748,6 @@ defmodule Explorer.Chain.Transaction do |> Repo.aggregate(:count, :hash) end - defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}], - do: limit(query, ^(@default_page_size + 1)) - - defp handle_random_access_paging_options(query, paging_options) do - query - |> (&if(paging_options |> Map.get(:page_number, 1) |> process_page_number() == 1, - do: &1, - else: __MODULE__.page_transaction(&1, paging_options) - )).() - |> handle_page(paging_options) - end - - defp handle_page(query, paging_options) do - page_number = paging_options |> Map.get(:page_number, 1) |> process_page_number() - page_size = Map.get(paging_options, :page_size, @default_page_size) - - cond do - page_in_bounds?(page_number, page_size) && page_number == 1 -> - query - |> limit(^(page_size + 1)) - - page_in_bounds?(page_number, page_size) -> - query - |> limit(^page_size) - |> offset(^((page_number - 2) * page_size)) - - true -> - query - |> limit(^(@default_page_size + 1)) - end - end - - defp process_page_number(number) when number < 1, do: 1 - - defp process_page_number(number), do: number - - defp page_in_bounds?(page_number, page_size), - do: page_size <= @limit_showing_transactions && @limit_showing_transactions - page_number * page_size >= 0 - @doc """ Finds and updates replaced transactions in the database. """ diff --git a/apps/explorer/lib/explorer/chain/withdrawal.ex b/apps/explorer/lib/explorer/chain/withdrawal.ex index 3ac453389174..ac857e73efce 100644 --- a/apps/explorer/lib/explorer/chain/withdrawal.ex +++ b/apps/explorer/lib/explorer/chain/withdrawal.ex @@ -110,10 +110,14 @@ defmodule Explorer.Chain.Withdrawal do """ @spec upsert_count_withdrawals(non_neg_integer()) :: {:ok, any()} | {:error, any()} def upsert_count_withdrawals(index) do - LastFetchedCounter.upsert(%{ - counter_type: "withdrawals_count", - value: index - }) + current_value = LastFetchedCounter.get("withdrawals_count") + + if index > current_value do + LastFetchedCounter.upsert(%{ + counter_type: "withdrawals_count", + value: index + }) + end end @doc """ diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index f2bb99cc08ba..4366add07b17 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -37,8 +37,7 @@ defmodule Explorer.Market do def handle_continue(attempt, _state) do attempt |> Kernel.**(3) |> :timer.seconds() |> :timer.sleep() - case Node.list() - |> Enum.filter(&Helper.indexer_node?/1) do + case Node.list() |> Enum.filter(&Helper.indexer_node?/1) do [] -> if attempt < 5 do {:noreply, nil, {:continue, attempt + 1}} diff --git a/apps/explorer/test/explorer/chain/cache/transactions_test.exs b/apps/explorer/test/explorer/chain/cache/transactions_test.exs index 8a768b7daef3..ae80c3077270 100644 --- a/apps/explorer/test/explorer/chain/cache/transactions_test.exs +++ b/apps/explorer/test/explorer/chain/cache/transactions_test.exs @@ -6,6 +6,13 @@ defmodule Explorer.Chain.Cache.TransactionsTest do @size 51 + setup do + old_value = Application.get_env(:explorer, :mode) + Application.put_env(:explorer, :mode, :all) + on_exit(fn -> Application.put_env(:explorer, :mode, old_value) end) + :ok + end + describe "update/1" do test "adds a new value to a new cache with preloads" do transaction = insert(:transaction) |> preload_all() @@ -86,10 +93,7 @@ defmodule Explorer.Chain.Cache.TransactionsTest do :block, created_contract_address: :names, from_address: :names, - to_address: :names, - token_transfers: :token, - token_transfers: :from_address, - token_transfers: :to_address + to_address: :names ]) end end diff --git a/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex b/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex index bdcdb20efcc1..8f3d18dac5e8 100644 --- a/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex +++ b/apps/indexer/lib/indexer/block/catchup/bound_interval_supervisor.ex @@ -12,7 +12,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisor do alias Indexer.Block.Catchup @type named_arguments :: %{ - required(:block_fetcher) => Block.Fetcher.t(), + required(:block_fetcher) => Block.Fetcher.t(Catchup.Fetcher), optional(:block_interval) => pos_integer, optional(:memory_monitor) => GenServer.server() } diff --git a/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex b/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex index ea3de2e8d538..94d73a558704 100644 --- a/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/massive_blocks_fetcher.ex @@ -12,16 +12,35 @@ defmodule Indexer.Block.Catchup.MassiveBlocksFetcher do @increased_interval 10000 - @spec start_link(term()) :: GenServer.on_start() - def start_link(_) do - GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + @type named_arguments :: %{required(:task_supervisor) => module()} + + @spec child_spec([named_arguments | GenServer.options(), ...]) :: Supervisor.child_spec() + def child_spec([named_arguments]) when is_map(named_arguments), do: child_spec([named_arguments, []]) + + def child_spec([named_arguments, gen_server_options] = start_link_arguments) + when is_map(named_arguments) and is_list(gen_server_options) do + Supervisor.child_spec( + %{id: __MODULE__, start: {__MODULE__, :start_link, start_link_arguments}, type: :worker}, + [] + ) + end + + @spec start_link(named_arguments :: map()) :: GenServer.on_start() + @spec start_link(named_arguments :: %{}, GenServer.options()) :: GenServer.on_start() + def start_link(named_arguments, gen_server_options \\ []) + when is_map(named_arguments) and is_list(gen_server_options) do + GenServer.start_link(__MODULE__, named_arguments, gen_server_options) end @impl true - def init(_) do + def init(opts) do + if !opts[:task_supervisor] do + raise ArgumentError, ":task_supervisor must be provided to #{__MODULE__}.start_link/1" + end + send_new_task() - {:ok, %{block_fetcher: generate_block_fetcher(), low_priority_blocks: []}} + {:ok, %{block_fetcher: generate_block_fetcher(opts), low_priority_blocks: []}} end @impl true @@ -72,7 +91,7 @@ defmodule Indexer.Block.Catchup.MassiveBlocksFetcher do [number] end - defp generate_block_fetcher do + defp generate_block_fetcher(opts) do receipts_batch_size = Application.get_env(:indexer, :receipts_batch_size) receipts_concurrency = Application.get_env(:indexer, :receipts_concurrency) json_rpc_named_arguments = Application.get_env(:indexer, :json_rpc_named_arguments) @@ -82,7 +101,8 @@ defmodule Indexer.Block.Catchup.MassiveBlocksFetcher do callback_module: Indexer.Block.Catchup.Fetcher, json_rpc_named_arguments: json_rpc_named_arguments, receipts_batch_size: receipts_batch_size, - receipts_concurrency: receipts_concurrency + receipts_concurrency: receipts_concurrency, + task_supervisor: opts[:task_supervisor] } end diff --git a/apps/indexer/lib/indexer/block/catchup/supervisor.ex b/apps/indexer/lib/indexer/block/catchup/supervisor.ex index 024c93c91a91..f11e4252e162 100644 --- a/apps/indexer/lib/indexer/block/catchup/supervisor.ex +++ b/apps/indexer/lib/indexer/block/catchup/supervisor.ex @@ -31,8 +31,15 @@ defmodule Indexer.Block.Catchup.Supervisor do [ {MissingRangesCollector, []}, {Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}, - {MassiveBlocksFetcher, []}, - {BoundIntervalSupervisor, [bound_interval_supervisor_arguments, [name: BoundIntervalSupervisor]]} + {MassiveBlocksFetcher, + [%{task_supervisor: Indexer.Block.Catchup.TaskSupervisor}, [name: MassiveBlocksFetcher]]}, + {BoundIntervalSupervisor, + [ + update_in(bound_interval_supervisor_arguments, [:block_fetcher], fn fetcher -> + %{fetcher | task_supervisor: Indexer.Block.Catchup.TaskSupervisor} + end), + [name: BoundIntervalSupervisor] + ]} ], strategy: :one_for_one ) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 89554c347519..85d3e84910c9 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -14,7 +14,7 @@ defmodule Indexer.Block.Fetcher do import EthereumJSONRPC, only: [quantity_to_integer: 1] alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries} - alias Explorer.Chain + alias Explorer.{Chain, Repo} alias Explorer.Chain.{Address, Block, Hash, Import, Transaction, Wei, Withdrawal} alias Explorer.Chain.Block.Reward alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions, Uncles} @@ -78,13 +78,20 @@ defmodule Indexer.Block.Fetcher do @type address_hash_to_fetched_balance_block_number :: %{String.t() => Block.block_number()} - @type t :: %__MODULE__{} + @type t(callback_module) :: %__MODULE__{ + broadcast: term(), + task_supervisor: module(), + callback_module: callback_module, + json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments(), + receipts_batch_size: pos_integer(), + receipts_concurrency: pos_integer() + } @doc """ Calculates the balances and internal transactions and imports those with the given data. """ @callback import( - t, + t(module()), %{ address_hash_to_fetched_balance_block_number: address_hash_to_fetched_balance_block_number, addresses: Import.Runner.options(), @@ -114,9 +121,10 @@ defmodule Indexer.Block.Fetcher do @doc false def default_receipts_concurrency, do: @receipts_concurrency - @enforce_keys ~w(json_rpc_named_arguments)a + @enforce_keys ~w(json_rpc_named_arguments task_supervisor)a defstruct broadcast: nil, callback_module: nil, + task_supervisor: nil, json_rpc_named_arguments: nil, receipts_batch_size: @receipts_batch_size, receipts_concurrency: @receipts_concurrency @@ -141,7 +149,7 @@ defmodule Indexer.Block.Fetcher do end @decorate span(tracer: Tracer) - @spec fetch_and_import_range(t, Range.t(), map) :: + @spec fetch_and_import_range(t(module()), Range.t(), map) :: {:ok, %{inserted: %{}, errors: [EthereumJSONRPC.Transport.error()]}} | {:error, {step :: atom(), reason :: [Ecto.Changeset.t()] | term()} @@ -150,7 +158,8 @@ defmodule Indexer.Block.Fetcher do %__MODULE__{ broadcast: _broadcast, callback_module: callback_module, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: task_supervisor } = state, _.._//_ = range, additional_options \\ %{} @@ -285,11 +294,14 @@ defmodule Indexer.Block.Fetcher do inserted = Map.merge(inserted, inserted_transaction_actions) Prometheus.Instrumenter.set_block_batch_fetch(fetch_time, callback_module) result = {:ok, %{inserted: inserted, errors: blocks_errors}} - update_block_cache(inserted[:blocks]) - update_transactions_cache(inserted[:transactions]) - update_addresses_cache(inserted[:addresses]) - update_uncles_cache(inserted[:block_second_degree_relations]) - update_withdrawals_cache(inserted[:withdrawals]) + + Task.Supervisor.start_child(task_supervisor, fn -> + update_block_cache(inserted[:blocks], inserted) + update_transactions_cache(inserted[:transactions], inserted) + update_addresses_cache(inserted[:addresses]) + update_uncles_cache(inserted[:block_second_degree_relations]) + update_withdrawals_cache(inserted[:withdrawals]) + end) async_match_arbitrum_messages_to_l2(arbitrum_transactions_for_further_handling) @@ -426,20 +438,49 @@ defmodule Indexer.Block.Fetcher do }) end - defp update_block_cache([]), do: :ok + defp update_block_cache([], _), do: :ok - defp update_block_cache(blocks) when is_list(blocks) do + defp update_block_cache(blocks, inserted) when is_list(blocks) do {min_block, max_block} = Enum.min_max_by(blocks, & &1.number) BlockNumber.update_all(max_block.number) BlockNumber.update_all(min_block.number) - BlocksCache.update(blocks) + + transactions_by_block = Enum.group_by(Map.get(inserted, :transactions, []), & &1.block_hash) + rewards_by_block = Enum.group_by(Map.get(inserted, :block_rewards, []), & &1.block_hash) + + blocks + |> Repo.preload( + transactions: fn block_hashes -> + Enum.flat_map(block_hashes, &Map.get(transactions_by_block, &1, [])) + end, + rewards: fn block_hashes -> + Enum.flat_map(block_hashes, &Map.get(rewards_by_block, &1, [])) + end + ) + |> BlocksCache.update() end - defp update_block_cache(_), do: :ok + defp update_block_cache(_, _), do: :ok + + defp update_transactions_cache(transactions, inserted) do + blocks_map = Map.new(Map.get(inserted, :blocks, []), fn block -> {block.hash, [block]} end) + token_transfers_transaction_hashes_set = MapSet.new(Map.get(inserted, :token_transfers, []), & &1.transaction_hash) - defp update_transactions_cache(transactions) do - Transactions.update(transactions) + transactions + |> Repo.preload( + block: fn transaction_block_hashes -> + Enum.flat_map(transaction_block_hashes, &Map.get(blocks_map, &1, [])) + end + ) + |> Enum.map(fn transaction -> + Map.put( + transaction, + :has_token_transfers, + MapSet.member?(token_transfers_transaction_hashes_set, transaction.hash) + ) + end) + |> Transactions.update() end defp update_addresses_cache(addresses), do: Accounts.drop(addresses) diff --git a/apps/indexer/lib/indexer/block/fetcher/receipts.ex b/apps/indexer/lib/indexer/block/fetcher/receipts.ex index fd1bef51bace..68c495947107 100644 --- a/apps/indexer/lib/indexer/block/fetcher/receipts.ex +++ b/apps/indexer/lib/indexer/block/fetcher/receipts.ex @@ -27,7 +27,7 @@ defmodule Indexer.Block.Fetcher.Receipts do and logs with block numbers added where missing - `{:error, reason}` - Error occurred during fetch or processing """ - @spec fetch(Block.Fetcher.t(), [map()]) :: {:ok, %{logs: [map()], receipts: [map()]}} | {:error, term()} + @spec fetch(Block.Fetcher.t(module()), [map()]) :: {:ok, %{logs: [map()], receipts: [map()]}} | {:error, term()} def fetch(state, transaction_params) def fetch(%Block.Fetcher{} = _state, []), do: {:ok, %{logs: [], receipts: []}} diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 37ecc87c08a0..579b07792bf9 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -61,13 +61,7 @@ defmodule Indexer.Block.Realtime.Fetcher do last_realtime_blocks: %{} @type t :: %__MODULE__{ - block_fetcher: %Block.Fetcher{ - broadcast: term(), - callback_module: __MODULE__, - json_rpc_named_arguments: EthereumJSONRPC.json_rpc_named_arguments(), - receipts_batch_size: pos_integer(), - receipts_concurrency: pos_integer() - }, + block_fetcher: Block.Fetcher.t(__MODULE__), subscription: Subscription.t(), previous_number: pos_integer() | nil, timer: reference(), diff --git a/apps/indexer/lib/indexer/block/realtime/supervisor.ex b/apps/indexer/lib/indexer/block/realtime/supervisor.ex index ea27e3d2c5ee..3d6e77b50262 100644 --- a/apps/indexer/lib/indexer/block/realtime/supervisor.ex +++ b/apps/indexer/lib/indexer/block/realtime/supervisor.ex @@ -5,12 +5,17 @@ defmodule Indexer.Block.Realtime.Supervisor do use Supervisor + alias Indexer.Block.Fetcher + def start_link([arguments, gen_server_options]) do Supervisor.start_link(__MODULE__, arguments, gen_server_options) end @impl Supervisor - def init(%{block_fetcher: block_fetcher, subscribe_named_arguments: subscribe_named_arguments}) do + def init(%{block_fetcher: %Fetcher{} = block_fetcher, subscribe_named_arguments: subscribe_named_arguments}) do + block_fetcher_with_task_supervisor = + %Fetcher{block_fetcher | task_supervisor: Indexer.Block.Realtime.TaskSupervisor} + children = case Keyword.fetch!(subscribe_named_arguments, :transport) do EthereumJSONRPC.WebSocket -> @@ -30,7 +35,10 @@ defmodule Indexer.Block.Realtime.Supervisor do {EthereumJSONRPC.WebSocket.Supervisor, transport_options}, {Indexer.Block.Realtime.Fetcher, [ - %{block_fetcher: block_fetcher, subscribe_named_arguments: block_fetcher_subscribe_named_arguments}, + %{ + block_fetcher: block_fetcher_with_task_supervisor, + subscribe_named_arguments: block_fetcher_subscribe_named_arguments + }, [name: Indexer.Block.Realtime.Fetcher] ]} ] @@ -40,7 +48,7 @@ defmodule Indexer.Block.Realtime.Supervisor do {Task.Supervisor, name: Indexer.Block.Realtime.TaskSupervisor}, {Indexer.Block.Realtime.Fetcher, [ - %{block_fetcher: block_fetcher, subscribe_named_arguments: nil}, + %{block_fetcher: block_fetcher_with_task_supervisor, subscribe_named_arguments: nil}, [name: Indexer.Block.Realtime.Fetcher] ]} ] diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 59a4df4583d8..177bfe88f474 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -109,6 +109,7 @@ defmodule Indexer.Supervisor do ) |> Enum.into(%{}) |> Map.put(:memory_monitor, memory_monitor) + |> Map.put(:task_supervisor, nil) |> Map.put_new(:realtime_overrides, %{}) %{ diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index 8fd2eb4fd445..45233a4f3147 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -238,11 +238,17 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do ReplacedTransaction.Supervisor.Case.start_supervised!() UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Indexer.Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor + } ) Catchup.Supervisor.Case.start_supervised!(%{ - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Indexer.Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor + } }) first_catchup_block_number = latest_block_number - 1 @@ -439,7 +445,6 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do end) MissingRangesCollector.start_link([]) - start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -531,7 +536,6 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do Application.put_env(:indexer, :block_ranges, "0..0") MissingRangesCollector.start_link([]) - start_supervised({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -540,7 +544,10 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do ReplacedTransaction.Supervisor.Case.start_supervised!() UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Indexer.Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor + } ) # from `setup :state` @@ -582,9 +589,14 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do end defp state(%{json_rpc_named_arguments: json_rpc_named_arguments}) do + start_supervised!({Task.Supervisor, name: Indexer.Block.Catchup.TaskSupervisor}) + {:ok, state} = Catchup.BoundIntervalSupervisor.init(%{ - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Indexer.Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor + } }) %{state: state} @@ -596,7 +608,14 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do pid = start_supervised!( {Catchup.BoundIntervalSupervisor, - [%{block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}}]} + [ + %{ + block_fetcher: %Indexer.Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor + } + } + ]} ) {:ok, %{pid: pid}} diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs index 9cfb13b2ede7..9703cbc70bf2 100644 --- a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -83,62 +83,65 @@ defmodule Indexer.Block.Catchup.FetcherTest do block_number = 0 assert {:ok, _} = - Fetcher.import(%Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}, %{ - addresses: %{ - params: [ - %{hash: miner_hash} - ] - }, - address_hash_to_fetched_balance_block_number: %{miner_hash => block_number}, - address_coin_balances: %{ - params: [ - %{ - address_hash: miner_hash, - block_number: block_number - } - ] - }, - blocks: %{ - params: [ - %{ - difficulty: 0, - gas_limit: 21000, - gas_used: 21000, - miner_hash: miner_hash, - nonce: 0, - number: block_number, - parent_hash: - block_hash() - |> to_string(), - size: 0, - timestamp: DateTime.utc_now(), - total_difficulty: 0, - hash: nephew_hash - } - ] - }, - block_rewards: %{errors: [], params: []}, - block_second_degree_relations: %{ - params: [ - %{ - nephew_hash: nephew_hash, - uncle_hash: uncle_hash, - index: nephew_index - } - ] - }, - tokens: %{ - params: [], - on_conflict: :nothing - }, - address_token_balances: %{ - params: [] - }, - transactions: %{ - params: [], - on_conflict: :nothing + Fetcher.import( + %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments, task_supervisor: nil}, + %{ + addresses: %{ + params: [ + %{hash: miner_hash} + ] + }, + address_hash_to_fetched_balance_block_number: %{miner_hash => block_number}, + address_coin_balances: %{ + params: [ + %{ + address_hash: miner_hash, + block_number: block_number + } + ] + }, + blocks: %{ + params: [ + %{ + difficulty: 0, + gas_limit: 21000, + gas_used: 21000, + miner_hash: miner_hash, + nonce: 0, + number: block_number, + parent_hash: + block_hash() + |> to_string(), + size: 0, + timestamp: DateTime.utc_now(), + total_difficulty: 0, + hash: nephew_hash + } + ] + }, + block_rewards: %{errors: [], params: []}, + block_second_degree_relations: %{ + params: [ + %{ + nephew_hash: nephew_hash, + uncle_hash: uncle_hash, + index: nephew_index + } + ] + }, + tokens: %{ + params: [], + on_conflict: :nothing + }, + address_token_balances: %{ + params: [] + }, + transactions: %{ + params: [], + on_conflict: :nothing + } } - }) + ) assert_receive {:uncles, [{^nephew_hash_bytes, ^nephew_index}]} end @@ -322,7 +325,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do Fetcher.task(%Fetcher{ block_fetcher: %Block.Fetcher{ callback_module: Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor } }) @@ -476,7 +480,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do Fetcher.task(%Fetcher{ block_fetcher: %Block.Fetcher{ callback_module: Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor } }) @@ -625,7 +630,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do Fetcher.task(%Fetcher{ block_fetcher: %Block.Fetcher{ callback_module: Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor } }) @@ -680,7 +686,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do Fetcher.task(%Fetcher{ block_fetcher: %Block.Fetcher{ callback_module: Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor } }) @@ -836,7 +843,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do Fetcher.task(%Fetcher{ block_fetcher: %Block.Fetcher{ callback_module: Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.Block.Catchup.TaskSupervisor } }) diff --git a/apps/indexer/test/indexer/block/fetcher/receipts_test.exs b/apps/indexer/test/indexer/block/fetcher/receipts_test.exs index a7c45106551e..f8ed67281f36 100644 --- a/apps/indexer/test/indexer/block/fetcher/receipts_test.exs +++ b/apps/indexer/test/indexer/block/fetcher/receipts_test.exs @@ -14,9 +14,12 @@ defmodule Indexer.Block.Fetcher.ReceiptsTest do describe "fetch/2" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + %{ block_fetcher: %Fetcher{ json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor, receipts_concurrency: 10 } } diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 64b1a02aa618..a0c5b5cad6e1 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -10,10 +10,8 @@ defmodule Indexer.Block.FetcherTest do import EthereumJSONRPC, only: [integer_to_quantity: 1] alias Explorer.Chain - alias Explorer.Chain.Celo.Legacy.Events - alias Explorer.Chain.Celo.PendingAccountOperation alias Explorer.Chain.{Address, Log, Transaction, Wei} - alias Explorer.TestHelper + # alias Explorer.TestHelper alias Indexer.Block.Fetcher alias Indexer.BufferedTask alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup @@ -75,8 +73,13 @@ defmodule Indexer.Block.FetcherTest do ReplacedTransaction.Supervisor.Case.start_supervised!() {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor + } ) Application.put_env(:indexer, Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, disabled?: true) @@ -87,7 +90,8 @@ defmodule Indexer.Block.FetcherTest do block_fetcher: %Fetcher{ broadcast: false, callback_module: Indexer.Block.Catchup.Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor } } end @@ -809,6 +813,9 @@ defmodule Indexer.Block.FetcherTest do end if @chain_identity == {:optimism, :celo} do + alias Explorer.Chain.Celo.Legacy.Events + alias Explorer.Chain.Celo.PendingAccountOperation + describe "import_range/2 celo" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -818,9 +825,13 @@ defmodule Indexer.Block.FetcherTest do TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor + } ) Application.put_env(:indexer, Indexer.Fetcher.Celo.EpochBlockOperations.Supervisor, disabled?: true) @@ -831,7 +842,8 @@ defmodule Indexer.Block.FetcherTest do block_fetcher: %Fetcher{ broadcast: false, callback_module: Indexer.Block.Catchup.Fetcher, - json_rpc_named_arguments: json_rpc_named_arguments + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor } } end diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index acd71a7e6acc..b66d98d02aa7 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -42,10 +42,13 @@ defmodule Indexer.Block.Realtime.FetcherTest do trace_block: "http://54.144.107.14:8545" ) + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + block_fetcher = %Indexer.Block.Fetcher{ broadcast: false, callback_module: Realtime.Fetcher, - json_rpc_named_arguments: core_json_rpc_named_arguments + json_rpc_named_arguments: core_json_rpc_named_arguments, + task_supervisor: Indexer.TaskSupervisor } TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) @@ -91,9 +94,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} - ) + UncleBlock.Supervisor.Case.start_supervised!(block_fetcher: block_fetcher) ReplacedTransaction.Supervisor.Case.start_supervised!() @@ -602,9 +603,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} - ) + UncleBlock.Supervisor.Case.start_supervised!(block_fetcher: block_fetcher) Indexer.Fetcher.Filecoin.AddressInfo.Supervisor.Case.start_supervised!( json_rpc_named_arguments: json_rpc_named_arguments @@ -863,9 +862,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} - ) + UncleBlock.Supervisor.Case.start_supervised!(block_fetcher: block_fetcher) ReplacedTransaction.Supervisor.Case.start_supervised!() diff --git a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs index 052b4b75f492..7029c3e59bac 100644 --- a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs +++ b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs @@ -124,7 +124,7 @@ defmodule Indexer.Fetcher.UncleBlockTest do end) UncleBlock.Supervisor.Case.start_supervised!( - block_fetcher: %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} + block_fetcher: %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments, task_supervisor: nil} ) wait(fn -> @@ -167,7 +167,10 @@ defmodule Indexer.Fetcher.UncleBlockTest do end) assert {:retry, ^entries} = - UncleBlock.run(entries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}) + UncleBlock.run(entries, %Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: nil + }) end test "retries only unique uncles on failed request", %{json_rpc_named_arguments: json_rpc_named_arguments} do @@ -197,7 +200,10 @@ defmodule Indexer.Fetcher.UncleBlockTest do end) assert {:retry, [^entry]} = - UncleBlock.run(entries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}) + UncleBlock.run(entries, %Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + task_supervisor: nil + }) end end @@ -229,7 +235,8 @@ defmodule Indexer.Fetcher.UncleBlockTest do params, %Block.Fetcher{ json_rpc_named_arguments: json_rpc_named_arguments, - callback_module: Indexer.Block.Realtime.Fetcher + callback_module: Indexer.Block.Realtime.Fetcher, + task_supervisor: nil }, [] ) diff --git a/config/runtime.exs b/config/runtime.exs index 6fb24231da94..033de9db4b51 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -597,24 +597,20 @@ config :explorer, Explorer.Chain.Cache.BlockNumber, global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) config :explorer, Explorer.Chain.Cache.Blocks, - ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), - global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) + ttl_check_interval: false, + global_ttl: nil config :explorer, Explorer.Chain.Cache.Transactions, - ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), - global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) - -config :explorer, Explorer.Chain.Cache.TransactionsApiV2, - ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), - global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) + ttl_check_interval: false, + global_ttl: nil config :explorer, Explorer.Chain.Cache.Accounts, ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) config :explorer, Explorer.Chain.Cache.Uncles, - ttl_check_interval: ConfigHelper.cache_ttl_check_interval(disable_indexer?), - global_ttl: ConfigHelper.cache_global_ttl(disable_indexer?) + ttl_check_interval: false, + global_ttl: nil celo_l2_migration_block = ConfigHelper.parse_integer_or_nil_env_var("CELO_L2_MIGRATION_BLOCK") celo_epoch_manager_contract_address = System.get_env("CELO_EPOCH_MANAGER_CONTRACT") diff --git a/cspell.json b/cspell.json index c42bb23f3082..411bff24021a 100644 --- a/cspell.json +++ b/cspell.json @@ -234,6 +234,7 @@ "epns", "epochrewards", "Erigon", + "erpc", "errora", "errorb", "erts", From 0cafd9865d40bb29dc78e49ea60f64e13e9603d8 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:42:29 +0300 Subject: [PATCH 0607/1006] feat: add Dynamic provider for account (#13786) --- .github/workflows/generate-swagger.yml | 2 +- apps/block_scout_web/config/test.exs | 2 + .../account/api/v2/authenticate_controller.ex | 169 +++++- .../account/api/v2/fallback_controller.ex | 7 + .../api/v2/csv_export_controller.ex | 6 - .../block_scout_web/routers/account_router.ex | 3 + .../lib/block_scout_web/routers/api_router.ex | 4 +- .../routers/smart_contracts_api_v2_router.ex | 4 +- .../routers/tokens_api_v2_router.ex | 4 +- .../block_scout_web/schemas/api/v2/account.ex | 85 +++ .../schemas/api/v2/account/session.ex | 26 + .../schemas/api/v2/account/user.ex | 22 + .../schemas/api/v2/error_responses.ex | 30 +- .../block_scout_web/schemas/api/v2/general.ex | 54 +- .../lib/block_scout_web/specs.ex | 24 + .../lib/block_scout_web/specs/private.ex | 33 ++ .../{api_spec.ex => specs/public.ex} | 15 +- .../api/v2/authenticate_controller_test.exs | 497 ++++++++++++++++++ .../test/support/api_schema_assertions.ex | 52 +- .../explorer/lib/explorer/account/identity.ex | 4 + apps/explorer/lib/explorer/application.ex | 1 + .../auth0/internal.ex | 6 +- .../third_party_integrations/dynamic.ex | 82 +++ .../dynamic/strategy.ex | 44 ++ .../third_party_integrations/dynamic/token.ex | 22 + apps/explorer/mix.exs | 1 + config/runtime.exs | 9 + cspell.json | 2 + docker-compose/envs/common-blockscout.env | 1 + mix.lock | 1 + 30 files changed, 1129 insertions(+), 83 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/session.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/user.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/specs.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/specs/private.ex rename apps/block_scout_web/lib/block_scout_web/{api_spec.ex => specs/public.ex} (59%) create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/authenticate_controller_test.exs create mode 100644 apps/explorer/lib/explorer/third_party_integrations/dynamic.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/dynamic/strategy.ex create mode 100644 apps/explorer/lib/explorer/third_party_integrations/dynamic/token.ex diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 3584b249d05f..9d336cfff49c 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -124,7 +124,7 @@ jobs: - name: mix openapi.spec.yaml run: | - mix openapi.spec.yaml --spec BlockScoutWeb.ApiSpec openapi.${{ matrix.chain-type }}.yaml --start-app=false + mix openapi.spec.yaml --spec BlockScoutWeb.Specs.Public openapi.${{ matrix.chain-type }}.yaml --start-app=false env: CHAIN_TYPE: ${{ matrix.chain-type != 'default' && matrix.chain-type || '' }} MUD_INDEXER_ENABLED: false diff --git a/apps/block_scout_web/config/test.exs b/apps/block_scout_web/config/test.exs index c5a40aa68bde..4ee026c86e3c 100644 --- a/apps/block_scout_web/config/test.exs +++ b/apps/block_scout_web/config/test.exs @@ -22,6 +22,8 @@ config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: f config :block_scout_web, BlockScoutWeb.Counters.InternalTransactionsIndexedCounter, enabled: false +config :oauth2, adapter: Explorer.Mock.TeslaAdapter + config :tesla, adapter: Explorer.Mock.TeslaAdapter config :ueberauth, Ueberauth, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex index 100897a3ecd4..9e850e6b62dc 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex @@ -1,31 +1,70 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs import BlockScoutWeb.Account.AuthController, only: [current_user: 1] alias BlockScoutWeb.AccessHelper alias BlockScoutWeb.Account.API.V2.UserView - alias BlockScoutWeb.API.V2.ApiView + + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.{ + ForbiddenResponse, + NotFoundResponse, + UnauthorizedResponse + } + + alias BlockScoutWeb.Schemas.API.V2.Account, as: AccountSchemas + alias Explorer.Account.Identity alias Explorer.Chain alias Explorer.Chain.Address - alias Explorer.ThirdPartyIntegrations.Auth0 + alias Explorer.ThirdPartyIntegrations.{Auth0, Dynamic} alias Plug.Conn action_fallback(BlockScoutWeb.Account.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["authentication"]) + + operation :authenticate_get, + summary: "Authenticate API Key", + description: "Authenticate using an API key passed as a query parameter.", + parameters: [admin_api_key_param_query()], + responses: %{ + ok: {"User session.", "application/json", AccountSchemas.Session}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response(), + unauthorized: UnauthorizedResponse.response(), + forbidden: ForbiddenResponse.response() + } + + @spec authenticate_get(Conn.t(), map()) :: Conn.t() | {:error, any()} def authenticate_get(conn, params) do authenticate(conn, params) end + operation :authenticate_post, + summary: "Authenticate API Key", + description: "Authenticate using an API key passed in a request body.", + request_body: admin_api_key_request_body(), + responses: %{ + ok: {"User session.", "application/json", AccountSchemas.Session}, + unprocessable_entity: JsonErrorResponse.response(), + not_found: NotFoundResponse.response(), + unauthorized: UnauthorizedResponse.response(), + forbidden: ForbiddenResponse.response() + } + + @spec authenticate_post(Conn.t(), map()) :: Conn.t() | {:error, any()} def authenticate_post(conn, params) do - authenticate(conn, params) + authenticate(conn, params |> Map.merge(conn.body_params)) end defp authenticate(conn, params) do with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, params["api_key"]}, + {:api_key, ^api_key} <- {:api_key, params[:api_key]}, {:auth, %{id: uid} = current_user} <- {:auth, current_user(conn)}, {:identity, %Identity{}} <- {:identity, Identity.find_identity(uid)} do conn @@ -34,6 +73,17 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do end end + operation :send_otp, + summary: "Send One-Time Password (OTP)", + description: "Sends a one-time password (OTP) to the specified email address.", + request_body: AccountSchemas.send_otp_request_body(), + responses: %{ + ok: {"Success message.", "application/json", message_response_schema()}, + unprocessable_entity: JsonErrorResponse.response(), + internal_server_error: {"Error message", "application/json", message_response_schema()}, + too_many_requests: {"Error message", "application/json", message_response_schema()} + } + @doc """ Sends a one-time password (OTP) to the specified email address. @@ -41,8 +91,7 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do with different behaviors based on the current user's authentication status and the relationship between the provided email and existing accounts. - The function first verifies the reCAPTCHA response to prevent abuse. Then, - it checks the current user's status and proceeds accordingly: + The function checks the current user's status and proceeds accordingly: 1. If no user is logged in, it sends an OTP for a new account. 2. If a user is logged in and the email matches their account, it returns an error. @@ -53,13 +102,12 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do - `conn`: The `Plug.Conn` struct representing the current connection. - `params`: A map containing: - `"email"`: The email address to which the OTP should be sent. - - `"recaptcha_v3_response"` or `"recaptcha_response"`: The reCAPTCHA response token. ## Returns - `:error`: If there's an unexpected error during the process. - `{:error, String.t()}`: If there's a specific error (e.g., email already linked). + - `{:format, :email}`: If the provided email format is invalid. - `{:interval, integer()}`: If an OTP was recently sent and the cooldown period hasn't elapsed. - - `{:recaptcha, false}`: If the reCAPTCHA verification fails. - `Plug.Conn.t()`: A modified connection struct with a 200 status and success message if the OTP is successfully sent. @@ -71,10 +119,13 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do @spec send_otp(Conn.t(), map()) :: :error | {:error, String.t()} + | {:format, :email} | {:interval, integer()} | Conn.t() - def send_otp(conn, %{"email" => email}) do - case conn |> Conn.fetch_session() |> current_user() do + def send_otp(conn, _params) do + email = Map.get(conn.body_params, :email) + + case conn |> current_user() do nil -> with :ok <- Auth0.send_otp(email, AccessHelper.conn_to_ip_string(conn)) do conn |> put_status(200) |> json(%{message: "Success"}) @@ -88,11 +139,21 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do %{} -> conn |> put_status(500) - |> put_view(ApiView) + |> put_view(UserView) |> render(:message, %{message: "This account already has an email"}) end end + operation :confirm_otp, + summary: "Confirm One-Time Password (OTP)", + description: "Confirms a one-time password (OTP) for a given email and updates the session.", + request_body: AccountSchemas.confirm_otp_request_body(), + responses: %{ + ok: {"User info.", "application/json", AccountSchemas.User}, + unprocessable_entity: JsonErrorResponse.response(), + internal_server_error: {"Error message", "application/json", message_response_schema()} + } + @doc """ Confirms a one-time password (OTP) for a given email and updates the session. @@ -129,12 +190,30 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do information. """ @spec confirm_otp(Conn.t(), map()) :: :error | {:error, any()} | Conn.t() - def confirm_otp(conn, %{"email" => email, "otp" => otp}) do + def confirm_otp(conn, %{email: email, otp: otp}) do with {:ok, auth} <- Auth0.confirm_otp_and_get_auth(email, otp, AccessHelper.conn_to_ip_string(conn)) do put_auth_to_session(conn, auth) end end + operation :siwe_message, + summary: "Generate SIWE Message", + description: "Generates a Sign-In with Ethereum (SIWE) message for a given Ethereum address.", + parameters: [ + %OpenApiSpex.Parameter{ + name: :address, + in: :query, + schema: Schemas.General.AddressHash, + required: true, + description: "Address hash in the query" + } + ], + responses: %{ + ok: {"SIWE message.", "application/json", AccountSchemas.siwe_message_response_schema()}, + unprocessable_entity: JsonErrorResponse.response(), + internal_server_error: {"Error message", "application/json", message_response_schema()} + } + @doc """ Generates a Sign-In with Ethereum (SIWE) message for a given Ethereum address. @@ -172,13 +251,23 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do - The SIWE message expires after 300 seconds from generation. """ @spec siwe_message(Conn.t(), map()) :: {:error, String.t()} | {:format, :error} | Conn.t() - def siwe_message(conn, %{"address" => address}) do + def siwe_message(conn, %{address: address}) do with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address)}, {:ok, message} <- Auth0.generate_siwe_message(Address.checksum(address_hash)) do conn |> put_status(200) |> json(%{siwe_message: message}) end end + operation :authenticate_via_wallet, + summary: "Authenticate via Ethereum Wallet", + description: "Authenticates a user using a signed Ethereum message (SIWE).", + request_body: AccountSchemas.authenticate_via_wallet_request_body(), + responses: %{ + ok: {"User info.", "application/json", AccountSchemas.User}, + unprocessable_entity: JsonErrorResponse.response(), + internal_server_error: {"Error message", "application/json", message_response_schema()} + } + @doc """ Authenticates a user via their Ethereum wallet using a signed message. @@ -213,12 +302,64 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do information. """ @spec authenticate_via_wallet(Conn.t(), map()) :: :error | {:error, any()} | Conn.t() - def authenticate_via_wallet(conn, %{"message" => message, "signature" => signature}) do + def authenticate_via_wallet(conn, _params) do + message = Map.get(conn.body_params, :message) + signature = Map.get(conn.body_params, :signature) + with {:ok, auth} <- Auth0.get_auth_with_web3(message, signature) do put_auth_to_session(conn, auth) end end + operation :authenticate_via_dynamic, + summary: "Authenticate via Dynamic JWT", + description: "Authenticates a user using a Dynamic JWT token from the Authorization header.", + security: [%{"dynamic_jwt" => []}], + responses: %{ + ok: {"User info.", "application/json", AccountSchemas.User}, + unauthorized: UnauthorizedResponse.response(), + internal_server_error: {"Error message", "application/json", message_response_schema()} + } + + @doc """ + Authenticates a request using a Dynamic JWT token from the Authorization header. + + This function extracts a Bearer token from the request's "authorization" + header, verifies it through Dynamic's authentication system, and upon + successful validation, establishes a user session with the extracted + identity information. + + The function accepts both "Bearer" and "bearer" prefixes in the + authorization header (case-insensitive prefix matching). + + ## Parameters + - `conn`: The `Plug.Conn` struct representing the current connection. + - `params`: Request parameters (unused). + + ## Returns + - `Conn.t()` with updated session and rendered user info on successful + authentication. + - `{:error, any()}` if token verification or session creation fails. + - `{:token, nil}` if the authorization header is missing or malformed. + + ## Notes + - Errors are handled by `BlockScoutWeb.Account.API.V2.FallbackController`. + """ + @spec authenticate_via_dynamic(Conn.t(), map()) :: {:error, any()} | {:token, nil} | Conn.t() + def authenticate_via_dynamic(conn, _params) do + token = + case get_req_header(conn, "authorization") do + ["Bearer " <> token] -> token + ["bearer " <> token] -> token + _ -> nil + end + + with {:token, not_nil_token} when not is_nil(not_nil_token) <- {:token, token}, + {:ok, auth} <- Dynamic.get_auth_from_token(not_nil_token) do + put_auth_to_session(conn, auth) + end + end + @doc """ Updates the session with authentication information and renders user info. diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex index 86e902e6c2f6..82d119c02d5c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex @@ -139,6 +139,13 @@ defmodule BlockScoutWeb.Account.API.V2.FallbackController do |> render(:message, %{message: "Invalid reCAPTCHA response"}) end + def call(conn, {:token, nil}) do + conn + |> put_status(:unauthorized) + |> put_view(UserView) + |> render(:message, %{message: "No Bearer token"}) + end + defp unauthorized_error(%{email_verified: false, email: email}) do %{message: "Unverified email", email: email} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex index b2e5f7f4f13c..00f81e619256 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/csv_export_controller.ex @@ -33,7 +33,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), @@ -127,7 +126,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), @@ -166,7 +164,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), @@ -205,7 +202,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), @@ -244,7 +240,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), @@ -284,7 +279,6 @@ defmodule BlockScoutWeb.API.V2.CsvExportController do base_params() ++ [ address_hash_param(), - address_id_param(), from_period_param(), to_period_param(), filter_type_param(), diff --git a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex index 606fd2cb3b07..2bda9462e249 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/account_router.ex @@ -51,6 +51,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do plug(:fetch_session) plug(:protect_from_forgery) plug(CheckAccountAPI) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Private) end pipeline :api_v2 do @@ -65,6 +66,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do plug(BlockScoutWeb.Plug.Logger, application: :api_v2) plug(:accepts, ["json"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Private) end scope "/auth", BlockScoutWeb do @@ -176,6 +178,7 @@ defmodule BlockScoutWeb.Routers.AccountRouter do pipe_through(:api_v2) post("/authenticate_via_wallet", AuthenticateController, :authenticate_via_wallet) + get("/authenticate_via_dynamic", AuthenticateController, :authenticate_via_dynamic) post("/send_otp", AuthenticateController, :send_otp) post("/confirm_otp", AuthenticateController, :confirm_otp) get("/siwe_message", AuthenticateController, :siwe_message) diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 8e4e0692afad..a89d549c1a0a 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -29,6 +29,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do mud_enabled?: [:explorer, [Explorer.Chain.Mud, :enabled]] alias BlockScoutWeb.Routers.{ + AccountRouter, AddressBadgesApiV2Router, APIKeyV2Router, SmartContractsApiV2Router, @@ -37,7 +38,6 @@ defmodule BlockScoutWeb.Routers.ApiRouter do } alias BlockScoutWeb.Plug.{CheckApiV2, CheckFeature} - alias BlockScoutWeb.Routers.AccountRouter @max_query_string_length 5_000 @@ -77,7 +77,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Public) end pipeline :api_v2_no_session do diff --git a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex index 033ccca6cac2..adfadae622e2 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Public) end pipeline :api_v2_no_forgery_protect do @@ -42,7 +42,7 @@ defmodule BlockScoutWeb.Routers.SmartContractsApiV2Router do plug(:accepts, ["json"]) plug(CheckApiV2) plug(:fetch_session) - plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Public) end scope "/", as: :api_v2 do diff --git a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex index 42c06b9a0bfe..faf4f5e8c16a 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(CheckApiV2) plug(:fetch_session) plug(:protect_from_forgery) - plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Public) end pipeline :api_v2_no_forgery_protect do @@ -42,7 +42,7 @@ defmodule BlockScoutWeb.Routers.TokensApiV2Router do plug(:accepts, ["json"]) plug(CheckApiV2) plug(:fetch_session) - plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.ApiSpec) + plug(OpenApiSpex.Plug.PutApiSpec, module: BlockScoutWeb.Specs.Public) end scope "/", as: :api_v2 do diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account.ex new file mode 100644 index 000000000000..586b08b13ae2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account.ex @@ -0,0 +1,85 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Account do + @moduledoc """ + Provides OpenAPI specification schemas for account-related API V2 endpoints. + + This module defines request body and response schemas used by the account + authentication endpoints. It supports two authentication flows: + + - **Email OTP authentication**: Users request a one-time password via email + and confirm it to authenticate + - **Wallet authentication**: Users authenticate using Sign-In With Ethereum + (SIWE) by signing a message with their wallet + + The schemas defined here are used with OpenApiSpex to document and validate + the API request and response payloads in the BlockScout API documentation. + """ + + @spec send_otp_request_body() :: OpenApiSpex.RequestBody.t() + def send_otp_request_body do + %OpenApiSpex.RequestBody{ + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: %OpenApiSpex.Schema{ + type: :object, + properties: %{ + email: %OpenApiSpex.Schema{type: :string, format: :email} + }, + required: [ + :email + ] + } + } + } + } + end + + @spec authenticate_via_wallet_request_body() :: OpenApiSpex.RequestBody.t() + def authenticate_via_wallet_request_body do + %OpenApiSpex.RequestBody{ + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: %OpenApiSpex.Schema{ + type: :object, + properties: %{ + message: %OpenApiSpex.Schema{type: :string}, + signature: %OpenApiSpex.Schema{type: :string} + }, + required: [:message, :signature] + } + } + } + } + end + + @spec confirm_otp_request_body() :: OpenApiSpex.RequestBody.t() + def confirm_otp_request_body do + %OpenApiSpex.RequestBody{ + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: %OpenApiSpex.Schema{ + type: :object, + properties: %{ + email: %OpenApiSpex.Schema{type: :string, format: :email}, + otp: %OpenApiSpex.Schema{type: :string} + }, + required: [ + :email, + :otp + ] + } + } + } + } + end + + @spec siwe_message_response_schema() :: OpenApiSpex.Schema.t() + def siwe_message_response_schema do + %OpenApiSpex.Schema{ + type: :object, + properties: %{ + siwe_message: %OpenApiSpex.Schema{type: :string} + }, + required: [:siwe_message] + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/session.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/session.ex new file mode 100644 index 000000000000..bfc0872b6357 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/session.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Account.Session do + @moduledoc """ + This module defines the schema for the account session. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General.AddressHashNullable + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + id: %Schema{type: :integer, minimum: 0, nullable: false}, + uid: %Schema{type: :string, nullable: false}, + email: %Schema{type: :string, format: :email, nullable: true}, + name: %Schema{type: :string, nullable: true}, + nickname: %Schema{type: :string, nullable: true}, + avatar: %Schema{type: :string, format: :uri, nullable: true}, + address_hash: AddressHashNullable, + watchlist_id: %Schema{type: :integer, minimum: 0, nullable: false}, + email_verified: %Schema{type: :boolean, nullable: false} + }, + required: [:id, :uid, :email, :nickname, :avatar, :address_hash, :email_verified], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/user.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/user.ex new file mode 100644 index 000000000000..c7a914c0fa1d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/account/user.ex @@ -0,0 +1,22 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Account.User do + @moduledoc """ + This module defines the schema for the user. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General.AddressHashNullable + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + email: %Schema{type: :string, format: :email, nullable: true}, + name: %Schema{type: :string, nullable: true}, + nickname: %Schema{type: :string, nullable: true}, + avatar: %Schema{type: :string, format: :uri, nullable: true}, + address_hash: AddressHashNullable + }, + required: [:email, :name, :nickname, :avatar, :address_hash], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex index 3f4eb8fa0f90..06300fb42f72 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex @@ -10,16 +10,15 @@ defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do @moduledoc false OpenApiSpex.schema(%{ title: "ForbiddenResponse", - description: "Response returned when the user is not authorized to access the resource", + description: "Response returned when the user is forbidden to access the resource", type: :object, properties: %{ message: %Schema{ type: :string, - description: "Error message indicating the user is not authorized to access the resource", - example: "Restricted access" + description: "Error message indicating the user is forbidden to access the resource", + example: "Unverified email" } - }, - additionalProperties: false + } }) @spec response() :: {String.t(), String.t(), module()} @@ -48,4 +47,25 @@ defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do {"Not Found", "application/json", __MODULE__} end end + + defmodule UnauthorizedResponse do + @moduledoc false + OpenApiSpex.schema(%{ + title: "UnauthorizedResponse", + description: "Response returned when the user is not authorized to access the resource", + type: :object, + properties: %{ + message: %Schema{ + type: :string, + description: "Error message indicating the user is not authorized to access the resource", + example: "Unauthorized" + } + } + }) + + @spec response() :: {String.t(), String.t(), module()} + def response do + {"Unauthorized", "application/json", __MODULE__} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index dbb0b64f8259..6fb967e943f8 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -73,22 +73,6 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end - # todo: It should be removed when the frontend stops sending the address_id parameter with the request - # https://github.com/blockscout/frontend/issues/3090 - @doc """ - Returns a parameter definition for an address hash in the path. - """ - @spec address_id_param() :: Parameter.t() - def address_id_param do - %Parameter{ - name: :address_id, - in: :query, - schema: AddressHash, - required: false, - description: "Address hash in the query" - } - end - @doc """ Returns a parameter definition for the start of the time period. """ @@ -677,6 +661,28 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for API key for sensitive endpoints in the request body. + """ + @spec admin_api_key_request_body() :: OpenApiSpex.RequestBody.t() + def admin_api_key_request_body do + %OpenApiSpex.RequestBody{ + content: %{ + "application/json" => %OpenApiSpex.MediaType{ + schema: %OpenApiSpex.Schema{ + type: :object, + properties: %{ + api_key: %Schema{type: :string} + }, + required: [ + :api_key + ] + } + } + } + } + end + @doc """ Returns a parameter definition for reCAPTCHA response token. """ @@ -822,6 +828,22 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a schema definition for a simple message response. + """ + @spec message_response_schema :: Schema.t() + def message_response_schema do + %Schema{ + type: :object, + properties: %{ + message: %Schema{type: :string} + }, + required: [:message], + nullable: false, + additionalProperties: false + } + end + # `%Schema{anyOf: [%Schema{type: :integer}, EmptyString]}` is used because, # `allowEmptyValue: true` does not allow empty string for some reasons (at least in this case) diff --git a/apps/block_scout_web/lib/block_scout_web/specs.ex b/apps/block_scout_web/lib/block_scout_web/specs.ex new file mode 100644 index 000000000000..af25c3a9c288 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/specs.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Specs do + @moduledoc """ + Provides utility functions for working with Phoenix router routes in API specification modules. + + This module contains shared functionality used by `BlockScoutWeb.Specs.Public` and + `BlockScoutWeb.Specs.Private` for route manipulation and processing. + """ + + @doc """ + Retrieves all routes from the given router module and prepends a prefix to each route's path. + + ## Parameters + - `router`: A Phoenix router module that implements `__routes__/0`. + - `prefix`: A string to prepend to each route's path. + + ## Returns + - A list of route maps with the `:path` values modified to include the prefix. + """ + @spec routes_with_prefix(module(), String.t()) :: [map()] + def routes_with_prefix(router, prefix) do + router.__routes__() + |> Enum.map(fn %{path: path} = route -> %{route | path: prefix <> path} end) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/specs/private.ex b/apps/block_scout_web/lib/block_scout_web/specs/private.ex new file mode 100644 index 000000000000..376ffb48fb01 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/specs/private.ex @@ -0,0 +1,33 @@ +defmodule BlockScoutWeb.Specs.Private do + @moduledoc """ + This module defines the private API specification for the BlockScoutWeb application. + """ + + alias BlockScoutWeb.Routers.AccountRouter + alias BlockScoutWeb.Specs + alias OpenApiSpex.{Components, Contact, Info, OpenApi, Paths, SecurityScheme, Server} + alias Utils.Helper + + @behaviour OpenApi + + @impl OpenApi + def spec do + %OpenApi{ + servers: [ + %Server{url: to_string(Helper.instance_url() |> URI.append_path("/api"))} + ], + info: %Info{ + title: "Blockscout Private API", + version: to_string(Application.spec(:block_scout_web, :vsn)), + contact: %Contact{ + email: "info@blockscout.com" + } + }, + paths: Paths.from_routes(Specs.routes_with_prefix(AccountRouter, "/account")), + components: %Components{ + securitySchemes: %{"dynamic_jwt" => %SecurityScheme{type: "http", scheme: "bearer", bearerFormat: "JWT"}} + } + } + |> OpenApiSpex.resolve_schema_modules() + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/api_spec.ex b/apps/block_scout_web/lib/block_scout_web/specs/public.ex similarity index 59% rename from apps/block_scout_web/lib/block_scout_web/api_spec.ex rename to apps/block_scout_web/lib/block_scout_web/specs/public.ex index a153f60a61b5..569de9b41a2d 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_spec.ex +++ b/apps/block_scout_web/lib/block_scout_web/specs/public.ex @@ -1,8 +1,10 @@ -defmodule BlockScoutWeb.ApiSpec do +defmodule BlockScoutWeb.Specs.Public do @moduledoc """ - This module defines the API specification for the BlockScoutWeb application. + This module defines the public API specification for the BlockScoutWeb application. """ + alias BlockScoutWeb.Routers.{ApiRouter, SmartContractsApiV2Router, TokensApiV2Router} + alias BlockScoutWeb.Specs alias OpenApiSpex.{Contact, Info, OpenApi, Paths, Server} alias Utils.Helper @@ -24,14 +26,9 @@ defmodule BlockScoutWeb.ApiSpec do paths: ApiRouter |> Paths.from_router() - |> Map.merge(Paths.from_routes(routes_with_prefix(TokensApiV2Router, "/v2/tokens"))) - |> Map.merge(Paths.from_routes(routes_with_prefix(SmartContractsApiV2Router, "/v2/smart-contracts"))) + |> Map.merge(Paths.from_routes(Specs.routes_with_prefix(TokensApiV2Router, "/v2/tokens"))) + |> Map.merge(Paths.from_routes(Specs.routes_with_prefix(SmartContractsApiV2Router, "/v2/smart-contracts"))) } |> OpenApiSpex.resolve_schema_modules() end - - defp routes_with_prefix(router, prefix) do - router.__routes__() - |> Enum.map(fn %{path: path} = route -> %{route | path: prefix <> path} end) - end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/authenticate_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/authenticate_controller_test.exs new file mode 100644 index 000000000000..59950f5dbd1e --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/authenticate_controller_test.exs @@ -0,0 +1,497 @@ +defmodule BlockScoutWeb.Account.API.V2.AuthenticateControllerTest do + use BlockScoutWeb.ConnCase, async: false + + alias Explorer.Account.Identity + alias Explorer.Chain.Address + alias Explorer.ThirdPartyIntegrations.Dynamic + alias Explorer.ThirdPartyIntegrations.Dynamic.Strategy + + import Mox + + describe "POST api/account/v2/send_otp" do + test "send OTP successfully", %{conn: conn} do + Tesla.Test.expect_tesla_call( + times: 3, + returns: fn + %{ + method: :post, + url: "https://example.com/oauth/token", + query: [], + headers: [{"Content-type", "application/json"}], + body: + ~s|{"audience":"https://example.com/api/v2/","client_id":"client_id","client_secret":"secrets","grant_type":"client_credentials"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s({"access_token": "test_token", "expires_in": 86400}) + }} + + %Tesla.Env{ + method: :get, + url: "https://example.com/api/v2/users", + query: [q: ~s|email:"test@example.com" OR user_metadata.email:"test@example.com"|], + headers: [{"accept", "application/json"}, {"authorization", "Bearer test_token"}], + body: "" + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|[]| + }} + + %{ + method: :post, + url: "https://example.com/passwordless/start", + query: %{}, + headers: [ + {"accept", "application/json"}, + {"auth0-forwarded-for", _ip}, + {"content-type", "application/json"} + ], + body: + ~s|{"send":"code","connection":"email","email":"test@example.com","client_id":"client_id","client_secret":"secrets"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|{"_id":"123","email":"test@example.com","email_verified":false}| + }} + end + ) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/account/v2/send_otp", JSON.encode!(%{"email" => "test@example.com"})) + |> json_response(200) + + assert response == %{"message" => "Success"} + end + + test "send OTP for linking email to an existing account successfully", %{conn: conn} do + auth = :auth |> build() |> put_in([Access.key!(:info), Access.key!(:email)], nil) + {:ok, user} = Identity.find_or_create(auth) + conn_with_user = Plug.Test.init_test_session(conn, current_user: user) + + Tesla.Test.expect_tesla_call( + times: 3, + returns: fn + %{ + method: :post, + url: "https://example.com/oauth/token", + query: [], + headers: [{"Content-type", "application/json"}], + body: + ~s|{"audience":"https://example.com/api/v2/","client_id":"client_id","client_secret":"secrets","grant_type":"client_credentials"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s({"access_token": "test_token", "expires_in": 86400}) + }} + + %Tesla.Env{ + method: :get, + url: "https://example.com/api/v2/users", + query: [q: ~s|email:"test@example.com" OR user_metadata.email:"test@example.com"|], + headers: [{"accept", "application/json"}, {"authorization", "Bearer test_token"}], + body: "" + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|[]| + }} + + %{ + method: :post, + url: "https://example.com/passwordless/start", + query: %{}, + headers: [ + {"accept", "application/json"}, + {"auth0-forwarded-for", _ip}, + {"content-type", "application/json"} + ], + body: + ~s|{"send":"code","connection":"email","email":"test@example.com","client_id":"client_id","client_secret":"secrets"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|{"_id":"123","email":"test@example.com","email_verified":false}| + }} + end + ) + + response = + conn_with_user + |> put_req_header("content-type", "application/json") + |> post("/api/account/v2/send_otp", JSON.encode!(%{"email" => "test@example.com"})) + |> json_response(200) + + assert response == %{"message" => "Success"} + end + + test "do not send OTP for linking email to an existing account when email is already linked", %{conn: conn} do + auth = :auth |> build() |> put_in([Access.key!(:info), Access.key!(:email)], nil) + {:ok, user} = Identity.find_or_create(auth) + conn_with_user = Plug.Test.init_test_session(conn, current_user: user) + + Tesla.Test.expect_tesla_call( + times: 3, + returns: fn + %{ + method: :post, + url: "https://example.com/oauth/token", + query: [], + headers: [{"Content-type", "application/json"}], + body: + ~s|{"audience":"https://example.com/api/v2/","client_id":"client_id","client_secret":"secrets","grant_type":"client_credentials"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s({"access_token": "test_token", "expires_in": 86400}) + }} + + %Tesla.Env{ + method: :get, + url: "https://example.com/api/v2/users", + query: [q: ~s|email:"test@example.com" OR user_metadata.email:"test@example.com"|], + headers: [{"accept", "application/json"}, {"authorization", "Bearer test_token"}], + body: "" + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + ~s([{"identities":[{"connection":"email","user_id":"123","provider":"email","isSocial":false}],"user_id":"email|123","email":"test@example.com"}]) + }} + + %{ + method: :post, + url: "https://example.com/passwordless/start", + query: %{}, + headers: [ + {"accept", "application/json"}, + {"auth0-forwarded-for", _ip}, + {"content-type", "application/json"} + ], + body: + ~s|{"send":"code","connection":"email","email":"test@example.com","client_id":"client_id","client_secret":"secrets"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|{"_id":"123","email":"test@example.com","email_verified":false}| + }} + end + ) + + response = + conn_with_user + |> put_req_header("content-type", "application/json") + |> post("/api/account/v2/send_otp", JSON.encode!(%{"email" => "test@example.com"})) + |> json_response(500) + + assert response == %{"message" => "Account with this email already exists"} + end + + test "do nothing for an account with an existing email", %{conn: conn} do + auth = build(:auth) + {:ok, user} = Identity.find_or_create(auth) + conn_with_user = Plug.Test.init_test_session(conn, current_user: user) + + Tesla.Test.expect_tesla_call( + times: 3, + returns: fn + %{ + method: :post, + url: "https://example.com/oauth/token", + query: [], + headers: [{"Content-type", "application/json"}], + body: + ~s|{"audience":"https://example.com/api/v2/","client_id":"client_id","client_secret":"secrets","grant_type":"client_credentials"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s({"access_token": "test_token", "expires_in": 86400}) + }} + + %Tesla.Env{ + method: :get, + url: "https://example.com/api/v2/users", + query: [q: ~s|email:"test@example.com" OR user_metadata.email:"test@example.com"|], + headers: [{"accept", "application/json"}, {"authorization", "Bearer test_token"}], + body: "" + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|[]| + }} + + %{ + method: :post, + url: "https://example.com/passwordless/start", + query: %{}, + headers: [ + {"accept", "application/json"}, + {"auth0-forwarded-for", _ip}, + {"content-type", "application/json"} + ], + body: + ~s|{"send":"code","connection":"email","email":"test@example.com","client_id":"client_id","client_secret":"secrets"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s|{"_id":"123","email":"test@example.com","email_verified":false}| + }} + end + ) + + response = + conn_with_user + |> put_req_header("content-type", "application/json") + |> post("/api/account/v2/send_otp", JSON.encode!(%{"email" => "test@example.com"})) + |> json_response(500) + + assert response == %{"message" => "This account already has an email"} + end + end + + describe "GET api/account/v2/siwe_message" do + test "get SIWE message successfully", %{conn: conn} do + address = build(:address) + + response = + conn + |> get("/api/account/v2/siwe_message?address=#{address.hash}") + |> json_response(200) + + assert String.contains?(response["siwe_message"], Address.checksum(address)) + end + + test "return error for an invalid address", %{conn: conn} do + response = + conn + |> get("/api/account/v2/siwe_message?address=invalid_address") + |> json_response(422) + + assert response == %{ + "errors" => [ + %{ + "title" => "Invalid value", + "source" => %{"pointer" => "/address"}, + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{40})$/" + } + ] + } + end + end + + describe "POST api/account/v2/authenticate_via_wallet" do + test "authenticate via wallet successfully", %{conn: conn} do + private_key = :crypto.strong_rand_bytes(32) + {:ok, <<0x04, public_key_raw::binary-size(64)>>} = ExSecp256k1.create_public_key(private_key) + + <<_::binary-size(12), address_bytes::binary-size(20)>> = + ExKeccak.hash_256(public_key_raw) + + address_string = Address.checksum(address_bytes) + + Tesla.Test.expect_tesla_call( + times: 2, + returns: fn + %{ + method: :post, + url: "https://example.com/oauth/token", + query: [], + headers: [{"Content-type", "application/json"}], + body: + ~s|{"audience":"https://example.com/api/v2/","client_id":"client_id","client_secret":"secrets","grant_type":"client_credentials"}| + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: ~s({"access_token": "test_token", "expires_in": 86400}) + }} + + %Tesla.Env{ + method: :get, + url: "https://example.com/api/v2/users", + query: _, + headers: [{"accept", "application/json"}, {"authorization", "Bearer test_token"}], + body: "" + }, + _opts -> + {:ok, + %Tesla.Env{ + status: 200, + body: + ~s([{"identities":[{"connection":"email","user_id":"123","provider":"email","isSocial":false}],"user_id":"email|123","email":"test@example.com","user_metadata":{"web3_address_hash":"#{address_string}"}}]) + }} + end + ) + + message = + conn + |> get("/api/account/v2/siwe_message?address=#{address_string}") + |> json_response(200) + |> Map.get("siwe_message") + + # cspell:disable-next-line + hash = ExKeccak.hash_256("\x19Ethereum Signed Message:\n#{byte_size(message)}" <> message) + + {:ok, {rs_binary, v}} = ExSecp256k1.sign_compact(hash, private_key) + signature = "0x" <> Base.encode16(rs_binary <> <>, case: :lower) + + response = + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/account/v2/authenticate_via_wallet", + JSON.encode!(%{"message" => message, "signature" => signature}) + ) + |> json_response(200) + + assert response["email"] == "test@example.com" and response["address_hash"] == address_string + end + + test "return error for invalid signature", %{conn: conn} do + response = + conn + |> put_req_header("content-type", "application/json") + |> post( + "/api/account/v2/authenticate_via_wallet", + JSON.encode!(%{"message" => "test_message", "signature" => "0x1234"}) + ) + |> json_response(500) + + assert %{"message" => _error_message} = response + end + end + + describe "GET api/account/v2/authenticate_via_dynamic" do + setup :set_mox_global + + test "authenticate via dynamic successfully", %{conn: conn} do + initial_dynamic_env = Application.get_env(:explorer, Dynamic) + initial_strategy_env = Application.get_env(:explorer, Strategy) + + Application.put_env( + :explorer, + Dynamic, + Keyword.merge(initial_dynamic_env, + enabled: true, + env_id: "test_env", + url: "https://app.dynamic.xyz/api/v0/sdk/test_env/.well-known/jwks" + ) + ) + + Application.put_env( + :explorer, + Strategy, + Keyword.merge(initial_strategy_env, enabled: true) + ) + + on_exit(fn -> + Application.put_env(:explorer, Dynamic, initial_dynamic_env) + Application.put_env(:explorer, Strategy, initial_strategy_env) + end) + + Tesla.Test.expect_tesla_call( + times: 1, + returns: fn %{url: "https://app.dynamic.xyz/api/v0/sdk/test_env/.well-known/jwks"}, _opts -> + {:ok, + %Tesla.Env{ + status: 200, + headers: [{"content-type", "application/json"}], + # cspell:disable + body: + ~s|{"keys":[{"alg":"RS256","e":"AQAB","ext":true,"key_ops":["verify"],"kty":"RSA","n":"xoFNCjaQg7I6oFW1LP3H733NWnvXwHCz8igFgJ9VhyjZkHfbETNIEVOSHmIHrLZln10UrPM1lwUnjV_Q27mApf0k_mNIQlH94npvAt4K9sC9tVx1TOzylBIynTEJv0u7Q2feRjwku2th6yBx2pSZxthbXzcy2trIxQE8NZHzQXgll4vJynemGFcqBS-uxlM6zdJDzfJGgs2q2d8GgZ6izc5N410zmbh7rmEuiNhVRhdBaxv2YSslI-dZiXdrcLhjLBpczBvxjJ-T6rQ7SrJTy7ELlolvP84gE0InuWDK6-RMCC_W_xc44sxPj1JRSUcH7MsGP2rzISA-HdNlSrWJEw","kid":"3SLxTe6F2vUW71mEKH0/tbt3/GxDVSb/rwqsefdZVCM=","use":"sig"}]}| + # cspell:enable + }} + end + ) + + start_supervised!(Strategy) + + :timer.sleep(500) + + response = + conn + |> put_req_header( + "authorization", + # cspell:disable-next-line + "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjNTTHhUZTZGMnZVVzcxbUVLSDAvdGJ0My9HeERWU2Ivcndxc2VmZFpWQ009In0.eyJraWQiOiI1ZDE0YzMzMS1lNWRjLTQwZDgtOGM5Yy00Zjc1N2YxMDYzMTgiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJhcHAuZHluYW1pY2F1dGguY29tL3Rlc3RfZW52Iiwic3ViIjoiNDBhM2NkYTEtNjU2Yy00NzM3LTkyZjgtZWMwYzAzNjZjNTVhIiwic2lkIjoiNDkwZjA3MzgtY2IwNi00MjFkLWIxNGEtZDJhMzJhODA1NzdjIiwic2Vzc2lvbl9wdWJsaWNfa2V5IjoiMDMwNTY0OGI0Nzc4MzU3MzUwZmZhNDk3ZmJmNzQ5ZjAwOWQ3Njk2ODkzNmI5Y2E4ZGI3MzY4OGY2MzIwN2RhZGE0IiwiYWxpYXMiOiJhbGlhcyIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImVudmlyb25tZW50X2lkIjoiOTA0ZTdiZmEtNDEyZi00NzY4LTk4YzUtOWEwMmY3Yzc0MTJkIiwiZmFtaWx5X25hbWUiOiJsbiIsImdpdmVuX25hbWUiOiJmbiIsImxpc3RzIjpbXSwibWlzc2luZ19maWVsZHMiOltdLCJ1c2VybmFtZSI6InVzZXJuYW1lIiwidmVyaWZpZWRfY3JlZGVudGlhbHMiOlt7ImFkZHJlc3MiOiIweDAzYzM2M2Y0OGM0RkUwRjJFYzZlZmJENDlGN2IxMTRiOEE2MWMxNGIiLCJjaGFpbiI6ImVpcDE1NSIsImlkIjoiNDQ2NDI4YTUtNTQxYS00OWY0LThkYmItYmIyYjhjODgwMjczIiwibmFtZV9zZXJ2aWNlIjp7fSwicHVibGljX2lkZW50aWZpZXIiOiIweDAzYzM2M2Y0OGM0RkUwRjJFYzZlZmJENDlGN2IxMTRiOEE2MWMxNGIiLCJ3YWxsZXRfbmFtZSI6Im1ldGFtYXNrIiwid2FsbGV0X3Byb3ZpZGVyIjoiYnJvd3NlckV4dGVuc2lvbiIsImZvcm1hdCI6ImJsb2NrY2hhaW4iLCJsYXN0U2VsZWN0ZWRBdCI6IjIwMjYtMDEtMDhUMDc6NDg6MTAuMzM0WiIsInNpZ25JbkVuYWJsZWQiOnRydWV9LHsiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiaWQiOiI2NDU4MWExMi0yZjMwLTRmZjgtOTU4OC1lMGM5NGVhMWM4OWIiLCJwdWJsaWNfaWRlbnRpZmllciI6InRlc3RAZXhhbXBsZS5jb20iLCJmb3JtYXQiOiJlbWFpbCIsInNpZ25JbkVuYWJsZWQiOnRydWUsInZlcmlmaWVkQXQiOiIyMDI1LTEyLTIzVDEwOjU1OjA5LjM2NVoifV0sImxhc3RfdmVyaWZpZWRfY3JlZGVudGlhbF9pZCI6IjQ0NjQyOGE1LTU0MWEtNDlmNC04ZGJiLWJiMmI4Yzg4MDI3MyIsImZpcnN0X3Zpc2l0IjoiMjAyNS0xMi0xOFQxMzo1Mjo1OC4yMDFaIiwibGFzdF92aXNpdCI6IjIwMjYtMDEtMDhUMDc6NDg6MTAuMzIxWiIsIm5ld191c2VyIjpmYWxzZSwibWV0YWRhdGEiOnt9LCJ2ZXJpZmllZENyZWRlbnRpYWxzSGFzaGVzIjp7ImJsb2NrY2hhaW4iOiI1NTAwMTUyZDMwMjc2MzIwMDNmZmUxOTRlMmM3YzFiYyIsImVtYWlsIjoiMjg1OGY0OWQ2YjE2Nzg3NTFlNTA0ZDQ3ODM0ZDcwMGEiLCJvYXV0aCI6ImZmMTA5N2I1MGVkMDNhNDA4MWRhMTA2NjBlMDEzMzg5In0sImhhc2hlZF9pcCI6ImMxNWE3NTgzNGVkY2JjZjI3NmQyYTQ3NmFmNmJjMTVmIiwicmVmcmVzaEV4cCI6MTc3MDQ1MDQ5MCwiaWF0IjoxNzY3ODU4NDkwLCJleHAiOjMwNjc4NjU2OTB9.S-9hkUbqr5P69xtu4qcSDbNrjUiUa4BnhvUHHSSCZ-7FHUvjRH8LXj4lGrbGIpoLAEMMdRzi8l9HkQSH7ASACP2-cm3JRDr5-p2-IN4Qm5GTo0o2ewzxxhqpNCQocUkPld6JUY-3O1XobaVCL7PNLnBUV4-jCGKkQbgpye50dezq7dqjV3CXxhpKt-80gmWxlVyIEkGENKawlvw6AUShtMYHhvqon-RqCtJsYRzGQXMdsAOkvV-0vXN8PVLk5fKJ6GInuW8hYB_i_V_HRChQnkvHsswMBj3-hEmwh5x6lZY2kq3fcoVsQI1lSaYK5ZctO-ij476o1VDgBIVmvw2Bug" + ) + |> get("/api/account/v2/authenticate_via_dynamic") + |> json_response(200) + + assert response["email"] == "test@example.com" + end + + test "without bearer token returns error", %{conn: conn} do + initial_dynamic_env = Application.get_env(:explorer, Dynamic) + initial_strategy_env = Application.get_env(:explorer, Strategy) + + Application.put_env( + :explorer, + Dynamic, + Keyword.merge(initial_dynamic_env, + enabled: true, + env_id: "test_env", + url: "https://app.dynamic.xyz/api/v0/sdk/test_env/.well-known/jwks" + ) + ) + + Application.put_env( + :explorer, + Strategy, + Keyword.merge(initial_strategy_env, enabled: true) + ) + + on_exit(fn -> + Application.put_env(:explorer, Dynamic, initial_dynamic_env) + Application.put_env(:explorer, Strategy, initial_strategy_env) + end) + + response = + conn + |> get("/api/account/v2/authenticate_via_dynamic") + |> json_response(401) + + assert response == %{"message" => "No Bearer token"} + end + + test "without config returns error", %{conn: conn} do + response = + conn + |> put_req_header( + "authorization", + "Bearer some_token" + ) + |> get("/api/account/v2/authenticate_via_dynamic") + |> json_response(500) + + assert response == %{"message" => "Dynamic integration is disabled"} + end + end +end diff --git a/apps/block_scout_web/test/support/api_schema_assertions.ex b/apps/block_scout_web/test/support/api_schema_assertions.ex index 70125a93bcd0..31c4de1b907c 100644 --- a/apps/block_scout_web/test/support/api_schema_assertions.ex +++ b/apps/block_scout_web/test/support/api_schema_assertions.ex @@ -20,43 +20,45 @@ defmodule BlockScoutWeb.TestApiSchemaAssertions do json end - defp maybe_assert_schema(%Plug.Conn{method: "GET", request_path: request_path} = _conn, status_code, json) + defp maybe_assert_schema(%Plug.Conn{method: method, request_path: request_path} = _conn, status_code, json) when is_integer(status_code) and is_binary(request_path) do - if String.starts_with?(request_path, "/api/v2") do - spec = BlockScoutWeb.ApiSpec.spec() + method_atom = String.downcase(method) |> String.to_atom() + public_spec = BlockScoutWeb.Specs.Public.spec() + private_spec = BlockScoutWeb.Specs.Private.spec() - with {:path_item, {:ok, %PathItem{} = path_item}} <- {:path_item, find_path_item(spec, request_path)}, - {:operation, %Operation{} = operation} <- {:operation, Map.get(path_item, :get)}, - {:schema, {:ok, schema}} <- {:schema, find_response_schema(operation, status_code)} do - Logger.info("Validated response against schema for path: #{request_path} and status code: #{status_code}") + with {:path_item, {:ok, spec, %PathItem{} = path_item}} <- + {:path_item, find_path_item([public_spec, private_spec], request_path)}, + {:operation, %Operation{} = operation} <- {:operation, Map.get(path_item, method_atom)}, + {:schema, {:ok, schema}} <- {:schema, find_response_schema(operation, status_code)} do + Logger.info("Validated response against schema for path: #{request_path} and status code: #{status_code}") - OpenApiSpex.TestAssertions.assert_raw_schema(json, schema, spec) - else - {:path_item, :error} -> - Logger.warning("No schema found for path: #{request_path}") - :ok + OpenApiSpex.TestAssertions.assert_raw_schema(json, schema, spec) + else + {:path_item, :error} -> + Logger.warning("No schema found for path: #{request_path}") + :ok - {:operation, _} -> - Logger.warning("No GET operation found for path: #{request_path}") - :ok + {:operation, _} -> + Logger.warning("No #{method} operation found for path: #{request_path}") + :ok - {:schema, :error} -> - Logger.warning("No schema found for path: #{request_path} and status code: #{status_code}") - :ok - end + {:schema, :error} -> + Logger.warning("No schema found for path: #{request_path} and status code: #{status_code}") + :ok end end defp maybe_assert_schema(_conn, _status_code, _json), do: :ok - defp find_path_item(%{paths: paths} = _spec, request_path) when is_map(paths) do + defp find_path_item(specs, request_path) do api_relative = strip_api_prefix(request_path) - with {:ok, {_, path_item}} <- match_template_path(paths, api_relative) do - {:ok, path_item} - else - _ -> :error - end + Enum.reduce_while(specs, :error, fn %{paths: paths} = spec, acc -> + case match_template_path(paths, api_relative) do + {:ok, {_, path_item}} -> {:halt, {:ok, spec, path_item}} + _ -> {:cont, acc} + end + end) end defp strip_api_prefix("/api" <> rest), do: rest diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex index 07196fc774d5..f4be04b8538d 100644 --- a/apps/explorer/lib/explorer/account/identity.ex +++ b/apps/explorer/lib/explorer/account/identity.ex @@ -335,6 +335,10 @@ defmodule Explorer.Account.Identity do - A string representation of the Ethereum address hash, or nil if not found. """ @spec address_hash_from_auth(Auth.t()) :: String.t() | nil + def address_hash_from_auth(%Auth{provider: :dynamic, extra: %Extra{raw_info: %{address_hash: address_hash}}}) do + address_hash + end + def address_hash_from_auth(%Auth{ extra: %Extra{raw_info: %{user: %{"user_metadata" => %{"web3_address_hash" => address_hash}}}} }) do diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 14afbe71fda5..bce52f13daa7 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -349,6 +349,7 @@ defmodule Explorer.Application do Explorer.Migrator.SwitchPendingOperations, configure_mode_dependent_process(Explorer.Utility.RateLimiter, :api), Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api), + configure_mode_dependent_process(Explorer.ThirdPartyIntegrations.Dynamic.Strategy, :api), # keep at the end configure_libcluster() ] diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex index 8f1dcb408fdb..2850d82fb771 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0/internal.ex @@ -113,8 +113,9 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Internal do ## Returns - `:ok`: If the OTP was successfully sent - `:error`: If there was an error sending the OTP + - `{:format, :email}`: If the email format is invalid """ - @spec send_otp(String.t(), String.t()) :: :ok | :error + @spec send_otp(String.t(), String.t()) :: :ok | :error | {:format, :email} def send_otp(email, ip) do client = OAuth.client() @@ -132,6 +133,9 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0.Internal do {:ok, %OAuth2.Response{status_code: 200}} -> :ok + {:error, %OAuth2.Response{status_code: 400, body: %{"error" => "bad.email"}}} -> + {:format, :email} + other -> Logger.error("Error while sending otp: ", inspect(other)) diff --git a/apps/explorer/lib/explorer/third_party_integrations/dynamic.ex b/apps/explorer/lib/explorer/third_party_integrations/dynamic.ex new file mode 100644 index 000000000000..ba89a8be19de --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/dynamic.ex @@ -0,0 +1,82 @@ +defmodule Explorer.ThirdPartyIntegrations.Dynamic do + @moduledoc """ + Provides integration with Dynamic for JWT-based user authentication. + + This module handles authentication by verifying JWT tokens issued by Dynamic + and transforming their claims into `Ueberauth.Auth` structs. It extracts user + identity information from token claims, including data from OAuth and + blockchain verified credentials. + + Dynamic is an authentication provider that supports multiple credential + formats, including OAuth accounts and blockchain wallet addresses. + """ + + alias Explorer.ThirdPartyIntegrations.Dynamic.Token + alias Ueberauth.Auth + alias Ueberauth.Auth.{Extra, Info} + + @doc """ + Authenticates a user by verifying a JWT token and extracting identity information from its claims. + + The function validates the provided JWT token and, upon successful + verification, constructs an authentication struct containing user identity + data. It extracts information from the token's claims including user ID, + email, name, and verified credentials (OAuth and blockchain). + + If the token's scopes include `"requiresAdditionalAuth"`, authentication + is rejected, requiring further verification steps. + + ## Parameters + - `token`: A JWT bearer token string to verify and extract claims from. + + ## Returns + - `{:ok, Auth.t()}` if the token is valid and no additional authentication + is required. + - `{:error, String.t()}` if token verification fails or additional + authentication is required. + """ + @spec get_auth_from_token(String.t()) :: {:ok, Auth.t()} | {:error, String.t()} + def get_auth_from_token(token) do + with {:enabled, true} <- {:enabled, Application.get_env(:explorer, __MODULE__)[:enabled]}, + {:ok, claims} <- Token.verify_and_validate(token) do + create_auth(claims) + else + {:enabled, false} -> {:error, "Dynamic integration is disabled"} + {:error, reason} -> {:error, inspect(reason)} + end + end + + defp create_auth(claims) do + if claims["scopes"] && "requiresAdditionalAuth" in claims["scopes"] do + {:error, "Additional verification required"} + else + {:ok, do_create_auth(claims)} + end + end + + defp do_create_auth(claims) do + oauth = + Enum.find(claims["verified_credentials"] || [], fn + %{"format" => "oauth"} -> true + _ -> false + end) + + blockchain = + Enum.find(claims["verified_credentials"] || [], fn + %{"format" => "blockchain"} -> true + _ -> false + end) + + %Auth{ + uid: claims["metadata"]["auth0UserId"] || claims["sub"], + provider: :dynamic, + info: %Info{ + email: claims["email"], + name: (oauth && oauth["oauth_display_name"]) || claims["name"], + nickname: claims["username"], + image: oauth && List.first(oauth["oauth_account_photos"] || []) + }, + extra: %Extra{raw_info: %{address_hash: blockchain && blockchain["address"]}} + } + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/dynamic/strategy.ex b/apps/explorer/lib/explorer/third_party_integrations/dynamic/strategy.ex new file mode 100644 index 000000000000..b6dff6d6f85e --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/dynamic/strategy.ex @@ -0,0 +1,44 @@ +defmodule Explorer.ThirdPartyIntegrations.Dynamic.Strategy do + @moduledoc """ + JWKS fetching strategy for Dynamic + """ + use GenServer, restart: :transient + + alias Explorer.ThirdPartyIntegrations.Dynamic + alias JokenJwks.{DefaultStrategyTemplate, SignerMatchStrategy} + + @behaviour SignerMatchStrategy + + def init_opts(opts) do + url = Application.get_env(:explorer, Dynamic)[:url] + Keyword.merge(opts, jwks_url: url) + end + + @impl SignerMatchStrategy + def match_signer_for_kid(kid, opts), + do: DefaultStrategyTemplate.match_signer_for_kid(__MODULE__, kid, opts) + + @doc false + def start_link(opts), do: DefaultStrategyTemplate.start_link(__MODULE__, opts) + + # Server (callbacks) + @doc false + @impl GenServer + def init(opts) do + # with this trick first fetch is done immediately and asynchronously, no application startup delay + # and no `:time_interval` time without keys after application startup + case DefaultStrategyTemplate.init(__MODULE__, opts |> Keyword.put(:time_interval, 0)) do + {:ok, state} -> {:ok, state |> Keyword.put(:time_interval, opts[:time_interval] || 15 * 1_000)} + other -> other + end + end + + @doc false + @impl GenServer + def handle_info(:check_fetch, state) do + DefaultStrategyTemplate.check_fetch(__MODULE__, state[:jwks_url], state) + DefaultStrategyTemplate.schedule_check_fetch(__MODULE__, state[:time_interval]) + + {:noreply, state} + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/dynamic/token.ex b/apps/explorer/lib/explorer/third_party_integrations/dynamic/token.ex new file mode 100644 index 000000000000..e1ac7f54b5d4 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/dynamic/token.ex @@ -0,0 +1,22 @@ +defmodule Explorer.ThirdPartyIntegrations.Dynamic.Token do + @moduledoc """ + JWT token verification for Dynamic.xyz + """ + use Joken.Config + + alias Explorer.ThirdPartyIntegrations.Dynamic + alias Explorer.ThirdPartyIntegrations.Dynamic.Strategy + + add_hook(JokenJwks, strategy: Strategy) + + @impl Joken.Config + def token_config do + env_id = Application.get_env(:explorer, Dynamic)[:env_id] + + [skip: [:aud]] + |> default_claims() + |> add_claim("iss", nil, fn iss -> + iss == "app.dynamicauth.com/#{env_id}" + end) + end +end diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index d17ec2e1cb81..c90c7440e652 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -129,6 +129,7 @@ defmodule Explorer.Mixfile do {:oauth2, "~> 2.0"}, {:siwe, github: "royal-markets/siwe-ex", ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"}, {:joken, "~> 2.6"}, + {:joken_jwks, "~> 1.7.0"}, {:utils, in_umbrella: true}, {:dns, "~> 2.4.0"}, {:inet_cidr, "~> 1.0.0"}, diff --git a/config/runtime.exs b/config/runtime.exs index 033de9db4b51..8e791f31e3d5 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -646,6 +646,15 @@ config :explorer, Explorer.ThirdPartyIntegrations.Xname, service_url: ConfigHelper.parse_url_env_var("XNAME_BASE_API_URL", "https://gateway.xname.app"), api_key: System.get_env("XNAME_API_TOKEN") +dynamic_env_id = System.get_env("ACCOUNT_DYNAMIC_ENV_ID") + +config :explorer, Explorer.ThirdPartyIntegrations.Dynamic, + enabled: !is_nil(dynamic_env_id), + env_id: dynamic_env_id, + url: "https://app.dynamic.xyz/api/v0/sdk/#{dynamic_env_id}/.well-known/jwks" + +config :explorer, Explorer.ThirdPartyIntegrations.Dynamic.Strategy, enabled: !is_nil(dynamic_env_id) + enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED", "true") # or "eth_bytecode_db" type = System.get_env("MICROSERVICE_SC_VERIFIER_TYPE", "sc_verifier") diff --git a/cspell.json b/cspell.json index 411bff24021a..a18840481b00 100644 --- a/cspell.json +++ b/cspell.json @@ -40,6 +40,7 @@ "AIRTABLE", "Aiubo", "alfajores", + "algs", "alloc", "Alookup", "amzootyukbugmx", @@ -217,6 +218,7 @@ "dparty", "dropzone", "dxgd", + "dynamicauth", "dyntsrohg", "Ebhwp", "econnrefused", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 0c7fb176db1a..372829ec7602 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -501,6 +501,7 @@ DECODE_NOT_A_CONTRACT_CALLS=true # ACCOUNT_PRIVATE_TAGS_LIMIT=2000 # ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15 # ACCOUNT_SIWE_MESSAGE= +# ACCOUNT_DYNAMIC_ENV_ID= ACCOUNT_CLOAK_KEY= ACCOUNT_ENABLED=false ACCOUNT_REDIS_URL=redis://redis-db:6379 diff --git a/mix.lock b/mix.lock index 6094b57cbb22..f61090f30ed3 100644 --- a/mix.lock +++ b/mix.lock @@ -94,6 +94,7 @@ "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, + "joken_jwks": {:hex, :joken_jwks, "1.7.0", "5db750732dddbfb51068c16da6e43439ad8a3932020bdd505e944dd918ec0771", [:mix], [{:hackney, "~> 1.18", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.6", [hex: :joken, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:tesla, "~> 1.4", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c1dd2d5161f079f660bda773c338d3ff2b90bb79673a0852becc0433416797d3"}, "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, From e7f516bfd37a19b3cc03ce1df9bbf333f6f82b79 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 21 Jan 2026 12:39:47 +0300 Subject: [PATCH 0608/1006] fix: Fix error in Indexer.Fetcher.OnDemand.TokenBalance module (#13890) --- apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex index d7511b125496..48f4f5746ed2 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/token_balance.ex @@ -126,7 +126,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do defp prepare_ctb_params(current_token_balances, initial_acc, block_number) do Enum.reduce(current_token_balances, initial_acc, fn %{token_id: token_id} = stale_current_token_balance, acc -> prepared_ctb = %{ - token_contract_address_hash: to_string(stale_current_token_balance.token.contract_address_hash), + token_contract_address_hash: to_string(stale_current_token_balance.token_contract_address_hash), address_hash: to_string(stale_current_token_balance.address_hash), block_number: block_number, token_id: token_id && Decimal.to_integer(token_id), @@ -136,7 +136,7 @@ defmodule Indexer.Fetcher.OnDemand.TokenBalance do updated_tokens = Map.put_new( acc[:tokens], - stale_current_token_balance.token.contract_address_hash.bytes, + stale_current_token_balance.token_contract_address_hash.bytes, stale_current_token_balance.token ) From ad9825c5e1622cc01b26737c9282a913ecdae342 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 21 Jan 2026 12:40:13 +0300 Subject: [PATCH 0609/1006] chore: Clear token "skip_metadata" property (#13891) --- ...0260121084059_reset_tokens_extended_skip_metadata.exs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 apps/explorer/priv/repo/migrations/20260121084059_reset_tokens_extended_skip_metadata.exs diff --git a/apps/explorer/priv/repo/migrations/20260121084059_reset_tokens_extended_skip_metadata.exs b/apps/explorer/priv/repo/migrations/20260121084059_reset_tokens_extended_skip_metadata.exs new file mode 100644 index 000000000000..fecd40aa509d --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20260121084059_reset_tokens_extended_skip_metadata.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.ResetTokensExtendedSkipMetadata do + use Ecto.Migration + + def change do + execute(""" + UPDATE tokens SET skip_metadata = null WHERE skip_metadata IS TRUE AND decimals IS NOT NULL AND name IS NOT NULL AND symbol IS NOT NULL + """) + end +end From 5d4534a38344bfdea9b02884dd37cb97227fcac8 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 21 Jan 2026 12:57:22 +0300 Subject: [PATCH 0610/1006] Fix swagger generation for Mud chain --- .github/workflows/generate-swagger.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 9d336cfff49c..03b90735da0c 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -132,7 +132,7 @@ jobs: - name: Generate MUD-enabled spec for Optimism if: matrix.chain-type == 'optimism' run: | - mix openapi.spec.yaml --spec BlockScoutWeb.ApiSpec openapi.mud.yaml --start-app=false + mix openapi.spec.yaml --spec BlockScoutWeb.Specs.Public openapi.mud.yaml --start-app=false env: CHAIN_TYPE: optimism MUD_INDEXER_ENABLED: true From 4e19c732a7dfce754157d6df20fdddd96371c304 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:07:15 +0300 Subject: [PATCH 0611/1006] feat: Allow adding EOA with code to watchlist (#13885) --- .../account/api/v2/user_controller_test.exs | 26 +++++++++++++++++++ .../account/notifier/forbidden_address.ex | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs index 4a333c873070..8fff7dcb28ab 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -576,6 +576,32 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do Application.put_env(:explorer, Explorer.Account, old_env) end + test "can't insert contract watchlist address", %{conn: conn} do + address = insert(:contract_address) + + response = + conn + |> post( + "/api/account/v2/user/watchlist", + build(:watchlist_address) |> Map.put("address_hash", to_string(address.hash)) + ) + |> json_response(422) + + assert response == %{"errors" => %{"address_hash" => ["This address isn't EOA"]}} + end + + test "can insert EOA with code watchlist address", %{conn: conn, user: user} do + address = insert(:contract_address, contract_code: "0xef01000000000000000000000000000000000000000123") + + response = + conn + |> post( + "/api/account/v2/user/watchlist", + build(:watchlist_address) |> Map.put("address_hash", to_string(address.hash)) + ) + |> json_response(200) + end + test "check watchlist tags pagination", %{conn: conn, user: user} do tags_address = for _ <- 0..50 do diff --git a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex index 16ab861c4627..e2b19c26b519 100644 --- a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex +++ b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex @@ -49,7 +49,7 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do defp contract?(%Explorer.Chain.Hash{} = address_hash) do case hash_to_address(address_hash) do {:error, :not_found} -> false - {:ok, address} -> Address.smart_contract?(address) + {:ok, address} -> Address.smart_contract?(address) && !Address.eoa_with_code?(address) end end From 897af3d52ae945e1c1b3fa2084cf18699dc8ee6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:55:18 +0300 Subject: [PATCH 0612/1006] chore(deps): bump phoenix_live_view from 1.1.19 to 1.1.20 (#13881) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index f61090f30ed3..be70d35ca11c 100644 --- a/mix.lock +++ b/mix.lock @@ -135,7 +135,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.19", "c95e9acbc374fb796ee3e24bfecc8213123c74d9f9e45667ca40bb0a4d242953", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d5ad357d6b21562a5b431f0ad09dfe76db9ce5648c6949f1aac334c8c4455d32"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.20", "4f20850ee700b309b21906a0e510af1b916b454b4f810fb8581ada016eb42dfc", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c16abd605a21f778165cb0079946351ef20ef84eb1ef467a862fb9a173b1d27d"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, From 8c9211193e2538a5a0d8ca1803e19c3a26bbb414 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Wed, 21 Jan 2026 14:49:42 +0300 Subject: [PATCH 0613/1006] fix: check token presence in address current token balance (#13892) --- apps/block_scout_web/lib/block_scout_web/notifier.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index d94cddc957ce..38ec79fd68f0 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -473,17 +473,18 @@ defmodule BlockScoutWeb.Notifier do %{view: view, compiler: compiler} end + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defp broadcast_token_balances(address_hash, token_type, balances) do sorted = Enum.sort_by( balances, fn ctb -> value = - if ctb.token.decimals, + if ctb.token && ctb.token.decimals, do: Decimal.div(ctb.value, Decimal.new(Integer.pow(10, Decimal.to_integer(ctb.token.decimals)))), else: ctb.value - {(ctb.token.fiat_value && Decimal.mult(value, ctb.token.fiat_value)) || Decimal.new(0), value} + {(ctb.token && ctb.token.fiat_value && Decimal.mult(value, ctb.token.fiat_value)) || Decimal.new(0), value} end, fn {fiat_value_1, value_1}, {fiat_value_2, value_2} -> case {Decimal.compare(fiat_value_1, fiat_value_2), Decimal.compare(value_1, value_2)} do From 36cc3520968bd8df7c9e87145bea6844d2dead68 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:13:42 +0300 Subject: [PATCH 0614/1006] fix: Fix error on loading thumbnails when public_r2_url is missed (#13895) --- .../chain/token/instance/media_urls.ex | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/token/instance/media_urls.ex b/apps/explorer/lib/explorer/chain/token/instance/media_urls.ex index 26f1bc2e972f..e37131dbefee 100644 --- a/apps/explorer/lib/explorer/chain/token/instance/media_urls.ex +++ b/apps/explorer/lib/explorer/chain/token/instance/media_urls.ex @@ -20,24 +20,27 @@ defmodule Explorer.Chain.Token.Instance.Thumbnails do def cast(_), do: :error def load([file_path, sizes, original_uploaded?]) do - uri = - Application.get_env(:ex_aws, :s3)[:public_r2_url] |> URI.parse() |> URI.append_path(file_path) |> URI.to_string() - - thumbnails = - sizes - |> Enum.map(fn size -> - key = "#{size}x#{size}" - {key, String.replace(uri, "{}", key)} - end) - |> Enum.into(%{}) - - {:ok, - if original_uploaded? do - key = "original" - Map.put(thumbnails, key, String.replace(uri, "{}", key)) - else - thumbnails - end} + if public_r2_url = Application.get_env(:ex_aws, :s3)[:public_r2_url] do + uri = public_r2_url |> URI.parse() |> URI.append_path(file_path) |> URI.to_string() + + thumbnails = + sizes + |> Enum.map(fn size -> + key = "#{size}x#{size}" + {key, String.replace(uri, "{}", key)} + end) + |> Enum.into(%{}) + + {:ok, + if original_uploaded? do + key = "original" + Map.put(thumbnails, key, String.replace(uri, "{}", key)) + else + thumbnails + end} + else + {:ok, nil} + end end def load(_), do: :error From 2165f753f1f7e82f1967c4b17cb20e70ea60e84d Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:55:22 +0400 Subject: [PATCH 0615/1006] feat: Optionally accrue burnt fees to the block miner (#13894) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../models/transaction_state_helper.ex | 4 +- .../views/api/rpc/block_view.ex | 4 +- .../views/api/v2/transaction_view.ex | 37 +++++++++++-------- .../transaction_state_controller_test.exs | 2 +- apps/explorer/lib/explorer/chain/block.ex | 13 ++++--- .../lib/explorer/chain/transaction.ex | 23 ++++++++++++ .../chain/transaction/state_change.ex | 32 +++++++++++++--- apps/explorer/lib/explorer/chain/wei.ex | 9 +++++ config/runtime.exs | 10 ++--- docker-compose/envs/common-blockscout.env | 1 + 10 files changed, 99 insertions(+), 36 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 3c91f71f9edb..9436f74fb028 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -137,7 +137,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do end defp coin_balance(address_hash, _block_number, _options) when is_nil(address_hash) do - %Wei{value: Decimal.new(0)} + Wei.zero() end defp coin_balance(address_hash, block_number, options) do @@ -147,7 +147,7 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do _ -> CoinBalanceOnDemand.trigger_historic_fetch(options[:ip], address_hash, block_number) - %Wei{value: Decimal.new(0)} + Wei.zero() end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex index 9144bcf43a3b..b8c40b2359cb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex @@ -9,14 +9,14 @@ defmodule BlockScoutWeb.API.RPC.BlockView do def render("block_reward.json", %{block: %Block{rewards: [_ | _]} = block}) do reward_as_string = block.rewards - |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :validator)) + |> Enum.find(%{reward: Wei.zero()}, &(&1.address_type == :validator)) |> Map.get(:reward) |> Wei.to(:wei) |> Decimal.to_string(:normal) static_reward = block.rewards - |> Enum.find(%{reward: %Wei{value: Decimal.new(0)}}, &(&1.address_type == :emission_funds)) + |> Enum.find(%{reward: Wei.zero()}, &(&1.address_type == :emission_funds)) |> Map.get(:reward) |> Wei.to(:wei) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index d65180bcdbc5..55f9c6802df2 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -3,7 +3,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do use Utils.RuntimeEnvHelper, chain_type: [:explorer, :chain_type], - chain_identity: [:explorer, :chain_identity] + chain_identity: [:explorer, :chain_identity], + miner_gets_burnt_fees?: [:explorer, [Explorer.Chain.Transaction, :block_miner_gets_burnt_fees?]] alias BlockScoutWeb.API.V2.{ApiView, Helper, InternalTransactionView, TokenTransferView, TokenView} @@ -461,8 +462,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do priority_fee_per_gas = Transaction.priority_fee_per_gas(max_priority_fee_per_gas, base_fee_per_gas, max_fee_per_gas) - burnt_fees = burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) - status = transaction |> Chain.transaction_to_status() |> format_status() revert_reason = revert_reason(status, transaction, single_transaction?) @@ -511,7 +510,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "max_priority_fee_per_gas" => transaction.max_priority_fee_per_gas, "base_fee_per_gas" => base_fee_per_gas, "priority_fee" => priority_fee_per_gas && Wei.mult(priority_fee_per_gas, transaction.gas_used), - "transaction_burnt_fee" => burnt_fees, + "transaction_burnt_fee" => burnt_fees(transaction, base_fee_per_gas), "nonce" => transaction.nonce, "position" => transaction.index, "revert_reason" => revert_reason, @@ -536,6 +535,24 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> with_chain_type_fields(transaction, single_transaction?, conn, watchlist_names) end + # Calculates burnt fees for a transaction. + # + # ## Parameters + # - `transaction`: The transaction entity containing info needed to calculate the fees. + # - `base_fee_per_gas`: Base fee per gas in Wei. + # + # ## Returns + # - The calculated amount of the burnt fees. + # - `nil` if the fees cannot be calculated. + @spec burnt_fees(Transaction.t(), Wei.t() | nil) :: Wei.t() | nil + defp burnt_fees(transaction, base_fee_per_gas) do + if miner_gets_burnt_fees?() do + Wei.zero() + else + Transaction.burnt_fees(transaction.gas_used, transaction.max_fee_per_gas, base_fee_per_gas) + end + end + def token_transfers(_, _conn, false), do: nil def token_transfers(%NotLoaded{}, _conn, _), do: nil @@ -578,18 +595,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do render("authorization_list.json", %{signed_authorizations: signed_authorizations}) end - defp burnt_fees(transaction, max_fee_per_gas, base_fee_per_gas) do - if !is_nil(max_fee_per_gas) and !is_nil(transaction.gas_used) and !is_nil(base_fee_per_gas) do - if Decimal.compare(max_fee_per_gas.value, 0) == :eq do - %Wei{value: Decimal.new(0)} - else - Wei.mult(base_fee_per_gas, transaction.gas_used) - end - else - nil - end - end - defp revert_reason(status, transaction, single_transaction?) do reverted? = is_binary(status) && status |> String.downcase() |> String.contains?("reverted") diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs index 3e91da15a317..5cb92cca2469 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs @@ -251,7 +251,7 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do full_text = Enum.join(items) assert(length(items) == 3) - assert(String.contains?(full_text, format_wei_value(%Wei{value: Decimal.new(0)}, :ether))) + assert(String.contains?(full_text, format_wei_value(Wei.zero(), :ether))) 1 |> :timer.seconds() |> :timer.sleep() conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"}) diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 34f4b603b2a1..73fcc389c086 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -217,6 +217,9 @@ defmodule Explorer.Chain.Block do use Explorer.Schema use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] + use Utils.RuntimeEnvHelper, + miner_gets_burnt_fees?: [:explorer, [Explorer.Chain.Transaction, :block_miner_gets_burnt_fees?]] + alias Explorer.Chain.{ Block, DenormalizationHelper, @@ -428,7 +431,7 @@ defmodule Explorer.Chain.Block do """ @spec burnt_fees(list(), Decimal.t() | nil) :: Decimal.t() def burnt_fees(transactions, base_fee_per_gas) do - if is_nil(base_fee_per_gas) do + if is_nil(base_fee_per_gas) or miner_gets_burnt_fees?() do Decimal.new(0) else transactions @@ -464,7 +467,7 @@ defmodule Explorer.Chain.Block do where: fragment("int8range(?, ?) <@ ?", ^block_number, ^(block_number + 1), er.block_range), select: er.reward ) - ) || %Wei{value: Decimal.new(0)} + ) || Wei.zero() uncles_count = if is_list(block.uncles), do: Enum.count(block.uncles), else: 0 @@ -730,7 +733,7 @@ defmodule Explorer.Chain.Block do end burnt_fees = - if is_nil(block_base_fee_per_gas) do + if is_nil(block_base_fee_per_gas) or miner_gets_burnt_fees?() do acc.burnt_fees else transaction.gas_used @@ -808,7 +811,7 @@ defmodule Explorer.Chain.Block do |> Wei.cast() |> case do {:ok, value} -> value - _ -> %Wei{value: Decimal.new(0)} + _ -> Wei.zero() end end @@ -1056,7 +1059,7 @@ defmodule Explorer.Chain.Block do initial_gas_payments = block_hashes - |> Enum.map(&{&1, %Wei{value: Decimal.new(0)}}) + |> Enum.map(&{&1, Wei.zero()}) |> Enum.into(%{}) existing_data = diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index fe4a41a85562..f40a416fe2df 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -2181,6 +2181,29 @@ defmodule Explorer.Chain.Transaction do |> Decimal.mult(gas_used) end + @doc """ + Calculates burnt fees for a transaction as `base_fee_per_gas * gas_used`. + + ## Parameters + - `gas_used`: Gas used by the transaction. + - `max_fee_per_gas`: Max fee per gas in Wei. + - `base_fee_per_gas`: Base fee per gas in Wei. + + ## Returns + - The calculated amount of the burnt fees. + - `nil` if any parameter is `nil`. + """ + @spec burnt_fees(Decimal.t() | nil, Wei.t() | nil, Wei.t() | nil) :: Wei.t() | nil + def burnt_fees(gas_used, max_fee_per_gas, base_fee_per_gas) do + if !is_nil(max_fee_per_gas) and !is_nil(gas_used) and !is_nil(base_fee_per_gas) do + if Decimal.compare(max_fee_per_gas.value, 0) == :eq do + Wei.zero() + else + Wei.mult(base_fee_per_gas, gas_used) + end + end + end + @doc """ Wrapper around `effective_gas_price/2` """ diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex index 16ef194776eb..04893df0f1d0 100644 --- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex +++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex @@ -3,6 +3,9 @@ defmodule Explorer.Chain.Transaction.StateChange do Helper functions and struct for storing state changes """ + use Utils.RuntimeEnvHelper, + miner_gets_burnt_fees?: [:explorer, [Explorer.Chain.Transaction, :block_miner_gets_burnt_fees?]] + alias Explorer.Chain alias Explorer.Chain.{Address, Block, Hash, InternalTransaction, TokenTransfer, Transaction, Wei} alias Explorer.Chain.Transaction.StateChange @@ -21,8 +24,6 @@ defmodule Explorer.Chain.Transaction.StateChange do @type coin_balances_map :: %{Hash.Address.t() => {Address.t(), Wei.t()}} - @zero_wei %Wei{value: Decimal.new(0)} - @spec coin_balances_before(Transaction.t(), [Transaction.t()], coin_balances_map()) :: coin_balances_map() def coin_balances_before(transaction, block_transactions, coin_balances_before_block) do block = transaction.block @@ -193,8 +194,20 @@ defmodule Explorer.Chain.Transaction.StateChange do transaction.value end + # Calculates block miner profit for the given transaction. + # + # Typically that is priority fee, but if `BLOCK_MINER_GETS_BURNT_FEES` is enabled, + # the profit is the sum of the priority fee and the fee which would be burnt. + # + # ## Parameters + # - `transaction`: The transaction entity containing info needed to calculate the profit. + # - `block`: The block entity containing info needed to calculate the profit. + # + # ## Returns + # - The miner profit amount in Wei. + @spec miner_profit(Transaction.t(), Block.t()) :: Wei.t() defp miner_profit(transaction, block) do - base_fee_per_gas = block.base_fee_per_gas || %Wei{value: Decimal.new(0)} + base_fee_per_gas = block.base_fee_per_gas || Wei.zero() max_priority_fee_per_gas = transaction.max_priority_fee_per_gas || transaction.gas_price max_fee_per_gas = transaction.max_fee_per_gas || transaction.gas_price @@ -203,7 +216,16 @@ defmodule Explorer.Chain.Transaction.StateChange do Wei.to(x, :wei) end) - Wei.mult(priority_fee_per_gas, transaction.gas_used) + burnt_fees_for_miner = + case miner_gets_burnt_fees?() && Transaction.burnt_fees(transaction.gas_used, max_fee_per_gas, base_fee_per_gas) do + false -> Wei.zero() + nil -> Wei.zero() + value -> value + end + + priority_fee_per_gas + |> Wei.mult(transaction.gas_used) + |> Wei.sum(burnt_fees_for_miner) end defp error?(transaction) do @@ -277,7 +299,7 @@ defmodule Explorer.Chain.Transaction.StateChange do defp update_balance(coin_balances, address_hash, update_function) do if Map.has_key?(coin_balances, address_hash) do - Map.update(coin_balances, address_hash, @zero_wei, fn {address, balance} -> + Map.update(coin_balances, address_hash, Wei.zero(), fn {address, balance} -> {address, update_function.(balance)} end) else diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 555476433708..a611ad42a404 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -315,6 +315,15 @@ defmodule Explorer.Chain.Wei do @spec to(t(), :wei) :: wei() | nil def to(nil, :wei), do: nil def to(%__MODULE__{value: wei}, :wei), do: wei + + @doc """ + Returns zero Wei value. + + ## Returns + - Zero Wei value. + """ + @spec zero() :: __MODULE__.t() + def zero, do: %__MODULE__{value: Decimal.new(0)} end defimpl Inspect, for: Explorer.Chain.Wei do diff --git a/config/runtime.exs b/config/runtime.exs index 8e791f31e3d5..970436ef2cbd 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -563,7 +563,11 @@ config :explorer, Explorer.Market.Fetcher.History, ConfigHelper.parse_integer_env_var("EXCHANGE_RATES_HISTORY_FIRST_FETCH_DAY_COUNT", 365) ) -config :explorer, Explorer.Chain.Transaction, suave_bid_contracts: System.get_env("SUAVE_BID_CONTRACTS", "") +config :explorer, Explorer.Chain.Transaction, + block_miner_gets_burnt_fees?: ConfigHelper.parse_bool_env_var("BLOCK_MINER_GETS_BURNT_FEES"), + suave_bid_contracts: System.get_env("SUAVE_BID_CONTRACTS", ""), + rootstock_remasc_address: System.get_env("ROOTSTOCK_REMASC_ADDRESS"), + rootstock_bridge_address: System.get_env("ROOTSTOCK_BRIDGE_ADDRESS") config :explorer, Explorer.Chain.Transaction.History.Historian, enabled: transactions_stats_enabled, @@ -748,10 +752,6 @@ config :explorer, Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand, fetch_interval: ConfigHelper.parse_time_env_var("MICROSERVICE_ETH_BYTECODE_DB_INTERVAL_BETWEEN_LOOKUPS", "10m"), max_concurrency: ConfigHelper.parse_integer_env_var("MICROSERVICE_ETH_BYTECODE_DB_MAX_LOOKUPS_CONCURRENCY", 10) -config :explorer, Explorer.Chain.Transaction, - rootstock_remasc_address: System.get_env("ROOTSTOCK_REMASC_ADDRESS"), - rootstock_bridge_address: System.get_env("ROOTSTOCK_BRIDGE_ADDRESS") - config :explorer, Explorer.Chain.Cache.Counters.AddressTabsElementsCount, ttl: ConfigHelper.parse_time_env_var("ADDRESSES_TABS_COUNTERS_TTL", "10m") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 372829ec7602..f639dc5319ec 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -655,6 +655,7 @@ RELEASE_COOKIE=secret_cookie # CONTRACT_MAX_STRING_LENGTH_WITHOUT_TRIMMING=2040 # CONTRACT_DISABLE_INTERACTION= # HIDE_BLOCK_MINER=false +# BLOCK_MINER_GETS_BURNT_FEES=false # DISPLAY_TOKEN_ICONS=false # MIXPANEL_TOKEN= # MIXPANEL_URL= From 894912bdff7162eef02abc45c25216c41dd4721b Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:34:52 +0400 Subject: [PATCH 0616/1006] fix: Wrong `next_page_params` in OP Deposits (#13870) Co-authored-by: POA <33550681+poa@users.noreply.github.com> --- .../lib/block_scout_web/chain.ex | 94 +++++++++---------- .../controllers/api/v2/optimism_controller.ex | 6 +- .../api/v2/optimism_controller_test.exs | 39 +++++++- .../withdrawal_controller_test.exs | 2 +- apps/explorer/test/support/factory.ex | 27 ++++++ 5 files changed, 117 insertions(+), 51 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 8730d1b0bf3c..ca31238487e5 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -48,12 +48,10 @@ defmodule BlockScoutWeb.Chain do Transaction, Transaction.StateChange, UserOperation, - Wei, - Withdrawal + Wei } alias Explorer.Chain.Cache.BlockNumber - alias Explorer.Chain.Optimism.Deposit, as: OptimismDeposit alias Explorer.Chain.Optimism.FrameSequence, as: OptimismFrameSequence alias Explorer.Chain.Optimism.InteropMessage, as: OptimismInteropMessage alias Explorer.Chain.Optimism.OutputRoot, as: OptimismOutputRoot @@ -565,6 +563,14 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{"nonce" => nonce}) when is_integer(nonce) do + [paging_options: %{@default_paging_options | key: {nonce}}] + end + + def paging_options(%{nonce: nonce}) do + [paging_options: %{@default_paging_options | key: {nonce}}] + end + def paging_options(%{"number" => number_string}) when is_binary(number_string) do case Integer.parse(number_string) do {number, ""} -> @@ -575,10 +581,6 @@ defmodule BlockScoutWeb.Chain do end end - def paging_options(%{"nonce" => nonce}) when is_integer(nonce) do - [paging_options: %{@default_paging_options | key: {nonce}}] - end - def paging_options(%{"number" => number}) when is_integer(number) do [paging_options: %{@default_paging_options | key: {number}}] end @@ -683,6 +685,16 @@ defmodule BlockScoutWeb.Chain do end end + def paging_options(%{l1_block_number: block_number, transaction_hash: transaction_hash}) do + case string_to_full_hash(transaction_hash) do + {:ok, transaction_hash} -> + [paging_options: %{@default_paging_options | key: {block_number, transaction_hash}}] + + _ -> + [paging_options: @default_paging_options] + end + end + # clause for pagination of entities: # - Account's entities # - Optimism frame sequences @@ -700,7 +712,17 @@ defmodule BlockScoutWeb.Chain do end end - def paging_options(%{"timestamp" => timestamp, "init_transaction_hash" => init_transaction_hash}) do + def paging_options(%{"id" => id}) when is_integer(id) do + [paging_options: %{@default_paging_options | key: {id}}] + end + + def paging_options(%{id: id}) do + [paging_options: %{@default_paging_options | key: {id}}] + end + + # Clause for `Explorer.Chain.Optimism.InteropMessage`, + # returned by `BlockScoutWeb.API.V2.OptimismController.interop_messages/2` (`/api/v2/optimism/interop/messages`) + def paging_options(%{timestamp: timestamp, init_transaction_hash: init_transaction_hash}) do with {ts, ""} <- Integer.parse(timestamp), {:ok, transaction_hash} <- string_to_full_hash(init_transaction_hash) do [paging_options: %{@default_paging_options | key: {ts, transaction_hash}}] @@ -710,17 +732,6 @@ defmodule BlockScoutWeb.Chain do end end - # clause for pagination of entities: - # - Account's entities - # - Optimism frame sequences - # - Polygon Edge Deposits - # - Polygon Edge Withdrawals - # - Arbitrum cross chain messages - # - Scroll cross chain messages - def paging_options(%{"id" => id}) when is_integer(id) do - [paging_options: %{@default_paging_options | key: {id}}] - end - def paging_options(%{ token_contract_address_hash: token_contract_address_hash, token_id: token_id, @@ -955,10 +966,6 @@ defmodule BlockScoutWeb.Chain do paging_params(token) end - defp paging_params(%OptimismFrameSequence{id: id}) do - %{"id" => id} - end - defp paging_params(%TagAddress{id: id}) do %{"id" => id} end @@ -1025,16 +1032,16 @@ defmodule BlockScoutWeb.Chain do %{smart_contract_id: smart_contract.id} end - defp paging_params(%OptimismDeposit{l1_block_number: l1_block_number, l2_transaction_hash: l2_transaction_hash}) do - %{"l1_block_number" => l1_block_number, "transaction_hash" => l2_transaction_hash} + defp paging_params(%OptimismFrameSequence{id: id}) do + %{id: id} end defp paging_params(%OptimismOutputRoot{l2_output_index: index}) do - %{"index" => index} + %{index: index} end defp paging_params(%OptimismInteropMessage{timestamp: timestamp, init_transaction_hash: init_transaction_hash}) do - %{"timestamp" => DateTime.to_unix(timestamp), "init_transaction_hash" => init_transaction_hash} + %{timestamp: DateTime.to_unix(timestamp), init_transaction_hash: init_transaction_hash} end defp paging_params(%SmartContract{} = smart_contract) do @@ -1050,20 +1057,17 @@ defmodule BlockScoutWeb.Chain do %{"id" => id} end - defp paging_params(%{index: index}) do - %{"index" => index} - end - - defp paging_params(%Withdrawal{index: index}) do - %{index: index} + defp paging_params(%Instance{token_id: token_id}) do + %{"unique_token" => Decimal.to_integer(token_id)} end - defp paging_params(%{msg_nonce: nonce}) do - %{"nonce" => nonce} + defp paging_params(%StateChange{}) do + # todo: remove in the future as this param is unused in the pagination of state changes + %{state_changes: nil} end - defp paging_params(%{l2_block_number: block_number}) do - %{"block_number" => block_number} + defp paging_params(%{index: index}) do + %{index: index} end # clause for zkEVM & Scroll batches pagination @@ -1071,18 +1075,14 @@ defmodule BlockScoutWeb.Chain do %{"number" => number} end - defp paging_params(%Instance{token_id: token_id}) do - %{"unique_token" => Decimal.to_integer(token_id)} + # clause for Optimism Deposits + defp paging_params(%{l1_block_number: l1_block_number, l2_transaction_hash: l2_transaction_hash}) do + %{l1_block_number: l1_block_number, transaction_hash: l2_transaction_hash} end - defp paging_params(%StateChange{}) do - # todo: remove in the future as this param is unused in the pagination of state changes - %{state_changes: nil} - end - - # clause for Polygon Edge Deposits and Withdrawals - defp paging_params(%{msg_id: msg_id}) do - %{"id" => msg_id} + # clause for Optimism Withdrawals + defp paging_params(%{msg_nonce: nonce}) do + %{nonce: nonce} end # clause for Shibarium Deposits diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 9c6bdd59380c..3998c9f60477 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -34,6 +34,8 @@ defmodule BlockScoutWeb.API.V2.OptimismController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + @api_true [api?: true] operation :batches, @@ -161,7 +163,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do Function to handle GET requests to `/api/v2/optimism/batches/da/celestia/:height/:commitment` endpoint. """ @spec batch_by_celestia_blob(Plug.Conn.t(), map()) :: Plug.Conn.t() - def batch_by_celestia_blob(conn, %{"height" => height, "commitment" => commitment}) do + def batch_by_celestia_blob(conn, %{height: height, commitment: commitment}) do {height, ""} = Integer.parse(height) commitment = @@ -205,7 +207,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do Function to handle GET requests to `/api/v2/optimism/batches/:number` endpoint. """ @spec batch_by_number(Plug.Conn.t(), map()) :: Plug.Conn.t() - def batch_by_number(conn, %{"number" => number}) do + def batch_by_number(conn, %{number: number}) do {number, ""} = Integer.parse(number) batch = FrameSequence.batch_by_number(number, api?: true) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs index 8929cc44df22..b85f15aee63c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/optimism_controller_test.exs @@ -4,11 +4,28 @@ defmodule BlockScoutWeb.API.V2.OptimismControllerTest do import Mox - alias Explorer.Chain.Data + alias Explorer.Chain.{Address, Data} + alias Explorer.Chain.Optimism.Deposit alias Explorer.TestHelper setup :set_mox_global + describe "/optimism/deposits" do + if @chain_type == :optimism do + test "deposits with next_page_params", %{conn: conn} do + deposits = insert_list(51, :op_deposit) + + request = get(conn, "/api/v2/optimism/deposits") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/optimism/deposits", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, deposits) + end + end + end + describe "/optimism/interop/messages" do if @chain_type == :optimism do test "handles message with 0x prefixed payload", %{conn: conn} do @@ -38,4 +55,24 @@ defmodule BlockScoutWeb.API.V2.OptimismControllerTest do end end end + + defp check_paginated_response(first_page_resp, second_page_resp, items) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(items, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(items, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(items, 0), Enum.at(second_page_resp["items"], 0)) + end + + defp compare_item(%Deposit{} = deposit, json) do + assert deposit.l1_block_number == json["l1_block_number"] + assert DateTime.to_iso8601(deposit.l1_block_timestamp) == json["l1_block_timestamp"] + assert to_string(deposit.l1_transaction_hash) == json["l1_transaction_hash"] + assert Address.checksum(deposit.l1_transaction_origin) == Address.checksum(json["l1_transaction_origin"]) + assert to_string(deposit.l2_transaction.hash) == json["l2_transaction_hash"] + assert to_string(deposit.l2_transaction.gas) == json["l2_transaction_gas_limit"] + end end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs index 5b9caf835d70..c152318a1cc5 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs @@ -40,7 +40,7 @@ defmodule BlockScoutWeb.WithdrawalControllerTest do conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) - expected_path = withdrawal_path(conn, :index, items_count: "50", index: index) + expected_path = withdrawal_path(conn, :index, index: index, items_count: "50") assert Map.get(json_response(conn, 200), "next_page_path") == expected_path end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index f66a64092ff6..33739a626a95 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -60,6 +60,8 @@ defmodule Explorer.Factory do alias Explorer.Chain.Zilliqa.Hash.BLSPublicKey alias Explorer.Chain.Zilliqa.Staker, as: ZilliqaStaker + alias Explorer.Chain.Optimism.Deposit, as: OptimismDeposit + alias Explorer.Chain.Celo.ElectionReward, as: CeloElectionReward alias Explorer.Chain.Celo.Epoch, as: CeloEpoch @@ -1414,6 +1416,31 @@ defmodule Explorer.Factory do } end + def op_deposit_factory do + block = insert(:block) + gas_used = Enum.random(21_000..100_000) + + l2_transaction = + insert( + :transaction, + block_number: block.number, + block_hash: block.hash, + cumulative_gas_used: gas_used, + gas_used: gas_used, + index: 0, + status: :ok + ) + + %OptimismDeposit{ + l1_block_number: block_number(), + l1_block_timestamp: DateTime.utc_now(), + l1_transaction_hash: transaction_hash(), + l1_transaction_origin: address_hash(), + l2_transaction_hash: l2_transaction.hash, + l2_transaction: l2_transaction + } + end + def db_migration_status_factory do %MigrationStatus{ migration_name: nil, From cb12aada0a9babdbba0c261bf7f8ef546aebb4b3 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 22 Jan 2026 18:22:15 +0300 Subject: [PATCH 0617/1006] fix: Fix token balances broadcasting function (#13902) --- .../lib/block_scout_web/notifier.ex | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 38ec79fd68f0..e01501bcd18b 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -38,6 +38,7 @@ defmodule BlockScoutWeb.Notifier do BlockNumberHelper, DenormalizationHelper, InternalTransaction, + Token, Token.Instance, Transaction } @@ -339,7 +340,7 @@ defmodule BlockScoutWeb.Notifier do def handle_event( {:chain_event, :token_total_supply, :on_demand, - [%Explorer.Chain.Token{contract_address_hash: contract_address_hash, total_supply: total_supply} = token]} + [%Token{contract_address_hash: contract_address_hash, total_supply: total_supply} = token]} ) when not is_nil(total_supply) do # TODO: delete duplicated event when old UI becomes deprecated @@ -479,12 +480,14 @@ defmodule BlockScoutWeb.Notifier do Enum.sort_by( balances, fn ctb -> - value = - if ctb.token && ctb.token.decimals, - do: Decimal.div(ctb.value, Decimal.new(Integer.pow(10, Decimal.to_integer(ctb.token.decimals)))), - else: ctb.value + case ctb.token do + %Token{decimals: decimals, fiat_value: fiat_value} when not is_nil(decimals) -> + value = Decimal.div(ctb.value, Decimal.new(Integer.pow(10, Decimal.to_integer(decimals)))) + {(fiat_value && Decimal.mult(value, fiat_value)) || Decimal.new(0), value} - {(ctb.token && ctb.token.fiat_value && Decimal.mult(value, ctb.token.fiat_value)) || Decimal.new(0), value} + _ -> + {Decimal.new(0), ctb.value} + end end, fn {fiat_value_1, value_1}, {fiat_value_2, value_2} -> case {Decimal.compare(fiat_value_1, fiat_value_2), Decimal.compare(value_1, value_2)} do From 93bd9957e419d4f3e29f81c0fedb7e1739fc117e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 22 Jan 2026 18:22:41 +0300 Subject: [PATCH 0618/1006] feat: Solady smart-contract proxy with immutable arguments support (#13794) --- .../proxy/basic_implementation_getter.ex | 2 + .../proxy/clone_with_immutable_arguments.ex | 12 +- .../chain/smart_contract/proxy/eip_1167.ex | 1 + .../chain/smart_contract/proxy/eip_1822.ex | 2 + .../chain/smart_contract/proxy/eip_1967.ex | 2 + .../chain/smart_contract/proxy/eip_2535.ex | 2 + .../chain/smart_contract/proxy/eip_7702.ex | 1 + .../chain/smart_contract/proxy/erc_7760.ex | 1 + .../chain/smart_contract/proxy/master_copy.ex | 1 + .../proxy/resolved_delegate_proxy.ex | 1 + .../clone_with_immutable_arguments_test.exs | 156 ++++++++++++++++++ cspell.json | 4 + 12 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 apps/explorer/test/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments_test.exs diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex index 0e8ab7c0d8d6..eb8cf1f666b7 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/basic_implementation_getter.ex @@ -15,6 +15,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.BasicImplementationGetter do # 0xbb82aa5e = keccak256(comptrollerImplementation()) @comptroller_implementation_signature <<0xBB82AA5E::4-unit(8)>> + @impl true def quick_resolve_implementations(address, proxy_type) do signature = case proxy_type do @@ -35,6 +36,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.BasicImplementationGetter do end end + @impl true def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do with {:ok, value} <- Map.fetch(prefetched_values, :implementation_getter), {:ok, address_hash} <- Proxy.extract_address_hash(value) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex index bb202bd383d9..17c98670be2c 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments.ex @@ -1,6 +1,8 @@ defmodule Explorer.Chain.SmartContract.Proxy.CloneWithImmutableArguments do @moduledoc """ - Module for fetching proxy implementation from https://github.com/wighawag/clones-with-immutable-args + Module for fetching proxy implementation from clone contracts with immutable arguments. + Supports both wighawag (https://github.com/wighawag/clones-with-immutable-args) and + solady (https://github.com/Vectorized/solady/blob/main/src/utils/LibClone.sol) variants. """ alias Explorer.Chain.Hash @@ -8,13 +10,21 @@ defmodule Explorer.Chain.SmartContract.Proxy.CloneWithImmutableArguments do @behaviour ResolverBehaviour + @impl true def quick_resolve_implementations(proxy_address, _proxy_type) do case proxy_address.contract_code && proxy_address.contract_code.bytes do + # wighawag variant: https://github.com/wighawag/clones-with-immutable-args/blob/196f1ecc6485c1bf2d41677fa01d3df4927ff9ce/src/ClonesWithImmutableArgs.sol <<0x3D3D3D3D363D3D3761::9-unit(8), _::2-bytes, 0x603736393661::6-unit(8), _::2-bytes, 0x013D73::3-unit(8), template_address::20-bytes, _::binary>> -> {:ok, template_address_hash} = Hash.Address.cast(template_address) {:ok, [template_address_hash]} + # solady variant: https://github.com/Vectorized/solady/blob/main/src/utils/LibClone.sol#L620 + <<0x363D3D373D3D3D363D73::10-unit(8), template_address::20-bytes, 0x5AF43D3D93803E602A57FD5BF3::13-unit(8), + _::binary>> -> + {:ok, template_address_hash} = Hash.Address.cast(template_address) + {:ok, [template_address_hash]} + _ -> nil end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex index af71b0b783f1..adefe2cba493 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1167.ex @@ -10,6 +10,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1167 do @behaviour ResolverBehaviour + @impl true def quick_resolve_implementations(proxy_address, _proxy_type) do case proxy_address.contract_code && proxy_address.contract_code.bytes do <<0x363D3D373D3D3D363D73::10-unit(8), template_address::20-bytes, 0x5AF43D82803E903D91602B57FD5BF3::15-unit(8)>> -> diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex index 0649eb723505..9a44c2ee2fcd 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1822.ex @@ -11,6 +11,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1822 do # keccak256("PROXIABLE") @storage_slot_proxiable "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7" + @impl true def quick_resolve_implementations(_proxy_address, _proxy_type), do: {:cont, @@ -18,6 +19,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1822 do implementation_slot: {:storage, @storage_slot_proxiable} }} + @impl true def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do with {:ok, value} <- Map.fetch(prefetched_values, :implementation_slot), {:ok, address_hash} <- Proxy.extract_address_hash(value) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex index 001b078e1898..e92da4f88f12 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_1967.ex @@ -20,6 +20,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1967 do # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation" @storage_slot_openzeppelin_contract_address "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3" + @impl true def quick_resolve_implementations(_proxy_address, proxy_type) do storage_slot = case proxy_type do @@ -39,6 +40,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP1967 do end end + @impl true def resolve_implementations(_proxy_address, proxy_type, prefetched_values) do with {:ok, value} <- Map.fetch(prefetched_values, :implementation_slot), {:ok, stored_address_hash} <- Proxy.extract_address_hash(value) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex index 80ae60c17f90..abd68aadbff3 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_2535.ex @@ -14,6 +14,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP2535 do @max_implementations_number_per_proxy 100 + @impl true def quick_resolve_implementations(_proxy_address, _proxy_type), do: {:cont, @@ -21,6 +22,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP2535 do implementation_getter: {:call, @facet_addresses_signature} }} + @impl true def resolve_implementations(_proxy_address, _proxy_type, prefetched_values) do with {:ok, value} <- Map.fetch(prefetched_values, :implementation_getter), {:ok, address_hashes} <- extract_address_hashes(value) do diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex index b7eb984ed49a..ab5bd885c480 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/eip_7702.ex @@ -8,6 +8,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.EIP7702 do @behaviour ResolverBehaviour + @impl true def quick_resolve_implementations(proxy_address, _proxy_type \\ :eip7702) do case proxy_address.contract_code && proxy_address.contract_code.bytes do <<0xEF0100::3-unit(8), template_address::20-bytes>> -> diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex index 5db8750ab491..c96965e8e7f7 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/erc_7760.ex @@ -21,6 +21,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.ERC7760 do @beacon_basic_variant <<0x363D3D373D3D363D602036600436635C60DA1B60E01B36527FA3F0AD74E5423AEBFD80D3EF4346578335A9A72AEAEE59FF6CB3582B35133D50545AFA5036515AF43D6000803E604D573D6000FD5B3D6000F3::656>> @beacon_i_variant <<0x363D3D373D3D363D602036600436635C60DA1B60E01B36527FA3F0AD74E5423AEBFD80D3EF4346578335A9A72AEAEE59FF6CB3582B35133D50545AFA361460525736515AF43D600060013E6052573D6001FD5B3D6001F3::696>> + @impl true # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity def quick_resolve_implementations(proxy_address, _proxy_type \\ :erc7760) do eip1967_proxy_type = diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex index a6c91dfaf909..4b1f495ec842 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/master_copy.ex @@ -19,6 +19,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.MasterCopy do @safe_proxy_v1_4_1_zksync <<0x00010000000000020000008004000039000000400040043F000000000301001900000060033002700000002E033001970000000100200190000000100000C13D000000000200041A0000003002200197000000000401043B000000360040009C0000004B0000C13D000000000020043F0000003701000041000000B40001042E0000000002000416000000000002004B000000490000C13D0000001F0530018F0000002F0630019800000080026000390000001C0000613D000000000701034F000000007807043C0000000004840436000000000024004B000000180000C13D000000000005004B000000290000613D000000000161034F0000000304500210000000000502043300000000054501CF000000000545022F000000000101043B0000010004400089000000000141022F00000000014101CF000000000151019F00000000001204350000008001300039000000400010043F000000200030008C000000490000413D000000800200043D0000003002200198000000A30000C13D00000033020000410000000000210435000000E40130003900000034020000410000000000210435000000C40130003900000035020000410000000000210435000000A40130003900000022020000390000000000210435000000840130003900000020020000390000000000210435000000400100043D000000000213004900000104022000390000002E0020009C0000002E0200804100000060022002100000002E0010009C0000002E010080410000004001100210000000000112019F000000B5000104300000000001000019000000B5000104300000001F0530018F0000002F04300198000000540000613D000000000601034F0000000007000019000000006806043C0000000007870436000000000047004B000000500000C13D000000000005004B000000610000613D000000000641034F0000000305500210000000000704043300000000075701CF000000000757022F000000000606043B0000010005500089000000000656022F00000000055601CF000000000575019F00000000005404350000000004000414000000040020008C0000007D0000C13D000000000331034F000000000100003100000038021001980000001F0410018F0000006F0000613D000000000503034F0000000006000019000000005705043C0000000006760436000000000026004B0000006B0000C13D000000000004004B0000009F0000613D000000000323034F0000000304400210000000000502043300000000054501CF000000000545022F000000000303043B0000010004400089000000000343022F00000000034301CF000000000353019F00000000003204350000009F0000013D00000060013002100000002E0040009C0000002E04008041000000C003400210000000000113019F00B300AE0000040F000000000301001900000060033002700000001F0530018F0000002E0030019D0000002F043001980000008F0000613D000000000601034F0000000007000019000000006806043C0000000007870436000000000047004B0000008B0000C13D000000000005004B0000009C0000613D000000000141034F0000000305500210000000000604043300000000065601CF000000000656022F000000000101043B0000010005500089000000000151022F00000000015101CF000000000161019F00000000001404350000002E013001970000000100200190000000AC0000613D0000002E0010009C0000002E010080410000006001100210000000B40001042E000000000100041A0000003101100197000000000121019F000000000010041B0000002001000039000001000010044300000120000004430000003201000041000000B40001042E0000006001100210000000B500010430000000B1002104250000000102000039000000000001042D0000000002000019000000000001042D000000B300000432000000B40001042E000000B5000104300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFFFFFFF00000000000000000000000000000000000000000000000000000000FFFFFFE0000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0000000000000000000000000000000000000000000000020000000000000000000000000000004000000100000000000000000008C379A0000000000000000000000000000000000000000000000000000000006564000000000000000000000000000000000000000000000000000000000000496E76616C69642073696E676C65746F6E20616464726573732070726F766964A619486E000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFE000000000000000000000000000000000000000000000000000000000000000004D3D6AC1CA9BD3D9AB93E603DE7967DA33803409D7B4500E28F36CE7E4DD5A21::15104>> @safe_proxy_v1_5_0 <<0x608060405260005463A619486E60003560E01C14156024578060601B606C5260206060F35B3660008037600080366000845AF43D6000803E806040573D6000FD5B3D6000F3FEA2646970667358221220E61834EBD2D8CD909D362BF67C47EF58FD665DF38E6DD036CE65611101D072E964736F6C63430007060033::984>> + @impl true def quick_resolve_implementations(proxy_address, _proxy_type) do with true <- proxy_address.contract_code && diff --git a/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex index 46741fe73979..7730e89809bf 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/proxy/resolved_delegate_proxy.ex @@ -11,6 +11,7 @@ defmodule Explorer.Chain.SmartContract.Proxy.ResolvedDelegateProxy do @resolved_delegate_proxy <<0x608060408181523060009081526001602090815282822054908290529181207FBF40FAC1000000000000000000000000000000000000000000000000000000009093529173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF9091169063BF40FAC19061006D9060846101E2565B602060405180830381865AFA15801561008A573D6000803E3D6000FD5B505050506040513D601F19601F820116820180604052508101906100AE91906102C5565B905073FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF8116610157576040517F08C379A000000000000000000000000000000000000000000000000000000000815260206004820152603960248201527F5265736F6C76656444656C656761746550726F78793A2074617267657420616460448201527F6472657373206D75737420626520696E697469616C697A656400000000000000606482015260840160405180910390FD5B6000808273FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF16600036604051610182929190610302565B600060405180830381855AF49150503D80600081146101BD576040519150601F19603F3D011682016040523D82523D6000602084013E6101C2565B606091505B5090925090508115156001036101DA57805160208201F35B805160208201FD5B600060208083526000845481600182811C91508083168061020457607F831692505B858310810361023A577F4E487B710000000000000000000000000000000000000000000000000000000085526022600452602485FD5B878601838152602001818015610257576001811461028B576102B6565B7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF008616825284151560051B820196506102B6565B60008B81526020902060005B868110156102B057815484820152908501908901610297565B83019750505B50949998505050505050505050565B6000602082840312156102D757600080FD5B815173FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF811681146102FB57600080FD5B9392505050565B818382376000910190815291905056FEA164736F6C634300080F000A::6392>> + @impl true def quick_resolve_implementations(proxy_address, _proxy_type) do with {:match, @resolved_delegate_proxy} <- {:match, proxy_address.contract_code && proxy_address.contract_code.bytes}, diff --git a/apps/explorer/test/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments_test.exs b/apps/explorer/test/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments_test.exs new file mode 100644 index 000000000000..5240329a3587 --- /dev/null +++ b/apps/explorer/test/explorer/chain/smart_contract/proxy/clone_with_immutable_arguments_test.exs @@ -0,0 +1,156 @@ +defmodule Explorer.Chain.SmartContract.Proxy.CloneWithImmutableArgumentsTest do + use Explorer.DataCase + + alias Explorer.Chain.SmartContract.Proxy.CloneWithImmutableArguments + + # Test address hex (20 bytes = 40 hex chars) + @test_impl_address "1234567890123456789012345678901234567890" + @test_impl_address_2 "AABBCCDDEE11223344556677889900112233AABB" + + describe "quick_resolve_implementations/2 with wighawag variant" do + test "returns implementation address hash for valid wighawag proxy address" do + # wighawag variant bytecode pattern + # 0x3D3D3D3D363D3D3761 (9 bytes) + 2 bytes + 0x603736393661 (6 bytes) + 2 bytes + 0x013D73 (3 bytes) + implementation address (20 bytes) + rest + bytecode = + "0x3D3D3D3D363D3D3761" <> + "AABB" <> + "603736393661" <> + "CCDD" <> + "013D73" <> + @test_impl_address <> + "5AF43D82803E903D91602B57FD5BF3" + + proxy_address = insert(:address, contract_code: bytecode) + {:ok, impl_hash} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address) + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + {:ok, [impl_hash]} + end + + test "returns implementation address hash for valid wighawag proxy with additional bytecode" do + bytecode = + "0x3D3D3D3D363D3D3761" <> + "AABB" <> + "603736393661" <> + "CCDD" <> + "013D73" <> + @test_impl_address <> + "5AF43D82803E903D91602B57FD5BF3" <> + "DEADBEEF" <> + "1234567890" + + proxy_address = insert(:address, contract_code: bytecode) + {:ok, impl_hash} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address) + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + {:ok, [impl_hash]} + end + + test "returns nil for invalid wighawag proxy address" do + proxy_address = insert(:address, contract_code: "0x60806040") + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + + test "returns nil for proxy address without contract code" do + proxy_address = insert(:address, contract_code: nil) + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + end + + describe "quick_resolve_implementations/2 with solady variant" do + test "returns implementation address hash for valid solady proxy address" do + # solady variant bytecode pattern + # 0x363d3d373d3d3d363d73 (10 bytes) + implementation address (20 bytes) + 0x5af43d3d93803e602a57fd5bf3 (13 bytes) + rest + bytecode = + "0x363d3d373d3d3d363d73" <> + @test_impl_address <> + "5af43d3d93803e602a57fd5bf3" + + proxy_address = insert(:address, contract_code: bytecode) + {:ok, impl_hash} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address) + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + {:ok, [impl_hash]} + end + + test "returns implementation address hash for valid solady proxy with immutable arguments" do + bytecode = + "0x363d3d373d3d3d363d73" <> + @test_impl_address <> + "5af43d3d93803e602a57fd5bf3" <> + "AABBCCDDEE11223344556677" <> + "8899" + + proxy_address = insert(:address, contract_code: bytecode) + {:ok, impl_hash} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address) + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + {:ok, [impl_hash]} + end + + test "returns nil for invalid solady proxy address" do + proxy_address = insert(:address, contract_code: "0x60806040") + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + end + + describe "quick_resolve_implementations/2 edge cases" do + test "returns nil for empty contract code" do + proxy_address = insert(:address, contract_code: "0x") + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + + test "returns nil for bytecode matching wighawag prefix but too short" do + proxy_address = insert(:address, contract_code: "0x3D3D3D3D363D3D3761AABB") + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + + test "returns nil for bytecode matching solady prefix but too short" do + proxy_address = insert(:address, contract_code: "0x363d3d373d3d3d363d73") + + assert CloneWithImmutableArguments.quick_resolve_implementations(proxy_address, :clone_with_immutable_arguments) == + nil + end + + test "correctly distinguishes between wighawag and solady variants" do + # Wighawag variant + wighawag_bytecode = + "0x3D3D3D3D363D3D3761" <> + "AABB" <> + "603736393661" <> + "CCDD" <> + "013D73" <> + @test_impl_address <> + "5AF43D82803E903D91602B57FD5BF3" + + wighawag_proxy = insert(:address, contract_code: wighawag_bytecode) + + # Solady variant - use different address for distinction + solady_bytecode = + "0x363d3d373d3d3d363d73" <> + @test_impl_address_2 <> + "5af43d3d93803e602a57fd5bf3" + + solady_proxy = insert(:address, contract_code: solady_bytecode) + + {:ok, impl_hash_1} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address) + {:ok, impl_hash_2} = Explorer.Chain.Hash.Address.cast("0x" <> @test_impl_address_2) + + assert CloneWithImmutableArguments.quick_resolve_implementations(wighawag_proxy, :clone_with_immutable_arguments) == + {:ok, [impl_hash_1]} + + assert CloneWithImmutableArguments.quick_resolve_implementations(solady_proxy, :clone_with_immutable_arguments) == + {:ok, [impl_hash_2]} + end + end +end diff --git a/cspell.json b/cspell.json index a18840481b00..e517211d977d 100644 --- a/cspell.json +++ b/cspell.json @@ -29,6 +29,7 @@ ], // words - list of words to be always considered correct "words": [ + "AABB", "aave", "absname", "acbs", @@ -120,6 +121,7 @@ "capturelog", "cattributes", "CBOR", + "CCDD", "Celestia", "cellspacing", "celo", @@ -604,6 +606,7 @@ "sobelow", "Sockex", "Sokol", + "solady", "solana", "SOLIDITYSCAN", "soljson", @@ -751,6 +754,7 @@ "whereis", "whiler", "WHISTLEBLOWER", + "wighawag", "WITHDRAWABILITY", "wobserver", "wysdvjkizxonu", From b089b162233a27a713f3441a030949268767ae91 Mon Sep 17 00:00:00 2001 From: Kirill Fedoseev Date: Fri, 23 Jan 2026 15:05:21 +0400 Subject: [PATCH 0619/1006] chore: add swagger spec for account abstraction endpoints (#13897) Co-authored-by: Victor Baranov --- .../controllers/api/v2/mud_controller.ex | 47 ++-- .../proxy/account_abstraction_controller.ex | 210 +++++++++++++++++- .../transaction_interpretation.ex | 6 +- .../lib/block_scout_web/paging_helper.ex | 6 +- .../schemas/api/v2/error_responses.ex | 42 ++++ .../block_scout_web/schemas/api/v2/general.ex | 70 ++++++ .../v2/proxy/account_abstraction/account.ex | 32 +++ .../v2/proxy/account_abstraction/bundle.ex | 32 +++ .../v2/proxy/account_abstraction/bundler.ex | 26 +++ .../v2/proxy/account_abstraction/factory.ex | 24 ++ .../v2/proxy/account_abstraction/paymaster.ex | 24 ++ .../v2/proxy/account_abstraction/status.ex | 30 +++ .../account_abstraction/user_operation.ex | 168 ++++++++++++++ .../user_operation_in_list.ex | 42 ++++ .../account_abstraction_controller_test.exs | 12 +- 15 files changed, 736 insertions(+), 35 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/account.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundle.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundler.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/factory.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/paymaster.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/status.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation_in_list.ex diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex index e2f5b0cae0be..e7c60d6dd3ee 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/mud_controller.ex @@ -22,6 +22,8 @@ defmodule BlockScoutWeb.API.V2.MudController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + tags(["mud"]) operation :worlds, @@ -53,7 +55,7 @@ defmodule BlockScoutWeb.API.V2.MudController do def worlds(conn, params) do {worlds, next_page} = params - |> mud_paging_options(["world"], [Hash.Address]) + |> mud_paging_options([:world], [Hash.Address]) |> Mud.worlds_list() |> split_list_by_page() @@ -111,7 +113,7 @@ defmodule BlockScoutWeb.API.V2.MudController do description: "Retrieves a paginated list of MUD tables in the specific MUD world.", parameters: base_params() ++ - [world_param()] ++ + [world_param(), q_param(), filter_namespace_param()] ++ define_paging_params([ "table_id", "items_count" @@ -134,9 +136,9 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/tables` endpoint. """ @spec world_tables(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_tables(conn, %{"world" => world_param} = params) do + def world_tables(conn, %{world: world_param} = params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)} do - options = params |> mud_paging_options(["table_id"], [Hash.Full]) |> Keyword.merge(mud_tables_filter(params)) + options = params |> mud_paging_options([:table_id], [Hash.Full]) |> Keyword.merge(mud_tables_filter(params)) {tables, next_page} = world @@ -167,7 +169,7 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/systems` endpoint. """ @spec world_systems(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_systems(conn, %{"world" => world_param} = _params) do + def world_systems(conn, %{world: world_param} = _params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)} do systems = world |> Mud.world_systems() @@ -190,7 +192,7 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/systems/:system` endpoint. """ @spec world_system(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_system(conn, %{"world" => world_param, "system" => system_param} = _params) do + def world_system(conn, %{world: world_param, system: system_param} = _params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)}, {:format, {:ok, system}} <- {:format, Hash.Address.cast(system_param)}, {:ok, system_id, abi} <- Mud.world_system(world, system, @api_true) do @@ -203,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.MudController do operation :world_tables_count, summary: "Number of known MUD world tables.", description: "Retrieves the total number of known MUD tables in the specific MUD world.", - parameters: [world_param() | base_params()], + parameters: base_params() ++ [world_param(), q_param(), filter_namespace_param()], responses: [ ok: {"Number of known MUD world tables.", "application/json", %Schema{type: :integer}}, unprocessable_entity: JsonErrorResponse.response() @@ -213,7 +215,7 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/count` endpoint. """ @spec world_tables_count(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_tables_count(conn, %{"world" => world_param} = params) do + def world_tables_count(conn, %{world: world_param} = params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)} do options = params |> mud_tables_filter() @@ -230,7 +232,14 @@ defmodule BlockScoutWeb.API.V2.MudController do description: "Retrieves a paginated list of records in the specific MUD world table.", parameters: base_params() ++ - [world_param(), table_id_param()] ++ + [ + world_param(), + table_id_param(), + filter_key0_param(), + filter_key1_param(), + sort_param(["key_bytes", "key0", "key1"]), + order_param() + ] ++ define_paging_params([ "key_bytes", "key0", @@ -263,13 +272,13 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records` endpoint. """ @spec world_table_records(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_table_records(conn, %{"world" => world_param, "table_id" => table_id_param} = params) do + def world_table_records(conn, %{world: world_param, table_id: table_id_param} = params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)}, {:format, {:ok, table_id}} <- {:format, Hash.Full.cast(table_id_param)}, {:ok, schema} <- Mud.world_table_schema(world, table_id) do options = params - |> mud_paging_options(["key_bytes", "key0", "key1"], [Data, Hash.Full, Hash.Full]) + |> mud_paging_options([:key_bytes, :key0, :key1], [Data, Hash.Full, Hash.Full]) |> Keyword.merge(mud_records_filter(params, schema)) |> Keyword.merge(mud_records_sorting(params)) @@ -298,7 +307,7 @@ defmodule BlockScoutWeb.API.V2.MudController do operation :world_table_records_count, summary: "Number of known MUD world table records.", description: "Retrieves the total number of records in the specific MUD world table.", - parameters: base_params() ++ [world_param(), table_id_param()], + parameters: base_params() ++ [world_param(), table_id_param(), filter_key0_param(), filter_key1_param()], responses: [ ok: {"Number of known MUD world table records.", "application/json", %Schema{type: :integer}}, unprocessable_entity: JsonErrorResponse.response() @@ -308,7 +317,7 @@ defmodule BlockScoutWeb.API.V2.MudController do Function to handle GET requests to `/api/v2/mud/worlds/:world/tables/:table_id/records/count` endpoint. """ @spec world_table_records_count(Plug.Conn.t(), map()) :: Plug.Conn.t() - def world_table_records_count(conn, %{"world" => world_param, "table_id" => table_id_param} = params) do + def world_table_records_count(conn, %{world: world_param, table_id: table_id_param} = params) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)}, {:format, {:ok, table_id}} <- {:format, Hash.Full.cast(table_id_param)}, {:ok, schema} <- Mud.world_table_schema(world, table_id) do @@ -343,7 +352,7 @@ defmodule BlockScoutWeb.API.V2.MudController do @spec world_table_record(Plug.Conn.t(), map()) :: Plug.Conn.t() def world_table_record( conn, - %{"world" => world_param, "table_id" => table_id_param, "record_id" => record_id_param} = _params + %{world: world_param, table_id: table_id_param, record_id: record_id_param} = _params ) do with {:format, {:ok, world}} <- {:format, Hash.Address.cast(world_param)}, {:format, {:ok, table_id}} <- {:format, Hash.Full.cast(table_id_param)}, @@ -361,10 +370,10 @@ defmodule BlockScoutWeb.API.V2.MudController do defp mud_tables_filter(params) do Enum.reduce(params, [], fn {key, value}, acc -> case key do - "filter_namespace" -> + :filter_namespace -> Keyword.put(acc, :filter_namespace, parse_namespace_string(value)) - "q" -> + :q -> Keyword.put(acc, :filter_search, parse_search_string(value)) _ -> @@ -405,8 +414,8 @@ defmodule BlockScoutWeb.API.V2.MudController do defp mud_records_filter(params, schema) do Enum.reduce(params, [], fn {key, value}, acc -> case key do - "filter_key0" -> Keyword.put(acc, :filter_key0, encode_filter(value, schema, 0)) - "filter_key1" -> Keyword.put(acc, :filter_key1, encode_filter(value, schema, 1)) + :filter_key0 -> Keyword.put(acc, :filter_key0, encode_filter(value, schema, 0)) + :filter_key1 -> Keyword.put(acc, :filter_key1, encode_filter(value, schema, 1)) _ -> acc end end) @@ -444,7 +453,7 @@ defmodule BlockScoutWeb.API.V2.MudController do |> Enum.reduce(%{}, fn {key, type}, acc -> with param when param != nil <- Map.get(params, key), {:ok, val} <- type.cast(param) do - acc |> Map.put(String.to_existing_atom(key), val) + acc |> Map.put(key, val) else _ -> acc end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex index 072baefea963..362518f54956 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex @@ -1,8 +1,10 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do use BlockScoutWeb, :controller + use OpenApiSpex.ControllerSpecs alias BlockScoutWeb.API.V2.Helper alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService + alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.{BadRequestResponse, NotFoundResponse, NotImplementedResponse} alias Explorer.Chain alias Explorer.MicroserviceInterfaces.AccountAbstraction @@ -10,22 +12,55 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do action_fallback(BlockScoutWeb.API.V2.FallbackController) + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + tags(["account_abstraction"]) + + operation :operation, + summary: "Get a user operation by hash", + description: "Retrieves a user operation by its hash.", + parameters: [operation_hash_param() | base_params()], + responses: [ + ok: {"User operation", "application/json", Schemas.Proxy.AccountAbstraction.UserOperation}, + not_found: NotFoundResponse.response(), + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations/:user_operation_hash_param` endpoint. """ @spec operation(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def operation(conn, %{"operation_hash_param" => operation_hash_string}) do + def operation(conn, %{operation_hash_param: operation_hash_string}) do operation_hash_string |> AccountAbstraction.get_user_ops_by_hash() |> process_response(conn) end + operation :summary, + summary: "Get a human-readable, LLM-based user operation summary", + description: "Retrieves a human-readable summary of what a user operation did, presented in natural language.", + parameters: base_params() ++ [operation_hash_param(), just_request_body_param()], + responses: [ + ok: + {"Human-readable summary of the specified user operation.", "application/json", + %Schema{ + anyOf: [ + Schemas.Transaction.Summary, + Schemas.Transaction.SummaryJustRequestBody + ] + }}, + not_found: NotFoundResponse.response(), + unprocessable_entity: JsonErrorResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations/:user_operation_hash_param/summary` endpoint. """ @spec summary(Plug.Conn.t(), map()) :: {:error | :format | :transaction_interpreter_enabled | non_neg_integer(), any()} | Plug.Conn.t() - def summary(conn, %{"operation_hash_param" => operation_hash_string, "just_request_body" => "true"}) do + def summary(conn, %{operation_hash_param: operation_hash_string, just_request_body: true}) do with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_full_hash(operation_hash_string)}, {:transaction_interpreter_enabled, true} <- {:transaction_interpreter_enabled, TransactionInterpretationService.enabled?()}, @@ -35,7 +70,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do end end - def summary(conn, %{"operation_hash_param" => operation_hash_string}) do + def summary(conn, %{operation_hash_param: operation_hash_string}) do with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_full_hash(operation_hash_string)}, {:transaction_interpreter_enabled, true} <- {:transaction_interpreter_enabled, TransactionInterpretationService.enabled?()}, @@ -53,16 +88,45 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do end end + operation :bundler, + summary: "Get a bundler by address hash", + description: "Retrieves a bundler by its address hash.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Bundler", "application/json", Schemas.Proxy.AccountAbstraction.Bundler}, + not_found: NotFoundResponse.response(), + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundlers/:address_hash_param` endpoint. """ @spec bundler(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def bundler(conn, %{"address_hash_param" => address_hash_string}) do + def bundler(conn, %{address_hash_param: address_hash_string}) do address_hash_string |> AccountAbstraction.get_bundler_by_hash() |> process_response(conn) end + operation :bundlers, + summary: "List of top bundlers", + description: "Retrieves a list of top bundlers.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of bundlers with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.Bundler, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "5,0x9B67A24A474e9EC7372c23B023f36ab28831e4C4" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundlers` endpoint. """ @@ -73,16 +137,45 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :factory, + summary: "Get a factory by address hash", + description: "Retrieves a factory by its address hash.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Factory", "application/json", Schemas.Proxy.AccountAbstraction.Factory}, + not_found: NotFoundResponse.response(), + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/factories/:address_hash_param` endpoint. """ @spec factory(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def factory(conn, %{"address_hash_param" => address_hash_string}) do + def factory(conn, %{address_hash_param: address_hash_string}) do address_hash_string |> AccountAbstraction.get_factory_by_hash() |> process_response(conn) end + operation :factories, + summary: "List of top wallet factories", + description: "Retrieves a list of top wallet factories.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of factories with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.Factory, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "3,0xC23957e7Fea98eBD017abe15a4e7770797ff6D8d" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/factories` endpoint. """ @@ -93,16 +186,45 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :paymaster, + summary: "Get a paymaster by address hash", + description: "Retrieves a paymaster by its address hash.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Paymaster", "application/json", Schemas.Proxy.AccountAbstraction.Paymaster}, + not_found: NotFoundResponse.response(), + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/paymasters/:address_hash_param` endpoint. """ @spec paymaster(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def paymaster(conn, %{"address_hash_param" => address_hash_string}) do + def paymaster(conn, %{address_hash_param: address_hash_string}) do address_hash_string |> AccountAbstraction.get_paymaster_by_hash() |> process_response(conn) end + operation :paymasters, + summary: "List of top paymasters", + description: "Retrieves a list of top paymasters.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of paymasters with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.Paymaster, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "19,0xB98Cb1dA4F9BD640879F0bBCb30A541c84163406" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/paymasters` endpoint. """ @@ -113,16 +235,45 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :account, + summary: "Get an account abstraction wallet by address hash", + description: "Retrieves an account abstraction wallet by its address hash.", + parameters: [address_hash_param() | base_params()], + responses: [ + ok: {"Account", "application/json", Schemas.Proxy.AccountAbstraction.Account}, + not_found: NotFoundResponse.response(), + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/accounts/:address_hash_param` endpoint. """ @spec account(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} - def account(conn, %{"address_hash_param" => address_hash_string}) do + def account(conn, %{address_hash_param: address_hash_string}) do address_hash_string |> AccountAbstraction.get_account_by_hash() |> process_response(conn) end + operation :accounts, + summary: "List of account abstraction wallets", + description: "Retrieves a list of account abstraction wallets.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of account abstraction wallets with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.Account, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "0x29cB129476609bBa26372b23427Af3b87cB23aF6" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/accounts` endpoint. """ @@ -133,6 +284,24 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :bundles, + summary: "List of recent bundles", + description: "Retrieves a list of recent bundles.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of bundles with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.Bundle, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "3949699,0x275f7110df7d73e530f3381daa9d422dea80f036b514e56427d2d7e492c8a81f,0" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundles` endpoint. """ @@ -143,6 +312,24 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :operations, + summary: "List of recent user operations", + description: "Retrieves a list of recent user operations.", + parameters: base_params() ++ define_paging_params(["page_size", "page_token"]), + responses: [ + ok: + {"List of user operations with pagination.", "application/json", + paginated_response( + items: Schemas.Proxy.AccountAbstraction.UserOperationInList, + next_page_params_example: %{ + "page_size" => 50, + "page_token" => "3937439,0xbb680271614883525ac8056c388489e80b3518ec12ec46e1b910c7238c46b565" + } + )}, + bad_request: BadRequestResponse.response(), + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations` endpoint. """ @@ -153,6 +340,15 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do |> process_response(conn) end + operation :status, + summary: "Get the status of the account abstraction microservice", + description: "Retrieves the status of the account abstraction microservice.", + parameters: base_params(), + responses: [ + ok: {"Status", "application/json", Schemas.Proxy.AccountAbstraction.Status}, + not_implemented: NotImplementedResponse.response() + ] + @doc """ Function to handle GET requests to `/api/v2/proxy/account-abstraction/status` endpoint. """ diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 19c471cdae1a..3f9414f5dd21 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -395,12 +395,12 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do type: 0, value: "0", method: Transaction.method_name(mock_transaction, Transaction.format_decoded_input(decoded_input), true), - status: user_op["status"], - actions: [], + status: (user_op["status"] && :ok) || :error, transaction_types: [], raw_input: user_op_call_data, decoded_input: decoded_input_json, - token_transfers: prepared_token_transfers + token_transfers: prepared_token_transfers, + internal_transactions: [] }, logs_data: %{items: prepared_logs}, chain_id: :block_scout_web |> Application.get_env(:chain_id) |> ExplorerHelper.parse_integer() diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index c66d27cd4db3..84b1472e0ca8 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -412,10 +412,8 @@ defmodule BlockScoutWeb.PagingHelper do defp do_validators_stability_sorting(_, _), do: [] - @spec mud_records_sorting(%{required(String.t()) => String.t()}) :: [ - {:sorting, SortingHelper.sorting_params()} - ] - def mud_records_sorting(%{"sort" => sort_field, "order" => order}) do + @spec mud_records_sorting(map()) :: [{:sorting, SortingHelper.sorting_params()}] + def mud_records_sorting(%{sort: sort_field, order: order}) do [sorting: do_mud_records_sorting(sort_field, order)] end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex index 06300fb42f72..e11591f79f96 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/error_responses.ex @@ -68,4 +68,46 @@ defmodule BlockScoutWeb.Schemas.API.V2.ErrorResponses do {"Unauthorized", "application/json", __MODULE__} end end + + defmodule BadRequestResponse do + @moduledoc false + OpenApiSpex.schema(%{ + title: "BadRequestResponse", + description: "Response returned when the request is invalid", + type: :object, + properties: %{ + message: %Schema{ + type: :string, + description: "Error message indicating the request is invalid", + example: "Invalid request" + } + } + }) + + @spec response() :: {String.t(), String.t(), module()} + def response do + {"Bad Request", "application/json", __MODULE__} + end + end + + defmodule NotImplementedResponse do + @moduledoc false + OpenApiSpex.schema(%{ + title: "NotImplementedResponse", + description: "Response returned when the feature is not implemented", + type: :object, + properties: %{ + message: %Schema{ + type: :string, + description: "Error message indicating the feature is not implemented", + example: "Feature not implemented" + } + } + }) + + @spec response() :: {String.t(), String.t(), module()} + def response do + {"Not Implemented", "application/json", __MODULE__} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index 6fb967e943f8..d0f58a668dd6 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -796,6 +796,62 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do } end + @doc """ + Returns a parameter definition for MUD tables namespace filter. + """ + @spec filter_namespace_param() :: Parameter.t() + def filter_namespace_param do + %Parameter{ + name: :filter_namespace, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Filter by namespace" + } + end + + @doc """ + Returns a parameter definition for MUD table records key0 filter. + """ + @spec filter_key0_param() :: Parameter.t() + def filter_key0_param do + %Parameter{ + name: :filter_key0, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Filter by key0" + } + end + + @doc """ + Returns a parameter definition for MUD table records key1 filter. + """ + @spec filter_key1_param() :: Parameter.t() + def filter_key1_param do + %Parameter{ + name: :filter_key1, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Filter by key1" + } + end + + @doc """ + Returns a parameter definition for a user operation hash in the path. + """ + @spec operation_hash_param() :: Parameter.t() + def operation_hash_param do + %Parameter{ + name: :operation_hash_param, + in: :path, + schema: FullHash, + required: true, + description: "User operation hash in the path" + } + end + @doc """ Returns a list of base parameters (api_key and key). """ @@ -1214,6 +1270,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do schema: FullHash, required: false, description: "MUD record key1 for paging" + }, + "page_size" => %Parameter{ + name: :page_size, + in: :query, + schema: %Schema{type: :integer, minimum: 1, maximum: 50}, + required: false, + description: "Number of items returned per page" + }, + "page_token" => %Parameter{ + name: :page_token, + in: :query, + schema: %Schema{type: :string}, + required: false, + description: "Page token for paging" } } diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/account.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/account.ex new file mode 100644 index 000000000000..a5aa5ad400e9 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/account.ex @@ -0,0 +1,32 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Account do + @moduledoc """ + This module defines the schema for the Account struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, AddressNullable, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Account struct.", + type: :object, + properties: %{ + address: Address, + creation_op_hash: General.FullHashNullable, + creation_transaction_hash: General.FullHashNullable, + creation_timestamp: General.TimestampNullable, + factory: AddressNullable, + total_ops: %Schema{type: :integer, nullable: false, minimum: 0} + }, + required: [ + :address, + :creation_op_hash, + :creation_transaction_hash, + :creation_timestamp, + :factory, + :total_ops + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundle.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundle.ex new file mode 100644 index 000000000000..c9f850bd7ef6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundle.ex @@ -0,0 +1,32 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Bundle do + @moduledoc """ + This module defines the schema for the Bundle struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Bundle struct.", + type: :object, + properties: %{ + transaction_hash: General.FullHash, + bundler: Address, + block_number: General.IntegerString, + bundle_index: %Schema{type: :integer, nullable: false, minimum: 0}, + timestamp: General.Timestamp, + total_ops: %Schema{type: :integer, nullable: false, minimum: 0} + }, + required: [ + :transaction_hash, + :bundler, + :block_number, + :bundle_index, + :timestamp, + :total_ops + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundler.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundler.ex new file mode 100644 index 000000000000..9963275a52ae --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/bundler.ex @@ -0,0 +1,26 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Bundler do + @moduledoc """ + This module defines the schema for the Bundler struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Bundler struct.", + type: :object, + properties: %{ + address: Address, + total_bundles: %Schema{type: :integer, nullable: false, minimum: 0}, + total_ops: %Schema{type: :integer, nullable: false, minimum: 0} + }, + required: [ + :address, + :total_bundles, + :total_ops + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/factory.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/factory.ex new file mode 100644 index 000000000000..4d81b927109f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/factory.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Factory do + @moduledoc """ + This module defines the schema for the Factory struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Factory struct.", + type: :object, + properties: %{ + address: Address, + total_accounts: %Schema{type: :integer, nullable: false, minimum: 0} + }, + required: [ + :address, + :total_accounts + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/paymaster.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/paymaster.ex new file mode 100644 index 000000000000..73db7994c9bc --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/paymaster.ex @@ -0,0 +1,24 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Paymaster do + @moduledoc """ + This module defines the schema for the Paymaster struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.Address + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Paymaster struct.", + type: :object, + properties: %{ + address: Address, + total_ops: %Schema{type: :integer, nullable: false, minimum: 0} + }, + required: [ + :address, + :total_ops + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/status.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/status.ex new file mode 100644 index 000000000000..63cb0e86e4e2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/status.ex @@ -0,0 +1,30 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.Status do + @moduledoc """ + This module defines the schema for the Status struct. + """ + require OpenApiSpex + + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "Status struct.", + type: :object, + properties: %{ + finished_past_indexing: %Schema{type: :boolean, nullable: false} + }, + required: [ + :finished_past_indexing + ], + additionalProperties: %Schema{ + type: :object, + properties: %{ + enabled: %Schema{type: :boolean, nullable: false}, + live: %Schema{type: :boolean, nullable: false}, + past_db_logs_indexing_finished: %Schema{type: :boolean, nullable: false}, + past_rpc_logs_indexing_finished: %Schema{type: :boolean, nullable: false} + }, + additionalProperties: false + }, + nullable: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation.ex new file mode 100644 index 000000000000..cea749a046b6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation.ex @@ -0,0 +1,168 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.UserOperation do + @moduledoc """ + This module defines the schema for the UserOperation struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, AddressNullable, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "UserOperation struct.", + type: :object, + properties: %{ + hash: General.FullHash, + sender: Address, + nonce: General.FullHash, + call_data: General.HexString, + call_gas_limit: General.IntegerString, + verification_gas_limit: General.IntegerString, + pre_verification_gas: General.IntegerString, + max_fee_per_gas: General.IntegerString, + max_priority_fee_per_gas: General.IntegerString, + signature: General.HexString, + raw: %Schema{ + description: "Raw user operation data.", + anyOf: [ + %Schema{ + description: "Raw user operation data v0.6.", + type: :object, + properties: %{ + sender: General.AddressHash, + nonce: General.IntegerString, + init_code: General.HexString, + call_data: General.HexString, + call_gas_limit: General.IntegerString, + verification_gas_limit: General.IntegerString, + pre_verification_gas: General.IntegerString, + max_fee_per_gas: General.IntegerString, + max_priority_fee_per_gas: General.IntegerString, + paymaster_and_data: General.HexString, + signature: General.HexString + }, + required: [ + :sender, + :nonce, + :init_code, + :call_data, + :call_gas_limit, + :verification_gas_limit, + :pre_verification_gas, + :max_fee_per_gas, + :max_priority_fee_per_gas, + :paymaster_and_data, + :signature + ], + nullable: false, + additionalProperties: false + }, + %Schema{ + description: "Raw user operation data v0.7-v0.9.", + type: :object, + properties: %{ + sender: General.AddressHash, + nonce: General.IntegerString, + init_code: General.HexString, + call_data: General.HexString, + account_gas_limits: General.FullHash, + pre_verification_gas: General.IntegerString, + gas_fees: General.FullHash, + paymaster_and_data: General.HexString, + signature: General.HexString + }, + required: [ + :sender, + :nonce, + :init_code, + :call_data, + :account_gas_limits, + :pre_verification_gas, + :gas_fees, + :paymaster_and_data, + :signature + ], + nullable: false, + additionalProperties: false + } + ] + }, + aggregator: General.AddressHashNullable, + aggregator_signature: General.HexStringNullable, + entry_point: Address, + entry_point_version: %Schema{ + type: :string, + enum: ["v0.6", "v0.7", "v0.8", "v0.9"], + nullable: false + }, + transaction_hash: General.FullHash, + block_number: General.IntegerString, + block_hash: General.FullHash, + bundler: Address, + bundle_index: %Schema{type: :integer, nullable: false}, + index: %Schema{type: :integer, nullable: false}, + factory: AddressNullable, + paymaster: AddressNullable, + status: %Schema{type: :boolean, nullable: false}, + revert_reason: General.HexStringNullable, + gas: General.IntegerString, + gas_price: General.IntegerString, + gas_used: General.IntegerString, + sponsor_type: %Schema{ + type: :string, + enum: ["wallet_deposit", "wallet_balance", "paymaster_sponsor", "paymaster_hybrid"], + nullable: false + }, + user_logs_start_index: %Schema{type: :integer, nullable: false}, + user_logs_count: %Schema{type: :integer, nullable: false}, + fee: General.IntegerString, + consensus: %Schema{type: :boolean, nullable: true}, + timestamp: General.TimestampNullable, + execute_target: AddressNullable, + execute_call_data: General.HexStringNullable, + decoded_call_data: %Schema{allOf: [General.DecodedInput], nullable: true}, + decoded_execute_call_data: %Schema{allOf: [General.DecodedInput], nullable: true} + }, + required: [ + :hash, + :sender, + :nonce, + :call_data, + :call_gas_limit, + :verification_gas_limit, + :pre_verification_gas, + :max_fee_per_gas, + :max_priority_fee_per_gas, + :signature, + :raw, + :aggregator, + :aggregator_signature, + :entry_point, + :entry_point_version, + :transaction_hash, + :block_number, + :block_hash, + :bundler, + :bundle_index, + :index, + :factory, + :paymaster, + :status, + :revert_reason, + :gas, + :gas_price, + :gas_used, + :sponsor_type, + :user_logs_start_index, + :user_logs_count, + :fee, + :consensus, + :timestamp, + :execute_target, + :execute_call_data, + :decoded_call_data, + :decoded_execute_call_data + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation_in_list.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation_in_list.ex new file mode 100644 index 000000000000..95a389541f6d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/proxy/account_abstraction/user_operation_in_list.ex @@ -0,0 +1,42 @@ +defmodule BlockScoutWeb.Schemas.API.V2.Proxy.AccountAbstraction.UserOperationInList do + @moduledoc """ + This module defines the schema for the UserOperationInList struct. + """ + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.{Address, General} + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + description: "UserOperationInList struct.", + type: :object, + properties: %{ + hash: General.FullHash, + address: Address, + entry_point: Address, + entry_point_version: %Schema{ + type: :string, + enum: ["v0.6", "v0.7", "v0.8", "v0.9"], + nullable: false + }, + transaction_hash: General.FullHash, + block_number: General.IntegerString, + status: %Schema{type: :boolean, nullable: false}, + fee: General.IntegerString, + timestamp: General.TimestampNullable + }, + required: [ + :hash, + :address, + :entry_point, + :entry_point_version, + :transaction_hash, + :block_number, + :status, + :fee, + :timestamp + ], + nullable: false, + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs index 4e623dc253e2..7b5d39df1b71 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller_test.exs @@ -99,7 +99,15 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionControllerTest do test "return 422 on invalid operation hash", %{conn: conn} do request = get(conn, "/api/v2/proxy/account-abstraction/operations/0x/summary?just_request_body=true") - assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + assert %{ + "errors" => [ + %{ + "detail" => "Invalid format. Expected ~r/^0x([A-Fa-f0-9]{64})$/", + "source" => %{"pointer" => "/operation_hash_param"}, + "title" => "Invalid value" + } + ] + } = json_response(request, 422) end test "return 404 on non existing operation", %{conn: conn, aa_bypass: aa_bypass, ti_bypass: ti_bypass} do @@ -211,7 +219,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionControllerTest do assert operation_hash == data["hash"] assert 0 == data["type"] assert "0" == data["value"] - assert true == data["status"] + assert "ok" == data["status"] assert Enum.count(data["token_transfers"]) == 50 end From e902294ee42b30844ec36d1de25305c7996cb84f Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Fri, 23 Jan 2026 15:07:10 +0300 Subject: [PATCH 0620/1006] feat: Add audit-reports import endpoint (#13884) --- .../block_scout_web/authentication_helper.ex | 15 ++++ .../account/api/v2/authenticate_controller.ex | 6 +- .../api/v2/address_badge_controller.ex | 15 +--- .../controllers/api/v2/import_controller.ex | 78 +++++++++++++++++-- .../controllers/api/v2/token_controller.ex | 7 +- .../lib/block_scout_web/routers/api_router.ex | 1 + .../views/account/api/v2/user_view.ex | 9 +-- .../block_scout_web/views/api/v2/api_view.ex | 11 +++ .../views/api/v2/smart_contract_view.ex | 8 +- .../lib/block_scout_web/views/error_helper.ex | 12 +++ .../chain/smart_contract/audit_report.ex | 32 ++++++++ 11 files changed, 155 insertions(+), 39 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/authentication_helper.ex diff --git a/apps/block_scout_web/lib/block_scout_web/authentication_helper.ex b/apps/block_scout_web/lib/block_scout_web/authentication_helper.ex new file mode 100644 index 000000000000..25a669c5fd62 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/authentication_helper.ex @@ -0,0 +1,15 @@ +defmodule BlockScoutWeb.AuthenticationHelper do + @moduledoc """ + Helper module for authentication. + """ + + @spec validate_sensitive_endpoints_api_key(binary() | nil) :: + :ok | {:sensitive_endpoints_api_key, any()} | {:api_key, any()} + def validate_sensitive_endpoints_api_key(api_key_from_request) do + with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- + {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, + {:api_key, ^api_key} <- {:api_key, api_key_from_request} do + :ok + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex index 9e850e6b62dc..a5bcc4fc935e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do import BlockScoutWeb.Account.AuthController, only: [current_user: 1] - alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.{AccessHelper, AuthenticationHelper} alias BlockScoutWeb.Account.API.V2.UserView alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.{ @@ -62,9 +62,7 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do end defp authenticate(conn, params) do - with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- - {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, params[:api_key]}, + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(params[:api_key]), {:auth, %{id: uid} = current_user} <- {:auth, current_user(conn)}, {:identity, %Identity{}} <- {:identity, Identity.find_identity(uid)} do conn diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex index 5a3d58d572ea..825ea61c11e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex @@ -2,6 +2,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeController do require Logger use BlockScoutWeb, :controller + alias BlockScoutWeb.AuthenticationHelper alias Explorer.Chain alias Explorer.Chain.Address.ScamBadgeToAddress alias Plug.Conn @@ -17,7 +18,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeController do } = params ) when is_list(address_hashes) do - with :ok <- check_sensitive_endpoint_api_key(params["api_key"]), + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(params["api_key"]), valid_address_hashes = filter_address_hashes(address_hashes), {_num_of_inserted, badge_to_address_list} <- ScamBadgeToAddress.add(valid_address_hashes) do conn @@ -38,7 +39,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeController do } = params ) when is_list(address_hashes) do - with :ok <- check_sensitive_endpoint_api_key(params["api_key"]), + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(params["api_key"]), valid_address_hashes = filter_address_hashes(address_hashes), {_num_of_deleted, badge_to_address_list} <- ScamBadgeToAddress.delete(valid_address_hashes) do conn @@ -55,7 +56,7 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeController do def show_badge_addresses(conn, _) do with {:ok, body, _conn} <- Conn.read_body(conn, []), {:ok, %{"api_key" => provided_api_key}} <- Jason.decode(body), - :ok <- check_sensitive_endpoint_api_key(provided_api_key) do + :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(provided_api_key) do badge_to_address_list = ScamBadgeToAddress.get(@api_true) conn @@ -69,14 +70,6 @@ defmodule BlockScoutWeb.API.V2.AddressBadgeController do end end - defp check_sensitive_endpoint_api_key(provided_api_key) do - with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- - {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, provided_api_key} do - :ok - end - end - defp filter_address_hashes(address_hashes) do address_hashes |> Enum.uniq() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex index 7f7746774f8a..9f6eaa6b378d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/import_controller.ex @@ -2,9 +2,11 @@ defmodule BlockScoutWeb.API.V2.ImportController do use BlockScoutWeb, :controller alias BlockScoutWeb.API.V2.ApiView + alias BlockScoutWeb.AuthenticationHelper alias Explorer.Chain alias Explorer.Chain.{Address, Data, Token} alias Explorer.Chain.Fetcher.LookUpSmartContractSourcesOnDemand + alias Explorer.Chain.SmartContract.AuditReport alias Explorer.SmartContract.EthBytecodeDBInterface alias Indexer.Fetcher.TokenUpdater alias Utils.ConfigHelper, as: UtilsConfigHelper @@ -110,9 +112,7 @@ defmodule BlockScoutWeb.API.V2.ImportController do | {:sensitive_endpoints_api_key, any()} | Plug.Conn.t() def try_to_search_contract(conn, %{"address_hash_param" => address_hash_string} = params) do - with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- - {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, get_api_key_header(conn)}, + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(get_api_key_header(conn)), {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:not_found, {:ok, address}} <- {:not_found, @@ -151,6 +151,19 @@ defmodule BlockScoutWeb.API.V2.ImportController do end end + @doc """ + Function to handle DELETE request at: + `/api/v2/import/token-info` + + Needed to delete token info via admin panel. + Protected by `x-api-key` header. + """ + @spec delete_token_info(Plug.Conn.t(), map()) :: + {:api_key, any()} + | {:format_address, :error} + | {:not_found, {:error, :not_found}} + | {:sensitive_endpoints_api_key, any()} + | Plug.Conn.t() def delete_token_info( conn, %{ @@ -177,6 +190,61 @@ defmodule BlockScoutWeb.API.V2.ImportController do end end + @doc """ + Function to handle POST request at: + `/api/v2/import/smart-contracts/{address_hash_param}/audit-reports` + + Needed to import audit report via admin panel. + Protected by `x-api-key` header. + """ + @spec import_audit_report(Plug.Conn.t(), map()) :: + {:api_key, any()} + | {:format_address, :error} + | {:not_found, {:error, :not_found}} + | {:sensitive_endpoints_api_key, any()} + | Plug.Conn.t() + def import_audit_report( + conn, + %{ + "address_hash_param" => address_hash_string, + "audit_report_url" => audit_report_url, + "audit_publish_date" => audit_publish_date, + "audit_company_name" => audit_company_name + } + ) do + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(get_api_key_header(conn)), + {:format_address, {:ok, address_hash}} <- + {:format_address, Chain.string_to_address_hash(address_hash_string)}, + {:not_found, {:ok, %{smart_contract: smart_contract}}} when not is_nil(smart_contract) <- + {:not_found, + Chain.hash_to_address(address_hash, + necessity_by_association: %{ + :smart_contract => :optional + }, + api?: true + )} do + case AuditReport.import(%{ + address_hash: address_hash, + audit_report_url: audit_report_url, + audit_publish_date: audit_publish_date, + audit_company_name: audit_company_name + }) do + {:ok, _} -> + conn + |> put_view(ApiView) + |> render(:message, %{message: "Success"}) + + {:error, changeset} -> + Logger.warning(fn -> ["Error on importing audit report: ", inspect(changeset)] end) + + conn + |> put_view(ApiView) + |> put_status(:bad_request) + |> render(:smart_contract_audit_report_changeset_errors, changeset: changeset) + end + end + end + defp params_to_contract_search_options(%{"import_from" => "verifier_alliance"}) do [only_verifier_alliance?: true] end @@ -218,9 +286,7 @@ defmodule BlockScoutWeb.API.V2.ImportController do end defp validate_api_key_address_hash_and_token(token_address_hash_string, provided_api_key) do - with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- - {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- {:api_key, provided_api_key}, + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(provided_api_key), {:format_address, {:ok, address_hash}} <- {:format_address, Chain.string_to_address_hash(token_address_hash_string)}, {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)} do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index 3c105f042587..25dcf94bc1d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]] use OpenApiSpex.ControllerSpecs - alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.{AccessHelper, AuthenticationHelper} alias BlockScoutWeb.API.V2.{AddressView, TransactionView} alias BlockScoutWeb.Schemas.API.V2.ErrorResponses.NotFoundResponse alias Explorer.{Chain, PagingOptions} @@ -741,10 +741,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do address_hash_string = params[:address_hash_param] ip = AccessHelper.conn_to_ip_string(conn) - with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- - {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, - {:api_key, ^api_key} <- - {:api_key, get_api_key(conn)}, + with :ok <- AuthenticationHelper.validate_sensitive_endpoints_api_key(get_api_key(conn)), {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), {:not_found, {:ok, token}} <- {:not_found, Chain.token_from_address_hash(address_hash, @api_true)}, diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index a89d549c1a0a..24f67d4cf2c0 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -122,6 +122,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do delete("/token-info", V2.ImportController, :delete_token_info) get("/smart-contracts/:address_hash_param", V2.ImportController, :try_to_search_contract) + post("/smart-contracts/:address_hash_param/audit-reports", V2.ImportController, :import_audit_report) if @chain_type == :optimism do post("/optimism/interop/", V2.OptimismController, :interop_import) diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex index 2c92484f2084..924090fc4466 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v2/user_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do alias BlockScoutWeb.Account.API.V2.AccountView alias BlockScoutWeb.API.V2.Helper - alias Ecto.Changeset + alias BlockScoutWeb.ErrorHelper alias Explorer.Account.WatchlistAddress alias Explorer.Chain alias Explorer.Chain.Address @@ -86,12 +86,7 @@ defmodule BlockScoutWeb.Account.API.V2.UserView do def render("changeset_errors.json", %{changeset: changeset}) do %{ - "errors" => - Changeset.traverse_errors(changeset, fn {msg, opts} -> - Regex.replace(~r"%{(\w+)}", msg, fn _, key -> - opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() - end) - end) + "errors" => ErrorHelper.changeset_to_errors(changeset) } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex index 1fde984d4a41..aed711a8beef 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/api_view.ex @@ -1,7 +1,18 @@ defmodule BlockScoutWeb.API.V2.ApiView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.ErrorHelper + def render("message.json", %{message: message}) do %{ "message" => message } end + + def render("smart_contract_audit_report_changeset_errors.json", %{changeset: changeset}) do + %{ + "errors" => ErrorHelper.changeset_to_errors(changeset), + "message" => "Error on inserting audit report" + } + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex index 3d48b4ac0dc9..6248a12df927 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/smart_contract_view.ex @@ -8,7 +8,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do alias BlockScoutWeb.{AddressContractView, SmartContractView} alias BlockScoutWeb.API.V2.Helper, as: APIV2Helper alias BlockScoutWeb.API.V2.TransactionView - alias Ecto.Changeset + alias BlockScoutWeb.ErrorHelper alias Explorer.Chain alias Explorer.Chain.{Address, SmartContract, SmartContractAdditionalSource} alias Explorer.Chain.SmartContract.Proxy @@ -38,11 +38,7 @@ defmodule BlockScoutWeb.API.V2.SmartContractView do end def render("changeset_errors.json", %{changeset: changeset}) do - Changeset.traverse_errors(changeset, fn {msg, opts} -> - Regex.replace(~r"%{(\w+)}", msg, fn _, key -> - opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() - end) - end) + ErrorHelper.changeset_to_errors(changeset) end def render("audit_reports.json", %{reports: reports}) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex b/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex index a29a482dd9bc..da4f3a455502 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/error_helper.ex @@ -56,4 +56,16 @@ defmodule BlockScoutWeb.ErrorHelper do Gettext.dgettext(BlockScoutWeb.Gettext, "errors", msg, opts) end end + + @doc """ + Converts a changeset to a list of errors. + """ + @spec changeset_to_errors(Changeset.t()) :: any() + def changeset_to_errors(changeset) do + Changeset.traverse_errors(changeset, fn {msg, opts} -> + Regex.replace(~r"%{(\w+)}", msg, fn _, key -> + opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string() + end) + end) + end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex index 52122d3326d8..87ba3928f372 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract/audit_report.ex @@ -113,6 +113,38 @@ defmodule Explorer.Chain.SmartContract.AuditReport do |> Repo.insert() end + @doc """ + Import a new audit report from admin panel. + """ + @spec import(map()) :: {:ok, __MODULE__.t()} | {:error, Ecto.Changeset.t()} + def import(%{ + address_hash: address_hash, + audit_report_url: audit_report_url, + audit_publish_date: audit_publish_date, + audit_company_name: audit_company_name + }) do + attrs = %{ + address_hash: address_hash, + audit_report_url: audit_report_url, + audit_publish_date: audit_publish_date, + audit_company_name: audit_company_name, + is_approved: true, + submitter_name: "api", + submitter_email: "api", + is_project_owner: true, + project_name: "api", + project_url: "api" + } + + %__MODULE__{} + |> cast(attrs, @optional_fields ++ @required_fields) + |> unique_constraint([:address_hash, :audit_report_url, :audit_publish_date, :audit_company_name], + message: "the report was submitted before", + name: :audit_report_unique_index + ) + |> Repo.insert() + end + defp get_reports_count_by_day_for_address_hash_by_day(address_hash) do __MODULE__ |> where( From 8e0fc4ab2e3e8a4b5568a326cfe303688ec8418f Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:27:52 +0400 Subject: [PATCH 0621/1006] feat: re-architect internal transaction format with call-type enum, error dictionary, and normalization (#13893) Co-authored-by: Victor Baranov --- .../address/internal_transactions.ex | 4 +- .../models/transaction_state_helper.ex | 9 +- .../views/api/rpc/address_view.ex | 2 +- .../views/api/v2/internal_transaction_view.ex | 4 +- .../views/internal_transaction_view.ex | 15 +- .../address/internal_transactions_test.exs | 3 + .../graphql/schema/query/transaction_test.exs | 2 +- .../lib/ethereum_jsonrpc/geth/call.ex | 22 +-- .../lib/ethereum_jsonrpc/nethermind/trace.ex | 14 +- .../test/ethereum_jsonrpc/filecoin_test.exs | 6 +- .../test/ethereum_jsonrpc/geth/call_test.exs | 2 +- .../test/ethereum_jsonrpc/geth_test.exs | 14 +- .../test/ethereum_jsonrpc/nethermind_test.exs | 4 +- apps/explorer/config/config.exs | 3 +- apps/explorer/config/runtime/test.exs | 1 + apps/explorer/lib/explorer/application.ex | 1 + apps/explorer/lib/explorer/chain/address.ex | 2 +- .../explorer/chain/address/coin_balance.ex | 6 +- .../chain/cache/background_migrations.ex | 12 +- .../import/runner/internal_transactions.ex | 52 ++++--- .../explorer/chain/internal_transaction.ex | 110 ++++++++++++--- .../chain/transaction/state_change.ex | 8 +- .../lib/explorer/chain/transaction_error.ex | 46 +++++++ apps/explorer/lib/explorer/etherscan.ex | 5 + .../empty_internal_transactions_data.ex | 130 ++++++++++++++++++ ...l_transactions_with_incompatible_status.ex | 4 +- ...transactions_drop_not_null_constraints.exs | 17 +++ ...all_type_enum_to_internal_transactions.exs | 25 ++++ ...250915135943_create_transaction_errors.exs | 74 ++++++++++ ...call_has_error_id_or_result_constraint.exs | 19 +++ .../test/explorer/chain/import_test.exs | 2 +- apps/explorer/test/explorer/chain_test.exs | 4 +- .../explorer/test/explorer/etherscan_test.exs | 4 +- .../empty_internal_transactions_data_test.exs | 70 ++++++++++ ...sactions_with_incompatible_status_test.exs | 3 + apps/explorer/test/support/factory.ex | 13 +- .../fetcher/on_demand/internal_transaction.ex | 4 + .../fetcher/internal_transaction_test.exs | 2 +- config/runtime.exs | 5 + cspell.json | 1 + docker-compose/envs/common-blockscout.env | 3 + 41 files changed, 632 insertions(+), 95 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/transaction_error.ex create mode 100644 apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex create mode 100644 apps/explorer/priv/repo/migrations/20250812000000_internal_transactions_drop_not_null_constraints.exs create mode 100644 apps/explorer/priv/repo/migrations/20250908062439_add_call_type_enum_to_internal_transactions.exs create mode 100644 apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs create mode 100644 apps/explorer/priv/shrunk_internal_transactions/migrations/20250919150023_drop_call_has_error_id_or_result_constraint.exs create mode 100644 apps/explorer/test/explorer/migrator/empty_internal_transactions_data_test.exs diff --git a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex index 7e3a11882f19..1eee7501e703 100644 --- a/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex +++ b/apps/block_scout_web/lib/block_scout_web/csv_export/address/internal_transactions.ex @@ -5,7 +5,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do import BlockScoutWeb.Chain, only: [address_to_internal_transactions: 2] - alias Explorer.Chain.{Address, Hash, Transaction, Wei} + alias Explorer.Chain.{Address, Hash, InternalTransaction, Transaction, Wei} alias Explorer.Chain.CsvExport.Helper @spec export(Hash.Address.t(), String.t(), String.t(), String.t() | nil, String.t() | nil) :: Enumerable.t() @@ -83,7 +83,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactions do Address.checksum(internal_transaction.to_address_hash), Address.checksum(internal_transaction.created_contract_address_hash), internal_transaction.type, - internal_transaction.call_type, + InternalTransaction.call_type(internal_transaction), internal_transaction.gas || "0", internal_transaction.gas_used, Wei.to(internal_transaction.value, :wei), diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 9436f74fb028..cd1edf1182b0 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -118,7 +118,14 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do ) end - defp internal_transaction_to_coin_balances(%InternalTransaction{call_type: :delegatecall}, _, _, acc), do: acc + defp internal_transaction_to_coin_balances( + %InternalTransaction{call_type: call_type, call_type_enum: call_type_enum}, + _, + _, + acc + ) + when :delegatecall in [call_type, call_type_enum], + do: acc defp internal_transaction_to_coin_balances(internal_transaction, previous_block_number, options, acc) do if internal_transaction.value |> Wei.to(:wei) |> Decimal.positive?() do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index e674ef074c31..ca59542b2233 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -153,7 +153,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do "index" => to_string(internal_transaction.index), "input" => "#{internal_transaction.input}", "type" => "#{internal_transaction.type}", - "callType" => "#{internal_transaction.call_type}", + "callType" => "#{InternalTransaction.call_type(internal_transaction)}", "gas" => to_string(internal_transaction.gas || 0), "gasUsed" => "#{internal_transaction.gas_used}", "isError" => if(internal_transaction.error, do: "1", else: "0"), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex index 3e6b5fe458c6..b7db6c019e47 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/internal_transaction_view.ex @@ -33,7 +33,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionView do %{ "error" => internal_transaction.error, "success" => is_nil(internal_transaction.error), - "type" => internal_transaction.call_type || internal_transaction.type, + "type" => InternalTransaction.call_type(internal_transaction) || internal_transaction.type, "transaction_hash" => internal_transaction.transaction_hash, "transaction_index" => internal_transaction.transaction_index, "from" => @@ -49,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionView do ), "value" => internal_transaction.value, "block_number" => internal_transaction.block_number, - "timestamp" => (block && block.timestamp) || internal_transaction.block.timestamp, + "timestamp" => (block && block.timestamp) || (internal_transaction.block && internal_transaction.block.timestamp), "index" => internal_transaction.index, "gas_limit" => internal_transaction.gas || Decimal.new(0), "block_index" => internal_transaction.block_index diff --git a/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex index 3d4cafd9f3f0..024f6f2a60cb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/internal_transaction_view.ex @@ -18,11 +18,16 @@ defmodule BlockScoutWeb.InternalTransactionView do iex> BlockScoutWeb.InternalTransactionView.type(%Explorer.Chain.InternalTransaction{type: :call, call_type: :delegatecall}) "Delegate Call" """ - def type(%InternalTransaction{type: :call, call_type: :call}), do: gettext("Call") - def type(%InternalTransaction{type: :call, call_type: :callcode}), do: gettext("Call Code") - def type(%InternalTransaction{type: :call, call_type: :delegatecall}), do: gettext("Delegate Call") - def type(%InternalTransaction{type: :call, call_type: :staticcall}), do: gettext("Static Call") - def type(%InternalTransaction{type: :call, call_type: :invalid}), do: gettext("Invalid") + def type(%InternalTransaction{type: :call} = it) do + case InternalTransaction.call_type(it) do + :call -> gettext("Call") + :callcode -> gettext("Call Code") + :delegatecall -> gettext("Delegate Call") + :staticcall -> gettext("Static Call") + :invalid -> gettext("Invalid") + end + end + def type(%InternalTransaction{type: :create}), do: gettext("Create") def type(%InternalTransaction{type: :create2}), do: gettext("Create2") def type(%InternalTransaction{type: :selfdestruct}), do: gettext("Self-Destruct") diff --git a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs index f707126ca447..ef3d9224037a 100644 --- a/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/csv_export/address/internal_transactions_test.exs @@ -198,6 +198,8 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do |> insert() |> with_block() + transaction_error = insert(:transaction_error, message: "reverted") + internal_transaction = insert(:internal_transaction, index: 1, @@ -207,6 +209,7 @@ defmodule BlockScoutWeb.CsvExport.Address.InternalTransactionsTest do block_hash: transaction.block_hash, transaction_index: transaction.index, error: "reverted", + error_id: transaction_error.id, gas_used: nil, output: nil ) diff --git a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs index 90b1d91a15d2..07071ca90048 100644 --- a/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs +++ b/apps/block_scout_web/test/block_scout_web/graphql/schema/query/transaction_test.exs @@ -199,7 +199,7 @@ defmodule BlockScoutWeb.GraphQL.Schema.Query.TransactionTest do "init" => to_string(internal_transaction.init), "input" => nil, "output" => nil, - "trace_address" => Jason.encode!(internal_transaction.trace_address), + "trace_address" => internal_transaction.trace_address, "type" => internal_transaction.type |> to_string() |> String.upcase(), "value" => to_string(internal_transaction.value.value), "block_number" => internal_transaction.block_number, diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex index 2835c61431ea..bf7acc5750fc 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/geth/call.ex @@ -78,7 +78,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas_used: 1225, input: "0xa83627de", output: nil, - value: 0 + value: nil } A call can reach the stack limit (1024): @@ -116,7 +116,7 @@ defmodule EthereumJSONRPC.Geth.Call do error: "stack limit reached 1024 (1024)", gas: 1445580, gas_used: 1445580, - value: 0, + value: nil, } A contract creation: @@ -151,7 +151,7 @@ defmodule EthereumJSONRPC.Geth.Call do init: "0x", created_contract_code: "0x", trace_address: [], - value: 0 + value: nil } A contract creation can fail: @@ -221,7 +221,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas_used: 6111, input: "0xeb9d50e46930b3227102b442f93b4aed3dead4ed76f850a76ee7f8b2cbe763428f2790530000000000000000000000000000000000000000000000000926708dfd7272e3", output: "0x", - value: 0 + value: nil } A static call calls another contract, but no state can change. This includes no value transfer, so the value for the @@ -259,7 +259,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas_used: 1040, input: "0x0f370699", output: "0x", - value: 0 + value: nil } A selfdestruct destroys the calling contract and sends any left over balance to the to address. @@ -290,7 +290,7 @@ defmodule EthereumJSONRPC.Geth.Call do to_address_hash: "0xff77830c100623316736b45c4983df970423aaf4", gas: 742088, gas_used: 718517, - value: 0 + value: nil } """ @@ -356,7 +356,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas_used: gas_used, input: input, output: params["output"], - value: value + value: if(value == 0, do: nil, else: value) } |> put_if_present(params, [ {"error", :error} @@ -390,7 +390,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas: gas, gas_used: gas_used, init: init, - value: value, + value: if(value == 0, do: nil, else: value), error: error } end @@ -422,7 +422,7 @@ defmodule EthereumJSONRPC.Geth.Call do gas: gas, gas_used: gas_used, init: init, - value: value + value: if(value == 0, do: nil, else: value) } |> put_if_present(params, [ {"error", :error}, @@ -455,7 +455,7 @@ defmodule EthereumJSONRPC.Geth.Call do to_address_hash: to_address_hash, gas: gas, gas_used: gas_used, - value: value + value: if(value == 0, do: nil, else: value) } end @@ -483,7 +483,7 @@ defmodule EthereumJSONRPC.Geth.Call do input: input, gas: gas, gas_used: gas_used, - value: value, + value: if(value == 0, do: nil, else: value), error: "execution stopped" } end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex index 17b781224ccd..51ce0ea9af00 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/nethermind/trace.ex @@ -44,7 +44,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do trace_address: [], transaction_hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", type: "create", - value: 0, + value: nil, transaction_index: 0 } @@ -78,7 +78,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do trace_address: [], transaction_hash: "0x3c624bb4852fb5e35a8f45644cec7a486211f6ba89034768a2b763194f22f97d", type: "create", - value: 0, + value: nil, transaction_index: 0 } @@ -121,7 +121,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do gas_used: 27770, input: "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef", output: "0x", - value: 0 + value: nil } Calls can error and be reverted @@ -196,7 +196,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do trace_address: [0], transaction_hash: "0xb012b8c53498c669d87d85ed90f57385848b86d3f44ed14b2784ec685d6fda98", type: "selfdestruct", - value: 0, + value: nil, transaction_index: 0 } @@ -231,7 +231,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do to_address_hash: to_address_hash, gas: gas, input: input, - value: value + value: if(value == 0, do: nil, else: value) } |> put_if_present(elixir, [ {"error", :error} @@ -261,7 +261,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do trace_address: trace_address, transaction_hash: transaction_hash, type: type, - value: value, + value: if(value == 0, do: nil, else: value), transaction_index: transaction_index } |> put_if_present(elixir, [ @@ -296,7 +296,7 @@ defmodule EthereumJSONRPC.Nethermind.Trace do trace_address: trace_address, transaction_hash: transaction_hash, type: "selfdestruct", - value: value, + value: if(value == 0, do: nil, else: value), transaction_index: transaction_index } end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs index 122c428a8f97..f51fe44ed82f 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/filecoin_test.exs @@ -105,7 +105,7 @@ defmodule EthereumJSONRPC.FilecoinTest do "0x868e10c40000000000000000000000000000000000000000000000000000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", output: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000578449007d2903b8000000004a000190f76c1adff180004c00907e2dd41a18e7c7a7f2bd82581a0001916cb98a2c3dfb67a389a588fb0e593f762dd6c9195851235601fba7e16707ee65746d4671e80aa2bb15bc7d6ebe3b000000000000000000", - value: 0 + value: nil }, %{ block_number: ^block_number, @@ -123,7 +123,7 @@ defmodule EthereumJSONRPC.FilecoinTest do "0x868e10c400000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000051000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000f2850d8182004081820d58c0960ee115a7a4b6f2fd36a83da26c608d49e4160a3737655d0f637b81be81b018539809d35519b0b75ca06304b3b4d40c810e50b954e82c5119a8b4a64c3e762a7ae8a2d465d1cd5bf096c87c56ab0da879568378e5a2368c902eea9898cf1e2a1974ddb479ec6257b69aca7734d3b3e1e70428c77f9e528ffcb3dc3f050f0193c2cc005927a765c39a4931d67fb29aaba6e99f2c7d2566b98fdbf30d6e15a2bbd63b8fa059cfad231ccba1d8964542b50419eaad4bc442d3a1dc1f41941944c11a0037e5f45820d41114bb6abbf966c2528f5705447a53ee37b7055cd4478503ea5eaf1fe165c60000000000000000000000000000", output: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000", - value: 0 + value: nil } ]} = Filecoin.fetch_block_internal_transactions( @@ -186,7 +186,7 @@ defmodule EthereumJSONRPC.FilecoinTest do gas_used: 29_242_108, init: "0xfe", created_contract_code: "0xfe", - value: 0 + value: nil } ]} = Filecoin.fetch_block_internal_transactions( diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs index 686ba4f13a1d..0a99fa5ab252 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth/call_test.exs @@ -41,7 +41,7 @@ defmodule EthereumJSONRPC.Geth.CallTest do transaction_hash: "0xbc38745b826f058ed2f6c93fa5b145323857f06bbb5230b6a6a50e09e0915857", transaction_index: 0, type: "call", - value: 0, + value: nil, error: "execution reverted" } end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs index 6617bbc2423a..31a79c0e2d85 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/geth_test.exs @@ -484,7 +484,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: "0xb342cafc6ac552c3be2090561453204c8784caf025ac8267320834e4cd163d96", transaction_index: 13, type: "stop", - value: 0 + value: nil } ]} = Geth.fetch_internal_transactions([transaction_params], json_rpc_named_arguments) end @@ -622,7 +622,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash, transaction_index: 0, type: "call", - value: 0 + value: nil }, %{ block_number: 3_287_375, @@ -639,7 +639,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash, transaction_index: 0, type: "call", - value: 0 + value: nil } ]} = Geth.fetch_block_internal_transactions([block_number], json_rpc_named_arguments) end @@ -742,7 +742,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash_1, transaction_index: 0, type: "call", - value: 0 + value: nil }, %{ block_number: ^block_number_1, @@ -759,7 +759,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash_1, transaction_index: 0, type: "call", - value: 0 + value: nil }, %{ block_number: ^block_number_2, @@ -776,7 +776,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash_2, transaction_index: 0, type: "call", - value: 0 + value: nil }, %{ block_number: ^block_number_2, @@ -793,7 +793,7 @@ defmodule EthereumJSONRPC.GethTest do transaction_hash: ^transaction_hash_2, transaction_index: 0, type: "call", - value: 0 + value: nil } ]} = Geth.fetch_block_internal_transactions([block_number_1, block_number_2], json_rpc_named_arguments) end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs index 7bf8a5720f88..b1bac210fa0f 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/nethermind_test.exs @@ -96,7 +96,7 @@ defmodule EthereumJSONRPC.NethermindTest do trace_address: trace_address, transaction_hash: transaction_hash, type: type, - value: value, + value: nil, transaction_index: transaction_index } ] @@ -190,7 +190,7 @@ defmodule EthereumJSONRPC.NethermindTest do trace_address: trace_address, transaction_hash: transaction_hash, type: type, - value: value, + value: nil, transaction_index: transaction_index }, block_hash: block_hash, diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 5ab9c8c4208f..c06545a1fb49 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -142,7 +142,8 @@ for migrator <- [ Explorer.Migrator.MergeAdjacentMissingBlockRanges, Explorer.Migrator.UnescapeQuotesInTokens, Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, - Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash + Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + Explorer.Migrator.EmptyInternalTransactionsData ] do config :explorer, migrator, enabled: true end diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index e890e8e2d3ad..65a6ea08b8f6 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -77,6 +77,7 @@ for migrator <- [ Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, Explorer.Migrator.DeleteZeroValueInternalTransactions, Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + Explorer.Migrator.EmptyInternalTransactionsData, # Heavy DB index operations Explorer.Migrator.HeavyDbIndexOperation.CreateLogsBlockHashIndex, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index bce52f13daa7..d9437d04bd75 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -206,6 +206,7 @@ defmodule Explorer.Application do Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, :indexer ), + configure_mode_dependent_process(Explorer.Migrator.EmptyInternalTransactionsData, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, :indexer diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 9aa87f35a538..056423645502 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -852,7 +852,7 @@ defmodule Explorer.Chain.Address do it in InternalTransaction, where: it.index > 0, order_by: [ - asc_nulls_first: it.error, + asc_nulls_first: coalesce(it.error, type(it.error_id, :string)), desc: it.block_number, desc: it.transaction_index, desc: it.index diff --git a/apps/explorer/lib/explorer/chain/address/coin_balance.ex b/apps/explorer/lib/explorer/chain/address/coin_balance.ex index 79075d16e2db..2b066aff2383 100644 --- a/apps/explorer/lib/explorer/chain/address/coin_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/coin_balance.ex @@ -308,14 +308,16 @@ defmodule Explorer.Chain.Address.CoinBalance do |> limit(1) end + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity defp preload_internal_transaction_query(balance) do InternalTransaction |> where( [internal_transaction], internal_transaction.block_number == ^balance.block_number and internal_transaction.type in ~w(call create create2 selfdestruct)a and - (is_nil(internal_transaction.call_type) or internal_transaction.call_type == :call) and - internal_transaction.value > ^0 and is_nil(internal_transaction.error) and + (is_nil(coalesce(type(internal_transaction.call_type_enum, :string), internal_transaction.call_type)) or + coalesce(type(internal_transaction.call_type_enum, :string), internal_transaction.call_type) == ^"call") and + internal_transaction.value > ^0 and is_nil(internal_transaction.error) and is_nil(internal_transaction.error_id) and (internal_transaction.to_address_hash == ^balance.address_hash or internal_transaction.from_address_hash == ^balance.address_hash or internal_transaction.created_contract_address_hash == ^balance.address_hash) diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 7c43f7573c1c..2f3880abe2cb 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -30,6 +30,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :arbitrum_da_records_normalization_finished, key: :sanitize_verified_addresses_finished, key: :smart_contract_language_finished, + key: :backfill_call_type_enum_finished, key: :heavy_indexes_create_logs_block_hash_index_finished, key: :heavy_indexes_drop_logs_block_number_asc_index_asc_index_finished, key: :heavy_indexes_create_logs_address_hash_block_number_desc_index_desc_index_finished, @@ -60,7 +61,8 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do key: :fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished, key: :heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished, key: :heavy_indexes_create_tokens_name_partial_fts_index_finished, - key: :heavy_indexes_update_internal_transactions_primary_key_finished + key: :heavy_indexes_update_internal_transactions_primary_key_finished, + key: :empty_internal_transactions_data_finished @dialyzer :no_match @@ -69,6 +71,7 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do AddressTokenBalanceTokenType, ArbitrumDaRecordsNormalization, BackfillMultichainSearchDB, + EmptyInternalTransactionsData, FillInternalTransactionToAddressHashWithCreatedContractAddressHash, SanitizeDuplicatedLogIndexLogs, SmartContractLanguage, @@ -378,6 +381,13 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end + defp handle_fallback(:empty_internal_transactions_data_finished) do + set_and_return_migration_status( + EmptyInternalTransactionsData, + &set_empty_internal_transactions_data_finished/1 + ) + end + defp handle_fallback(:sanitize_verified_addresses_finished) do {:return, false} end diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index 07a6d2e4753e..dad3a47a20c7 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -18,7 +18,8 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do InternalTransaction, PendingOperationsHelper, PendingTransactionOperation, - Transaction + Transaction, + TransactionError } alias Explorer.Chain.Events.Publisher @@ -251,7 +252,12 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do when is_list(valid_internal_transactions) do on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) - ordered_changes_list = Enum.sort_by(valid_internal_transactions, &{&1.transaction_hash, &1.index}) + ordered_changes_list = + valid_internal_transactions + |> Enum.map(fn internal_transaction -> + Map.put(internal_transaction, :trace_address, nil) + end) + |> Enum.sort_by(&{&1.transaction_hash, &1.index}) conflict_target = if InternalTransactionHelper.primary_key_updated?() do @@ -282,9 +288,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do update: [ set: [ call_type: fragment("EXCLUDED.call_type"), + call_type_enum: fragment("EXCLUDED.call_type_enum"), created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), created_contract_code: fragment("EXCLUDED.created_contract_code"), - error: fragment("EXCLUDED.error"), + error_id: fragment("EXCLUDED.error_id"), from_address_hash: fragment("EXCLUDED.from_address_hash"), gas: fragment("EXCLUDED.gas"), gas_used: fragment("EXCLUDED.gas_used"), @@ -292,7 +299,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do input: fragment("EXCLUDED.input"), output: fragment("EXCLUDED.output"), to_address_hash: fragment("EXCLUDED.to_address_hash"), - trace_address: fragment("EXCLUDED.trace_address"), transaction_hash: fragment("EXCLUDED.transaction_hash"), type: fragment("EXCLUDED.type"), value: fragment("EXCLUDED.value"), @@ -306,12 +312,13 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do # `IS DISTINCT FROM` is used because it allows `NULL` to be equal to itself where: fragment( - "(EXCLUDED.transaction_hash, EXCLUDED.call_type, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.trace_address, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.transaction_hash, EXCLUDED.call_type, EXCLUDED.call_type_enum, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error_id, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", internal_transaction.transaction_hash, internal_transaction.call_type, + internal_transaction.call_type_enum, internal_transaction.created_contract_address_hash, internal_transaction.created_contract_code, - internal_transaction.error, + internal_transaction.error_id, internal_transaction.from_address_hash, internal_transaction.gas, internal_transaction.gas_used, @@ -319,7 +326,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do internal_transaction.input, internal_transaction.output, internal_transaction.to_address_hash, - internal_transaction.trace_address, internal_transaction.type, internal_transaction.value ) @@ -331,9 +337,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do set: [ block_number: fragment("EXCLUDED.block_number"), call_type: fragment("EXCLUDED.call_type"), + call_type_enum: fragment("EXCLUDED.call_type_enum"), created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), created_contract_code: fragment("EXCLUDED.created_contract_code"), - error: fragment("EXCLUDED.error"), + error_id: fragment("EXCLUDED.error_id"), from_address_hash: fragment("EXCLUDED.from_address_hash"), gas: fragment("EXCLUDED.gas"), gas_used: fragment("EXCLUDED.gas_used"), @@ -342,7 +349,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do input: fragment("EXCLUDED.input"), output: fragment("EXCLUDED.output"), to_address_hash: fragment("EXCLUDED.to_address_hash"), - trace_address: fragment("EXCLUDED.trace_address"), transaction_hash: fragment("EXCLUDED.transaction_hash"), transaction_index: fragment("EXCLUDED.transaction_index"), type: fragment("EXCLUDED.type"), @@ -356,13 +362,14 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do # `IS DISTINCT FROM` is used because it allows `NULL` to be equal to itself where: fragment( - "(EXCLUDED.transaction_hash, EXCLUDED.index, EXCLUDED.call_type, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.trace_address, EXCLUDED.transaction_index, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "(EXCLUDED.transaction_hash, EXCLUDED.index, EXCLUDED.call_type, EXCLUDED.call_type_enum, EXCLUDED.created_contract_address_hash, EXCLUDED.created_contract_code, EXCLUDED.error_id, EXCLUDED.from_address_hash, EXCLUDED.gas, EXCLUDED.gas_used, EXCLUDED.init, EXCLUDED.input, EXCLUDED.output, EXCLUDED.to_address_hash, EXCLUDED.transaction_index, EXCLUDED.type, EXCLUDED.value) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", internal_transaction.transaction_hash, internal_transaction.index, internal_transaction.call_type, + internal_transaction.call_type_enum, internal_transaction.created_contract_address_hash, internal_transaction.created_contract_code, - internal_transaction.error, + internal_transaction.error_id, internal_transaction.from_address_hash, internal_transaction.gas, internal_transaction.gas_used, @@ -370,7 +377,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do internal_transaction.input, internal_transaction.output, internal_transaction.to_address_hash, - internal_transaction.trace_address, internal_transaction.transaction_index, internal_transaction.type, internal_transaction.value @@ -505,34 +511,43 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do else blocks_map = Map.new(transactions, &{&1.block_number, &1.block_hash}) + error_to_error_id_map = + internal_transactions_params + |> Enum.map(&sanitize_error/1) + |> Enum.map(&Map.get(&1, :error)) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + |> TransactionError.find_or_create_multiple() + valid_internal_transactions = internal_transactions_params |> Enum.group_by(& &1.block_number) |> Map.drop(invalid_block_numbers) |> Enum.flat_map(fn item -> - compose_entry_wrapper(item, blocks_map) + compose_entry_wrapper(item, blocks_map, error_to_error_id_map) end) {:ok, valid_internal_transactions} end end - defp compose_entry_wrapper(item, blocks_map) do + defp compose_entry_wrapper(item, blocks_map, error_to_error_id_map) do case item do {block_number, entries} -> - compose_entry(entries, blocks_map, block_number) + compose_entry(entries, blocks_map, error_to_error_id_map, block_number) _ -> [] end end - defp compose_entry(entries, blocks_map, block_number) do + defp compose_entry(entries, blocks_map, error_to_error_id_map, block_number) do if Map.has_key?(blocks_map, block_number) do if InternalTransactionHelper.primary_key_updated?() do Enum.map(entries, fn entry -> entry |> sanitize_error() + |> put_error_id(error_to_error_id_map) |> shift_created_contract_address_hash() end) else @@ -549,6 +564,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do |> Map.put(:block_hash, block_hash) |> Map.put(:block_index, index) |> sanitize_error() + |> put_error_id(error_to_error_id_map) |> shift_created_contract_address_hash() end) end @@ -557,6 +573,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end + defp put_error_id(entry, error_to_error_id_map) do + Map.put(entry, :error_id, Map.get(entry, :error_id) || error_to_error_id_map[Map.get(entry, :error)]) + end + defp valid_internal_transactions_without_first_trace(valid_internal_transactions) do json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) variant = Keyword.fetch!(json_rpc_named_arguments, :variant) diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index 29e448639c2f..ac32f2ffcb3d 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -12,6 +12,7 @@ defmodule Explorer.Chain.InternalTransaction do Hash, PendingOperationsHelper, Transaction, + TransactionError, Wei } @@ -34,6 +35,7 @@ defmodule Explorer.Chain.InternalTransaction do * `call_type` - the type of call. `nil` when `type` is not `:call`. * `created_contract_code` - the code of the contract that was created when `type` is `:create`. * `error` - error message when `:call` or `:create` `type` errors + * `error_id` - foreign key for `t:Explorer.Chain.TransactionError.t/0` * `from_address` - the source of the `value` * `from_address_hash` - hash of the source of the `value` * `gas` - the amount of gas allowed @@ -58,18 +60,20 @@ defmodule Explorer.Chain.InternalTransaction do typed_schema "internal_transactions" do # todo: consider using enum: `field(:call_type, Ecto.Enum, values: [:call, :callcode, :delegatecall, :staticcall])` field(:call_type, CallType) + field(:call_type_enum, Ecto.Enum, values: [:call, :callcode, :delegatecall, :staticcall, :invalid]) field(:created_contract_code, Data) field(:error, :string) + field(:error_id, :integer) field(:gas, :decimal) field(:gas_used, :decimal) field(:index, :integer, primary_key: true, null: false) field(:init, Data) field(:input, Data) field(:output, Data) - field(:trace_address, {:array, :integer}, null: false) + field(:trace_address, {:array, :integer}) # todo: consider using enum field(:type, Type, null: false) - field(:value, Wei, null: false) + field(:value, Wei) field(:block_number, :integer, primary_key: true) field(:transaction_index, :integer, primary_key: true, null: false) # TODO: remove field after update PK migration is completed @@ -412,23 +416,26 @@ defmodule Explorer.Chain.InternalTransaction do type_changeset(changeset, attrs, type) end - @call_optional_fields ~w(error gas_used output block_number)a - @call_required_fields ~w(call_type from_address_hash gas input to_address_hash trace_address transaction_hash value)a + @call_optional_fields ~w(error error_id gas_used output block_number value)a + @call_required_fields ~w(call_type_enum from_address_hash gas input to_address_hash transaction_hash)a @call_allowed_fields @call_optional_fields ++ @call_required_fields defp type_changeset(changeset, attrs, :call) do changeset - |> cast(attrs, @call_allowed_fields) + |> cast(adjust_call_type(attrs), @call_allowed_fields) |> validate_required(@call_required_fields) # TODO consider removing |> validate_call_error_or_result() - |> check_constraint(:call_type, message: ~S|can't be blank when type is 'call'|, name: :call_has_call_type) + |> check_constraint(:call_type_enum, + message: ~S|can't be blank when type is 'call'|, + name: :call_has_call_type_enum + ) |> check_constraint(:input, message: ~S|can't be blank when type is 'call'|, name: :call_has_input) |> foreign_key_constraint(:transaction_hash) end - @create_optional_fields ~w(error created_contract_code created_contract_address_hash gas_used block_number)a - @create_required_fields ~w(from_address_hash gas init trace_address transaction_hash value)a + @create_optional_fields ~w(error error_id created_contract_code created_contract_address_hash gas_used block_number value)a + @create_required_fields ~w(from_address_hash gas init transaction_hash)a @create_allowed_fields @create_optional_fields ++ @create_required_fields defp type_changeset(changeset, attrs, type) when type in [:create, :create2] do @@ -441,8 +448,8 @@ defmodule Explorer.Chain.InternalTransaction do |> foreign_key_constraint(:transaction_hash) end - @selfdestruct_optional_fields ~w(block_number)a - @selfdestruct_required_fields ~w(from_address_hash to_address_hash trace_address transaction_hash type value)a + @selfdestruct_optional_fields ~w(block_number value)a + @selfdestruct_required_fields ~w(from_address_hash to_address_hash transaction_hash type)a @selfdestruct_allowed_fields @selfdestruct_optional_fields ++ @selfdestruct_required_fields defp type_changeset(changeset, attrs, :selfdestruct) do @@ -451,8 +458,8 @@ defmodule Explorer.Chain.InternalTransaction do |> validate_required(@selfdestruct_required_fields) end - @stop_optional_fields ~w(from_address_hash gas gas_used error)a - @stop_required_fields ~w(block_number transaction_hash type value trace_address)a + @stop_optional_fields ~w(from_address_hash gas gas_used error error_id value)a + @stop_required_fields ~w(block_number transaction_hash type)a @stop_allowed_fields @stop_optional_fields ++ @stop_required_fields defp type_changeset(changeset, attrs, :stop) do @@ -463,6 +470,16 @@ defmodule Explorer.Chain.InternalTransaction do defp type_changeset(changeset, _, nil), do: changeset + defp adjust_call_type(%{call_type_enum: _} = attrs), do: attrs + + defp adjust_call_type(%{call_type: call_type} = attrs) when not is_nil(call_type) do + attrs + |> Map.delete(:call_type) + |> Map.put(:call_type_enum, call_type) + end + + defp adjust_call_type(attrs), do: attrs + defp validate_disallowed(changeset, field, named_arguments) when is_atom(field) do case get_field(changeset, field) do nil -> changeset @@ -478,8 +495,8 @@ defmodule Explorer.Chain.InternalTransaction do # Validates that :call `type` changeset either has an `error` or both `gas_used` and `output` defp validate_call_error_or_result(changeset) do - case get_field(changeset, :error) do - nil -> + case {get_field(changeset, :error), get_field(changeset, :error_id)} do + {nil, nil} -> validate_required(changeset, [:gas_used, :output], message: "can't be blank for successful call") _ -> @@ -495,8 +512,8 @@ defmodule Explorer.Chain.InternalTransaction do # Validates that :create `type` changeset either has an `:error` or both `:created_contract_code` and # `:created_contract_address_hash` defp validate_create_error_or_result(changeset) do - case get_field(changeset, :error) do - nil -> + case {get_field(changeset, :error), get_field(changeset, :error_id)} do + {nil, nil} -> validate_required(changeset, @create_success_fields, message: "can't be blank for successful create") _ -> @@ -634,6 +651,7 @@ defmodule Explorer.Chain.InternalTransaction do |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.index) |> Chain.select_repo(options).all() + |> preload_error(options) end @spec transaction_to_internal_transactions(Hash.Full.t(), [ @@ -656,6 +674,7 @@ defmodule Explorer.Chain.InternalTransaction do |> order_by([internal_transaction], asc: internal_transaction.index) |> preload(:block) |> Chain.select_repo(options).all() + |> preload_error(options) end @spec block_to_internal_transactions(non_neg_integer(), [ @@ -667,8 +686,8 @@ defmodule Explorer.Chain.InternalTransaction do def block_to_internal_transactions(block_number, options \\ []) when is_list(options) do necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) paging_options = Keyword.get(options, :paging_options, @default_paging_options) - type_filter = Keyword.get(options, :type) - call_type_filter = Keyword.get(options, :call_type) + type_filter = Keyword.get(options, :type, []) + call_type_filter = Keyword.get(options, :call_type, []) __MODULE__ |> where([internal_transaction], internal_transaction.block_number == ^block_number) @@ -681,6 +700,7 @@ defmodule Explorer.Chain.InternalTransaction do |> limit(^paging_options.page_size) |> order_by([internal_transaction], asc: internal_transaction.transaction_index, asc: internal_transaction.index) |> Chain.select_repo(options).all() + |> preload_error(options) end @doc """ @@ -810,6 +830,7 @@ defmodule Explorer.Chain.InternalTransaction do |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() |> deduplicate_and_trim_internal_transactions(paging_options) + |> preload_error(options) else __MODULE__ |> where_nonpending_operation() @@ -820,6 +841,7 @@ defmodule Explorer.Chain.InternalTransaction do |> preload(:block) |> Chain.join_associations(necessity_by_association) |> Chain.select_repo(options).all() + |> preload_error(options) end end @@ -970,7 +992,10 @@ defmodule Explorer.Chain.InternalTransaction do defp filter_by_call_type(query, call_types) do query - |> where([internal_transaction], internal_transaction.call_type in ^call_types) + |> where( + [internal_transaction], + internal_transaction.call_type_enum in ^call_types or internal_transaction.call_type in ^call_types + ) end @doc """ @@ -1012,6 +1037,7 @@ defmodule Explorer.Chain.InternalTransaction do |> limit(^paging_options.page_size) |> preload(^preloads) |> Chain.select_repo(options).all() + |> preload_error(options) end end @@ -1168,7 +1194,8 @@ defmodule Explorer.Chain.InternalTransaction do {:ok, nil} end - value = %Wei{value: Decimal.new(first_trace.value)} + value = + if is_nil(first_trace.value), do: %Wei{value: Decimal.new("0")}, else: %Wei{value: Decimal.new(first_trace.value)} first_trace_formatted = first_trace @@ -1190,4 +1217,47 @@ defmodule Explorer.Chain.InternalTransaction do {:ok, [first_trace_formatted]} end + + @spec call_type(map()) :: atom() | nil + def call_type(%{call_type: call_type, call_type_enum: call_type_enum}) do + if BackgroundMigrations.get_empty_internal_transactions_data_finished() do + call_type_enum + else + call_type_enum || call_type + end + end + + @doc """ + Fills internal transaction `error` field based on `error_id` + """ + @spec preload_error(__MODULE__.t() | [__MODULE__.t()], Keyword.t()) :: __MODULE__.t() | [__MODULE__.t()] + def preload_error(internal_transactions, options \\ [api?: true]) + + def preload_error(internal_transactions, options) when is_list(internal_transactions) do + error_ids = + internal_transactions + |> Enum.filter(&is_nil(&1.error)) + |> Enum.map(& &1.error_id) + |> Enum.uniq() + |> Enum.reject(&is_nil/1) + + if error_ids == [] do + internal_transactions + else + error_id_to_error_map = + TransactionError + |> where([te], te.id in ^error_ids) + |> select([te], {te.id, te.message}) + |> Chain.select_repo(options).all() + |> Map.new() + + Enum.map(internal_transactions, &Map.put(&1, :error, &1.error || error_id_to_error_map[&1.error_id])) + end + end + + def preload_error(internal_transaction, options) do + [internal_transaction] + |> preload_error(options) + |> List.first() + end end diff --git a/apps/explorer/lib/explorer/chain/transaction/state_change.ex b/apps/explorer/lib/explorer/chain/transaction/state_change.ex index 04893df0f1d0..24fd1927706a 100644 --- a/apps/explorer/lib/explorer/chain/transaction/state_change.ex +++ b/apps/explorer/lib/explorer/chain/transaction/state_change.ex @@ -58,8 +58,12 @@ defmodule Explorer.Chain.Transaction.StateChange do end end - defp update_coin_balances_from_internal_transaction(%InternalTransaction{call_type: :delegatecall}, coin_balances), - do: coin_balances + defp update_coin_balances_from_internal_transaction( + %InternalTransaction{call_type: call_type, call_type_enum: call_type_enum}, + coin_balances + ) + when :delegatecall in [call_type, call_type_enum], + do: coin_balances defp update_coin_balances_from_internal_transaction(%InternalTransaction{index: 0}, coin_balances), do: coin_balances diff --git a/apps/explorer/lib/explorer/chain/transaction_error.ex b/apps/explorer/lib/explorer/chain/transaction_error.ex new file mode 100644 index 000000000000..ea03d7bf5fa9 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/transaction_error.ex @@ -0,0 +1,46 @@ +defmodule Explorer.Chain.TransactionError do + @moduledoc """ + Stores errors for transactions and internal transactions. + """ + + use Explorer.Schema + + alias Explorer.Repo + + typed_schema "transaction_errors" do + field(:message, :string, null: false) + + timestamps(updated_at: false) + end + + def changeset(%__MODULE__{} = transaction_error, attrs) do + cast(transaction_error, attrs, [:message]) + end + + def find_or_create_multiple(error_messages) do + filtered_error_messages = Enum.uniq(error_messages) + + existing_map = + __MODULE__ + |> where([te], te.message in ^filtered_error_messages) + |> select([te], {te.message, te.id}) + |> Repo.all() + |> Map.new() + + missing_errors = filtered_error_messages -- Map.keys(existing_map) + + now = DateTime.utc_now() + insert_params = Enum.map(missing_errors, &%{message: &1, inserted_at: now}) + + {_total, inserted} = + Repo.insert_all(__MODULE__, insert_params, + on_conflict: {:replace, [:inserted_at]}, + conflict_target: [:message], + returning: true + ) + + new_records_map = Map.new(inserted, &{&1.message, &1.id}) + + Map.merge(existing_map, new_records_map) + end +end diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index df71d6bf0422..a929f8410218 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -93,9 +93,11 @@ defmodule Explorer.Etherscan do input type call_type + call_type_enum gas gas_used error + error_id )a @doc """ @@ -158,6 +160,7 @@ defmodule Explorer.Etherscan do |> InternalTransaction.where_nonpending_operation() |> InternalTransaction.include_zero_value(options.include_zero_value) |> Repo.replica().all() + |> InternalTransaction.preload_error() end def list_internal_transactions( @@ -204,6 +207,7 @@ defmodule Explorer.Etherscan do |> offset(^options_to_offset(options)) |> limit(^options.page_size) |> Repo.replica().all() + |> InternalTransaction.preload_error() end def list_internal_transactions( @@ -221,6 +225,7 @@ defmodule Explorer.Etherscan do |> where_start_block_match_internal_transaction(options) |> where_end_block_match_internal_transaction(options) |> Repo.replica().all() + |> InternalTransaction.preload_error() end defp consensus_internal_transactions_with_transactions_and_blocks_query(options) do diff --git a/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex b/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex new file mode 100644 index 000000000000..72a2f4d4c7d3 --- /dev/null +++ b/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex @@ -0,0 +1,130 @@ +defmodule Explorer.Migrator.EmptyInternalTransactionsData do + @moduledoc """ + Searches for all internal transactions with non-empty `trace_address` and empties it. + Also searches for all internal transactions with zero `value` and empties it. + Also updates `call_type` with `call_type_enum`. + Also empties `error` column and fills `transaction_errors` with its values. + """ + + use Explorer.Migrator.FillingMigration + + import Ecto.Query + import Explorer.QueryHelper, only: [select_ctid: 1, join_on_ctid: 2] + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.{InternalTransaction, TransactionError} + alias Explorer.Migrator.FillingMigration + alias Explorer.Repo + + @migration_name "empty_internal_transactions_data" + + @impl FillingMigration + def migration_name, do: @migration_name + + @impl FillingMigration + def last_unprocessed_identifiers(%{"max_block_number" => -1} = state), do: {[], state} + + def last_unprocessed_identifiers(%{"max_block_number" => from_block_number} = state) do + limit = batch_size() * concurrency() + to_block_number = max(from_block_number - limit + 1, 0) + + {Enum.to_list(from_block_number..to_block_number), %{state | "max_block_number" => to_block_number - 1}} + end + + def last_unprocessed_identifiers(state) do + query = + from( + it in InternalTransaction, + where: + not is_nil(it.trace_address) or it.value == ^0 or (is_nil(it.call_type_enum) and not is_nil(it.call_type)) or + not is_nil(it.error), + select: max(it.block_number) + ) + + max_block_number = Repo.one(query, timeout: :infinity) + + state + |> Map.put("max_block_number", max_block_number || -1) + |> last_unprocessed_identifiers() + end + + @impl FillingMigration + def unprocessed_data_query, do: nil + + @impl FillingMigration + def update_batch(block_numbers) do + Repo.transaction( + fn -> + lock_query = + from( + it in InternalTransaction, + select: select_ctid(it), + where: it.block_number in ^block_numbers, + order_by: [asc: it.transaction_hash, asc: it.index], + lock: "FOR UPDATE" + ) + + extract_error_query = + from(it in InternalTransaction, + where: it.block_number in ^block_numbers, + where: not is_nil(it.error), + select: it.error + ) + + error_messages = Repo.all(extract_error_query, timeout: :infinity) + + TransactionError.find_or_create_multiple(error_messages) + + error_update_query = + from(it in InternalTransaction, + inner_join: te in TransactionError, + on: te.message == it.error, + inner_join: locked_it in subquery(lock_query), + on: join_on_ctid(it, locked_it), + where: not is_nil(it.error), + update: [set: [error: nil, error_id: te.id]] + ) + + Repo.update_all(error_update_query, [], timeout: :infinity) + + call_type_query = + from(it in InternalTransaction, + inner_join: locked_it in subquery(lock_query), + on: join_on_ctid(it, locked_it), + where: is_nil(it.call_type_enum) and not is_nil(it.call_type), + update: [ + set: [call_type_enum: fragment("?::internal_transactions_call_type", it.call_type), call_type: nil] + ] + ) + + Repo.update_all(call_type_query, [], timeout: :infinity) + + trace_address_query = + from(it in InternalTransaction, + inner_join: locked_it in subquery(lock_query), + on: join_on_ctid(it, locked_it), + where: not is_nil(it.trace_address), + update: [set: [trace_address: nil]] + ) + + Repo.update_all(trace_address_query, [], timeout: :infinity) + + value_query = + from(it in InternalTransaction, + inner_join: locked_it in subquery(lock_query), + on: join_on_ctid(it, locked_it), + where: it.value == ^0, + update: [set: [value: nil]] + ) + + Repo.update_all(value_query, [], timeout: :infinity) + end, + timeout: :infinity + ) + end + + @impl FillingMigration + def update_cache do + BackgroundMigrations.set_empty_internal_transactions_data_finished(true) + end +end diff --git a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex index e70304f7ba91..a7a852f30b36 100644 --- a/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex +++ b/apps/explorer/lib/explorer/migrator/reindex_internal_transactions_with_incompatible_status.ex @@ -59,7 +59,9 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatus do it_error_query = from( it in InternalTransaction, - where: parent_as(:transaction).hash == it.transaction_hash and not is_nil(it.error) and it.index > 0, + where: + parent_as(:transaction).hash == it.transaction_hash and it.index > 0 and + (not is_nil(it.error) or not is_nil(it.error_id)), select: 1 ) diff --git a/apps/explorer/priv/repo/migrations/20250812000000_internal_transactions_drop_not_null_constraints.exs b/apps/explorer/priv/repo/migrations/20250812000000_internal_transactions_drop_not_null_constraints.exs new file mode 100644 index 000000000000..ebcfa15fcb48 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250812000000_internal_transactions_drop_not_null_constraints.exs @@ -0,0 +1,17 @@ +defmodule Explorer.Repo.Migrations.InternalTransactionsDropNotNullConstraints do + use Ecto.Migration + + def up do + alter table(:internal_transactions) do + modify(:trace_address, {:array, :integer}, null: true) + modify(:value, :numeric, precision: 100, null: true) + end + end + + def down do + alter table(:internal_transactions) do + modify(:trace_address, {:array, :integer}, null: false) + modify(:value, :numeric, precision: 100, null: false) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20250908062439_add_call_type_enum_to_internal_transactions.exs b/apps/explorer/priv/repo/migrations/20250908062439_add_call_type_enum_to_internal_transactions.exs new file mode 100644 index 000000000000..20b1976e8883 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250908062439_add_call_type_enum_to_internal_transactions.exs @@ -0,0 +1,25 @@ +defmodule Explorer.Repo.Migrations.AddCallTypeEnumToInternalTransactions do + use Ecto.Migration + + def change do + execute( + "CREATE TYPE internal_transactions_call_type AS ENUM ('call', 'callcode', 'delegatecall', 'staticcall', 'invalid')", + "DROP TYPE internal_transactions_call_type" + ) + + alter table(:internal_transactions) do + add(:call_type_enum, :internal_transactions_call_type, null: true) + end + + create( + constraint(:internal_transactions, :call_has_call_type_enum, + check: "type != 'call' OR call_type IS NOT NULL OR call_type_enum IS NOT NULL", + validate: false + ) + ) + + drop(constraint(:internal_transactions, :call_has_call_type, check: "type != 'call' OR call_type IS NOT NULL"), + validate: false + ) + end +end diff --git a/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs b/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs new file mode 100644 index 000000000000..1f64e53303d9 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs @@ -0,0 +1,74 @@ +defmodule Explorer.Repo.Migrations.CreateTransactionErrors do + use Ecto.Migration + + def change do + create table(:transaction_errors, primary_key: false) do + add(:id, :smallserial, primary_key: true) + add(:message, :string, null: false) + + timestamps(updated_at: false) + end + + create(unique_index(:transaction_errors, [:message])) + + alter table(:internal_transactions) do + add(:error_id, references(:transaction_errors, column: :id, type: :smallint)) + end + + drop_if_exists( + constraint( + :internal_transactions, + :create_has_error_or_result, + check: """ + type != 'create' OR + (gas IS NOT NULL AND + ((error IS NULL AND created_contract_address_hash IS NOT NULL AND created_contract_code IS NOT NULL AND gas_used IS NOT NULL) OR + (error IS NOT NULL AND created_contract_address_hash IS NULL AND created_contract_code IS NULL AND gas_used IS NULL))) + """, + validate: false + ) + ) + + create( + constraint( + :internal_transactions, + :create_has_error_id_or_result, + check: """ + type != 'create' OR + (gas IS NOT NULL AND + ((error_id IS NULL AND created_contract_address_hash IS NOT NULL AND created_contract_code IS NOT NULL AND gas_used IS NOT NULL) OR + (error_id IS NOT NULL AND created_contract_address_hash IS NULL AND created_contract_code IS NULL AND gas_used IS NULL))) + """, + validate: false + ) + ) + + drop_if_exists( + constraint( + :internal_transactions, + :call_has_error_or_result, + check: """ + type != 'call' OR + (gas IS NOT NULL AND + ((error IS NULL AND gas_used IS NOT NULL AND output IS NOT NULL) OR + (error IS NOT NULL AND output is NULL))) + """, + validate: false + ) + ) + + create( + constraint( + :internal_transactions, + :call_has_error_id_or_result, + check: """ + type != 'call' OR + (gas IS NOT NULL AND + ((error_id IS NULL AND gas_used IS NOT NULL AND output IS NOT NULL) OR + (error_id IS NOT NULL AND output is NULL))) + """, + validate: false + ) + ) + end +end diff --git a/apps/explorer/priv/shrunk_internal_transactions/migrations/20250919150023_drop_call_has_error_id_or_result_constraint.exs b/apps/explorer/priv/shrunk_internal_transactions/migrations/20250919150023_drop_call_has_error_id_or_result_constraint.exs new file mode 100644 index 000000000000..f715875a7ea4 --- /dev/null +++ b/apps/explorer/priv/shrunk_internal_transactions/migrations/20250919150023_drop_call_has_error_id_or_result_constraint.exs @@ -0,0 +1,19 @@ +defmodule Explorer.Repo.ShrunkInternalTransactions.Migrations.DropCallHasErrorIdOrResultConstraint do + use Ecto.Migration + + def change do + drop_if_exists( + constraint( + :internal_transactions, + :call_has_error_id_or_result, + check: """ + type != 'call' OR + (gas IS NOT NULL AND + ((error_id IS NULL AND gas_used IS NOT NULL AND output IS NOT NULL) OR + (error_id IS NOT NULL AND output is NULL))) + """, + validate: false + ) + ) + end +end diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index 23eb193f9f8b..c27ef3766e38 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -510,7 +510,7 @@ defmodule Explorer.Chain.ImportTest do update_in(@import_data, [:internal_transactions, :params, Access.at(0)], &Map.delete(&1, :call_type)) assert {:error, [changeset]} = Import.all(invalid_import_data) - assert changeset_errors(changeset)[:call_type] == ["can't be blank"] + assert changeset_errors(changeset)[:call_type_enum] == ["can't be blank"] end test "publishes addresses with updated fetched_coin_balance data to subscribers on insert" do diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 3f4223f31490..231f7425606d 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1265,7 +1265,7 @@ defmodule Explorer.ChainTest do ], internal_transactions: [ %InternalTransaction{ - call_type: :call, + call_type_enum: :call, created_contract_code: nil, error: nil, gas: ^gas_int, @@ -1278,7 +1278,7 @@ defmodule Explorer.ChainTest do 231, 234, 137, 179, 40, 255, 234, 134, 26, 179, 239>> }, output: %Data{bytes: ""}, - trace_address: [], + trace_address: nil, type: :call, block_number: 37, transaction_index: 1, diff --git a/apps/explorer/test/explorer/etherscan_test.exs b/apps/explorer/test/explorer/etherscan_test.exs index ca7f9160700b..7cc0a49f0be3 100644 --- a/apps/explorer/test/explorer/etherscan_test.exs +++ b/apps/explorer/test/explorer/etherscan_test.exs @@ -871,9 +871,11 @@ defmodule Explorer.EtherscanTest do transaction_index: internal_transaction.transaction_index, type: internal_transaction.type, call_type: internal_transaction.call_type, + call_type_enum: internal_transaction.call_type_enum, gas: internal_transaction.gas, gas_used: internal_transaction.gas_used, - error: internal_transaction.error + error: internal_transaction.error, + error_id: internal_transaction.error_id } assert found_internal_transaction == expected diff --git a/apps/explorer/test/explorer/migrator/empty_internal_transactions_data_test.exs b/apps/explorer/test/explorer/migrator/empty_internal_transactions_data_test.exs new file mode 100644 index 000000000000..4a27f7d5f6ab --- /dev/null +++ b/apps/explorer/test/explorer/migrator/empty_internal_transactions_data_test.exs @@ -0,0 +1,70 @@ +defmodule Explorer.Migrator.EmptyInternalTransactionsDataTest do + use Explorer.DataCase, async: false + + import Ecto.Query + + alias Explorer.Chain.Cache.BackgroundMigrations + alias Explorer.Chain.{InternalTransaction, Wei} + alias Explorer.Migrator.{EmptyInternalTransactionsData, MigrationStatus} + alias Explorer.Repo + + test "empties data" do + _trace_address_internal_transactions = insert_batch_of_internal_transactions(trace_address: [0]) + _value_internal_transactions = insert_batch_of_internal_transactions(value: 0) + _call_type_internal_transactions = insert_batch_of_internal_transactions(call_type: :call) + %{id: transaction_error_id} = insert(:transaction_error, message: "error") + %{id: fake_transaction_error_id} = insert(:transaction_error) + + _error_internal_transactions = + insert_batch_of_internal_transactions( + error: "error", + error_id: fake_transaction_error_id, + gas_used: nil, + output: nil + ) + + assert MigrationStatus.get_status("empty_internal_transactions_data") == nil + + EmptyInternalTransactionsData.start_link([]) + + wait_for_results(fn -> + Repo.one!( + from(ms in MigrationStatus, + where: ms.migration_name == ^"empty_internal_transactions_data" and ms.status == "completed" + ) + ) + end) + + all_internal_transactions = Repo.all(InternalTransaction) + + assert Enum.all?(all_internal_transactions, &is_nil(&1.trace_address)) + assert Enum.all?(all_internal_transactions, &(is_nil(&1.value) or Decimal.gt?(Wei.to(&1.value, :wei), 0))) + assert Enum.all?(all_internal_transactions, &is_nil(&1.call_type)) + refute Enum.all?(all_internal_transactions, &is_nil(&1.call_type_enum)) + assert Enum.all?(all_internal_transactions, &is_nil(&1.error)) + refute Enum.all?(all_internal_transactions, &is_nil(&1.error_id)) + assert Enum.all?(all_internal_transactions, &(is_nil(&1.error_id) or &1.error_id == transaction_error_id)) + + assert BackgroundMigrations.get_empty_internal_transactions_data_finished() == true + end + + defp insert_batch_of_internal_transactions(additional_fields) do + Enum.map(1..10, fn index -> + transaction = + :transaction + |> insert() + |> with_block() + + base_fields = [ + transaction: transaction, + index: index, + block_number: transaction.block_number, + transaction_index: transaction.index, + block_hash: transaction.block_hash, + block_index: index + ] + + insert(:internal_transaction, Keyword.merge(base_fields, additional_fields)) + end) + end +end diff --git a/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs b/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs index df99bb065a32..94dcce6a6574 100644 --- a/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs +++ b/apps/explorer/test/explorer/migrator/reindex_internal_transactions_with_incompatible_status_test.exs @@ -31,6 +31,8 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatusTes block.number end) + transaction_error = insert(:transaction_error, message: "error") + Enum.each(1..5, fn i -> block = insert(:block) transaction = :transaction |> insert() |> with_block(block, status: :error) @@ -42,6 +44,7 @@ defmodule Explorer.Migrator.ReindexInternalTransactionsWithIncompatibleStatusTes block: block, block_number: block.number, error: "error", + error_id: transaction_error.id, output: nil ) end) diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 33739a626a95..0cc17f9cabd3 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -51,6 +51,7 @@ defmodule Explorer.Factory do TokenTransfer, Token.Instance, Transaction, + TransactionError, Wei, Withdrawal } @@ -835,7 +836,7 @@ defmodule Explorer.Factory do input: %Data{bytes: <<1>>}, output: %Data{bytes: <<2>>}, # caller MUST supply `index` - trace_address: [], + trace_address: nil, # caller MUST supply `transaction` because it can't be built lazily to allow overrides without creating an extra # transaction # caller MUST supply `block_hash` (usually the same as the transaction's) @@ -858,7 +859,7 @@ defmodule Explorer.Factory do gas_used: gas_used, # caller MUST supply `index` init: data(:internal_transaction_init), - trace_address: [], + trace_address: nil, # caller MUST supply `transaction` because it can't be built lazily to allow overrides without creating an extra # transaction # caller MUST supply `block_hash` (usually the same as the transaction's) @@ -870,7 +871,7 @@ defmodule Explorer.Factory do def internal_transaction_selfdestruct_factory() do %InternalTransaction{ from_address: build(:address), - trace_address: [], + trace_address: nil, # caller MUST supply `transaction` because it can't be built lazily to allow overrides without creating an extra # transaction type: :selfdestruct, @@ -878,6 +879,12 @@ defmodule Explorer.Factory do } end + def transaction_error_factory do + %TransactionError{ + message: "error_#{sequence("transaction_error_message", & &1)}" + } + end + def log_factory do block = build(:block) diff --git a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex index c847b70b1727..4e3a15d5ce5f 100644 --- a/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/on_demand/internal_transaction.ex @@ -126,6 +126,7 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do |> page_internal_transaction(paging_options) |> Enum.take(paging_options.page_size) |> Repo.preload(:block) + |> InternalTransaction.preload_error() :ignore -> [transaction.block_number] @@ -139,6 +140,7 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do |> page_internal_transaction(paging_options) |> Enum.take(paging_options.page_size) |> Repo.preload(:block) + |> InternalTransaction.preload_error() error -> Logger.error( @@ -191,6 +193,7 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do |> then(&if unlimited?, do: &1, else: Enum.take(&1, paging_options.page_size)) |> add_block_hashes(block.hash) |> join_associations(necessity_by_association) + |> InternalTransaction.preload_error() end @doc """ @@ -270,6 +273,7 @@ defmodule Indexer.Fetcher.OnDemand.InternalTransaction do |> add_block_hashes() |> join_associations(necessity_by_association) |> Repo.preload(:block) + |> InternalTransaction.preload_error() end defp do_fetch_for_address(address_id, to_block, from_block, limit, sum_mode, sort_direction, acc \\ []) diff --git a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs index 1f372c52f785..9ee0eab9bc6c 100644 --- a/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs +++ b/apps/indexer/test/indexer/fetcher/internal_transaction_test.exs @@ -628,7 +628,7 @@ defmodule Indexer.Fetcher.InternalTransactionTest do last_internal_transaction = List.last(internal_transactions) assert last_internal_transaction.type == :call - assert last_internal_transaction.call_type == :invalid + assert last_internal_transaction.call_type_enum == :invalid end end diff --git a/config/runtime.exs b/config/runtime.exs index 970436ef2cbd..3c140dd57072 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -842,6 +842,11 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} +config :explorer, Explorer.Migrator.EmptyInternalTransactionsData, + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY", 1), + timeout: ConfigHelper.parse_time_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT", "0s") + config :explorer, Explorer.Migrator.CeloAggregatedElectionRewards, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} diff --git a/cspell.json b/cspell.json index e517211d977d..0f2439a43889 100644 --- a/cspell.json +++ b/cspell.json @@ -600,6 +600,7 @@ "siwe", "SJONRPC", "smallint", + "smallserial", "smth", "snapshotted", "snapshotting", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index f639dc5319ec..b02037316c74 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -577,6 +577,9 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL= +# MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE= +# MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY= +# MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT= # HEALTH_MONITOR_CHECK_INTERVAL=1m # HEALTH_MONITOR_BLOCKS_PERIOD=5m # HEALTH_MONITOR_BATCHES_PERIOD=4h From 4731755d9735dc72add7c4fe5e048075fbbc8f9a Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 23 Jan 2026 15:48:37 +0300 Subject: [PATCH 0622/1006] v10.0.0 --- .github/workflows/generate-swagger.yml | 2 +- .github/workflows/pre-release-arbitrum.yml | 2 +- .github/workflows/pre-release-celo.yml | 2 +- .github/workflows/pre-release-eth.yml | 2 +- .github/workflows/pre-release-filecoin.yml | 2 +- .github/workflows/pre-release-fuse.yml | 2 +- .github/workflows/pre-release-gnosis.yml | 2 +- .github/workflows/pre-release-optimism.yml | 2 +- .../workflows/pre-release-polygon-zkevm.yml | 2 +- .github/workflows/pre-release-rootstock.yml | 2 +- .github/workflows/pre-release-scroll.yml | 2 +- .github/workflows/pre-release-zilliqa.yml | 2 +- .github/workflows/pre-release-zksync.yml | 2 +- .github/workflows/pre-release.yml | 2 +- .../publish-docker-image-custom-build.yml | 2 +- .../publish-docker-image-every-push.yml | 2 +- .../publish-docker-image-for-arbitrum.yml | 2 +- .../publish-docker-image-for-celo.yml | 2 +- .../publish-docker-image-for-core.yml | 2 +- .../publish-docker-image-for-eth-sepolia.yml | 2 +- .../publish-docker-image-for-eth.yml | 2 +- .../publish-docker-image-for-filecoin.yml | 2 +- .../publish-docker-image-for-fuse.yml | 2 +- .../publish-docker-image-for-gnosis-chain.yml | 2 +- .../publish-docker-image-for-l2-staging.yml | 2 +- .../publish-docker-image-for-lukso.yml | 2 +- ...ocker-image-for-optimism-exeperimental.yml | 2 +- ...h-docker-image-for-optimism-worldchain.yml | 2 +- .../publish-docker-image-for-optimism.yml | 2 +- .../publish-docker-image-for-rootstock.yml | 2 +- .../publish-docker-image-for-scroll.yml | 2 +- .../publish-docker-image-for-zetachain.yml | 2 +- .../publish-docker-image-for-zilliqa.yml | 2 +- .../publish-docker-image-for-zkevm.yml | 2 +- .../publish-docker-image-for-zksync.yml | 2 +- .../workflows/publish-docker-image-old-ui.yml | 2 +- ...publish-docker-image-staging-on-demand.yml | 2 +- ...publish-regular-docker-image-on-demand.yml | 2 +- .github/workflows/release-arbitrum.yml | 2 +- .github/workflows/release-celo.yml | 2 +- .github/workflows/release-default.yml | 2 +- .github/workflows/release-eth.yml | 2 +- .github/workflows/release-filecoin.yml | 2 +- .github/workflows/release-fuse.yml | 2 +- .github/workflows/release-gnosis.yml | 2 +- .github/workflows/release-optimism.yml | 2 +- .github/workflows/release-polygon-zkevm.yml | 2 +- .github/workflows/release-rootstock.yml | 2 +- .github/workflows/release-scroll.yml | 2 +- .github/workflows/release-zetachain.yml | 2 +- .github/workflows/release-zilliqa.yml | 2 +- .github/workflows/release-zksync.yml | 2 +- CHANGELOG.md | 96 +++++++++++++++++++ apps/block_scout_web/mix.exs | 2 +- apps/ethereum_jsonrpc/mix.exs | 2 +- .../lib/explorer/token/metadata_retriever.ex | 2 +- apps/explorer/mix.exs | 2 +- apps/indexer/mix.exs | 2 +- apps/nft_media_handler/mix.exs | 2 +- apps/utils/mix.exs | 2 +- docker/Makefile | 2 +- mix.exs | 2 +- rel/config.exs | 2 +- 63 files changed, 158 insertions(+), 62 deletions(-) diff --git a/.github/workflows/generate-swagger.yml b/.github/workflows/generate-swagger.yml index 03b90735da0c..4eddc6174f4a 100644 --- a/.github/workflows/generate-swagger.yml +++ b/.github/workflows/generate-swagger.yml @@ -17,7 +17,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 jobs: matrix-builder: diff --git a/.github/workflows/pre-release-arbitrum.yml b/.github/workflows/pre-release-arbitrum.yml index 713fe658c7c5..06b8bca7d349 100644 --- a/.github/workflows/pre-release-arbitrum.yml +++ b/.github/workflows/pre-release-arbitrum.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-celo.yml b/.github/workflows/pre-release-celo.yml index 53075f3776f3..0808057fcc37 100644 --- a/.github/workflows/pre-release-celo.yml +++ b/.github/workflows/pre-release-celo.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/pre-release-eth.yml b/.github/workflows/pre-release-eth.yml index 0a6a7a4a0eae..402856fe6a4b 100644 --- a/.github/workflows/pre-release-eth.yml +++ b/.github/workflows/pre-release-eth.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-filecoin.yml b/.github/workflows/pre-release-filecoin.yml index 0335fe5d053a..3da59cab58b5 100644 --- a/.github/workflows/pre-release-filecoin.yml +++ b/.github/workflows/pre-release-filecoin.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-fuse.yml b/.github/workflows/pre-release-fuse.yml index 28e9ac3bf9f7..acd08ce687f7 100644 --- a/.github/workflows/pre-release-fuse.yml +++ b/.github/workflows/pre-release-fuse.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-gnosis.yml b/.github/workflows/pre-release-gnosis.yml index f3a710fd1510..394ae60af973 100644 --- a/.github/workflows/pre-release-gnosis.yml +++ b/.github/workflows/pre-release-gnosis.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-optimism.yml b/.github/workflows/pre-release-optimism.yml index d78647276f5e..593df31cb1bd 100644 --- a/.github/workflows/pre-release-optimism.yml +++ b/.github/workflows/pre-release-optimism.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-polygon-zkevm.yml b/.github/workflows/pre-release-polygon-zkevm.yml index 296fced45414..275aa26c8b56 100644 --- a/.github/workflows/pre-release-polygon-zkevm.yml +++ b/.github/workflows/pre-release-polygon-zkevm.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-rootstock.yml b/.github/workflows/pre-release-rootstock.yml index 688cd54b104f..8263c94e9031 100644 --- a/.github/workflows/pre-release-rootstock.yml +++ b/.github/workflows/pre-release-rootstock.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-scroll.yml b/.github/workflows/pre-release-scroll.yml index b964d90c05d0..8b74e5acdcdf 100644 --- a/.github/workflows/pre-release-scroll.yml +++ b/.github/workflows/pre-release-scroll.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zilliqa.yml b/.github/workflows/pre-release-zilliqa.yml index 2fcadd539f2e..d825589f2872 100644 --- a/.github/workflows/pre-release-zilliqa.yml +++ b/.github/workflows/pre-release-zilliqa.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release-zksync.yml b/.github/workflows/pre-release-zksync.yml index 4f138a0fbaa4..d8c360ecf486 100644 --- a/.github/workflows/pre-release-zksync.yml +++ b/.github/workflows/pre-release-zksync.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index ae0178cbb178..d769889b2dbc 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -21,7 +21,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-custom-build.yml b/.github/workflows/publish-docker-image-custom-build.yml index 4521a13e8c38..3128fa423c6d 100644 --- a/.github/workflows/publish-docker-image-custom-build.yml +++ b/.github/workflows/publish-docker-image-custom-build.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-every-push.yml b/.github/workflows/publish-docker-image-every-push.yml index 9176da5873d9..118782ed5c8d 100644 --- a/.github/workflows/publish-docker-image-every-push.yml +++ b/.github/workflows/publish-docker-image-every-push.yml @@ -11,7 +11,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 permissions: contents: read diff --git a/.github/workflows/publish-docker-image-for-arbitrum.yml b/.github/workflows/publish-docker-image-for-arbitrum.yml index 7d0a5fb2af14..48f343187930 100644 --- a/.github/workflows/publish-docker-image-for-arbitrum.yml +++ b/.github/workflows/publish-docker-image-for-arbitrum.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: arbitrum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-celo.yml b/.github/workflows/publish-docker-image-for-celo.yml index da1cb23f62b5..ce7a840a6f62 100644 --- a/.github/workflows/publish-docker-image-for-celo.yml +++ b/.github/workflows/publish-docker-image-for-celo.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: optimism-celo API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: diff --git a/.github/workflows/publish-docker-image-for-core.yml b/.github/workflows/publish-docker-image-for-core.yml index 03430db8b3aa..a64969fda67a 100644 --- a/.github/workflows/publish-docker-image-for-core.yml +++ b/.github/workflows/publish-docker-image-for-core.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: poa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth-sepolia.yml b/.github/workflows/publish-docker-image-for-eth-sepolia.yml index 74ef28730787..8b36443f4b55 100644 --- a/.github/workflows/publish-docker-image-for-eth-sepolia.yml +++ b/.github/workflows/publish-docker-image-for-eth-sepolia.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: eth-sepolia steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-eth.yml b/.github/workflows/publish-docker-image-for-eth.yml index cf88648ff32c..fee50318176e 100644 --- a/.github/workflows/publish-docker-image-for-eth.yml +++ b/.github/workflows/publish-docker-image-for-eth.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: ethereum steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-filecoin.yml b/.github/workflows/publish-docker-image-for-filecoin.yml index ae2008a34535..97bb87f7cdc8 100644 --- a/.github/workflows/publish-docker-image-for-filecoin.yml +++ b/.github/workflows/publish-docker-image-for-filecoin.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: filecoin steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-fuse.yml b/.github/workflows/publish-docker-image-for-fuse.yml index fb70d1044c16..03d3bd4befb7 100644 --- a/.github/workflows/publish-docker-image-for-fuse.yml +++ b/.github/workflows/publish-docker-image-for-fuse.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: fuse steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-gnosis-chain.yml b/.github/workflows/publish-docker-image-for-gnosis-chain.yml index 07deba7959d8..f7731cb01469 100644 --- a/.github/workflows/publish-docker-image-for-gnosis-chain.yml +++ b/.github/workflows/publish-docker-image-for-gnosis-chain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: xdai steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-l2-staging.yml b/.github/workflows/publish-docker-image-for-l2-staging.yml index 3d9efa6d6462..423e05a7daf2 100644 --- a/.github/workflows/publish-docker-image-for-l2-staging.yml +++ b/.github/workflows/publish-docker-image-for-l2-staging.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: optimism-l2-advanced steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-lukso.yml b/.github/workflows/publish-docker-image-for-lukso.yml index 87dc20188bb4..c82f4a1208ce 100644 --- a/.github/workflows/publish-docker-image-for-lukso.yml +++ b/.github/workflows/publish-docker-image-for-lukso.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: lukso steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml index 415d0c6f1ac1..d6bbbfae80a3 100644 --- a/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml +++ b/.github/workflows/publish-docker-image-for-optimism-exeperimental.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml index 5f8037ded372..ddf71b677434 100644 --- a/.github/workflows/publish-docker-image-for-optimism-worldchain.yml +++ b/.github/workflows/publish-docker-image-for-optimism-worldchain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-optimism.yml b/.github/workflows/publish-docker-image-for-optimism.yml index 4c0b3016e50b..0bf9fe99bcc6 100644 --- a/.github/workflows/publish-docker-image-for-optimism.yml +++ b/.github/workflows/publish-docker-image-for-optimism.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: optimism steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-rootstock.yml b/.github/workflows/publish-docker-image-for-rootstock.yml index 9190bcd76c12..c1f3c25507c4 100644 --- a/.github/workflows/publish-docker-image-for-rootstock.yml +++ b/.github/workflows/publish-docker-image-for-rootstock.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: rsk steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-scroll.yml b/.github/workflows/publish-docker-image-for-scroll.yml index cb743d9727fc..916f7226faae 100644 --- a/.github/workflows/publish-docker-image-for-scroll.yml +++ b/.github/workflows/publish-docker-image-for-scroll.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: scroll steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zetachain.yml b/.github/workflows/publish-docker-image-for-zetachain.yml index bd5a29eee3c2..00caa3c64ed9 100644 --- a/.github/workflows/publish-docker-image-for-zetachain.yml +++ b/.github/workflows/publish-docker-image-for-zetachain.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: zetachain steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zilliqa.yml b/.github/workflows/publish-docker-image-for-zilliqa.yml index a0342c8c3e4e..1eea129d9a9f 100644 --- a/.github/workflows/publish-docker-image-for-zilliqa.yml +++ b/.github/workflows/publish-docker-image-for-zilliqa.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: zilliqa steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zkevm.yml b/.github/workflows/publish-docker-image-for-zkevm.yml index d3ed70acb574..614e23cf512b 100644 --- a/.github/workflows/publish-docker-image-for-zkevm.yml +++ b/.github/workflows/publish-docker-image-for-zkevm.yml @@ -14,7 +14,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: zkevm steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-for-zksync.yml b/.github/workflows/publish-docker-image-for-zksync.yml index e65e84d4cddc..a4ebda7f4462 100644 --- a/.github/workflows/publish-docker-image-for-zksync.yml +++ b/.github/workflows/publish-docker-image-for-zksync.yml @@ -13,7 +13,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 DOCKER_CHAIN_NAME: zksync steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/publish-docker-image-old-ui.yml b/.github/workflows/publish-docker-image-old-ui.yml index f01f7cdd2967..d94facd45ddc 100644 --- a/.github/workflows/publish-docker-image-old-ui.yml +++ b/.github/workflows/publish-docker-image-old-ui.yml @@ -16,7 +16,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/publish-docker-image-staging-on-demand.yml b/.github/workflows/publish-docker-image-staging-on-demand.yml index 6b4f6d4c2240..ae6e9436bbca 100644 --- a/.github/workflows/publish-docker-image-staging-on-demand.yml +++ b/.github/workflows/publish-docker-image-staging-on-demand.yml @@ -12,7 +12,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 permissions: contents: read diff --git a/.github/workflows/publish-regular-docker-image-on-demand.yml b/.github/workflows/publish-regular-docker-image-on-demand.yml index a09d89e07e49..f5a069ab19a4 100644 --- a/.github/workflows/publish-regular-docker-image-on-demand.yml +++ b/.github/workflows/publish-regular-docker-image-on-demand.yml @@ -5,7 +5,7 @@ on: env: OTP_VERSION: '27.3.4.6' ELIXIR_VERSION: '1.19.4' - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 permissions: contents: read diff --git a/.github/workflows/release-arbitrum.yml b/.github/workflows/release-arbitrum.yml index bf64e5635610..cee9e75b0bcc 100644 --- a/.github/workflows/release-arbitrum.yml +++ b/.github/workflows/release-arbitrum.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-celo.yml b/.github/workflows/release-celo.yml index c17dd8151ac2..0f36249acce2 100644 --- a/.github/workflows/release-celo.yml +++ b/.github/workflows/release-celo.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 API_GRAPHQL_MAX_COMPLEXITY: 10400 steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/release-default.yml b/.github/workflows/release-default.yml index 5c51640eab4f..961ded0fe35b 100644 --- a/.github/workflows/release-default.yml +++ b/.github/workflows/release-default.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-eth.yml b/.github/workflows/release-eth.yml index a55c70df58e0..220ab923ac53 100644 --- a/.github/workflows/release-eth.yml +++ b/.github/workflows/release-eth.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-filecoin.yml b/.github/workflows/release-filecoin.yml index 3a403aa0ed02..14a5ddb86932 100644 --- a/.github/workflows/release-filecoin.yml +++ b/.github/workflows/release-filecoin.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-fuse.yml b/.github/workflows/release-fuse.yml index 26cf0eaba4c7..db143e8b0a27 100644 --- a/.github/workflows/release-fuse.yml +++ b/.github/workflows/release-fuse.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-gnosis.yml b/.github/workflows/release-gnosis.yml index 886d69032fed..0dbb6c4bebd4 100644 --- a/.github/workflows/release-gnosis.yml +++ b/.github/workflows/release-gnosis.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-optimism.yml b/.github/workflows/release-optimism.yml index 1de8b2536a2b..b12f5b5da413 100644 --- a/.github/workflows/release-optimism.yml +++ b/.github/workflows/release-optimism.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-polygon-zkevm.yml b/.github/workflows/release-polygon-zkevm.yml index b6d5b5774cc0..a1f55427dd9b 100644 --- a/.github/workflows/release-polygon-zkevm.yml +++ b/.github/workflows/release-polygon-zkevm.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-rootstock.yml b/.github/workflows/release-rootstock.yml index 8491b46952df..19d83ada6904 100644 --- a/.github/workflows/release-rootstock.yml +++ b/.github/workflows/release-rootstock.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-scroll.yml b/.github/workflows/release-scroll.yml index 77662dc88a53..5eaff3b1f241 100644 --- a/.github/workflows/release-scroll.yml +++ b/.github/workflows/release-scroll.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zetachain.yml b/.github/workflows/release-zetachain.yml index cce8e7bcb54b..597ec1e0f22d 100644 --- a/.github/workflows/release-zetachain.yml +++ b/.github/workflows/release-zetachain.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zilliqa.yml b/.github/workflows/release-zilliqa.yml index 5de2529bfe61..4bfc0c5d7eea 100644 --- a/.github/workflows/release-zilliqa.yml +++ b/.github/workflows/release-zilliqa.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/.github/workflows/release-zksync.yml b/.github/workflows/release-zksync.yml index fc39e7ac8d8d..b018861f3376 100644 --- a/.github/workflows/release-zksync.yml +++ b/.github/workflows/release-zksync.yml @@ -18,7 +18,7 @@ jobs: name: Push Docker image to GitHub Container Registry runs-on: build env: - RELEASE_VERSION: 9.3.2 + RELEASE_VERSION: 10.0.0 steps: - uses: actions/checkout@v5 - name: Setup repo diff --git a/CHANGELOG.md b/CHANGELOG.md index c9bfffd57937..2a80750843c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,101 @@ # Changelog +## 10.0.0 + +### 🚀 Features + +- Re-architect internal transaction format with call-type enum, error dictionary, and normalization ([#13893](https://github.com/blockscout/blockscout/issues/13893)) +- Add audit-reports import endpoint ([#13884](https://github.com/blockscout/blockscout/issues/13884)) +- Solady smart-contract proxy with immutable arguments support ([#13794](https://github.com/blockscout/blockscout/issues/13794)) +- Optionally accrue burnt fees to the block miner ([#13894](https://github.com/blockscout/blockscout/issues/13894)) +- Allow adding EOA with code to watchlist ([#13885](https://github.com/blockscout/blockscout/issues/13885)) +- Add Dynamic provider for account ([#13786](https://github.com/blockscout/blockscout/issues/13786)) +- Distributed cache ([#13698](https://github.com/blockscout/blockscout/issues/13698)) +- Return timestamps in the event logs list API endpoints ([#13779](https://github.com/blockscout/blockscout/issues/13779)) +- Support EigenDA blobs by Optimism batch indexer ([#13709](https://github.com/blockscout/blockscout/issues/13709)) +- `txlistinternal` API endpoint pending item status ([#13758](https://github.com/blockscout/blockscout/issues/13758)) +- Setup universal proxy config from the JSON content in ENV variable ([#13787](https://github.com/blockscout/blockscout/issues/13787)) +- Missed L1-to-L2 messages catchup on Arbitrum rollups ([#13792](https://github.com/blockscout/blockscout/issues/13792)) +- Expose CHAIN_TYPE in the REST API ([#13805](https://github.com/blockscout/blockscout/issues/13805)) +- REST API endpoint to list uncompleted DB migrations ([#13835](https://github.com/blockscout/blockscout/issues/13835)) +- Show ENS domains without resolved address in search ([#13638](https://github.com/blockscout/blockscout/issues/13638)) +- Mark contract addresses in search results ([#13636](https://github.com/blockscout/blockscout/issues/13636)) + +### 🐛 Bug Fixes + +- Fix token balances broadcasting function ([#13902](https://github.com/blockscout/blockscout/issues/13902)) +- Wrong `next_page_params` in OP Deposits ([#13870](https://github.com/blockscout/blockscout/issues/13870)) +- Fix error on loading thumbnails when public_r2_url is missed ([#13895](https://github.com/blockscout/blockscout/issues/13895)) +- Check token presence in address current token balance ([#13892](https://github.com/blockscout/blockscout/issues/13892)) +- Fix swagger generation for Mud chain +- Fix error in Indexer.Fetcher.OnDemand.TokenBalance module ([#13890](https://github.com/blockscout/blockscout/issues/13890)) +- Actualize indexer tests ([#13887](https://github.com/blockscout/blockscout/issues/13887)) +- Clear bytecode for smart-contracts self destructed in a separate transaction ([#13834](https://github.com/blockscout/blockscout/issues/13834)) +- Skip hot contracts fetching if last 30 days not indexed ([#13873](https://github.com/blockscout/blockscout/issues/13873)) +- Add block range filtering to TokenBalance fetcher ([#13874](https://github.com/blockscout/blockscout/issues/13874)) +- Filter traceable data in InternalTransaction.async_fetch ([#13872](https://github.com/blockscout/blockscout/issues/13872)) +- Take into account empty arrays in Explorer.Migrator.SanitizeIncorectNFTTokenTransfers ([#13852](https://github.com/blockscout/blockscout/issues/13852)) +- Fix search for ERC-1155 with null symbol ([#13632](https://github.com/blockscout/blockscout/issues/13632)) +- Return date to logs ([#13858](https://github.com/blockscout/blockscout/issues/13858)) +- Convert token id to string from refetch metadata in the socket ([#13762](https://github.com/blockscout/blockscout/issues/13762)) +- Prevent DeleteZeroValueInternalTransactions from running while ShrinkInternalTransactions is in progress ([#13847](https://github.com/blockscout/blockscout/issues/13847)) +- Remove contract code and verified data on lose consensus ([#13829](https://github.com/blockscout/blockscout/issues/13829)) +- Exclude 0 index internal transactions from /api/v2/internal-transactions endpoint ([#13841](https://github.com/blockscout/blockscout/issues/13841)) +- Fix NaN gas limit for `selfdestruct` internal transaction in the REST API ([#13827](https://github.com/blockscout/blockscout/issues/13827)) +- Handle normal termination of Indexer.Fetcher.OnDemand.ContractCode process ([#13828](https://github.com/blockscout/blockscout/issues/13828)) +- Validate block number in the api/v2/blocks/:block_number API endpoint ([#13795](https://github.com/blockscout/blockscout/issues/13795)) +- Fix block reindex condition in ContractCreator on-demand ([#13831](https://github.com/blockscout/blockscout/issues/13831)) +- Fix methodId detection ([#13811](https://github.com/blockscout/blockscout/issues/13811)) +- Improve Arbitrum L1->L2 message discovery for reorg and RPC consistency ([#13770](https://github.com/blockscout/blockscout/issues/13770)) + +### 🚜 Refactor + +- Improve error handling in `EthereumJSONRPC.execute_contract_function/3` ([#13764](https://github.com/blockscout/blockscout/issues/13764)) + +### ⚙️ Miscellaneous Tasks + +- Add swagger spec for account abstraction endpoints ([#13897](https://github.com/blockscout/blockscout/issues/13897)) +- Clear token "skip_metadata" property ([#13891](https://github.com/blockscout/blockscout/issues/13891)) +- Refactor internal transaction logic from "block_index" to "transaction_index" and "index" ([#12474](https://github.com/blockscout/blockscout/issues/12474)) +- Cover Optimism API endpoints with swagger docs ([#13672](https://github.com/blockscout/blockscout/issues/13672)) +- Duplicate internal transaction created_contract_address_hash to to_address_hash ([#13846](https://github.com/blockscout/blockscout/issues/13846)) +- Add "openapi_spec_folder_name" to the response of api/v2/config/backend endpoint ([#13845](https://github.com/blockscout/blockscout/issues/13845)) +- Re-use parse_url_env_var/3 function for all *_URL env variables ([#13800](https://github.com/blockscout/blockscout/issues/13800)) +- Add swagger spec for MUD endpoints ([#13793](https://github.com/blockscout/blockscout/issues/13793)) +- Set unique block numbers in handle_partially_imported_blocks/1 ([#13657](https://github.com/blockscout/blockscout/issues/13657)) +- Disband 37% of Explorer.Chain module ([#13755](https://github.com/blockscout/blockscout/issues/13755)) +- Disable MissingRangesManipulator ([#13359](https://github.com/blockscout/blockscout/issues/13359)) +- Improve replica usage ([#13344](https://github.com/blockscout/blockscout/issues/13344)) +- Make "jsonrpc" field in response optional ([#13724](https://github.com/blockscout/blockscout/issues/13724)) + +### New ENV variables + +| Variable | Description | Parameters | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `BLOCK_MINER_GETS_BURNT_FEES` | If `true`, the Burnt fees are added to block miner profit and displayed in UI as zero. Implemented in [#13894](https://github.com/blockscout/blockscout/pull/13894). | Version: v10.0.0\+
Default: `false`
Applications: API | +| `UNIVERSAL_PROXY_CONFIG` | JSON-encoded configuration string used to define settings for the universal proxy. Implemented in [#13787](https://github.com/blockscout/blockscout/pull/13787). | Version: v10.0.0\+
Default: (empty)
Applications: API | +| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_BATCH_SIZE` | Specifies the block batch size selected for the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `100`
Applications: Indexer | +| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_CONCURRENCY` | Specifies how many concurrent processes can handle the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `1`
Applications: Indexer | +| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_TIMEOUT` | Defines the timeout between processing each batch (`batch_size` \* `concurrency`) in the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `0s`
Applications: Indexer | +| `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE` | Number of internal transactions to clear their data in the batch. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `100`
Applications: Indexer | +| `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY` | Number of parallel clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `1`
Applications: Indexer | +| `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT` | Timeout between clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `0`
Applications: Indexer | +| `CACHE_PENDING_OPERATIONS_COUNT_PERIOD` | Time interval to restart the task which calculates the total pending operations count. Introduced in [#12474](https://github.com/blockscout/blockscout/pull/12474). | Version: v10.0.0\+
Default: `5m`
Applications: API, Indexer | +| `ACCOUNT_DYNAMIC_ENV_ID` | Dynamic Environment ID, can be found here https://app.dynamic.xyz/dashboard/developer/api. Implemented in [#13786](https://github.com/blockscout/blockscout/pull/13786). | Version: v10.0.0\+
Default: (empty)
Applications: API | +| `INDEXER_OPTIMISM_L1_BATCH_EIGENDA_BLOBS_API_URL` | Defines a URL to DA indexer supporting EigenDA layer to retrieve L1 blobs from that. Example: `https://da-indexer-dev.k8s-prod-3.blockscout.com/api/v1/eigenda/v2/blobs`. Implemented in [#13709](https://github.com/blockscout/blockscout/pull/13709). | Version: v10.0.0+
Default: (empty)
Applications: Indexer | +| `INDEXER_OPTIMISM_L1_BATCH_EIGENDA_PROXY_BASE_URL` | Defines a URL to EigenDA proxy node which is used by the DA indexer (planned to be optional in the future). Example for MegaETH: `http://megaeth-eigenda-proxy.node.blockscout.com:3100`. Implemented in [#13709](https://github.com/blockscout/blockscout/pull/13709). | Version: v10.0.0+
Default: (empty)
Applications: Indexer | +| `INDEXER_ARBITRUM_MESSAGES_TRACKING_FAILURE_THRESHOLD` | The time threshold for L1 message tracking tasks. If a task has not run successfully within this threshold, it is marked as failed and enters a cooldown period before retrying. Implemented in [#13792](https://github.com/blockscout/blockscout/pull/13792). | Version: v10.0.0+
Default: `10m`
Applications: Indexer | +| `INDEXER_ARBITRUM_MISSED_MESSAGE_IDS_RANGE` | Size of each message ID range inspected when discovering L1-to-L2 messages with missing L1 origination information. Implemented in [#13792](https://github.com/blockscout/blockscout/pull/13792). | Version: v10.0.0+
Default: `10000`
Applications: Indexer | + + +### Deprecated ENV variables + +| Variable | Description | Default | Version | Deprecated in Version | +| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------------- | --------------------- | +| `CACHE_PBO_COUNT_PERIOD` | Time interval to restart the task which calculates the total pending_block_operations count. | `20m` | v5.2.0+ | | v10.0.0 | + + + ## 9.3.2 ### 🐛 Bug Fixes diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 6266e5513330..28bc06241ccf 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -19,7 +19,7 @@ defmodule BlockScoutWeb.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.2", + version: "10.0.0", xref: [ exclude: [ Explorer.Chain.PolygonZkevm.Reader, diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index aa4c63546b44..2eb14cf8ae8a 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -19,7 +19,7 @@ defmodule EthereumJSONRPC.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.2" + version: "10.0.0" ] end diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 7f3b43fb73e7..ad4d3438f444 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,7 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" @invalid_base64_data "invalid data:application/json;base64" - @default_headers [{"User-Agent", "blockscout-9.3.2"}] + @default_headers [{"User-Agent", "blockscout-10.0.0"}] # https://eips.ethereum.org/EIPS/eip-1155#metadata @erc1155_token_id_placeholder "{id}" diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index c90c7440e652..98ab22fe2713 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -20,7 +20,7 @@ defmodule Explorer.Mixfile do lockfile: "../../mix.lock", package: package(), start_permanent: Mix.env() == :prod, - version: "9.3.2", + version: "10.0.0", xref: [exclude: [BlockScoutWeb.Routers.WebRouter.Helpers, Indexer.Helper, Indexer.Fetcher.InternalTransaction]] ] end diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index 0ef7e743e798..a0c5ef1d7552 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -14,7 +14,7 @@ defmodule Indexer.MixProject do elixirc_paths: elixirc_paths(Mix.env()), lockfile: "../../mix.lock", start_permanent: Mix.env() == :prod, - version: "9.3.2", + version: "10.0.0", xref: [ exclude: [ Explorer.Chain.Optimism.Deposit, diff --git a/apps/nft_media_handler/mix.exs b/apps/nft_media_handler/mix.exs index 79c2f1991153..3240b708dd67 100644 --- a/apps/nft_media_handler/mix.exs +++ b/apps/nft_media_handler/mix.exs @@ -4,7 +4,7 @@ defmodule NFTMediaHandler.MixProject do def project do [ app: :nft_media_handler, - version: "9.3.2", + version: "10.0.0", build_path: "../../_build", config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index b0412655e2b2..e74bcb649434 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -4,7 +4,7 @@ defmodule Utils.MixProject do def project do [ app: :utils, - version: "9.3.2", + version: "10.0.0", build_path: "../../_build", # config_path: "../../config/config.exs", deps_path: "../../deps", diff --git a/docker/Makefile b/docker/Makefile index 4fe5727f6f2e..fef86079ca34 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -10,7 +10,7 @@ STATS_CONTAINER_NAME := stats STATS_DB_CONTAINER_NAME := stats-db PROXY_CONTAINER_NAME := proxy PG_CONTAINER_NAME := postgres -RELEASE_VERSION ?= '9.3.2' +RELEASE_VERSION ?= '10.0.0' TAG := $(RELEASE_VERSION)-commit-$(shell git log -1 --pretty=format:"%h") STABLE_TAG := $(RELEASE_VERSION) diff --git a/mix.exs b/mix.exs index 7601eaba9f4b..f978cb367659 100644 --- a/mix.exs +++ b/mix.exs @@ -7,7 +7,7 @@ defmodule BlockScout.Mixfile do [ # app: :block_scout, # aliases: aliases(config_env()), - version: "9.3.2", + version: "10.0.0", apps_path: "apps", deps: deps(), dialyzer: dialyzer(), diff --git a/rel/config.exs b/rel/config.exs index 7906ed07e184..30223e2e3a5a 100644 --- a/rel/config.exs +++ b/rel/config.exs @@ -71,7 +71,7 @@ end # will be used by default release :blockscout do - set version: "9.3.2" + set version: "10.0.0" set applications: [ :runtime_tools, block_scout_web: :permanent, From 47bc08507442cd972770f0f007e13906c914aa89 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 23 Jan 2026 17:50:24 +0300 Subject: [PATCH 0623/1006] fix: Fix Explorer.Repo.Migrations.AlterContractVerificationStatusTable migration (#13905) --- CHANGELOG.md | 4 ++-- ...0004_alter_contract_verification_status_table.exs | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a80750843c5..402d4248f1fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,12 +34,12 @@ - Skip hot contracts fetching if last 30 days not indexed ([#13873](https://github.com/blockscout/blockscout/issues/13873)) - Add block range filtering to TokenBalance fetcher ([#13874](https://github.com/blockscout/blockscout/issues/13874)) - Filter traceable data in InternalTransaction.async_fetch ([#13872](https://github.com/blockscout/blockscout/issues/13872)) -- Take into account empty arrays in Explorer.Migrator.SanitizeIncorectNFTTokenTransfers ([#13852](https://github.com/blockscout/blockscout/issues/13852)) +- Take into account empty arrays in Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers ([#13852](https://github.com/blockscout/blockscout/issues/13852)) - Fix search for ERC-1155 with null symbol ([#13632](https://github.com/blockscout/blockscout/issues/13632)) - Return date to logs ([#13858](https://github.com/blockscout/blockscout/issues/13858)) - Convert token id to string from refetch metadata in the socket ([#13762](https://github.com/blockscout/blockscout/issues/13762)) - Prevent DeleteZeroValueInternalTransactions from running while ShrinkInternalTransactions is in progress ([#13847](https://github.com/blockscout/blockscout/issues/13847)) -- Remove contract code and verified data on lose consensus ([#13829](https://github.com/blockscout/blockscout/issues/13829)) +- Remove contract code and verified data on lose consensus ([#13829](https://github.com/blockscout/blockscout/issues/13829), [#13905](https://github.com/blockscout/blockscout/pull/13905)) - Exclude 0 index internal transactions from /api/v2/internal-transactions endpoint ([#13841](https://github.com/blockscout/blockscout/issues/13841)) - Fix NaN gas limit for `selfdestruct` internal transaction in the REST API ([#13827](https://github.com/blockscout/blockscout/issues/13827)) - Handle normal termination of Indexer.Fetcher.OnDemand.ContractCode process ([#13828](https://github.com/blockscout/blockscout/issues/13828)) diff --git a/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs b/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs index e9a8e8b325c8..e1c2e428e2af 100644 --- a/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs +++ b/apps/explorer/priv/repo/migrations/20260107090004_alter_contract_verification_status_table.exs @@ -5,23 +5,11 @@ defmodule Explorer.Repo.Migrations.AlterContractVerificationStatusTable do rename(table(:contract_verification_status), to: table(:smart_contract_verification_statuses)) execute("ALTER INDEX contract_verification_status_pkey RENAME TO smart_contract_verification_statuses_pkey") rename(table(:smart_contract_verification_statuses), :address_hash, to: :contract_address_hash) - - alter table(:smart_contract_verification_statuses) do - modify( - :contract_address_hash, - references(:smart_contracts, column: :address_hash, on_delete: :delete_all, type: :bytea) - ) - end end def down do rename(table(:smart_contract_verification_statuses), to: table(:contract_verification_status)) - execute(""" - ALTER TABLE contract_verification_status - DROP CONSTRAINT smart_contract_verification_statuses_contract_address_hash_fkey - """) - execute("ALTER INDEX smart_contract_verification_statuses_pkey RENAME TO contract_verification_status_pkey") rename(table(:contract_verification_status), :contract_address_hash, to: :address_hash) From 80bbe83bcc589eb7b0b1d3250ff16074d29fec79 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 26 Jan 2026 11:28:49 +0300 Subject: [PATCH 0624/1006] fix: handle nil in `update_transactions_cache/2` (#13911) Co-authored-by: Victor Baranov --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain/transaction.ex | 2 +- apps/indexer/lib/indexer/block/fetcher.ex | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 402d4248f1fb..4a956dad31a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### 🐛 Bug Fixes +- Handle nil in update_transactions_cache/2 ([#13911](https://github.com/blockscout/blockscout/pull/13911)) - Fix token balances broadcasting function ([#13902](https://github.com/blockscout/blockscout/issues/13902)) - Wrong `next_page_params` in OP Deposits ([#13870](https://github.com/blockscout/blockscout/issues/13870)) - Fix error on loading thumbnails when public_r2_url is missed ([#13895](https://github.com/blockscout/blockscout/issues/13895)) diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index f40a416fe2df..19abe45f0425 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -2569,7 +2569,7 @@ defmodule Explorer.Chain.Transaction do method_id_filter = Keyword.get(options, :method) type_filter = Keyword.get(options, :type) - case !paging_options.key && !method_id_filter && !type_filter && + case !paging_options.key && method_id_filter in [nil, []] && type_filter in [nil, []] && Transactions.atomic_take_enough(paging_options.page_size) do transactions when is_list(transactions) -> transactions |> Chain.select_repo(options).preload(Map.keys(necessity_by_association)) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 85d3e84910c9..9fea7796ef18 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -463,7 +463,9 @@ defmodule Indexer.Block.Fetcher do defp update_block_cache(_, _), do: :ok - defp update_transactions_cache(transactions, inserted) do + defp update_transactions_cache([], _), do: :ok + + defp update_transactions_cache(transactions, inserted) when is_list(transactions) do blocks_map = Map.new(Map.get(inserted, :blocks, []), fn block -> {block.hash, [block]} end) token_transfers_transaction_hashes_set = MapSet.new(Map.get(inserted, :token_transfers, []), & &1.transaction_hash) @@ -483,6 +485,8 @@ defmodule Indexer.Block.Fetcher do |> Transactions.update() end + defp update_transactions_cache(_, _), do: :ok + defp update_addresses_cache(addresses), do: Accounts.drop(addresses) defp update_uncles_cache(updated_relations) do From 7ba852d50fb725aa134fc5a9c30a0dc9d54598d3 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 26 Jan 2026 16:05:13 +0300 Subject: [PATCH 0625/1006] chore: disable Auth0 when dynamic enabled (#13912) --- CHANGELOG.md | 1 + .../account/api/v2/authenticate_controller.ex | 21 ++++++++++++------- .../account/api/v2/email_controller.ex | 3 ++- .../account/api/v2/fallback_controller.ex | 7 +++++++ .../third_party_integrations/auth0.ex | 14 ++++++++++--- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a956dad31a1..bddb04f96037 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ ### ⚙️ Miscellaneous Tasks +- Disable Auth0 when Dynamic enabled ([#13912](https://github.com/blockscout/blockscout/pull/13912)) - Add swagger spec for account abstraction endpoints ([#13897](https://github.com/blockscout/blockscout/issues/13897)) - Clear token "skip_metadata" property ([#13891](https://github.com/blockscout/blockscout/issues/13891)) - Refactor internal transaction logic from "block_index" to "transaction_index" and "index" ([#12474](https://github.com/blockscout/blockscout/issues/12474)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex index a5bcc4fc935e..aa6edf230b22 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/authenticate_controller.ex @@ -117,13 +117,14 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do @spec send_otp(Conn.t(), map()) :: :error | {:error, String.t()} + | {:enabled, false} | {:format, :email} | {:interval, integer()} | Conn.t() def send_otp(conn, _params) do email = Map.get(conn.body_params, :email) - case conn |> current_user() do + case Auth0.enabled?() && conn |> current_user() do nil -> with :ok <- Auth0.send_otp(email, AccessHelper.conn_to_ip_string(conn)) do conn |> put_status(200) |> json(%{message: "Success"}) @@ -139,6 +140,9 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do |> put_status(500) |> put_view(UserView) |> render(:message, %{message: "This account already has an email"}) + + false -> + {:enabled, false} end end @@ -187,9 +191,10 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do perform additional operations such as setting cookies or rendering user information. """ - @spec confirm_otp(Conn.t(), map()) :: :error | {:error, any()} | Conn.t() + @spec confirm_otp(Conn.t(), map()) :: :error | {:error, any()} | {:enabled, false} | Conn.t() def confirm_otp(conn, %{email: email, otp: otp}) do - with {:ok, auth} <- Auth0.confirm_otp_and_get_auth(email, otp, AccessHelper.conn_to_ip_string(conn)) do + with {:enabled, true} <- {:enabled, Auth0.enabled?()}, + {:ok, auth} <- Auth0.confirm_otp_and_get_auth(email, otp, AccessHelper.conn_to_ip_string(conn)) do put_auth_to_session(conn, auth) end end @@ -248,9 +253,10 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do - The nonce is cached for the address to prevent replay attacks. - The SIWE message expires after 300 seconds from generation. """ - @spec siwe_message(Conn.t(), map()) :: {:error, String.t()} | {:format, :error} | Conn.t() + @spec siwe_message(Conn.t(), map()) :: {:error, String.t()} | {:enabled, false} | {:format, :error} | Conn.t() def siwe_message(conn, %{address: address}) do - with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address)}, + with {:enabled, true} <- {:enabled, Auth0.enabled?()}, + {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address)}, {:ok, message} <- Auth0.generate_siwe_message(Address.checksum(address_hash)) do conn |> put_status(200) |> json(%{siwe_message: message}) end @@ -299,12 +305,13 @@ defmodule BlockScoutWeb.Account.API.V2.AuthenticateController do perform additional operations such as setting cookies or rendering user information. """ - @spec authenticate_via_wallet(Conn.t(), map()) :: :error | {:error, any()} | Conn.t() + @spec authenticate_via_wallet(Conn.t(), map()) :: :error | {:error, any()} | {:enabled, false} | Conn.t() def authenticate_via_wallet(conn, _params) do message = Map.get(conn.body_params, :message) signature = Map.get(conn.body_params, :signature) - with {:ok, auth} <- Auth0.get_auth_with_web3(message, signature) do + with {:enabled, true} <- {:enabled, Auth0.enabled?()}, + {:ok, auth} <- Auth0.get_auth_with_web3(message, signature) do put_auth_to_session(conn, auth) end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex index 8b4af23a2a06..eefb007dd97d 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/email_controller.ex @@ -17,7 +17,8 @@ defmodule BlockScoutWeb.Account.API.V2.EmailController do plug(:fetch_cookies, signed: [@invalid_session_key]) def resend_email(conn, _params) do - with user <- conn.cookies[@invalid_session_key], + with {:enabled, true} <- {:enabled, Auth0.enabled?()}, + user <- conn.cookies[@invalid_session_key], {:auth, false} <- {:auth, is_nil(user)}, {:email_verified, false} <- {:email_verified, user[:email_verified]}, {:identity, %Identity{} = identity} <- {:identity, Identity.find_identity(user[:id])}, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex index 82d119c02d5c..1026c17d8770 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v2/fallback_controller.ex @@ -146,6 +146,13 @@ defmodule BlockScoutWeb.Account.API.V2.FallbackController do |> render(:message, %{message: "No Bearer token"}) end + def call(conn, {:enabled, false}) do + conn + |> put_status(:not_found) + |> put_view(UserView) + |> render(:message, %{message: "This endpoint is not configured"}) + end + defp unauthorized_error(%{email_verified: false, email: email}) do %{message: "Unverified email", email: email} end diff --git a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex index ab6561681104..f9d2dab8f537 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/auth0.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/auth0.ex @@ -7,6 +7,7 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do alias Explorer.{Account, Helper, HttpClient} alias Explorer.Account.Identity alias Explorer.ThirdPartyIntegrations.Auth0.Internal + alias Explorer.ThirdPartyIntegrations.Dynamic alias Ueberauth.Auth alias Ueberauth.Strategy.Auth0.OAuth @@ -15,6 +16,12 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do @misconfiguration_detected "Misconfiguration detected, please contact support." @json_content_type [{"Content-type", "application/json"}] + @spec enabled? :: boolean() + def enabled? do + Application.get_env(:ueberauth, OAuth)[:domain] not in [nil, ""] and + !Application.get_env(:explorer, Dynamic)[:enabled] + end + @doc """ Retrieves a machine-to-machine JWT for interacting with the Auth0 Management API. @@ -30,9 +37,9 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do get_m2m_jwt_inner(Redix.command(:redix, ["GET", Internal.redis_key()])) end - def get_m2m_jwt_inner({:ok, token}) when not is_nil(token), do: token + defp get_m2m_jwt_inner({:ok, token}) when not is_nil(token), do: token - def get_m2m_jwt_inner(_) do + defp get_m2m_jwt_inner(_) do config = Application.get_env(:ueberauth, OAuth) body = %{ @@ -52,7 +59,8 @@ defmodule Explorer.ThirdPartyIntegrations.Auth0 do nil end - _ -> + error -> + Logger.error("Error while fetching Auth0 M2M JWT: #{inspect(error)}") nil end end From 8b6a002d2fe8582f6b74aae1993db7d7f9e0e8fc Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:20:17 +0400 Subject: [PATCH 0626/1006] chore: Improve EmptyInternalTransactionsData migration (#13918) --- apps/explorer/config/config.exs | 1 - apps/explorer/config/runtime/test.exs | 1 - apps/explorer/lib/explorer/application.ex | 4 - .../chain/cache/background_migrations.ex | 8 -- .../explorer/chain/internal_transaction.ex | 4 +- apps/explorer/lib/explorer/etherscan.ex | 2 +- .../empty_internal_transactions_data.ex | 90 ++++++++++-------- ...ll_internal_transaction_to_address_hash.ex | 66 ------------- ...er_transaction_index_index_unique_index.ex | 4 +- ...lock_hash_transaction_index_index_index.ex | 4 +- ...250915135943_create_transaction_errors.exs | 2 +- ...ernal_transaction_to_address_hash_test.exs | 93 ------------------- config/runtime.exs | 17 ---- 13 files changed, 59 insertions(+), 237 deletions(-) delete mode 100644 apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex delete mode 100644 apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index c06545a1fb49..a696e6231b9f 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -142,7 +142,6 @@ for migrator <- [ Explorer.Migrator.MergeAdjacentMissingBlockRanges, Explorer.Migrator.UnescapeQuotesInTokens, Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, - Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, Explorer.Migrator.EmptyInternalTransactionsData ] do config :explorer, migrator, enabled: true diff --git a/apps/explorer/config/runtime/test.exs b/apps/explorer/config/runtime/test.exs index 65a6ea08b8f6..638802746eb0 100644 --- a/apps/explorer/config/runtime/test.exs +++ b/apps/explorer/config/runtime/test.exs @@ -76,7 +76,6 @@ for migrator <- [ Explorer.Migrator.UnescapeQuotesInTokens, Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, Explorer.Migrator.DeleteZeroValueInternalTransactions, - Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, Explorer.Migrator.EmptyInternalTransactionsData, # Heavy DB index operations diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index d9437d04bd75..579f9dabc06f 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -202,10 +202,6 @@ defmodule Explorer.Application do configure_mode_dependent_process(Explorer.Migrator.ReindexBlocksWithMissingTransactions, :indexer), configure_mode_dependent_process(Explorer.Migrator.SanitizeDuplicateSmartContractAdditionalSources, :indexer), configure_mode_dependent_process(Explorer.Migrator.DeleteZeroValueInternalTransactions, :indexer), - configure_mode_dependent_process( - Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, - :indexer - ), configure_mode_dependent_process(Explorer.Migrator.EmptyInternalTransactionsData, :indexer), configure_mode_dependent_process( Explorer.Migrator.HeavyDbIndexOperation.CreateAddressesVerifiedIndex, diff --git a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex index 2f3880abe2cb..2a66f04f8421 100644 --- a/apps/explorer/lib/explorer/chain/cache/background_migrations.ex +++ b/apps/explorer/lib/explorer/chain/cache/background_migrations.ex @@ -72,7 +72,6 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ArbitrumDaRecordsNormalization, BackfillMultichainSearchDB, EmptyInternalTransactionsData, - FillInternalTransactionToAddressHashWithCreatedContractAddressHash, SanitizeDuplicatedLogIndexLogs, SmartContractLanguage, TokenTransferTokenType, @@ -353,13 +352,6 @@ defmodule Explorer.Chain.Cache.BackgroundMigrations do ) end - defp handle_fallback(:fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished) do - set_and_return_migration_status( - FillInternalTransactionToAddressHashWithCreatedContractAddressHash, - &set_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished/1 - ) - end - defp handle_fallback(:heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished) do set_and_return_migration_status( DropInternalTransactionsCreatedContractAddressHashPartialIndex, diff --git a/apps/explorer/lib/explorer/chain/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex index ac32f2ffcb3d..9776676df5e5 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -542,7 +542,7 @@ defmodule Explorer.Chain.InternalTransaction do from_address_hash, created_contract_address_hash from internal_transactions' table. """ def where_address_fields_match(query, address_hash, :to) do - if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() do + if BackgroundMigrations.get_empty_internal_transactions_data_finished() do where(query, [t], t.to_address_hash == ^address_hash) else where( @@ -559,7 +559,7 @@ defmodule Explorer.Chain.InternalTransaction do end def where_address_fields_match(query, address_hash, :to_address_hash) do - if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() do + if BackgroundMigrations.get_empty_internal_transactions_data_finished() do where(query, [it], it.to_address_hash == ^address_hash and is_nil(it.created_contract_address_hash)) else where(query, [it], it.to_address_hash == ^address_hash) diff --git a/apps/explorer/lib/explorer/etherscan.ex b/apps/explorer/lib/explorer/etherscan.ex index a929f8410218..269b46d426e1 100644 --- a/apps/explorer/lib/explorer/etherscan.ex +++ b/apps/explorer/lib/explorer/etherscan.ex @@ -172,7 +172,7 @@ defmodule Explorer.Etherscan do options |> options_to_directions() |> then(fn directions -> - if BackgroundMigrations.get_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished() and + if BackgroundMigrations.get_empty_internal_transactions_data_finished() and Enum.member?(directions, :to_address_hash) do directions |> Kernel.--([:created_contract_address_hash, :to_address_hash]) diff --git a/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex b/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex index 72a2f4d4c7d3..b1ced301aa4c 100644 --- a/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex +++ b/apps/explorer/lib/explorer/migrator/empty_internal_transactions_data.ex @@ -4,6 +4,7 @@ defmodule Explorer.Migrator.EmptyInternalTransactionsData do Also searches for all internal transactions with zero `value` and empties it. Also updates `call_type` with `call_type_enum`. Also empties `error` column and fills `transaction_errors` with its values. + Also fills `to_address_hash` column with the data from `created_contract_address_hash`. """ use Explorer.Migrator.FillingMigration @@ -37,7 +38,7 @@ defmodule Explorer.Migrator.EmptyInternalTransactionsData do it in InternalTransaction, where: not is_nil(it.trace_address) or it.value == ^0 or (is_nil(it.call_type_enum) and not is_nil(it.call_type)) or - not is_nil(it.error), + not is_nil(it.error) or (not is_nil(it.created_contract_address_hash) and is_nil(it.to_address_hash)), select: max(it.block_number) ) @@ -59,15 +60,22 @@ defmodule Explorer.Migrator.EmptyInternalTransactionsData do from( it in InternalTransaction, select: select_ctid(it), - where: it.block_number in ^block_numbers, + select_merge: %{error: it.error}, + where: + it.block_number in ^block_numbers and + (not is_nil(it.trace_address) or it.value == ^0 or + (is_nil(it.call_type_enum) and not is_nil(it.call_type)) or + not is_nil(it.error) or (not is_nil(it.created_contract_address_hash) and is_nil(it.to_address_hash))), order_by: [asc: it.transaction_hash, asc: it.index], lock: "FOR UPDATE" ) extract_error_query = from(it in InternalTransaction, - where: it.block_number in ^block_numbers, + inner_join: locked_it in subquery(lock_query), + on: join_on_ctid(it, locked_it), where: not is_nil(it.error), + distinct: true, select: it.error ) @@ -75,49 +83,53 @@ defmodule Explorer.Migrator.EmptyInternalTransactionsData do TransactionError.find_or_create_multiple(error_messages) - error_update_query = + update_query = from(it in InternalTransaction, - inner_join: te in TransactionError, - on: te.message == it.error, inner_join: locked_it in subquery(lock_query), on: join_on_ctid(it, locked_it), - where: not is_nil(it.error), - update: [set: [error: nil, error_id: te.id]] - ) - - Repo.update_all(error_update_query, [], timeout: :infinity) - - call_type_query = - from(it in InternalTransaction, - inner_join: locked_it in subquery(lock_query), - on: join_on_ctid(it, locked_it), - where: is_nil(it.call_type_enum) and not is_nil(it.call_type), + left_join: te in TransactionError, + on: te.message == locked_it.error, update: [ - set: [call_type_enum: fragment("?::internal_transactions_call_type", it.call_type), call_type: nil] + set: [ + error: nil, + error_id: + fragment( + "CASE WHEN ? IS NOT NULL THEN COALESCE(?, ?) ELSE ? END", + it.error, + te.id, + it.error_id, + it.error_id + ), + call_type_enum: + fragment( + "CASE WHEN ? IS NULL AND ? IS NOT NULL THEN (?::internal_transactions_call_type) ELSE ? END", + it.call_type_enum, + it.call_type, + it.call_type, + it.call_type_enum + ), + call_type: + fragment( + "CASE WHEN ? IS NULL AND ? IS NOT NULL THEN NULL ELSE ? END", + it.call_type_enum, + it.call_type, + it.call_type + ), + trace_address: nil, + value: fragment("CASE WHEN ? = 0 THEN NULL ELSE ? END", it.value, it.value), + to_address_hash: + fragment( + "CASE WHEN ? IS NOT NULL AND ? IS NULL THEN ? ELSE ? END", + it.created_contract_address_hash, + it.to_address_hash, + it.created_contract_address_hash, + it.to_address_hash + ) + ] ] ) - Repo.update_all(call_type_query, [], timeout: :infinity) - - trace_address_query = - from(it in InternalTransaction, - inner_join: locked_it in subquery(lock_query), - on: join_on_ctid(it, locked_it), - where: not is_nil(it.trace_address), - update: [set: [trace_address: nil]] - ) - - Repo.update_all(trace_address_query, [], timeout: :infinity) - - value_query = - from(it in InternalTransaction, - inner_join: locked_it in subquery(lock_query), - on: join_on_ctid(it, locked_it), - where: it.value == ^0, - update: [set: [value: nil]] - ) - - Repo.update_all(value_query, [], timeout: :infinity) + Repo.update_all(update_query, [], timeout: :infinity) end, timeout: :infinity ) diff --git a/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex b/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex deleted file mode 100644 index 7d18a385b625..000000000000 --- a/apps/explorer/lib/explorer/migrator/fill_internal_transaction_to_address_hash.ex +++ /dev/null @@ -1,66 +0,0 @@ -defmodule Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash do - @moduledoc """ - Fills `to_address_hash` column with the data from `created_contract_address_hash` - """ - - use Explorer.Migrator.FillingMigration - - import Ecto.Query - - alias Explorer.Chain.Cache.BackgroundMigrations - alias Explorer.Chain.InternalTransaction - alias Explorer.Migrator.FillingMigration - alias Explorer.Repo - - @migration_name "fill_internal_transaction_to_address_hash_with_created_contract_address_hash" - - @impl FillingMigration - def migration_name, do: @migration_name - - @impl FillingMigration - def last_unprocessed_identifiers(%{"max_block_number" => -1} = state), do: {[], state} - - def last_unprocessed_identifiers(%{"max_block_number" => from_block_number} = state) do - limit = batch_size() * concurrency() - to_block_number = max(from_block_number - limit + 1, 0) - - {Enum.to_list(from_block_number..to_block_number), %{state | "max_block_number" => to_block_number - 1}} - end - - def last_unprocessed_identifiers(state) do - query = - from( - it in InternalTransaction, - where: not is_nil(it.created_contract_address_hash), - where: is_nil(it.to_address_hash), - select: max(it.block_number) - ) - - max_block_number = Repo.one(query, timeout: :infinity) - - state - |> Map.put("max_block_number", max_block_number || -1) - |> last_unprocessed_identifiers() - end - - @impl FillingMigration - def unprocessed_data_query, do: nil - - @impl FillingMigration - def update_batch(block_numbers) do - query = - from(it in InternalTransaction, - where: it.block_number in ^block_numbers, - where: not is_nil(it.created_contract_address_hash), - where: is_nil(it.to_address_hash), - update: [set: [to_address_hash: it.created_contract_address_hash]] - ) - - Repo.update_all(query, [], timeout: :infinity) - end - - @impl FillingMigration - def update_cache do - BackgroundMigrations.set_fill_internal_transaction_to_address_hash_with_created_contract_address_hash_finished(true) - end -end diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex index 0e79a16a998d..1069de2539f7 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/create_internal_transactions_block_number_transaction_index_index_unique_index.ex @@ -8,7 +8,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBloc require Logger alias Explorer.Migrator.{ - FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + EmptyInternalTransactionsData, HeavyDbIndexOperation, MigrationStatus, ReindexDuplicatedInternalTransactions @@ -34,7 +34,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateInternalTransactionsBloc def dependent_from_migrations, do: [ ReindexDuplicatedInternalTransactions.migration_name(), - FillInternalTransactionToAddressHashWithCreatedContractAddressHash.migration_name() + EmptyInternalTransactionsData.migration_name() ] @impl HeavyDbIndexOperation diff --git a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex index 0e0a2a82a41a..757617cad3b7 100644 --- a/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex +++ b/apps/explorer/lib/explorer/migrator/heavy_db_index_operation/drop_internal_transactions_block_hash_transaction_index_index_index.ex @@ -6,7 +6,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockH use Explorer.Migrator.HeavyDbIndexOperation alias Explorer.Migrator.{ - FillInternalTransactionToAddressHashWithCreatedContractAddressHash, + EmptyInternalTransactionsData, HeavyDbIndexOperation, MigrationStatus } @@ -29,7 +29,7 @@ defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsBlockH @impl HeavyDbIndexOperation def dependent_from_migrations do [ - FillInternalTransactionToAddressHashWithCreatedContractAddressHash.migration_name() + EmptyInternalTransactionsData.migration_name() ] end diff --git a/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs b/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs index 1f64e53303d9..135298e18119 100644 --- a/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs +++ b/apps/explorer/priv/repo/migrations/20250915135943_create_transaction_errors.exs @@ -12,7 +12,7 @@ defmodule Explorer.Repo.Migrations.CreateTransactionErrors do create(unique_index(:transaction_errors, [:message])) alter table(:internal_transactions) do - add(:error_id, references(:transaction_errors, column: :id, type: :smallint)) + add(:error_id, :smallint) end drop_if_exists( diff --git a/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs b/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs deleted file mode 100644 index e25cde671c4e..000000000000 --- a/apps/explorer/test/explorer/migrator/fill_internal_transaction_to_address_hash_test.exs +++ /dev/null @@ -1,93 +0,0 @@ -defmodule Explorer.Migrator.FillInternalTransactionToAddressHashTest do - use Explorer.DataCase, async: false - - alias Explorer.Chain.InternalTransaction - alias Explorer.Migrator.{MigrationStatus, FillInternalTransactionToAddressHashWithCreatedContractAddressHash} - alias Explorer.Repo - - setup_all do - opts = Application.get_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash) - - Application.put_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash, - batch_size: 1, - concurrency: 1, - timeout: 0 - ) - - on_exit(fn -> - Application.put_env(:explorer, FillInternalTransactionToAddressHashWithCreatedContractAddressHash, opts) - end) - end - - test "Fill to_address_hash with created_contract_address_hash" do - Enum.each(1..3, fn index -> - transaction = - :transaction - |> insert() - |> with_block() - - insert( - :internal_transaction, - transaction: transaction, - index: index, - block_number: transaction.block_number, - transaction_index: transaction.index, - block_hash: transaction.block_hash - ) - end) - - Enum.each(1..4, fn index -> - transaction = - :transaction - |> insert() - |> with_block() - - insert(:internal_transaction_create, - transaction: transaction, - index: index, - block_hash: transaction.block_hash, - block_number: transaction.block_number, - transaction_index: transaction.index - ) - end) - - internal_transactions_before = Repo.all(InternalTransaction) - - assert Enum.count(internal_transactions_before, fn it -> - not is_nil(it.to_address_hash) and is_nil(it.created_contract_address_hash) - end) == 3 - - assert Enum.count(internal_transactions_before, fn it -> - is_nil(it.to_address_hash) and not is_nil(it.created_contract_address_hash) - end) == 4 - - assert MigrationStatus.get_status("fill_internal_transaction_to_address_hash_with_created_contract_address_hash") == - nil - - FillInternalTransactionToAddressHashWithCreatedContractAddressHash.start_link([]) - - wait_for_results(fn -> - Repo.one!( - from(ms in MigrationStatus, - where: - ms.migration_name == ^"fill_internal_transaction_to_address_hash_with_created_contract_address_hash" and - ms.status == "completed" - ) - ) - end) - - internal_transactions_after = Repo.all(InternalTransaction) - - assert Enum.count(internal_transactions_after, fn it -> - not is_nil(it.to_address_hash) and is_nil(it.created_contract_address_hash) - end) == 3 - - assert Enum.count(internal_transactions_after, fn it -> - is_nil(it.to_address_hash) and not is_nil(it.created_contract_address_hash) - end) == 0 - - assert Enum.count(internal_transactions_after, fn it -> - it.to_address_hash == it.created_contract_address_hash - end) == 4 - end -end diff --git a/config/runtime.exs b/config/runtime.exs index 3c140dd57072..d76d409af452 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -884,23 +884,6 @@ config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, check_interval: ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL", "1m") -config :explorer, Explorer.Migrator.FillInternalTransactionToAddressHashWithCreatedContractAddressHash, - batch_size: - ConfigHelper.parse_integer_env_var( - "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_BATCH_SIZE", - 100 - ), - concurrency: - ConfigHelper.parse_integer_env_var( - "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_CONCURRENCY", - 1 - ), - timeout: - ConfigHelper.parse_time_env_var( - "MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_TIMEOUT", - "0s" - ) - config :explorer, Explorer.Chain.BridgedToken, eth_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_ETH_OMNI_BRIDGE_MEDIATOR"), bsc_omni_bridge_mediator: System.get_env("BRIDGED_TOKENS_BSC_OMNI_BRIDGE_MEDIATOR"), From 3c5987113b46fb63ef366d1143c42d8d4020b7f3 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:26:24 +0400 Subject: [PATCH 0627/1006] chore: Move current token balances into a separate fetcher (#13923) --- .../chain/address/current_token_balance.ex | 55 +++++- .../explorer/chain/address/token_balance.ex | 4 +- .../runner/address/current_token_balances.ex | 2 + ...add_current_token_balance_retry_fields.exs | 10 + .../lib/indexer/block/catchup/fetcher.ex | 2 + apps/indexer/lib/indexer/block/fetcher.ex | 31 ++- .../lib/indexer/block/realtime/fetcher.ex | 2 + .../indexer/fetcher/token_balance/current.ex | 117 +++++++++++ .../helper.ex} | 187 +++++------------- .../fetcher/token_balance/historical.ex | 108 ++++++++++ .../indexer/fetcher/zilliqa/zrc2_tokens.ex | 3 + apps/indexer/lib/indexer/supervisor.ex | 7 +- .../bound_interval_supervisor_test.exs | 8 +- .../indexer/block/catchup/fetcher_test.exs | 13 +- .../test/indexer/block/fetcher_test.exs | 6 +- .../indexer/block/realtime/fetcher_test.exs | 4 +- .../historical_test.exs} | 46 ++--- .../test/indexer/token_balances_test.exs | 4 +- .../fetcher/token_balance_supervisor_case.ex | 6 +- config/runtime.exs | 6 +- docker-compose/envs/common-blockscout.env | 2 + 21 files changed, 423 insertions(+), 200 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20260128160608_add_current_token_balance_retry_fields.exs create mode 100644 apps/indexer/lib/indexer/fetcher/token_balance/current.ex rename apps/indexer/lib/indexer/fetcher/{token_balance.ex => token_balance/helper.ex} (52%) create mode 100644 apps/indexer/lib/indexer/fetcher/token_balance/historical.ex rename apps/indexer/test/indexer/fetcher/{token_balance_test.exs => token_balance/historical_test.exs} (89%) diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 566dbaa0957d..f40f03ed7a5a 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -16,6 +16,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do alias Explorer.{Chain, PagingOptions, Repo} alias Explorer.Chain.{Address, Block, CurrencyHelper, Hash, Token} alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Cache.BackgroundMigrations @default_paging_options %PagingOptions{page_size: 50} @@ -28,6 +29,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do * `value` - The value that's represents the balance. * `token_id` - The token_id of the transferred token (applicable for ERC-1155) * `token_type` - The type of the token + * `refetch_after` - when to refetch the balance + * `retries_count` - number of times the balance has been retried """ typed_schema "address_current_token_balances" do field(:value, :decimal) @@ -40,6 +43,8 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do field(:distinct_token_instances_count, :integer, virtual: true) field(:token_ids, {:array, :decimal}, virtual: true) field(:preloaded_token_instances, {:array, :any}, virtual: true) + field(:refetch_after, :utc_datetime_usec) + field(:retries_count, :integer) # A transient field for deriving token holder count deltas during address_current_token_balances upserts field(:old_value, :decimal) @@ -58,7 +63,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do timestamps() end - @optional_fields ~w(value value_fetched_at token_id)a + @optional_fields ~w(value value_fetched_at token_id refetch_after retries_count)a @required_fields ~w(address_hash block_number token_contract_address_hash token_type)a @allowed_fields @optional_fields ++ @required_fields @@ -354,4 +359,52 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do Stream.concat([row_names], holders_list) end + + @doc """ + Returns a stream of all current token balances that weren't fetched values. + """ + @spec stream_unfetched_current_token_balances( + initial :: accumulator, + reducer :: (entry :: __MODULE__.t(), accumulator -> accumulator), + limited? :: boolean() + ) :: {:ok, accumulator} + when accumulator: term() + def stream_unfetched_current_token_balances(initial, reducer, limited? \\ false) when is_function(reducer, 2) do + unfetched_current_token_balances() + |> TokenBalance.add_token_balances_fetcher_limit(limited?) + |> Repo.stream_reduce(initial, reducer) + end + + @doc """ + Builds an `Ecto.Query` to fetch the unfetched current token balances. + + Unfetched current token balances are the ones that have the column `value_fetched_at` nil or the value is null. This query also + ignores the burn_address for tokens ERC-721 since the most tokens ERC-721 don't allow get the + balance for burn_address. + """ + # credo:disable-for-next-line /Complexity/ + def unfetched_current_token_balances do + if BackgroundMigrations.get_ctb_token_type_finished() do + from( + ctb in __MODULE__, + where: + ((ctb.address_hash != ^@burn_address_hash and ctb.token_type == "ERC-721") or ctb.token_type == "ERC-20" or + ctb.token_type == "ZRC-2" or + ctb.token_type == "ERC-1155" or ctb.token_type == "ERC-404") and + (is_nil(ctb.value_fetched_at) or is_nil(ctb.value)) and + (is_nil(ctb.refetch_after) or ctb.refetch_after < ^Timex.now()) + ) + else + from( + ctb in __MODULE__, + join: t in Token, + on: ctb.token_contract_address_hash == t.contract_address_hash, + where: + ((ctb.address_hash != ^@burn_address_hash and t.type == "ERC-721") or t.type == "ERC-20" or t.type == "ZRC-2" or + t.type == "ERC-1155" or t.type == "ERC-404") and + (is_nil(ctb.value_fetched_at) or is_nil(ctb.value)) and + (is_nil(ctb.refetch_after) or ctb.refetch_after < ^Timex.now()) + ) + end + end end diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 1e58ad5fa8c8..02710fbb6ec0 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -164,9 +164,9 @@ defmodule Explorer.Chain.Address.TokenBalance do |> Repo.stream_reduce(initial, reducer) end - defp add_token_balances_fetcher_limit(query, false), do: query + def add_token_balances_fetcher_limit(query, false), do: query - defp add_token_balances_fetcher_limit(query, true) do + def add_token_balances_fetcher_limit(query, true) do token_balances_fetcher_limit = Application.get_env(:indexer, :token_balances_fetcher_init_limit) limit(query, ^token_balances_fetcher_limit) diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex index a6614a599104..307733c7073c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex @@ -383,6 +383,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do value_fetched_at: fragment("EXCLUDED.value_fetched_at"), old_value: current_token_balance.value, token_type: fragment("EXCLUDED.token_type"), + refetch_after: fragment("EXCLUDED.refetch_after"), + retries_count: fragment("EXCLUDED.retries_count"), inserted_at: fragment("LEAST(EXCLUDED.inserted_at, ?)", current_token_balance.inserted_at), updated_at: fragment("GREATEST(EXCLUDED.updated_at, ?)", current_token_balance.updated_at) ] diff --git a/apps/explorer/priv/repo/migrations/20260128160608_add_current_token_balance_retry_fields.exs b/apps/explorer/priv/repo/migrations/20260128160608_add_current_token_balance_retry_fields.exs new file mode 100644 index 000000000000..7509acdc269e --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20260128160608_add_current_token_balance_retry_fields.exs @@ -0,0 +1,10 @@ +defmodule Explorer.Repo.Migrations.AddCurrentTokenBalanceRetryFields do + use Ecto.Migration + + def change do + alter table(:address_current_token_balances) do + add(:refetch_after, :utc_datetime_usec) + add(:retries_count, :smallint) + end + end +end diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 34115b9b39b4..46688edfcfe2 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -20,6 +20,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_replaced_transactions: 2, async_import_signed_authorizations_statuses: 2, async_import_token_balances: 2, + async_import_current_token_balances: 2, async_import_token_instances: 1, async_import_tokens: 2, async_import_uncles: 2, @@ -142,6 +143,7 @@ defmodule Indexer.Block.Catchup.Fetcher do async_import_internal_transactions(imported, realtime?) async_import_tokens(imported, realtime?) async_import_token_balances(imported, realtime?) + async_import_current_token_balances(imported, realtime?) async_import_uncles(imported, realtime?) async_import_replaced_transactions(imported, realtime?) async_import_token_instances(imported) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 9fea7796ef18..1706602881f5 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -30,6 +30,8 @@ defmodule Indexer.Block.Fetcher do alias Indexer.Fetcher.CoinBalance.Realtime, as: CoinBalanceRealtime alias Indexer.Fetcher.Filecoin.AddressInfo, as: FilecoinAddressInfo alias Indexer.Fetcher.PolygonZkevm.BridgeL1Tokens, as: PolygonZkevmBridgeL1Tokens + alias Indexer.Fetcher.TokenBalance.Current, as: TokenBalanceCurrent + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.Zilliqa.Zrc2Tokens @@ -44,7 +46,6 @@ defmodule Indexer.Block.Fetcher do ReplacedTransaction, SignedAuthorizationStatus, Token, - TokenBalance, UncleBlock } @@ -613,7 +614,7 @@ defmodule Indexer.Block.Fetcher do def async_import_tokens(_, _), do: :ok @doc """ - Triggers async import of token balances just inserted into the database by the realtime or catchup indexer + Triggers async import of historical token balances just inserted into the database by the realtime or catchup indexer or internal transactions fetcher. ## Parameters @@ -629,11 +630,35 @@ defmodule Indexer.Block.Fetcher do def async_import_token_balances(%{address_token_balances: []}, _realtime?), do: :ok def async_import_token_balances(%{address_token_balances: token_balances}, realtime?) do - TokenBalance.async_fetch(token_balances, realtime?) + TokenBalanceHistorical.async_fetch(token_balances, realtime?) end def async_import_token_balances(_, _), do: :ok + @doc """ + Triggers async import of current token balances just inserted into the database by the realtime or catchup indexer. + + ## Parameters + - `%{address_current_token_balances: current_token_balances}`: A map returned by the `Chain.import` function containing + the list of inserted current token balances. + - `realtime?`: A boolean flag indicating whether to insert the items to the beginning (true) + or to the end (false) of the import queue. + + ## Returns + - :ok + """ + @spec async_import_current_token_balances( + %{:address_current_token_balances => list(), optional(any()) => any()}, + boolean() + ) :: :ok + def async_import_current_token_balances(%{address_current_token_balances: []}, _realtime?), do: :ok + + def async_import_current_token_balances(%{address_current_token_balances: current_token_balances}, realtime?) do + TokenBalanceCurrent.async_fetch(current_token_balances, realtime?) + end + + def async_import_current_token_balances(_, _), do: :ok + def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}, realtime?) do UncleBlock.async_fetch_blocks(block_second_degree_relations, realtime?) end diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 579b07792bf9..988843c0145a 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -25,6 +25,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_replaced_transactions: 2, async_import_signed_authorizations_statuses: 2, async_import_token_balances: 2, + async_import_current_token_balances: 2, async_import_token_instances: 1, async_import_tokens: 2, async_import_uncles: 2, @@ -540,6 +541,7 @@ defmodule Indexer.Block.Realtime.Fetcher do async_import_internal_transactions(imported, realtime?) async_import_tokens(imported, realtime?) async_import_token_balances(imported, realtime?) + async_import_current_token_balances(imported, realtime?) async_import_token_instances(imported) async_import_uncles(imported, realtime?) async_import_replaced_transactions(imported, realtime?) diff --git a/apps/indexer/lib/indexer/fetcher/token_balance/current.ex b/apps/indexer/lib/indexer/fetcher/token_balance/current.ex new file mode 100644 index 000000000000..161f9265b76b --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_balance/current.ex @@ -0,0 +1,117 @@ +defmodule Indexer.Fetcher.TokenBalance.Current do + @moduledoc """ + Fetches current token balances and sends the ones that were fetched to be imported in `Address.CurrentTokenBalance`. + + The module responsible for fetching current token balances in the Smart Contract is the `Indexer.TokenBalances`. This module + only prepares the params, sends them to `Indexer.TokenBalances` and relies on its return. + + It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many + token balances will be fetched at the same time. + + Also, this module set a `refetch_after` for each token balance in case of failure to avoid fetching the ones + that always raise errors interacting with the Smart Contract. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + require Logger + + alias Explorer.Chain + alias Explorer.Chain.Address.CurrentTokenBalance + alias Explorer.Chain.Events.Publisher + alias Explorer.Chain.Hash + alias Indexer.{BufferedTask, Tracer} + alias Indexer.Fetcher.TokenBalance.Helper + + @behaviour BufferedTask + + @timeout :timer.minutes(10) + + @spec async_fetch( + [ + %{ + token_contract_address_hash: Hash.Address.t(), + address_hash: Hash.Address.t(), + block_number: non_neg_integer(), + token_type: String.t(), + token_id: non_neg_integer() + } + ], + boolean() + ) :: :ok + def async_fetch(current_token_balances, realtime?) do + Helper.async_fetch(__MODULE__, current_token_balances, realtime?) + end + + @doc false + def child_spec(args) do + Helper.child_spec(args, __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + Helper.init(reducer, &CurrentTokenBalance.stream_unfetched_current_token_balances(initial, &1, true)) + end + + @doc """ + Fetches the given entries (token_balances) from the Smart Contract and import them in our database. + + It also set the `refetch_after` in case of failure to avoid fetching token balances that always raise errors + when reading their balance in the Smart Contract. + """ + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.CurrentTokenBalance.run/2", + tracer: Tracer, + service: :indexer + ) + def run(entries, _json_rpc_named_arguments) do + entries + |> Helper.fetch_token_balances() + |> import_token_balances() + |> case do + :ok -> :ok + _ -> {:retry, entries} + end + end + + def import_token_balances(ctb_params) do + addresses_params = Helper.format_and_filter_address_params(ctb_params) + formatted_current_token_balances_params = Helper.format_and_filter_token_balance_params(ctb_params) + + import_params = %{ + addresses: %{params: addresses_params}, + address_current_token_balances: %{params: formatted_current_token_balances_params}, + timeout: @timeout + } + + case Chain.import(import_params) do + {:ok, %{address_current_token_balances: imported_ctbs}} -> + imported_ctbs + |> Enum.group_by(& &1.address_hash) + |> Enum.each(fn {address_hash, ctbs} -> + Publisher.broadcast( + %{ + address_current_token_balances: %{ + address_hash: to_string(address_hash), + address_current_token_balances: ctbs + } + }, + :realtime + ) + end) + + {:ok, _} -> + :ok + + {:error, reason} -> + Logger.debug(fn -> ["failed to import current token balances: ", inspect(reason)] end, + error_count: Enum.count(ctb_params) + ) + + :error + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/token_balance.ex b/apps/indexer/lib/indexer/fetcher/token_balance/helper.ex similarity index 52% rename from apps/indexer/lib/indexer/fetcher/token_balance.ex rename to apps/indexer/lib/indexer/fetcher/token_balance/helper.ex index b68aa5228e23..dc3374946844 100644 --- a/apps/indexer/lib/indexer/fetcher/token_balance.ex +++ b/apps/indexer/lib/indexer/fetcher/token_balance/helper.ex @@ -1,53 +1,21 @@ -defmodule Indexer.Fetcher.TokenBalance do +defmodule Indexer.Fetcher.TokenBalance.Helper do @moduledoc """ - Fetches token balances and sends the ones that were fetched to be imported in `Address.CurrentTokenBalance` and - `Address.TokenBalance`. - - The module responsible for fetching token balances in the Smart Contract is the `Indexer.TokenBalances`. This module - only prepares the params, sends them to `Indexer.TokenBalances` and relies on its return. - - It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many - token balances will be fetched at the same time. - - Also, this module set a `refetch_after` for each token balance in case of failure to avoid fetching the ones - that always raise errors interacting with the Smart Contract. + Common functions for `Indexer.Fetcher.TokenBalance.Historical` and `Indexer.Fetcher.TokenBalance.Current` modules """ - use Indexer.Fetcher, restart: :permanent - use Spandex.Decorators - require Logger alias EthereumJSONRPC.Utility.RangesHelper - alias Explorer.Chain alias Explorer.Chain.Address.{CurrentTokenBalance, TokenBalance} - alias Explorer.Chain.Events.Publisher alias Explorer.Chain.{Hash, Token} alias Explorer.Utility.MissingBalanceOfToken - alias Indexer.{BufferedTask, TokenBalances, Tracer} - alias Indexer.Fetcher.TokenBalance.Supervisor, as: TokenBalanceSupervisor - - @behaviour BufferedTask + alias Indexer.{BufferedTask, TokenBalances} @default_max_batch_size 100 @default_max_concurrency 10 - @timeout :timer.minutes(10) - - @spec async_fetch( - [ - %{ - token_contract_address_hash: Hash.Address.t(), - address_hash: Hash.Address.t(), - block_number: non_neg_integer(), - token_type: String.t(), - token_id: non_neg_integer() - } - ], - boolean() - ) :: :ok - def async_fetch(token_balances, realtime?) do - if TokenBalanceSupervisor.disabled?() do + def async_fetch(module, token_balances, realtime?) do + if Module.concat(module, Supervisor).disabled?() do :ok else filtered_balances = @@ -57,30 +25,30 @@ defmodule Indexer.Fetcher.TokenBalance do formatted_params = Enum.map(filtered_balances, &entry/1) - BufferedTask.buffer(__MODULE__, formatted_params, realtime?, :infinity) + BufferedTask.buffer(module, formatted_params, realtime?, :infinity) end end @doc false - def child_spec([init_options, gen_server_options]) do + def child_spec([init_options, gen_server_options], module) do {state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments) if !state do raise ArgumentError, - ":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <> + ":json_rpc_named_arguments must be provided to `#{module}.child_spec " <> "to allow for json_rpc calls when running." end merged_init_opts = - defaults() + module + |> defaults() |> Keyword.merge(mergeable_init_options) |> Keyword.put(:state, state) - Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__) + Supervisor.child_spec({BufferedTask, [{module, merged_init_opts}, gen_server_options]}, id: module) end - @impl BufferedTask - def init(initial, reducer, _) do + def init(reducer, stream_func) do entry_reducer = fn token_balance, acc -> token_balance |> entry() @@ -92,25 +60,24 @@ defmodule Indexer.Fetcher.TokenBalance do |> RangesHelper.stream_reducer_by_block_ranges() |> RangesHelper.stream_reducer_traceable() - {:ok, final} = - TokenBalance.stream_unfetched_token_balances( - initial, - stream_reducer, - true - ) + {:ok, final} = stream_func.(stream_reducer) final end - @doc """ - Fetches the given entries (token_balances) from the Smart Contract and import them in our database. + def format_and_filter_address_params(token_balances_params) do + token_balances_params + |> Enum.map(&%{hash: &1.address_hash}) + |> Enum.uniq() + end - It also set the `refetch_after` in case of failure to avoid fetching token balances that always raise errors - when reading their balance in the Smart Contract. - """ - @impl BufferedTask - @decorate trace(name: "fetch", resource: "Indexer.Fetcher.TokenBalance.run/2", tracer: Tracer, service: :indexer) - def run(entries, _json_rpc_named_arguments) do + def format_and_filter_token_balance_params(token_balances_params) do + {params_without_type, params_with_type} = Enum.split_with(token_balances_params, &is_nil(&1.token_type)) + + params_with_type ++ put_token_type_to_balance_objects(params_without_type) + end + + def fetch_token_balances(entries) do params = Enum.map(entries, &format_params/1) missing_balance_of_tokens = @@ -119,20 +86,24 @@ defmodule Indexer.Fetcher.TokenBalance do |> Enum.uniq() |> MissingBalanceOfToken.get_by_hashes() - result = - params - |> MissingBalanceOfToken.filter_token_balances_params(true, missing_balance_of_tokens) - |> fetch_from_blockchain(missing_balance_of_tokens) - |> import_token_balances() + params + |> MissingBalanceOfToken.filter_token_balances_params(true, missing_balance_of_tokens) + |> fetch_from_blockchain(missing_balance_of_tokens) + end - if result == :ok do - :ok - else - {:retry, entries} - end + defp put_token_type_to_balance_objects([]), do: [] + + defp put_token_type_to_balance_objects(token_balances) do + token_types_map = + token_balances + |> Enum.map(& &1.token_contract_address_hash) + |> Token.get_token_types() + |> Map.new() + + Enum.map(token_balances, &Map.put(&1, :token_type, token_types_map[&1.token_contract_address_hash])) end - def fetch_from_blockchain(params_list, missing_balance_of_tokens) do + defp fetch_from_blockchain(params_list, missing_balance_of_tokens) do params_list = Enum.uniq_by(params_list, &Map.take(&1, [:token_contract_address_hash, :token_id, :address_hash, :block_number])) @@ -205,7 +176,7 @@ defmodule Indexer.Fetcher.TokenBalance do end defp define_refetch_after(retries_count) do - config = Application.get_env(:indexer, __MODULE__) + config = Application.get_env(:indexer, Indexer.Fetcher.TokenBalance.Historical) coef = config[:exp_timeout_coeff] max_refetch_interval = config[:max_refetch_interval] @@ -216,74 +187,6 @@ defmodule Indexer.Fetcher.TokenBalance do Timex.shift(Timex.now(), seconds: value) end - def import_token_balances(token_balances_params) do - addresses_params = format_and_filter_address_params(token_balances_params) - formatted_token_balances_params = format_and_filter_token_balance_params(token_balances_params) - - import_params = %{ - addresses: %{params: addresses_params}, - address_token_balances: %{params: formatted_token_balances_params}, - address_current_token_balances: %{ - params: TokenBalances.to_address_current_token_balances(formatted_token_balances_params) - }, - timeout: @timeout - } - - case Chain.import(import_params) do - {:ok, %{address_current_token_balances: imported_ctbs}} -> - imported_ctbs - |> Enum.group_by(& &1.address_hash) - |> Enum.each(fn {address_hash, ctbs} -> - Publisher.broadcast( - %{ - address_current_token_balances: %{ - address_hash: to_string(address_hash), - address_current_token_balances: ctbs - } - }, - :realtime - ) - end) - - {:ok, _} -> - :ok - - {:error, reason} -> - Logger.debug(fn -> ["failed to import token balances: ", inspect(reason)] end, - error_count: Enum.count(token_balances_params) - ) - - :error - end - end - - defp format_and_filter_address_params(token_balances_params) do - token_balances_params - |> Enum.map(&%{hash: &1.address_hash}) - |> Enum.uniq() - end - - defp format_and_filter_token_balance_params(token_balances_params) do - token_balances_params - |> Enum.map(fn token_balance -> - if token_balance.token_type do - token_balance - else - put_token_type_to_balance_object(token_balance) - end - end) - end - - defp put_token_type_to_balance_object(token_balance) do - token_type = Token.get_token_type(token_balance.token_contract_address_hash) - - if token_type do - Map.put(token_balance, :token_type, token_type) - else - token_balance - end - end - defp entry( %{ token_contract_address_hash: token_contract_address_hash, @@ -326,12 +229,12 @@ defmodule Indexer.Fetcher.TokenBalance do } end - defp defaults do + defp defaults(module) do [ flush_interval: 300, - max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size, - max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency, - task_supervisor: Indexer.Fetcher.TokenBalance.TaskSupervisor + max_batch_size: Application.get_env(:indexer, module)[:batch_size] || @default_max_batch_size, + max_concurrency: Application.get_env(:indexer, module)[:concurrency] || @default_max_concurrency, + task_supervisor: Module.concat(module, TaskSupervisor) ] end end diff --git a/apps/indexer/lib/indexer/fetcher/token_balance/historical.ex b/apps/indexer/lib/indexer/fetcher/token_balance/historical.ex new file mode 100644 index 000000000000..4d333b6c6683 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_balance/historical.ex @@ -0,0 +1,108 @@ +defmodule Indexer.Fetcher.TokenBalance.Historical do + @moduledoc """ + Fetches historical token balances and sends the ones that were fetched to be imported in `Address.TokenBalance`. + + The module responsible for fetching token balances in the Smart Contract is the `Indexer.TokenBalances`. This module + only prepares the params, sends them to `Indexer.TokenBalances` and relies on its return. + + It behaves as a `BufferedTask`, so we can configure the `max_batch_size` and the `max_concurrency` to control how many + token balances will be fetched at the same time. + + Also, this module set a `refetch_after` for each token balance in case of failure to avoid fetching the ones + that always raise errors interacting with the Smart Contract. + """ + + use Indexer.Fetcher, restart: :permanent + use Spandex.Decorators + + require Logger + + alias Explorer.Chain + alias Explorer.Chain.Address.TokenBalance + alias Explorer.Chain.Hash + alias Indexer.{BufferedTask, Tracer} + alias Indexer.Fetcher.TokenBalance.Helper + + @behaviour BufferedTask + + @timeout :timer.minutes(10) + + @spec async_fetch( + [ + %{ + token_contract_address_hash: Hash.Address.t(), + address_hash: Hash.Address.t(), + block_number: non_neg_integer(), + token_type: String.t(), + token_id: non_neg_integer() | nil + } + ], + boolean() + ) :: :ok + def async_fetch(token_balances, realtime?) do + Helper.async_fetch(__MODULE__, token_balances, realtime?) + end + + @doc false + def child_spec(args) do + Helper.child_spec(args, __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + Helper.init(reducer, &TokenBalance.stream_unfetched_token_balances(initial, &1, true)) + end + + @doc """ + Fetches the given entries (token balances) from the smart contract and imports them. + + It also sets `refetch_after` on failure to avoid repeated smart contract errors. + + ## Parameters + - `entries`: Token balance entries to fetch. + - `json_rpc_named_arguments`: JSON-RPC configuration (unused). + + ## Returns + - `:ok` on success + - `{:retry, entries}` on failure + """ + @impl BufferedTask + @decorate trace( + name: "fetch", + resource: "Indexer.Fetcher.TokenBalance.Historical.run/2", + tracer: Tracer, + service: :indexer + ) + def run(entries, _json_rpc_named_arguments) do + entries + |> Helper.fetch_token_balances() + |> import_token_balances() + |> case do + :ok -> :ok + _ -> {:retry, entries} + end + end + + def import_token_balances(token_balances_params) do + addresses_params = Helper.format_and_filter_address_params(token_balances_params) + formatted_token_balances_params = Helper.format_and_filter_token_balance_params(token_balances_params) + + import_params = %{ + addresses: %{params: addresses_params}, + address_token_balances: %{params: formatted_token_balances_params}, + timeout: @timeout + } + + case Chain.import(import_params) do + {:ok, _} -> + :ok + + {:error, reason} -> + Logger.debug(fn -> ["failed to import token balances: ", inspect(reason)] end, + error_count: Enum.count(token_balances_params) + ) + + :error + end + end +end diff --git a/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex b/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex index a22a23e9f46d..601b626bd5d1 100644 --- a/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex +++ b/apps/indexer/lib/indexer/fetcher/zilliqa/zrc2_tokens.ex @@ -43,6 +43,7 @@ defmodule Indexer.Fetcher.Zilliqa.Zrc2Tokens do import Indexer.Block.Fetcher, only: [ async_import_token_balances: 2, + async_import_current_token_balances: 2, async_import_tokens: 2 ] @@ -360,6 +361,7 @@ defmodule Indexer.Fetcher.Zilliqa.Zrc2Tokens do async_import_tokens(imported, calling_module == Indexer.Block.Realtime.Fetcher) async_import_token_balances(imported, calling_module == Indexer.Block.Realtime.Fetcher) + async_import_current_token_balances(imported, calling_module == Indexer.Block.Realtime.Fetcher) fetch_zrc2_token_adapters( logs, @@ -554,6 +556,7 @@ defmodule Indexer.Fetcher.Zilliqa.Zrc2Tokens do async_import_tokens(imported, false) async_import_token_balances(imported, false) + async_import_current_token_balances(imported, false) end Enum.each(token_transfers, fn tt -> diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 177bfe88f474..057de8cc7305 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -31,6 +31,8 @@ defmodule Indexer.Supervisor do alias Indexer.Fetcher.MultichainSearchDb.TokenInfoExportQueue, as: MultichainSearchDbTokenInfoExportQueue alias Indexer.Fetcher.Stability.Validator, as: ValidatorStability alias Indexer.Fetcher.Stats.HotSmartContracts + alias Indexer.Fetcher.TokenBalance.Current, as: TokenBalanceCurrent + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical alias Indexer.Fetcher.TokenInstance.Realtime, as: TokenInstanceRealtime alias Indexer.Fetcher.TokenInstance.Retry, as: TokenInstanceRetry alias Indexer.Fetcher.TokenInstance.Sanitize, as: TokenInstanceSanitize @@ -48,7 +50,6 @@ defmodule Indexer.Supervisor do ReplacedTransaction, RootstockData, Token, - TokenBalance, TokenCountersUpdater, TokenTotalSupplyUpdater, TokenUpdater, @@ -153,7 +154,9 @@ defmodule Indexer.Supervisor do configure(TransactionAction.Supervisor, [[memory_monitor: memory_monitor]]), {ContractCode.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, - {TokenBalance.Supervisor, + {TokenBalanceHistorical.Supervisor, + [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, + {TokenBalanceCurrent.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, {TokenUpdater.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index 45233a4f3147..c4c59f32a2a3 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -12,13 +12,13 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do alias Indexer.Block.Catchup alias Indexer.Block.Catchup.MissingRangesCollector alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical alias Indexer.Fetcher.{ ContractCode, InternalTransaction, ReplacedTransaction, Token, - TokenBalance, UncleBlock } @@ -234,7 +234,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() UncleBlock.Supervisor.Case.start_supervised!( @@ -449,7 +449,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() # from `setup :state` @@ -540,7 +540,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() UncleBlock.Supervisor.Case.start_supervised!( diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs index 9703cbc70bf2..2ff4e7b95e35 100644 --- a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -13,8 +13,9 @@ defmodule Indexer.Block.Catchup.FetcherTest do alias Indexer.Block.Catchup.Fetcher alias Indexer.Block.Catchup.MissingRangesCollector alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup - alias Indexer.Fetcher.{BlockReward, InternalTransaction, Token, TokenBalance, UncleBlock} + alias Indexer.Fetcher.{BlockReward, InternalTransaction, Token, UncleBlock} alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical @moduletag capture_log: true @@ -53,7 +54,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Indexer.Fetcher.Filecoin.AddressInfo.Supervisor.Case.start_supervised!( json_rpc_named_arguments: json_rpc_named_arguments @@ -185,7 +186,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) latest_block_number = 2 @@ -345,7 +346,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) latest_block_number = 2 @@ -502,7 +503,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) latest_block_number = 2 @@ -706,7 +707,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do CoinBalanceCatchup.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) MissingRangesCollector.start_link([]) latest_block_number = 3 diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index a0c5b5cad6e1..6a7aa29c93e0 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -16,13 +16,13 @@ defmodule Indexer.Block.FetcherTest do alias Indexer.BufferedTask alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical alias Indexer.Fetcher.{ ContractCode, InternalTransaction, ReplacedTransaction, Token, - TokenBalance, UncleBlock } @@ -69,7 +69,7 @@ defmodule Indexer.Block.FetcherTest do ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) @@ -822,7 +822,7 @@ defmodule Indexer.Block.FetcherTest do ContractCode.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) ReplacedTransaction.Supervisor.Case.start_supervised!() {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) diff --git a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs index b66d98d02aa7..9b6429487735 100644 --- a/apps/indexer/test/indexer/block/realtime/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -17,11 +17,11 @@ defmodule Indexer.Block.Realtime.FetcherTest do InternalTransaction, ReplacedTransaction, Token, - TokenBalance, UncleBlock } alias Indexer.Fetcher.OnDemand.ContractCreator, as: ContractCreatorOnDemand + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical @moduletag capture_log: true @@ -51,7 +51,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do task_supervisor: Indexer.TaskSupervisor } - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) CoinBalanceRealtime.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) {:ok, _pid} = ContractCreatorOnDemand.start_link([[], []]) diff --git a/apps/indexer/test/indexer/fetcher/token_balance_test.exs b/apps/indexer/test/indexer/fetcher/token_balance/historical_test.exs similarity index 89% rename from apps/indexer/test/indexer/fetcher/token_balance_test.exs rename to apps/indexer/test/indexer/fetcher/token_balance/historical_test.exs index 7d388c13e4bc..5f82295bf3ee 100644 --- a/apps/indexer/test/indexer/fetcher/token_balance_test.exs +++ b/apps/indexer/test/indexer/fetcher/token_balance/historical_test.exs @@ -1,4 +1,4 @@ -defmodule Indexer.Fetcher.TokenBalanceTest do +defmodule Indexer.Fetcher.TokenBalance.HistoricalTest do use EthereumJSONRPC.Case use Explorer.DataCase @@ -8,7 +8,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do alias Explorer.Chain.Events.Subscriber alias Explorer.Repo alias Explorer.Utility.MissingBalanceOfToken - alias Indexer.Fetcher.TokenBalance + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical @moduletag :capture_log @@ -25,7 +25,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do insert(:token_balance, value_fetched_at: DateTime.utc_now()) - assert TokenBalance.init([], &[&1 | &2], nil) == [ + assert TokenBalanceHistorical.init([], &[&1 | &2], nil) == [ {address_hash_bytes, token_contract_address_hash_bytes, 1000, "ERC-20", nil, 0} ] end @@ -41,7 +41,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do insert(:token_balance, refetch_after: Timex.shift(Timex.now(), hours: 1)) - assert TokenBalance.init([], &[&1 | &2], nil) == [ + assert TokenBalanceHistorical.init([], &[&1 | &2], nil) == [ {address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0} ] end @@ -49,7 +49,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do describe "run/3" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) :ok end @@ -78,7 +78,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do end ) - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [{address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}], nil ) == :ok @@ -88,18 +88,6 @@ defmodule Indexer.Fetcher.TokenBalanceTest do expected_value = Decimal.new(1_000_000_000_000_000_000_000_000) assert token_balance_updated.value == expected_value assert token_balance_updated.value_fetched_at != nil - - address_hash_string = to_string(address_hash) - - assert_receive( - {:chain_event, :address_current_token_balances, :realtime, - %{ - address_hash: ^address_hash_string, - address_current_token_balances: [ - %{value: ^expected_value, token_contract_address_hash: ^token_contract_address_hash} - ] - }} - ) end test "fetches duplicate params only once" do @@ -124,7 +112,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do end ) - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [ {address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0}, {address_hash_bytes, token_contract_address_hash_bytes, block_number, "ERC-20", nil, 0} @@ -141,7 +129,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do address = insert(:address) missing_balance_of_token = insert(:missing_balance_of_token, currently_implemented: true) - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [ {address.hash.bytes, missing_balance_of_token.token_contract_address_hash.bytes, missing_balance_of_token.block_number, "ERC-20", nil, 0} @@ -174,7 +162,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do refute missing_balance_of_token.currently_implemented - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [ {address.hash.bytes, missing_balance_of_token.token_contract_address_hash.bytes, missing_balance_of_token.block_number + window_size + 1, "ERC-20", nil, 0} @@ -212,7 +200,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do end ) - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [ {address.hash.bytes, token_contract_address_hash.bytes, 1, "ERC-20", nil, 0} ], @@ -254,7 +242,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do end ) - assert TokenBalance.run( + assert TokenBalanceHistorical.run( [ {address.hash.bytes, token_contract_address_hash.bytes, 1, "ERC-20", nil, 0} ], @@ -268,7 +256,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do describe "import_token_balances/1" do test "ignores when it receives a empty list" do - assert TokenBalance.import_token_balances([]) == :ok + assert TokenBalanceHistorical.import_token_balances([]) == :ok end test "returns :error when the token balances has invalid data" do @@ -286,7 +274,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do } ] - assert TokenBalance.import_token_balances(token_balances_params) == :error + assert TokenBalanceHistorical.import_token_balances(token_balances_params) == :error end test "insert the missing address, import the token balances and return :ok when the address does not exist yet" do @@ -304,7 +292,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do ] {:ok, address_hash} = Explorer.Chain.string_to_address_hash("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") - assert TokenBalance.import_token_balances(token_balances_params) == :ok + assert TokenBalanceHistorical.import_token_balances(token_balances_params) == :ok assert {:ok, _} = Explorer.Chain.hash_to_address(address_hash) end @@ -330,7 +318,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do } ] - assert TokenBalance.import_token_balances(token_balances_params) == :ok + assert TokenBalanceHistorical.import_token_balances(token_balances_params) == :ok end test "import the token balances and return :ok when there are multiple balances for the same address on the batch (ERC-1155)" do @@ -356,7 +344,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do } ] - assert TokenBalance.import_token_balances(token_balances_params) == :ok + assert TokenBalanceHistorical.import_token_balances(token_balances_params) == :ok end test "import ERC-404 token balances and return :ok" do @@ -381,7 +369,7 @@ defmodule Indexer.Fetcher.TokenBalanceTest do } ] - assert TokenBalance.import_token_balances(token_balances_params) == :ok + assert TokenBalanceHistorical.import_token_balances(token_balances_params) == :ok end end end diff --git a/apps/indexer/test/indexer/token_balances_test.exs b/apps/indexer/test/indexer/token_balances_test.exs index 92265544486c..747ea4300ee1 100644 --- a/apps/indexer/test/indexer/token_balances_test.exs +++ b/apps/indexer/test/indexer/token_balances_test.exs @@ -5,7 +5,7 @@ defmodule Indexer.TokenBalancesTest do doctest Indexer.TokenBalances alias Indexer.TokenBalances - alias Indexer.Fetcher.TokenBalance + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical alias Explorer.Chain.Hash import Mox @@ -16,7 +16,7 @@ defmodule Indexer.TokenBalancesTest do describe "fetch_token_balances_from_blockchain/2" do setup %{json_rpc_named_arguments: json_rpc_named_arguments} do - TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + TokenBalanceHistorical.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) :ok end diff --git a/apps/indexer/test/support/indexer/fetcher/token_balance_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/token_balance_supervisor_case.ex index 45ad1713ae76..b1243e51c0d2 100644 --- a/apps/indexer/test/support/indexer/fetcher/token_balance_supervisor_case.ex +++ b/apps/indexer/test/support/indexer/fetcher/token_balance_supervisor_case.ex @@ -1,5 +1,5 @@ -defmodule Indexer.Fetcher.TokenBalance.Supervisor.Case do - alias Indexer.Fetcher.TokenBalance +defmodule Indexer.Fetcher.TokenBalance.Historical.Supervisor.Case do + alias Indexer.Fetcher.TokenBalance.Historical, as: TokenBalanceHistorical def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do merged_fetcher_arguments = @@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.TokenBalance.Supervisor.Case do ) [merged_fetcher_arguments] - |> TokenBalance.Supervisor.child_spec() + |> TokenBalanceHistorical.Supervisor.child_spec() |> ExUnit.Callbacks.start_supervised!() end end diff --git a/config/runtime.exs b/config/runtime.exs index d76d409af452..b24088421e3f 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1051,12 +1051,16 @@ config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor, config :indexer, Indexer.Fetcher.Token, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_CONCURRENCY", 10) -config :indexer, Indexer.Fetcher.TokenBalance, +config :indexer, Indexer.Fetcher.TokenBalance.Historical, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_BATCH_SIZE", 100), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_CONCURRENCY", 10), max_refetch_interval: ConfigHelper.parse_time_env_var("INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL", "168h"), exp_timeout_coeff: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF", 100) +config :indexer, Indexer.Fetcher.TokenBalance.Current, + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_CURRENT_TOKEN_BALANCES_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_CURRENT_TOKEN_BALANCES_CONCURRENCY", 10) + config :indexer, Indexer.Fetcher.TokenCountersUpdater, milliseconds_interval: ConfigHelper.parse_time_env_var("TOKEN_COUNTERS_UPDATE_INTERVAL", "3h") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index b02037316c74..c7f49c636191 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -256,6 +256,8 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_TOKEN_BALANCES_CONCURRENCY= # INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL= # INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF= +# INDEXER_CURRENT_TOKEN_BALANCES_BATCH_SIZE= +# INDEXER_CURRENT_TOKEN_BALANCES_CONCURRENCY= # INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED=true # INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_INTERVAL= # INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_MAX_AGE= From 81ad52f2c53f1a37d7d1d7a235c8df5e0dffc5ce Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:35:43 +0300 Subject: [PATCH 0628/1006] fix: Fix 500 on empty ens domain search (#13928) --- apps/explorer/lib/explorer/chain/search.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index eaa09d14a74f..8da7d39a5be5 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -1095,7 +1095,7 @@ defmodule Explorer.Chain.Search do metadata: dynamic(type(^nil, :map)), addresses_index: 0, reputation: "ok", - is_smart_contract_address: dynamic(type(^nil, :boolean)) + is_smart_contract_address: nil } end From b258718035b3386e515e7a7a2c7f13fe6f8f6606 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 3 Feb 2026 13:00:18 +0300 Subject: [PATCH 0629/1006] fix: limit `getlogs` after filtering consensus (#13934) --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/etherscan/logs.ex | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddb04f96037..ceefff452e70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### 🐛 Bug Fixes +- Limit `getlogs` after filtering consensus ([#13934](https://github.com/blockscout/blockscout/pull/13934)) - Handle nil in update_transactions_cache/2 ([#13911](https://github.com/blockscout/blockscout/pull/13911)) - Fix token balances broadcasting function ([#13902](https://github.com/blockscout/blockscout/issues/13902)) - Wrong `next_page_params` in OP Deposits ([#13870](https://github.com/blockscout/blockscout/issues/13870)) diff --git a/apps/explorer/lib/explorer/etherscan/logs.ex b/apps/explorer/lib/explorer/etherscan/logs.ex index 1a925eb480fc..26ec06cf0044 100644 --- a/apps/explorer/lib/explorer/etherscan/logs.ex +++ b/apps/explorer/lib/explorer/etherscan/logs.ex @@ -5,7 +5,7 @@ defmodule Explorer.Etherscan.Logs do """ - import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3] + import Ecto.Query, only: [from: 2, where: 3, subquery: 1, order_by: 3] alias Explorer.{Chain, Repo} alias Explorer.Chain.{DenormalizationHelper, Log, Transaction} @@ -81,7 +81,6 @@ defmodule Explorer.Etherscan.Logs do |> where([log], log.address_hash == ^address_hash) |> where([log], log.block_number >= ^prepared_filter.from_block) |> where([log], log.block_number <= ^prepared_filter.to_block) - |> limit(1000) |> page_logs(paging_options) if DenormalizationHelper.transactions_denormalization_finished?() do @@ -99,7 +98,8 @@ defmodule Explorer.Etherscan.Logs do block_number: transaction.block_number, block_timestamp: transaction.block_timestamp, block_consensus: transaction.block_consensus - } + }, + limit: 1000 ) all_transaction_logs_query @@ -122,7 +122,8 @@ defmodule Explorer.Etherscan.Logs do block_number: transaction.block_number, block_timestamp: block.timestamp, block_consensus: block.consensus - } + }, + limit: 1000 ) all_transaction_logs_query From 3223b43c1f3faa2f042ee850fbb7688f6acab88e Mon Sep 17 00:00:00 2001 From: varasev <33550681+varasev@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:42:33 +0400 Subject: [PATCH 0630/1006] fix: Multichain counter starting time and small fixes (#13920) Co-authored-by: POA <33550681+poa@users.noreply.github.com> Co-authored-by: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> --- .../controllers/api/v2/optimism_controller.ex | 12 ++---------- apps/explorer/lib/explorer/application.ex | 2 +- .../lib/explorer/chain/cache/chain_id.ex | 18 ++++++++++++++++-- .../explorer/chain/optimism/dispute_game.ex | 8 +------- apps/explorer/lib/explorer/history/process.ex | 2 +- .../multichain_search_db/counters_fetcher.ex | 7 +++++-- 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex index 3998c9f60477..796b7752abf1 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/optimism_controller.ex @@ -460,11 +460,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do {nonce, ""} <- Integer.parse(nonce_string, 16), msg = InteropMessage.get_message(init_chain_id, nonce), false <- is_nil(msg) do - current_chain_id = - case ChainId.get_id() do - nil -> Application.get_env(:block_scout_web, :chain_id) - chain_id -> chain_id - end + current_chain_id = ChainId.get_id() relay_chain_id = msg.relay_chain_id @@ -527,11 +523,7 @@ defmodule BlockScoutWeb.API.V2.OptimismController do """ @spec interop_messages(Plug.Conn.t(), map()) :: Plug.Conn.t() def interop_messages(conn, params) do - current_chain_id = - case ChainId.get_id() do - nil -> Application.get_env(:block_scout_web, :chain_id) - chain_id -> chain_id - end + current_chain_id = ChainId.get_id() {messages, next_page} = params diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 579f9dabc06f..987627a3e6c1 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -128,7 +128,7 @@ defmodule Explorer.Application do configure(Explorer.Chain.Cache.TransactionActionTokensData), configure(Explorer.Chain.Cache.TransactionActionUniswapPools), configure(Explorer.Chain.Cache.Counters.WithdrawalsSum), - configure(Explorer.Chain.Transaction.History.Historian), + configure_mode_dependent_process(Explorer.Chain.Transaction.History.Historian, :indexer), configure(Explorer.Chain.Events.Listener), configure(Explorer.Chain.Cache.Counters.AddressesCount), configure(Explorer.Chain.Cache.Counters.AddressTransactionsCount), diff --git a/apps/explorer/lib/explorer/chain/cache/chain_id.ex b/apps/explorer/lib/explorer/chain/cache/chain_id.ex index dd4636b0592a..5e9410ca025c 100644 --- a/apps/explorer/lib/explorer/chain/cache/chain_id.ex +++ b/apps/explorer/lib/explorer/chain/cache/chain_id.ex @@ -5,6 +5,8 @@ defmodule Explorer.Chain.Cache.ChainId do The chain ID is fetched from the node using `eth_chainId` JSON-RPC call when the cache is empty. This helps improve performance by avoiding repeated RPC calls for this frequently needed value. + + If the chain ID cannot be got from the RPC, the `CHAIN_ID` env variable is used. """ require Logger @@ -20,10 +22,22 @@ defmodule Explorer.Chain.Cache.ChainId do {:error, reason} -> Logger.debug([ - "Couldn't fetch eth_chainId, reason: #{inspect(reason)}" + "Couldn't fetch eth_chainId. CHAIN_ID env will be used instead. Reason: #{inspect(reason)}" ]) - {:return, nil} + return = + case Application.get_env(:block_scout_web, :chain_id) do + nil -> + nil + + chain_id -> + chain_id + |> to_string() + |> String.trim() + |> String.to_integer() + end + + {:return, return} end end diff --git a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex index c7ea7274d1f3..a18d5f9e5182 100644 --- a/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex +++ b/apps/explorer/lib/explorer/chain/optimism/dispute_game.ex @@ -101,14 +101,8 @@ defmodule Explorer.Chain.Optimism.DisputeGame do def l2_block_number_from_extra_data(nil), do: 0 def l2_block_number_from_extra_data(%Data{bytes: extra_data}) do - current_chain_id = - case ChainId.get_id() do - nil -> Application.get_env(:block_scout_web, :chain_id) - chain_id -> chain_id - end - first_bits = - if current_chain_id in [ + if ChainId.get_id() in [ @chain_id_bob_mainnet, @chain_id_bob_sepolia, @chain_id_megaeth_mainnet, diff --git a/apps/explorer/lib/explorer/history/process.ex b/apps/explorer/lib/explorer/history/process.ex index 3b9c190140d2..f9fe75579149 100644 --- a/apps/explorer/lib/explorer/history/process.ex +++ b/apps/explorer/lib/explorer/history/process.ex @@ -124,7 +124,7 @@ defmodule Explorer.History.Process do defp calculate_delay_until_next_midnight do now = DateTime.utc_now() # was added for testing possibility - time_to_fetch_at = config_or_default(:time_to_fetch_at, Time.new!(0, 0, 0, 0)) + time_to_fetch_at = config_or_default(:time_to_fetch_at, Time.new!(0, 0, 1, 0)) days_to_add = config_or_default(:days_to_add, 1) tomorrow = DateTime.new!(Date.add(Date.utc_today(), days_to_add), time_to_fetch_at, now.time_zone) diff --git a/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex index 7535335bf933..29815353a29b 100644 --- a/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex +++ b/apps/indexer/lib/indexer/fetcher/multichain_search_db/counters_fetcher.ex @@ -93,7 +93,10 @@ defmodule Indexer.Fetcher.MultichainSearchDb.CountersFetcher do daily_transactions_number = number_of_transactions total_transactions_number = - Repo.aggregate(from(t in Transaction, where: t.block_timestamp <= ^yesterday_dt), :count, :hash, + Repo.aggregate( + from(t in Transaction, where: t.block_timestamp <= ^yesterday_dt and t.block_consensus == true), + :count, + :hash, timeout: :infinity ) @@ -124,7 +127,7 @@ defmodule Indexer.Fetcher.MultichainSearchDb.CountersFetcher do defp calculate_delay_until_next_midnight do now = DateTime.utc_now() - tomorrow = DateTime.new!(Date.add(Date.utc_today(), 1), Time.new!(0, 0, 0, 0), now.time_zone) + tomorrow = DateTime.new!(Date.add(Date.utc_today(), 1), Time.new!(0, 0, 1, 0), now.time_zone) DateTime.diff(tomorrow, now, :millisecond) end From d835ad59e324331ff5f8413349a917ff35c86713 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 3 Feb 2026 19:16:27 +0400 Subject: [PATCH 0631/1006] chore: Add missing CHANGELOG entries (#13942) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ceefff452e70..ca36dc54ea1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### 🚀 Features +- Move current token balances into a separate fetcher ([#13923](https://github.com/blockscout/blockscout/issues/13923)) - Re-architect internal transaction format with call-type enum, error dictionary, and normalization ([#13893](https://github.com/blockscout/blockscout/issues/13893)) - Add audit-reports import endpoint ([#13884](https://github.com/blockscout/blockscout/issues/13884)) - Solady smart-contract proxy with immutable arguments support ([#13794](https://github.com/blockscout/blockscout/issues/13794)) @@ -23,6 +24,8 @@ ### 🐛 Bug Fixes +- Multichain counter starting time and small fixes ([#13920](https://github.com/blockscout/blockscout/pull/13920)) +- Fix 500 on empty ens domain search ([#13928](https://github.com/blockscout/blockscout/pull/13928)) - Limit `getlogs` after filtering consensus ([#13934](https://github.com/blockscout/blockscout/pull/13934)) - Handle nil in update_transactions_cache/2 ([#13911](https://github.com/blockscout/blockscout/pull/13911)) - Fix token balances broadcasting function ([#13902](https://github.com/blockscout/blockscout/issues/13902)) @@ -56,6 +59,7 @@ ### ⚙️ Miscellaneous Tasks +- Improve EmptyInternalTransactionsData migration ([#13918](https://github.com/blockscout/blockscout/pull/13918)) - Disable Auth0 when Dynamic enabled ([#13912](https://github.com/blockscout/blockscout/pull/13912)) - Add swagger spec for account abstraction endpoints ([#13897](https://github.com/blockscout/blockscout/issues/13897)) - Clear token "skip_metadata" property ([#13891](https://github.com/blockscout/blockscout/issues/13891)) From 46f8eaa5a086013dd79c6f82c24f1ffe1b85d6f0 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:48:28 +0400 Subject: [PATCH 0632/1006] chore: Update env variables in CHANGELOG (#13943) --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca36dc54ea1f..43ea89267f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,9 +81,6 @@ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `BLOCK_MINER_GETS_BURNT_FEES` | If `true`, the Burnt fees are added to block miner profit and displayed in UI as zero. Implemented in [#13894](https://github.com/blockscout/blockscout/pull/13894). | Version: v10.0.0\+
Default: `false`
Applications: API | | `UNIVERSAL_PROXY_CONFIG` | JSON-encoded configuration string used to define settings for the universal proxy. Implemented in [#13787](https://github.com/blockscout/blockscout/pull/13787). | Version: v10.0.0\+
Default: (empty)
Applications: API | -| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_BATCH_SIZE` | Specifies the block batch size selected for the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `100`
Applications: Indexer | -| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_CONCURRENCY` | Specifies how many concurrent processes can handle the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `1`
Applications: Indexer | -| `MIGRATION_FILL_INTERNAL_TRANSACTION_TO_ADDRESS_HASH_WITH_CREATED_CONTRACT_ADDRESS_HASH_TIMEOUT` | Defines the timeout between processing each batch (`batch_size` \* `concurrency`) in the fill internal transaction `to_address_hash` migration. Implemented in [#13846](https://github.com/blockscout/blockscout/pull/13846). | Version: v10.0.0\+
Default: `0s`
Applications: Indexer | | `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE` | Number of internal transactions to clear their data in the batch. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `100`
Applications: Indexer | | `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY` | Number of parallel clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `1`
Applications: Indexer | | `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT` | Timeout between clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `0`
Applications: Indexer | @@ -93,6 +90,8 @@ | `INDEXER_OPTIMISM_L1_BATCH_EIGENDA_PROXY_BASE_URL` | Defines a URL to EigenDA proxy node which is used by the DA indexer (planned to be optional in the future). Example for MegaETH: `http://megaeth-eigenda-proxy.node.blockscout.com:3100`. Implemented in [#13709](https://github.com/blockscout/blockscout/pull/13709). | Version: v10.0.0+
Default: (empty)
Applications: Indexer | | `INDEXER_ARBITRUM_MESSAGES_TRACKING_FAILURE_THRESHOLD` | The time threshold for L1 message tracking tasks. If a task has not run successfully within this threshold, it is marked as failed and enters a cooldown period before retrying. Implemented in [#13792](https://github.com/blockscout/blockscout/pull/13792). | Version: v10.0.0+
Default: `10m`
Applications: Indexer | | `INDEXER_ARBITRUM_MISSED_MESSAGE_IDS_RANGE` | Size of each message ID range inspected when discovering L1-to-L2 messages with missing L1 origination information. Implemented in [#13792](https://github.com/blockscout/blockscout/pull/13792). | Version: v10.0.0+
Default: `10000`
Applications: Indexer | +| `INDEXER_CURRENT_TOKEN_BALANCES_BATCH_SIZE` | Batch size for current token balances fetcher. Implemented in [#13923](https://github.com/blockscout/blockscout/pull/13923). | Version: v10.0.0+
Default: `100`
Applications: Indexer | +| `INDEXER_CURRENT_TOKEN_BALANCES_CONCURRENCY` | Concurrency for current token balances fetcher. Implemented in [#13923](https://github.com/blockscout/blockscout/pull/13923). | Version: v10.0.0+
Default: `10`
Applications: Indexer | ### Deprecated ENV variables From fe930ed089b33250e7514a9c69ba17c20aabeecd Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:48:24 +0400 Subject: [PATCH 0633/1006] chore: Allow to set IT storage period not only in days (#13932) --- .../migrator/delete_zero_value_internal_transactions.ex | 8 ++++---- apps/utils/lib/utils/config_helper.ex | 1 + config/runtime.exs | 4 ++-- docker-compose/envs/common-blockscout.env | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 223d47b96445..6b33fafa2f23 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -1,7 +1,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do @moduledoc """ Continuously deletes all zero-value calls older than - `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` from DB. + `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD` from DB. """ use GenServer @@ -22,7 +22,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do @not_completed_check_interval 10 @default_check_interval :timer.minutes(1) @default_batch_size 100 - @default_storage_period_days 30 + @default_storage_period :timer.hours(24) * 30 @spec start_link(term()) :: GenServer.on_start() def start_link(_) do @@ -235,8 +235,8 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end defp get_border_number do - storage_period = Application.get_env(:explorer, __MODULE__)[:storage_period_days] || @default_storage_period_days - border_timestamp = Timex.shift(Timex.now(), days: -storage_period) + storage_period = Application.get_env(:explorer, __MODULE__)[:storage_period] || @default_storage_period + border_timestamp = Timex.shift(Timex.now(), milliseconds: -storage_period) Block |> where([b], b.timestamp < ^border_timestamp) diff --git a/apps/utils/lib/utils/config_helper.ex b/apps/utils/lib/utils/config_helper.ex index 40814c79368a..49f0dd1d0de1 100644 --- a/apps/utils/lib/utils/config_helper.ex +++ b/apps/utils/lib/utils/config_helper.ex @@ -10,6 +10,7 @@ defmodule Utils.ConfigHelper do def parse_time_value(value) do case value |> String.downcase() |> Integer.parse() do {milliseconds, "ms"} -> milliseconds + {days, "d"} -> :timer.hours(24) * days {hours, "h"} -> :timer.hours(hours) {minutes, "m"} -> :timer.minutes(minutes) {seconds, s} when s in ["s", ""] -> :timer.seconds(seconds) diff --git a/config/runtime.exs b/config/runtime.exs index b24088421e3f..7142cfbce6fc 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -879,8 +879,8 @@ config :explorer, Explorer.Migrator.MergeAdjacentMissingBlockRanges, config :explorer, Explorer.Migrator.DeleteZeroValueInternalTransactions, enabled: ConfigHelper.parse_bool_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED", "false"), batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE", 100), - storage_period_days: - ConfigHelper.parse_integer_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS", 30), + storage_period: + ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD", "30d"), check_interval: ConfigHelper.parse_time_env_var("MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL", "1m") diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index c7f49c636191..f00d1d12f6f8 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -577,7 +577,7 @@ ACCOUNT_REDIS_URL=redis://redis-db:6379 # MIGRATION_MERGE_ADJACENT_MISSING_BLOCK_RANGES_BATCH_SIZE= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_ENABLED= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_BATCH_SIZE= -# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS= +# MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD= # MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_CHECK_INTERVAL= # MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE= # MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY= From 31adad97f0a4878bb5dd0ea384f60e319b46dbf1 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Wed, 4 Feb 2026 19:14:56 +0400 Subject: [PATCH 0634/1006] chore: Replace ZeroValueDeleteQueue with filtering on import (#13921) --- .../import/runner/internal_transactions.ex | 75 ++++++++----------- .../zero_value_delete_queue.ex | 14 ---- ...delete_zero_value_internal_transactions.ex | 27 +------ ...l_transactions_zero_value_delete_queue.exs | 7 ++ 4 files changed, 40 insertions(+), 83 deletions(-) delete mode 100644 apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex create mode 100644 apps/explorer/priv/repo/migrations/20260128120316_drop_internal_transactions_zero_value_delete_queue.exs diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index dad3a47a20c7..fabc2af90428 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -24,7 +24,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do alias Explorer.Chain.Events.Publisher alias Explorer.Chain.Import.Runner - alias Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue alias Explorer.Migrator.DeleteZeroValueInternalTransactions alias Explorer.Prometheus.Instrumenter alias Explorer.Repo, as: ExplorerRepo @@ -149,14 +148,27 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do :maybe_shrink_internal_transactions_params ) end) + |> Multi.run(:maybe_reject_zero_value, fn _, + %{ + maybe_shrink_internal_transactions_params: + maybe_shrink_internal_transactions_params + } -> + Instrumenter.block_import_stage_runner( + fn -> + maybe_reject_zero_value(maybe_shrink_internal_transactions_params) + end, + :block_pending, + :internal_transactions, + :maybe_reject_zero_value + ) + end) |> Multi.run(:internal_transactions, fn repo, %{ - maybe_shrink_internal_transactions_params: - shrink_internal_transactions_params + maybe_reject_zero_value: internal_transactions_params } -> Instrumenter.block_import_stage_runner( fn -> - insert(repo, shrink_internal_transactions_params, insert_options) + insert(repo, internal_transactions_params, insert_options) end, :block_pending, :internal_transactions, @@ -195,14 +207,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do :update_pending_blocks_status ) end) - |> Multi.run(:save_zero_value_to_delete, fn repo, %{internal_transactions: internal_transactions} -> - Instrumenter.block_import_stage_runner( - fn -> save_zero_value_to_delete(repo, internal_transactions, insert_options) end, - :block_pending, - :internal_transactions, - :save_zero_value_to_delete - ) - end) |> Multi.run(:empty_selfdestructed_contracts_bytecode, fn repo, %{ valid_internal_transactions: valid_internal_transactions @@ -230,8 +234,11 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do # Enforce ShareLocks tables order (see docs: sharelocks.md) with {:ok, data} <- Multi.new() - |> Multi.run(:internal_transactions, fn repo, _ -> - insert(repo, internal_transactions_params, insert_options) + |> Multi.run(:maybe_reject_zero_value, fn _, _ -> + maybe_reject_zero_value(internal_transactions_params) + end) + |> Multi.run(:internal_transactions, fn repo, %{maybe_reject_zero_value: maybe_reject_zero_value} -> + insert(repo, maybe_reject_zero_value, insert_options) end) |> ExplorerRepo.transaction() do Publisher.broadcast(data, :on_demand) @@ -610,6 +617,16 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end + defp maybe_reject_zero_value(internal_transactions) do + with true <- Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled], + border_number when is_integer(border_number) <- DeleteZeroValueInternalTransactions.border_number() do + {:ok, + Enum.reject(internal_transactions, &(&1.block_number <= border_number and &1.type == :call and &1.value == 0))} + else + _ -> {:ok, internal_transactions} + end + end + defp sanitize_error(entry) do error = Map.get(entry, :error) @@ -986,36 +1003,6 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do end end - defp save_zero_value_to_delete(repo, internal_transactions, %{timeout: timeout, timestamps: timestamps}) do - with true <- Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled], - border_number when is_integer(border_number) <- DeleteZeroValueInternalTransactions.border_number() do - internal_transactions - |> Enum.map(& &1.block_number) - |> Enum.uniq() - |> Enum.filter(&(not is_nil(&1) and &1 <= border_number)) - |> Enum.map(&Map.put(timestamps, :block_number, &1)) - |> case do - [] -> - {:ok, []} - - insert_params -> - {_total, result} = - repo.insert_all( - ZeroValueDeleteQueue, - insert_params, - conflict_target: [:block_number], - on_conflict: {:replace, [:updated_at]}, - returning: [:block_number], - timeout: timeout - ) - - {:ok, result} - end - else - _ -> {:ok, []} - end - end - defp empty_selfdestructed_contracts_bytecode(repo, valid_internal_transactions, timestamps) do # Find all selfdestruct internal transactions selfdestruct_addresses = diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex b/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex deleted file mode 100644 index bc47724d1d1f..000000000000 --- a/apps/explorer/lib/explorer/chain/internal_transaction/zero_value_delete_queue.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue do - @moduledoc """ - Stores numbers for blocks, whose zero-value internal transactions should be deleted - """ - - use Explorer.Schema - - @primary_key false - typed_schema "internal_transactions_zero_value_delete_queue" do - field(:block_number, :integer, primary_key: true) - - timestamps() - end -end diff --git a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex index 6b33fafa2f23..d156f4889889 100644 --- a/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex +++ b/apps/explorer/lib/explorer/migrator/delete_zero_value_internal_transactions.ex @@ -11,7 +11,6 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do alias Explorer.Chain.{Block, InternalTransaction} alias Explorer.Chain.Cache.Counters.AverageBlockTime - alias Explorer.Chain.InternalTransaction.ZeroValueDeleteQueue alias Explorer.Migrator.MigrationStatus alias Explorer.Repo alias Explorer.Utility.{AddressIdToAddressHash, InternalTransactionsAddressPlaceholder} @@ -56,8 +55,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do border_number = get_border_number() to_number = border_number && min(max_number + batch_size(), border_number) clear_internal_transactions(max_number, to_number) - clear_from_delete_queue() - completed? = to_number == border_number + completed? = not is_nil(border_number) and to_number == border_number new_max_number = (to_number && to_number + 1) || max_number new_state = @@ -69,7 +67,7 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end MigrationStatus.update_meta(@migration_name, new_state) - schedule_check(completed?) + schedule_check(completed? or is_nil(border_number)) {:noreply, new_state} end @@ -99,17 +97,6 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do end end - defp clear_from_delete_queue do - batch_size = batch_size() - - ZeroValueDeleteQueue - |> order_by([z], z.block_number) - |> select([z], z.block_number) - |> limit(^batch_size) - |> Repo.all() - |> clear_internal_transactions() - end - defp clear_internal_transactions(from_number, to_number) when is_integer(from_number) and is_integer(to_number) and from_number < to_number do dynamic_condition = dynamic([it], it.block_number >= ^from_number and it.block_number <= ^to_number) @@ -119,12 +106,6 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do defp clear_internal_transactions(_from, _to), do: :ok - defp clear_internal_transactions(block_numbers) when is_list(block_numbers) do - dynamic_condition = dynamic([it], it.block_number in ^block_numbers) - - do_clear_internal_transactions(dynamic_condition) - end - @smallint_max_value 32767 defp do_clear_internal_transactions(dynamic_condition) do Repo.transaction( @@ -155,10 +136,6 @@ defmodule Explorer.Migrator.DeleteZeroValueInternalTransactions do {_count, deleted_internal_transactions} = Repo.delete_all(delete_query, timeout: :infinity) - ZeroValueDeleteQueue - |> where([it], ^dynamic_condition) - |> Repo.delete_all(timeout: :infinity) - address_hashes = deleted_internal_transactions |> Enum.flat_map(&[&1.from_address_hash, &1.to_address_hash]) diff --git a/apps/explorer/priv/repo/migrations/20260128120316_drop_internal_transactions_zero_value_delete_queue.exs b/apps/explorer/priv/repo/migrations/20260128120316_drop_internal_transactions_zero_value_delete_queue.exs new file mode 100644 index 000000000000..68c086d073e4 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20260128120316_drop_internal_transactions_zero_value_delete_queue.exs @@ -0,0 +1,7 @@ +defmodule Explorer.Repo.Migrations.DropInternalTransactionsZeroValueDeleteQueue do + use Ecto.Migration + + def change do + drop(table(:internal_transactions_zero_value_delete_queue)) + end +end From efe34eaa7052b9462fd7c6042beec81582ea1a17 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:25:38 +0300 Subject: [PATCH 0635/1006] chore(deps): bump tesla from 1.15.3 to 1.16.0 (#13916) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/ethereum_jsonrpc/mix.exs | 2 +- apps/explorer/mix.exs | 2 +- apps/utils/mix.exs | 2 +- mix.exs | 2 +- mix.lock | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/ethereum_jsonrpc/mix.exs b/apps/ethereum_jsonrpc/mix.exs index 2eb14cf8ae8a..99c2dfc65d78 100644 --- a/apps/ethereum_jsonrpc/mix.exs +++ b/apps/ethereum_jsonrpc/mix.exs @@ -75,7 +75,7 @@ defmodule EthereumJSONRPC.MixProject do {:spandex, "~> 3.0"}, # `:spandex` integration with Datadog {:spandex_datadog, "~> 1.0"}, - {:tesla, "~> 1.15.3"}, + {:tesla, "~> 1.16.0"}, # Convert unix timestamps in JSONRPC to DateTimes {:timex, "~> 3.7.1"}, # Encode/decode function names and arguments diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 98ab22fe2713..99576c187fda 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -115,7 +115,7 @@ defmodule Explorer.Mixfile do # `Timex.Duration` for `Explorer.Chain.Cache.Counters.AverageBlockTime.average_block_time/0` {:timex, "~> 3.7.1"}, {:con_cache, "~> 1.0"}, - {:tesla, "~> 1.15.3"}, + {:tesla, "~> 1.16.0"}, {:cbor, "~> 1.0"}, {:cloak_ecto, "~> 1.3.0"}, {:redix, "~> 1.1"}, diff --git a/apps/utils/mix.exs b/apps/utils/mix.exs index e74bcb649434..5e988bc63066 100644 --- a/apps/utils/mix.exs +++ b/apps/utils/mix.exs @@ -33,7 +33,7 @@ defmodule Utils.MixProject do {:credo, "~> 1.5", only: [:test, :dev], runtime: false}, {:httpoison, "~> 2.0"}, {:mime, "~> 2.0"}, - {:tesla, "~> 1.15.3"} + {:tesla, "~> 1.16.0"} ] end diff --git a/mix.exs b/mix.exs index f978cb367659..01e75d769341 100644 --- a/mix.exs +++ b/mix.exs @@ -105,7 +105,7 @@ defmodule BlockScout.Mixfile do [ {:prometheus_ex, "~> 5.0.0", override: true}, {:absinthe_plug, git: "https://github.com/blockscout/absinthe_plug.git", tag: "1.5.8", override: true}, - {:tesla, "~> 1.15.3"}, + {:tesla, "~> 1.16.0"}, {:mint, "~> 1.7.1"}, # Documentation {:ex_doc, "~> 0.39.1", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index be70d35ca11c..e703645aab0d 100644 --- a/mix.lock +++ b/mix.lock @@ -170,7 +170,7 @@ "statistex": {:hex, :statistex, "1.1.0", "7fec1eb2f580a0d2c1a05ed27396a084ab064a40cfc84246dbfb0c72a5c761e5", [:mix], [], "hexpm", "f5950ea26ad43246ba2cce54324ac394a4e7408fdcf98b8e230f503a0cba9cf5"}, "sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, - "tesla": {:hex, :tesla, "1.15.3", "3a2b5c37f09629b8dcf5d028fbafc9143c0099753559d7fe567eaabfbd9b8663", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "98bb3d4558abc67b92fb7be4cd31bb57ca8d80792de26870d362974b58caeda7"}, + "tesla": {:hex, :tesla, "1.16.0", "de77d083aea08ebd1982600693ff5d779d68a4bb835d136a0394b08f69714660", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, ">= 1.0.0", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.21", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.2", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:mox, "~> 1.0", [hex: :mox, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "eb3bdfc0c6c8a23b4e3d86558e812e3577acff1cb4acb6cfe2da1985a1035b89"}, "timex": {:hex, :timex, "3.7.13", "0688ce11950f5b65e154e42b47bf67b15d3bc0e0c3def62199991b8a8079a1e2", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "09588e0522669328e973b8b4fd8741246321b3f0d32735b589f78b136e6d4c54"}, "ton": {:hex, :ton, "0.5.1", "79745434a93e5f7de3572fdcf04feb048620f0edab9794fc412a73528672927d", [:mix], [{:cafezinho, "~> 0.4.4", [hex: :cafezinho, repo: "hexpm", optional: false]}, {:evil_crc32c, "~> 0.2.9", [hex: :evil_crc32c, repo: "hexpm", optional: false]}, {:ex_pbkdf2, "~> 0.8.4", [hex: :ex_pbkdf2, repo: "hexpm", optional: false]}, {:mnemoniac, "~> 0.1.4", [hex: :mnemoniac, repo: "hexpm", optional: false]}], "hexpm", "916f656c870902a61690347da9500c5ce27f04c02e02441363bac7b128030f07"}, "typed_ecto_schema": {:hex, :typed_ecto_schema, "0.4.3", "1e5f3b6c763f9b5725975d3ab7f1554525f1f1399b966f2425acf04f9d8dd4fe", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "dcbd9b35b9fda5fa9258e0ae629a99cf4473bd7adfb85785d3f71dfe7a9b2bc0"}, From 0324b888c2a325b373d797b8695afd3cbe696d05 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:33:01 +0300 Subject: [PATCH 0636/1006] chore(deps-dev): bump ex_doc from 0.39.3 to 0.40.1 (#13936) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.exs | 2 +- mix.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.exs b/mix.exs index 01e75d769341..aeececca7db5 100644 --- a/mix.exs +++ b/mix.exs @@ -108,7 +108,7 @@ defmodule BlockScout.Mixfile do {:tesla, "~> 1.16.0"}, {:mint, "~> 1.7.1"}, # Documentation - {:ex_doc, "~> 0.39.1", only: :dev, runtime: false}, + {:ex_doc, "~> 0.40.1", only: :dev, runtime: false}, {:number, "~> 1.0.3"} ] end diff --git a/mix.lock b/mix.lock index e703645aab0d..cebffed4f9ee 100644 --- a/mix.lock +++ b/mix.lock @@ -60,7 +60,7 @@ "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.0", "b5564b57d3769c85e16689472a9bb65804f71ccd3484144e31998398fda25ad1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b29e4d723c69db5d0a3f3bcef7583a0bc87dda1cd642187c589fec4bfc59a703"}, "ex_cldr_units": {:hex, :ex_cldr_units, "3.20.1", "b27ec81814a67ed77d1c06c64ef74519b141497b74244ec18e1a1a2f78f8e313", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "879af22563b06570f16c28bed3decaadc0c1233906f4516b2d5d28e2bbbadee0"}, - "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, + "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "ex_eth_bls": {:hex, :ex_eth_bls, "0.1.0", "33c2bf424b360e4b64d7630dd72ec028dac63df56802d0a14ade54a23ad1c743", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "25f6ffc36de4952e55adff1c712f0b9680850773678550f1da970d4d18329365"}, "ex_hash_ring": {:hex, :ex_hash_ring, "6.0.4", "bef9d2d796afbbe25ab5b5a7ed746e06b99c76604f558113c273466d52fa6d6b", [:mix], [], "hexpm", "89adabf31f7d3dfaa36802ce598ce918e9b5b33bae8909ac1a4d052e1e567d18"}, "ex_json_schema": {:hex, :ex_json_schema, "0.11.2", "8f8200e6afa5473f37dbebd1e72bf97d8d5dd0128125123ed1532611267f0138", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "395f4aaf32ea0a14d861b16695e7bc8a1b5d841e0fd374d25aef9701bf8da825"}, From 89b3cd9c75c4b41718370dce395fb921cfae6815 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:51:42 +0300 Subject: [PATCH 0637/1006] chore(deps): bump cldr_utils from 2.29.2 to 2.29.4 (#13914) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index cebffed4f9ee..aa9e0c76a759 100644 --- a/mix.lock +++ b/mix.lock @@ -20,7 +20,7 @@ "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"}, "certifi": {:hex, :certifi, "2.15.0", "0e6e882fcdaaa0a5a9f2b3db55b1394dba07e8d6d9bcad08318fb604c6839712", [:rebar3], [], "hexpm", "b147ed22ce71d72eafdad94f055165c1c182f61a2ff49df28bcc71d1d5b94a60"}, - "cldr_utils": {:hex, :cldr_utils, "2.29.2", "58355887416ac8e015aa2fc1ca23a594e7b3bf670bb860ac61cc635ce83b18e3", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "c3e29ea34d88c6ae2e7c35f8db5040a249584f7edd7db697b129ba2bc8f25845"}, + "cldr_utils": {:hex, :cldr_utils, "2.29.4", "11437b0bf9a0d57db4eccdf751c49f675a04fa4261c5dae1e23552a0347e25c9", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "e72a43e69a3f546979085cbdbeae7e9049998cd21cedfdd796cff9155998114e"}, "cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"}, "cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, From 307996ff85c4f7a3c71019e0c0da65f175aeead4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:10:08 +0300 Subject: [PATCH 0638/1006] chore(deps): bump phoenix_live_view from 1.1.20 to 1.1.22 (#13939) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index aa9e0c76a759..3fd88dbaf176 100644 --- a/mix.lock +++ b/mix.lock @@ -135,7 +135,7 @@ "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.2", "b18b0773a1ba77f28c52decbb0f10fd1ac4d3ae5b8632399bbf6986e3b665f62", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "d1f89c18114c50d394721365ffb428cce24f1c13de0467ffa773e2ff4a30d5b9"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.20", "4f20850ee700b309b21906a0e510af1b916b454b4f810fb8581ada016eb42dfc", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c16abd605a21f778165cb0079946351ef20ef84eb1ef467a862fb9a173b1d27d"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.1.22", "9b3c985bfe38e82668594a8ce90008548f30b9f23b718ebaea4701710ce9006f", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e1395d5622d8bf02113cb58183589b3da6f1751af235768816e90cc3ec5f1188"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, From 6517b3c02b2fbf115cd87e91417688c07969ce90 Mon Sep 17 00:00:00 2001 From: Qwerty5Uiop <105209995+Qwerty5Uiop@users.noreply.github.com> Date: Thu, 5 Feb 2026 13:25:53 +0400 Subject: [PATCH 0639/1006] fix: Fix maybe_reject_zero_value condition (#13947) --- .../explorer/chain/import/runner/internal_transactions.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index fabc2af90428..a57423cd14a0 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -621,7 +621,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do with true <- Application.get_env(:explorer, DeleteZeroValueInternalTransactions)[:enabled], border_number when is_integer(border_number) <- DeleteZeroValueInternalTransactions.border_number() do {:ok, - Enum.reject(internal_transactions, &(&1.block_number <= border_number and &1.type == :call and &1.value == 0))} + Enum.reject( + internal_transactions, + &(&1.block_number <= border_number and &1.type == :call and Decimal.eq?(&1.value.value, 0)) + )} else _ -> {:ok, internal_transactions} end From b810d86dd6b0d4fa2c2cd0998e91371c4295ff10 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Thu, 5 Feb 2026 12:29:24 +0300 Subject: [PATCH 0640/1006] fix: `HttpClient.get` usage in genesis data module (#13945) --- CHANGELOG.md | 1 + apps/explorer/lib/explorer/chain_spec/genesis_data.ex | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ea89267f59..3c8a8f28f9a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ ### 🐛 Bug Fixes +- `HttpClient.get` usage in genesis data module ([#13945](https://github.com/blockscout/blockscout/pull/13945)) - Multichain counter starting time and small fixes ([#13920](https://github.com/blockscout/blockscout/pull/13920)) - Fix 500 on empty ens domain search ([#13928](https://github.com/blockscout/blockscout/pull/13928)) - Limit `getlogs` after filtering consensus ([#13934](https://github.com/blockscout/blockscout/pull/13934)) diff --git a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex index 5eb5a5099529..cbeae6750c99 100644 --- a/apps/explorer/lib/explorer/chain_spec/genesis_data.ex +++ b/apps/explorer/lib/explorer/chain_spec/genesis_data.ex @@ -174,7 +174,7 @@ defmodule Explorer.ChainSpec.GenesisData do # Fetches JSON data from a provided URL. @spec fetch_from_url(binary()) :: {:ok, list() | map()} | {:error, Jason.DecodeError.t() | any()} defp fetch_from_url(url) do - case HttpClient.get(url, timeout: 60_000, recv_timeout: 60_000) do + case HttpClient.get(url, [], timeout: 60_000, recv_timeout: 60_000) do {:ok, %{body: body, status_code: 200}} -> {:ok, Jason.decode!(body)} From bd4005d5a51164f35377486ea2c840d46ae5e297 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Thu, 5 Feb 2026 23:07:14 +0300 Subject: [PATCH 0641/1006] chore: Fixate chromedriver version (#13949) --- .../channels/v2/exchange_rate_channel_test.exs | 2 +- .../controllers/account/api/v2/user_controller_test.exs | 4 ++-- .../block_scout_web/controllers/address_controller_test.exs | 2 +- .../api/v2/internal_transaction_controller_test.exs | 4 ++-- .../controllers/api/v2/transaction_controller_test.exs | 4 ++-- .../controllers/transaction_state_controller_test.exs | 1 - .../test/block_scout_web/routers/chain_type_scope_test.exs | 2 +- .../test/block_scout_web/views/address_view_test.exs | 2 +- bin/install_chrome_headless.sh | 5 +++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs index 1264eea2d62b..f2af726161f2 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/v2/exchange_rate_channel_test.exs @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do import Mox alias BlockScoutWeb.Notifier - alias Explorer.Market.Fetcher.{Coin, History} + alias Explorer.Market.Fetcher.Coin alias Explorer.Market.{MarketHistory, MarketHistoryCache, Token} alias Explorer.Market.Source.OneCoinSource alias Explorer.Market diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs index 8fff7dcb28ab..24543b4447bc 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v2/user_controller_test.exs @@ -590,10 +590,10 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do assert response == %{"errors" => %{"address_hash" => ["This address isn't EOA"]}} end - test "can insert EOA with code watchlist address", %{conn: conn, user: user} do + test "can insert EOA with code watchlist address", %{conn: conn, user: _user} do address = insert(:contract_address, contract_code: "0xef01000000000000000000000000000000000000000123") - response = + _response = conn |> post( "/api/account/v2/user/watchlist", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs index d896d7b94633..d15289cb531a 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs @@ -25,7 +25,7 @@ defmodule BlockScoutWeb.AddressControllerTest do test "returns top addresses", %{conn: conn} do address_hashes = - 4..1 + 4..1//-1 |> Enum.map(&insert(:address, fetched_coin_balance: &1)) |> Enum.map(& &1.hash) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs index 8a2f38381c0f..7b24cdae528e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/internal_transaction_controller_test.exs @@ -95,7 +95,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do ) # Insert internal transaction with index 1 - should be included - it_1 = + _it_1 = insert(:internal_transaction, transaction: tx, transaction_index: 0, @@ -106,7 +106,7 @@ defmodule BlockScoutWeb.API.V2.InternalTransactionControllerTest do ) # Insert internal transaction with index 2 - should be included - it_2 = + _it_2 = insert(:internal_transaction, transaction: tx, transaction_index: 0, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs index 4c3a8aaf5517..5bdd9ff9252e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs @@ -657,7 +657,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do ) internal_transactions = - 51..1 + 51..1//-1 |> Enum.map(fn index -> insert(:internal_transaction, transaction: transaction, @@ -763,7 +763,7 @@ defmodule BlockScoutWeb.API.V2.TransactionControllerTest do |> with_block() logs = - 50..0 + 50..0//-1 |> Enum.map(fn index -> insert(:log, transaction: transaction, diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs index 5cb92cca2469..484cb288a928 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs @@ -9,7 +9,6 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do alias Explorer.Chain.Wei alias Indexer.Fetcher.CoinBalance.Catchup, as: CoinBalanceCatchup alias Explorer.Chain.Cache.Counters.{AddressesCount, AverageBlockTime} - alias Indexer.Fetcher.OnDemand.CoinBalance, as: CoinBalanceOnDemand setup :set_mox_global diff --git a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs index acabb81702cf..5c4574f00b7b 100644 --- a/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs +++ b/apps/block_scout_web/test/block_scout_web/routers/chain_type_scope_test.exs @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.Routers.ChainTypeScopeTest do test "stability validators counters are accessible when chain type is stability", %{conn: conn} do Application.put_env(:explorer, :chain_type, :stability) - assert response = + assert _response = conn |> get("/api/v2/validators/stability/counters") |> json_response(200) diff --git a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs index faefd2355ed3..f4a559f7491c 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.AddressViewTest do use BlockScoutWeb.ConnCase, async: true - alias Explorer.Chain.{Address, Data, Hash, Transaction} + alias Explorer.Chain.{Address, Hash, Transaction} alias BlockScoutWeb.{AddressView, Endpoint} describe "address_partial_selector/4" do diff --git a/bin/install_chrome_headless.sh b/bin/install_chrome_headless.sh index 63f2d98a2493..fab11cd9b32e 100755 --- a/bin/install_chrome_headless.sh +++ b/bin/install_chrome_headless.sh @@ -1,12 +1,13 @@ export DISPLAY=:99.0 sh -e /etc/init.d/xvfb start -export CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json" | jq -r '.channels' | jq -r '.Stable' | jq -r '.version') +#export CHROMEDRIVER_VERSION=$(curl -s "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions.json" | jq -r '.channels' | jq -r '.Stable' | jq -r '.version') +export CHROMEDRIVER_VERSION=144.0.7559.133 curl -L -O "https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/${CHROMEDRIVER_VERSION}/linux64/chromedriver-linux64.zip" unzip -j chromedriver-linux64.zip sudo chmod +x chromedriver sudo mv chromedriver /usr/local/bin wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -sudo apt install ./google-chrome-stable_current_amd64.deb sudo apt-get update sudo apt-get install libstdc++6 +sudo apt install ./google-chrome-stable_current_amd64.deb From 7e1cca7cd80e796175ad02917e0c2e09b5a30ff4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:32:51 +0300 Subject: [PATCH 0642/1006] chore(deps): bump ex_cldr from 2.45.2 to 2.46.0 (#13917) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- mix.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.lock b/mix.lock index 3fd88dbaf176..fffe72786cea 100644 --- a/mix.lock +++ b/mix.lock @@ -55,7 +55,7 @@ "ex_aws": {:hex, :ex_aws, "2.6.1", "194582c7b09455de8a5ab18a0182e6dd937d53df82be2e63c619d01bddaccdfa", [:mix], [{:configparser_ex, "~> 5.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "67842a08c90a1d9a09dbe4ac05754175c7ca253abe4912987c759395d4bd9d26"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.9", "862b7792f2e60d7010e2920d79964e3fab289bc0fd951b0ba8457a3f7f9d1199", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "a480d2bb2da64610014021629800e1e9457ca5e4a62f6775bffd963360c2bf90"}, "ex_brotli": {:hex, :ex_brotli, "0.5.0", "573645db5201317b6176b8858b668ea4ca89dc5c21852e84b9867579d483c220", [:mix], [{:phoenix, ">= 0.0.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:rustler, "~> 0.29", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8447d98d51f8f312629fd38619d4f564507dcf3a03d175c3f8f4ddf98e46dd92"}, - "ex_cldr": {:hex, :ex_cldr, "2.45.2", "3295bf1ffd82768035e5e5a1b4fc646905422eff3874b6b639c3c1288edf8d57", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "82671e5e4e320bfc54d422ce25858f542d593804a3417e664c9e5e5a35f741da"}, + "ex_cldr": {:hex, :ex_cldr, "2.46.0", "29b5bb638932ca4fc4339595145e327b797f59963a398c12a6aee1efe5a35b1b", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "14157ac16694e99c1339ac25a4f10d3df0e0d15cc1a35073b37e195487c1b6cb"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.17.0", "c38d76339dbee413f7dd1aba4cdf05758bd4c0bbfe9c3b1c8602f96082c2890a", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "9af59bd29407dcca59fa39ded8c1649ae1cf6ec29fd0611576dcad0279bce0db"}, "ex_cldr_lists": {:hex, :ex_cldr_lists, "2.11.1", "ad18f861d7c5ca82aac6d173469c6a2339645c96790172ab0aa255b64fb7303b", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "00161c04510ccb3f18b19a6b8562e50c21f1e9c15b8ff4c934bea5aad0b4ade2"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.0", "b5564b57d3769c85e16689472a9bb65804f71ccd3484144e31998398fda25ad1", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b29e4d723c69db5d0a3f3bcef7583a0bc87dda1cd642187c589fec4bfc59a703"}, From 7af967a912ee7bf3fceccfe1031bbaff296bfd66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:57:01 +0300 Subject: [PATCH 0643/1006] chore(deps): bump ex_secp256k1 from 0.7.6 to 0.8.0 (#13938) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- apps/indexer/mix.exs | 2 +- mix.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/indexer/mix.exs b/apps/indexer/mix.exs index a0c5ef1d7552..de723c0265f4 100644 --- a/apps/indexer/mix.exs +++ b/apps/indexer/mix.exs @@ -60,7 +60,7 @@ defmodule Indexer.MixProject do # Importing to database {:explorer, in_umbrella: true}, # ex_secp256k1 crypto functions - {:ex_secp256k1, "~> 0.7.0"}, + {:ex_secp256k1, "~> 0.8.0"}, # Log errors and application output to separate files {:logger_file_backend, "~> 0.0.10"}, {:logger_json, "~> 7.0"}, diff --git a/mix.lock b/mix.lock index fffe72786cea..544f8d7338f0 100644 --- a/mix.lock +++ b/mix.lock @@ -68,7 +68,7 @@ "ex_machina": {:hex, :ex_machina, "2.8.0", "a0e847b5712065055ec3255840e2c78ef9366634d62390839d4880483be38abe", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "79fe1a9c64c0c1c1fab6c4fa5d871682cb90de5885320c187d117004627a7729"}, "ex_pbkdf2": {:hex, :ex_pbkdf2, "0.8.5", "583733a1134a0c8b4df70c6b3e874d1896bf48f2aaafdf332ec5480c7b5117bf", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f138b2acdc5a4d87880e42742e9bead4d1235fd9b1b2b29be0d1da1e01c3a312"}, "ex_rlp": {:hex, :ex_rlp, "0.6.0", "985391d2356a7cb8712a4a9a2deb93f19f2fbca0323f5c1203fcaf64d077e31e", [:mix], [], "hexpm", "7135db93b861d9e76821039b60b00a6a22d2c4e751bf8c444bffe7a042f1abaf"}, - "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.6", "49ea3b07dbc55e2b339e2cd54c6fcb3434620e2de1dfbb4b4f41351384119579", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "b84c9c4d85b47410cd951222b4d2b644cdbff96f4f83bc5ac96ee15d850a113c"}, + "ex_secp256k1": {:hex, :ex_secp256k1, "0.8.0", "aade42e790638de82a2b951a83f55b9cc6545b8c105b20a0112ff6b78e1800cd", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "87257ef7110c45ac396a110eb93023371db96b6f65ed0676046a11e866d1e3a0"}, "ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm", "66d4fe75285948f2d1e69c2a5ddd651c398c813574f8d36a9eef11dc20356ef6"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm", "1222419f706e01bfa1095aec9acf6421367dcfab798a6f67c54cf784733cd6b5"}, "exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm", "32e95820a97cffea67830e91514a2ad53b888850442d6d395f53a1ac60c82e07"}, @@ -158,7 +158,7 @@ "redix": {:hex, :redix, "1.5.3", "4eaae29c75e3285c0ff9957046b7c209aa7f72a023a17f0a9ea51c2a50ab5b0f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b06fb5246373af41f5826b03334dfa3f636347d4d5d98b4d455b699d425ae7e"}, "remote_ip": {:hex, :remote_ip, "1.2.0", "fb078e12a44414f4cef5a75963c33008fe169b806572ccd17257c208a7bc760f", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "2ff91de19c48149ce19ed230a81d377186e4412552a597d6a5137373e5877cb7"}, "req": {:hex, :req, "0.5.6", "8fe1eead4a085510fe3d51ad854ca8f20a622aae46e97b302f499dfb84f726ac", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cfaa8e720945d46654853de39d368f40362c2641c4b2153c886418914b372185"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.3", "4e741024b0b097fe783add06e53ae9a6f23ddc78df1010f215df0c02915ef5a8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "c23f5f33cb6608542de4d04faf0f0291458c352a4648e4d28d17ee1098cddcc4"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.4", "700a878312acfac79fb6c572bb8b57f5aae05fe1cf70d34b5974850bbf2c05bf", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "3b33d99b540b15f142ba47944f7a163a25069f6d608783c321029bc1ffb09514"}, "siwe": {:git, "https://github.com/royal-markets/siwe-ex.git", "51c9c08240eb7eea3c35693011f8d260cd9bb3be", [ref: "51c9c08240eb7eea3c35693011f8d260cd9bb3be"]}, "sleeplocks": {:hex, :sleeplocks, "1.1.3", "96a86460cc33b435c7310dbd27ec82ca2c1f24ae38e34f8edde97f756503441a", [:rebar3], [], "hexpm", "d3b3958552e6eb16f463921e70ae7c767519ef8f5be46d7696cc1ed649421321"}, "sobelow": {:hex, :sobelow, "0.14.1", "2f81e8632f15574cba2402bcddff5497b413c01e6f094bc0ab94e83c2f74db81", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8fac9a2bd90fdc4b15d6fca6e1608efb7f7c600fa75800813b794ee9364c87f2"}, From d3c04fdff4835a14ee1dcbf15a724d36b925019e Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 6 Feb 2026 13:02:47 +0300 Subject: [PATCH 0644/1006] chore: Increase default for MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE to 1000 (#13953) --- CHANGELOG.md | 3 ++- config/runtime.exs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c8a8f28f9a2..719db022a727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ ### ⚙️ Miscellaneous Tasks +- Increase default for MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE to 1000 ([#13953](https://github.com/blockscout/blockscout/pull/13953)) - Improve EmptyInternalTransactionsData migration ([#13918](https://github.com/blockscout/blockscout/pull/13918)) - Disable Auth0 when Dynamic enabled ([#13912](https://github.com/blockscout/blockscout/pull/13912)) - Add swagger spec for account abstraction endpoints ([#13897](https://github.com/blockscout/blockscout/issues/13897)) @@ -82,7 +83,7 @@ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | `BLOCK_MINER_GETS_BURNT_FEES` | If `true`, the Burnt fees are added to block miner profit and displayed in UI as zero. Implemented in [#13894](https://github.com/blockscout/blockscout/pull/13894). | Version: v10.0.0\+
Default: `false`
Applications: API | | `UNIVERSAL_PROXY_CONFIG` | JSON-encoded configuration string used to define settings for the universal proxy. Implemented in [#13787](https://github.com/blockscout/blockscout/pull/13787). | Version: v10.0.0\+
Default: (empty)
Applications: API | -| `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE` | Number of internal transactions to clear their data in the batch. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `100`
Applications: Indexer | +| `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE` | Number of internal transactions to clear their data in the batch. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `1000`
Applications: Indexer | | `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY` | Number of parallel clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `1`
Applications: Indexer | | `MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT` | Timeout between clearing internal transaction data batches processing. Implemented in [#13893](https://github.com/blockscout/blockscout/pull/13893). | Version: v10.0.0\+
Default: `0`
Applications: Indexer | | `CACHE_PENDING_OPERATIONS_COUNT_PERIOD` | Time interval to restart the task which calculates the total pending operations count. Introduced in [#12474](https://github.com/blockscout/blockscout/pull/12474). | Version: v10.0.0\+
Default: `5m`
Applications: API, Indexer | diff --git a/config/runtime.exs b/config/runtime.exs index 7142cfbce6fc..7833ba26705b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -843,7 +843,7 @@ config :explorer, Explorer.Migrator.FilecoinPendingAddressOperations, config :explorer, Explorer.Migrator.CeloAccounts, enabled: ConfigHelper.chain_identity() == {:optimism, :celo} config :explorer, Explorer.Migrator.EmptyInternalTransactionsData, - batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE", 100), + batch_size: ConfigHelper.parse_integer_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE", 1000), concurrency: ConfigHelper.parse_integer_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_CONCURRENCY", 1), timeout: ConfigHelper.parse_time_env_var("MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_TIMEOUT", "0s") From afda98efae284107034fb7e6e3f7cf5beb02e272 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 6 Feb 2026 14:02:13 +0300 Subject: [PATCH 0645/1006] chore: Introduce env to disable archival token balances fetcher (#13954) --- .devcontainer/.blockscout_config.example | 4 ++-- config/runtime.exs | 12 ++++++++---- docker-compose/envs/common-blockscout.env | 9 +++++---- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.devcontainer/.blockscout_config.example b/.devcontainer/.blockscout_config.example index 737933b067b6..209cb7db8592 100644 --- a/.devcontainer/.blockscout_config.example +++ b/.devcontainer/.blockscout_config.example @@ -30,7 +30,7 @@ INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE=1 INDEXER_BLOCK_REWARD_BATCH_SIZE=1 INDEXER_RECEIPTS_BATCH_SIZE=10 INDEXER_COIN_BALANCES_BATCH_SIZE=1 -INDEXER_TOKEN_BALANCES_BATCH_SIZE=1 +INDEXER_ARCHIVAL_TOKEN_BALANCES_BATCH_SIZE=1 INDEXER_CATCHUP_BLOCKS_CONCURRENCY=1 MIGRATION_TOKEN_INSTANCE_OWNER_BATCH_SIZE=1 @@ -39,7 +39,7 @@ INDEXER_BLOCK_REWARD_CONCURRENCY=1 INDEXER_RECEIPTS_CONCURRENCY=1 INDEXER_COIN_BALANCES_CONCURRENCY=1 INDEXER_TOKEN_CONCURRENCY=1 -INDEXER_TOKEN_BALANCES_CONCURRENCY=1 +INDEXER_ARCHIVAL_TOKEN_BALANCES_CONCURRENCY=1 INDEXER_TOKEN_INSTANCE_RETRY_CONCURRENCY=1 INDEXER_TOKEN_INSTANCE_REALTIME_CONCURRENCY=1 INDEXER_TOKEN_INSTANCE_SANITIZE_CONCURRENCY=1 diff --git a/config/runtime.exs b/config/runtime.exs index 7833ba26705b..92f4a3df9268 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -1052,10 +1052,14 @@ config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor, config :indexer, Indexer.Fetcher.Token, concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_CONCURRENCY", 10) config :indexer, Indexer.Fetcher.TokenBalance.Historical, - batch_size: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_BATCH_SIZE", 100), - concurrency: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_CONCURRENCY", 10), - max_refetch_interval: ConfigHelper.parse_time_env_var("INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL", "168h"), - exp_timeout_coeff: ConfigHelper.parse_integer_env_var("INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF", 100) + batch_size: ConfigHelper.parse_integer_env_var("INDEXER_ARCHIVAL_TOKEN_BALANCES_BATCH_SIZE", 100), + concurrency: ConfigHelper.parse_integer_env_var("INDEXER_ARCHIVAL_TOKEN_BALANCES_CONCURRENCY", 10), + max_refetch_interval: ConfigHelper.parse_time_env_var("INDEXER_ARCHIVAL_TOKEN_BALANCES_MAX_REFETCH_INTERVAL", "168h"), + exp_timeout_coeff: + ConfigHelper.parse_integer_env_var("INDEXER_ARCHIVAL_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF", 100) + +config :indexer, Indexer.Fetcher.TokenBalance.Historical.Supervisor, + disabled?: ConfigHelper.parse_bool_env_var("INDEXER_DISABLE_ARCHIVAL_TOKEN_BALANCES_FETCHER") config :indexer, Indexer.Fetcher.TokenBalance.Current, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_CURRENT_TOKEN_BALANCES_BATCH_SIZE", 100), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index f00d1d12f6f8..dd53623e2b1a 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -182,6 +182,7 @@ API_V1_WRITE_METHODS_DISABLED=false # DISABLE_REALTIME_INDEXER=false # DISABLE_CATCHUP_INDEXER=false # INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER=false +# INDEXER_DISABLE_ARCHIVAL_TOKEN_BALANCES_FETCHER=false # INDEXER_DISABLE_TOKEN_INSTANCE_REALTIME_FETCHER=false # INDEXER_DISABLE_TOKEN_INSTANCE_RETRY_FETCHER=false # INDEXER_DISABLE_TOKEN_INSTANCE_SANITIZE_FETCHER=false @@ -252,10 +253,10 @@ API_V1_WRITE_METHODS_DISABLED=false # INDEXER_RECEIPTS_BATCH_SIZE= # INDEXER_RECEIPTS_CONCURRENCY= # INDEXER_TOKEN_CONCURRENCY= -# INDEXER_TOKEN_BALANCES_BATCH_SIZE= -# INDEXER_TOKEN_BALANCES_CONCURRENCY= -# INDEXER_TOKEN_BALANCES_MAX_REFETCH_INTERVAL= -# INDEXER_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF= +# INDEXER_ARCHIVAL_TOKEN_BALANCES_BATCH_SIZE= +# INDEXER_ARCHIVAL_TOKEN_BALANCES_CONCURRENCY= +# INDEXER_ARCHIVAL_TOKEN_BALANCES_MAX_REFETCH_INTERVAL= +# INDEXER_ARCHIVAL_TOKEN_BALANCES_EXPONENTIAL_TIMEOUT_COEFF= # INDEXER_CURRENT_TOKEN_BALANCES_BATCH_SIZE= # INDEXER_CURRENT_TOKEN_BALANCES_CONCURRENCY= # INDEXER_DB_EVENT_NOTIFICATIONS_CLEANUP_ENABLED=true From d1d1aa586367f64b7d1a5e74a8b42cd3ea071c21 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 6 Feb 2026 20:47:40 +0300 Subject: [PATCH 0646/1006] chore: Enhance missing_blocks_count indexer metric: take into account min block number in the chain (#13955) --- .../lib/explorer/chain/metrics/queries/indexer_metrics.ex | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex index fcb0d9d9b600..ec87abacf750 100644 --- a/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex +++ b/apps/explorer/lib/explorer/chain/metrics/queries/indexer_metrics.ex @@ -5,6 +5,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do import Ecto.Query alias Ecto.Adapters.SQL + alias EthereumJSONRPC.Utility.RangesHelper alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.MultichainSearchDb.BalancesExportQueue, as: MultichainSearchDbBalancesExportQueue alias Explorer.Chain.MultichainSearchDb.CountersExportQueue, as: MultichainSearchDbCountersExportQueue @@ -20,9 +21,11 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do # sobelow_skip ["SQL"] @spec missing_blocks_count() :: integer() def missing_blocks_count do + first_block = RangesHelper.get_min_block_number_from_range_string(Application.get_env(:indexer, :block_ranges)) + sql_string = """ SELECT COUNT(DISTINCT b1.number) AS missing_blocks_count - FROM generate_series(0, (SELECT MAX(number) FROM blocks)) AS b1(number) + FROM generate_series($1, (SELECT MAX(number) FROM blocks)) AS b1(number) WHERE NOT EXISTS ( SELECT 1 FROM blocks b2 WHERE b2.number=b1.number @@ -30,7 +33,7 @@ defmodule Explorer.Chain.Metrics.Queries.IndexerMetrics do ); """ - case SQL.query(Repo, sql_string, [], timeout: :infinity) do + case SQL.query(Repo, sql_string, [first_block], timeout: :infinity) do {:ok, %Postgrex.Result{command: :select, columns: ["missing_blocks_count"], rows: [[missing_blocks_count]]}} -> missing_blocks_count From 1af3717b7f388401067bd3fce5184de140d08854 Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 6 Feb 2026 21:03:41 +0300 Subject: [PATCH 0647/1006] Add 9.3.3 release notes --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 719db022a727..bf5f9ad4adf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,25 @@ | `CACHE_PBO_COUNT_PERIOD` | Time interval to restart the task which calculates the total pending_block_operations count. | `20m` | v5.2.0+ | | v10.0.0 | +## 9.3.3 + +### ⚙️ Miscellaneous Tasks + +- Replace ZeroValueDeleteQueue with filtering on import ([#13921](https://github.com/blockscout/blockscout/pull/13921), [#13947](https://github.com/blockscout/blockscout/pull/13947)) +- Allow to set IT storage period not only in days ([#13932](https://github.com/blockscout/blockscout/pull/13932)) + +### New ENV variables + +| Variable | Description | Parameters | +|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------| +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD` | Specifies the period for which recent zero-value calls won't be deleted in delete zero-value calls migration. Implemented in [#13932](https://github.com/blockscout/blockscout/pull/13932). | Version: v9.3.3\+
Default: `30d`
Applications: Indexer | + +### Deprecated ENV variables + +| Variable | Description | Default | Version | Deprecated in Version | +|--------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|---------|-----------|-----------------------| +| `MIGRATION_DELETE_ZERO_VALUE_INTERNAL_TRANSACTIONS_STORAGE_PERIOD_DAYS` | Specifies the period for which recent zero-value calls won't be deleted in delete zero-value calls migration. | `30` | v9.3.0+ | v9.3.3 | + ## 9.3.2 From 6d1361a3bcf150922cd637daa9de0a701e799ff3 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:47:45 +0300 Subject: [PATCH 0648/1006] chore: don't send historic rate for recent txs (#13960) --- CHANGELOG.md | 1 + .../views/api/v2/transaction_view.ex | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf5f9ad4adf5..e4aef1b01322 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ ### ⚙️ Miscellaneous Tasks +- Don't send historic rate for recent txs ([#13960](https://github.com/blockscout/blockscout/pull/13960)) - Increase default for MIGRATION_EMPTY_INTERNAL_TRANSACTIONS_DATA_BATCH_SIZE to 1000 ([#13953](https://github.com/blockscout/blockscout/pull/13953)) - Improve EmptyInternalTransactionsData migration ([#13918](https://github.com/blockscout/blockscout/pull/13918)) - Disable Auth0 when Dynamic enabled ([#13912](https://github.com/blockscout/blockscout/pull/13912)) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index 55f9c6802df2..647872f7a85f 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -468,12 +468,14 @@ defmodule BlockScoutWeb.API.V2.TransactionView do decoded_input_data = decoded_input(decoded_input) + block_timestamp = block_timestamp(transaction) + result = %{ "hash" => transaction.hash, "result" => status, "status" => transaction.status, "block_number" => transaction.block_number, - "timestamp" => block_timestamp(transaction), + "timestamp" => block_timestamp, "from" => Helper.address_with_info( single_transaction? && conn, @@ -520,8 +522,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_transaction?), "actions" => transaction_actions(transaction.transaction_actions), "exchange_rate" => Market.get_coin_exchange_rate().fiat_value, - "historic_exchange_rate" => - Market.get_coin_exchange_rate_at_date(block_timestamp(transaction), @api_true).fiat_value, + "historic_exchange_rate" => historic_exchange_rate(block_timestamp), "method" => Transaction.method_name(transaction, decoded_input), "transaction_types" => transaction_types(transaction), "transaction_tag" => @@ -847,7 +848,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do @doc """ Returns block's timestamp from Block/Transaction """ - @spec block_timestamp(any()) :: :utc_datetime_usec | nil + @spec block_timestamp(any()) :: DateTime.t() | nil def block_timestamp(%Transaction{block_timestamp: block_ts}) when not is_nil(block_ts), do: block_ts def block_timestamp(%Transaction{block: %Block{} = block}), do: block.timestamp def block_timestamp(%Block{} = block), do: block.timestamp @@ -903,6 +904,14 @@ defmodule BlockScoutWeb.API.V2.TransactionView do Map.merge(map, %{"change" => change}) end + defp historic_exchange_rate(nil), do: nil + + defp historic_exchange_rate(block_timestamp) do + if DateTime.before?(block_timestamp, DateTime.shift(DateTime.utc_now(), day: -1)) do + Market.get_coin_exchange_rate_at_date(block_timestamp, @api_true).fiat_value + end + end + defp with_chain_type_transformations(transactions) do do_with_chain_type_transformations(chain_type(), transactions) end From d51471c3fba99439bd2bf072182be1148eac3cce Mon Sep 17 00:00:00 2001 From: Maksym Zub Date: Wed, 11 Feb 2026 11:23:36 +0100 Subject: [PATCH 0649/1006] feat: ERC-7984 Confidential Tokens (#13593) Co-authored-by: Denis Co-authored-by: l0gun0v <98526047+l0gun0v@users.noreply.github.com> --- .../controllers/api/rpc/address_controller.ex | 4 + .../api/v2/advanced_filter_controller.ex | 2 +- .../lib/block_scout_web/paging_helper.ex | 2 +- .../block_scout_web/schemas/api/v2/general.ex | 4 +- .../block_scout_web/schemas/api/v2/token.ex | 2 +- .../schemas/api/v2/token_transfer.ex | 19 ++++ .../_token_balances.html.eex | 9 ++ .../address_token_balance/_tokens.html.eex | 11 ++- .../views/api/rpc/address_view.ex | 6 ++ .../block_scout_web/views/tokens/helper.ex | 8 ++ .../views/tokens/holder_view.ex | 4 + .../block_scout_web/views/transaction_view.ex | 1 + .../api/rpc/address_controller_test.exs | 94 +++++++++++++++++++ .../api/v2/address_controller_test.exs | 4 +- .../v2/advanced_filter_controller_test.exs | 22 +++-- .../api/v2/token_controller_test.exs | 19 ++++ .../api/v2/token_transfer_controller_test.exs | 33 +++++++ .../api/v2/transaction_controller_test.exs | 4 +- .../views/tokens/helper_test.exs | 7 ++ .../views/tokens/holder_view_test.exs | 7 ++ .../lib/explorer/account/notifier/summary.ex | 16 +++- .../lib/explorer/chain/address/counters.ex | 2 +- .../chain/address/current_token_balance.ex | 10 +- .../lib/explorer/chain/address/token.ex | 2 +- apps/explorer/lib/explorer/chain/token.ex | 2 + .../lib/explorer/chain/token_transfer.ex | 3 + .../chain/transaction/state_change.ex | 65 +++++++------ apps/explorer/lib/explorer/etherscan.ex | 9 ++ .../utility/missing_balance_of_token.ex | 2 +- .../account/notifier/summary_test.exs | 60 ++++++++++++ .../explorer/chain/token_transfer_test.exs | 37 ++++++++ apps/explorer/test/support/factory.ex | 63 +++++++++++++ apps/indexer/lib/indexer/token_balances.ex | 4 +- .../lib/indexer/transform/token_transfers.ex | 56 ++++++++++- .../transform/token_transfers_test.exs | 38 ++++++++ 35 files changed, 567 insertions(+), 64 deletions(-) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index 28bc57c6dc17..8ba6b34091fb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -255,6 +255,10 @@ defmodule BlockScoutWeb.API.RPC.AddressController do do_tokentx(conn, params, :erc404) end + def token7984tx(conn, params) do + do_tokentx(conn, params, :erc7984) + end + defp do_tokentx(conn, params, transfers_type) do with {:params, {:ok, options}} <- {:params, optional_params(params)}, {:address, {:ok, address_hash}} <- {:address, to_address_hash_optional(params["address"])}, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 2f08a88f82dc..e2dba447805a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -235,7 +235,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do ] end - @default_allowed_transaction_types ~w(COIN_TRANSFER CONTRACT_INTERACTION CONTRACT_CREATION ERC-20 ERC-404 ERC-721 ERC-1155) + @default_allowed_transaction_types ~w(COIN_TRANSFER CONTRACT_INTERACTION CONTRACT_CREATION ERC-20 ERC-404 ERC-721 ERC-1155 ERC-7984) if @chain_type == :zilliqa do @chain_type_allowed_transaction_types ~w(ZRC-2) diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 84b1472e0ca8..d45e04273e20 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.PagingHelper do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @allowed_filter_labels ["validated", "pending"] - @allowed_base_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + @allowed_base_token_transfer_type_labels ["ERC-20", "ERC-721", "ERC-1155", "ERC-404", "ERC-7984"] if @chain_type == :zilliqa do @allowed_chain_type_token_transfer_type_labels ["ZRC-2"] else diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex index d0f58a668dd6..f4400fbed242 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/general.ex @@ -29,9 +29,9 @@ defmodule BlockScoutWeb.Schemas.API.V2.General do @hex_string_pattern ~r"^0x([A-Fa-f0-9]*)$" if @chain_type == :zilliqa do - @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2)(,(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2))*\]?$/i + @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2|ERC-7984)(,(ERC-20|ERC-721|ERC-1155|ERC-404|ZRC-2|ERC-7984))*\]?$/i else - @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404)(,(ERC-20|ERC-721|ERC-1155|ERC-404))*\]?$/i + @token_type_pattern ~r/^\[?(ERC-20|ERC-721|ERC-1155|ERC-404|ERC-7984)(,(ERC-20|ERC-721|ERC-1155|ERC-404|ERC-7984))*\]?$/i end # Matches ISO-like datetime strings where separators between time fields can be ':' or percent-encoded '%3A'. diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex index accf82d4ef2c..fe8d837aa6e2 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token.ex @@ -104,7 +104,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.Token.Type do use Utils.CompileTimeEnvHelper, chain_type: [:explorer, :chain_type] - @token_types ["ERC-20", "ERC-721", "ERC-1155", "ERC-404"] + @token_types ["ERC-20", "ERC-721", "ERC-1155", "ERC-404", "ERC-7984"] if @chain_type == :zilliqa do @chain_type_token_types ["ZRC-2"] diff --git a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex index f9a39847acfd..118f8c8578e9 100644 --- a/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex +++ b/apps/block_scout_web/lib/block_scout_web/schemas/api/v2/token_transfer.ex @@ -31,6 +31,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do Total, TotalERC1155, TotalERC721, + TotalERC7984, TransactionHashCustomization } @@ -46,6 +47,7 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer do anyOf: [ TotalERC721, TotalERC1155, + TotalERC7984, Total ], nullable: true @@ -131,3 +133,20 @@ defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.Total do additionalProperties: false }) end + +defmodule BlockScoutWeb.Schemas.API.V2.TokenTransfer.TotalERC7984 do + @moduledoc false + require OpenApiSpex + + alias BlockScoutWeb.Schemas.API.V2.General + + OpenApiSpex.schema(%{ + type: :object, + properties: %{ + value: General.IntegerStringNullable, + decimals: General.IntegerStringNullable + }, + required: [:value, :decimals], + additionalProperties: false + }) +end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex index 2096d38a7cb7..ec5efcfe32c9 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex @@ -73,6 +73,15 @@ type: "ERC-20" ) %> <% end %> + + <%= if Enum.any?(@token_balances, fn token_balance -> token_balance.token.type == "ERC-7984" end) do %> + <%= render( + "_tokens.html", + conn: @conn, + token_balances: filter_by_type(@token_balances, "ERC-7984"), + type: "ERC-7984" + ) %> + <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex index 1350b792df96..3b245f17efd4 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex @@ -49,10 +49,13 @@